如何在树结构中合并 JSONB 字段?

How to merge JSONB field in a tree structure?

我在 Postgres 中有一个 table 存储树结构。每个节点都有一个jsonb字段:params_diff:

CREATE TABLE tree (id INT, parent_id INT, params_diff JSONB);
INSERT INTO tree VALUES
  (1, NULL, '{ "some_key": "some value" }'::jsonb)
, (2, 1,    '{ "some_key": "other value", "other_key": "smth" }'::jsonb)
, (3, 2,    '{ "other_key": "smth else" }'::jsonb);

我需要的是 select id 的一个节点,附加生成的 params 字段包含合并整个父链中所有 params_diff 的结果:

SELECT tree.*, /* some magic here */ AS params FROM tree WHERE id = 3;

 id | parent_id |        params_diff         |                        params
----+-----------+----------------------------+-------------------------------------------------------
  3 |         2 | {"other_key": "smth else"} | {"some_key": "other value", "other_key": "smth else"}

一般来说,一个 recursive CTE 就可以完成这项工作。示例:

  • Use table alias in another query to traverse a tree

我们只需要更多的魔法来分解、处理和重新assemble JSON 结果。我从您的示例中假设,您只需要每个键一次,搜索路径中的第一个值(自下而上):

WITH RECURSIVE cte AS (
   SELECT id, parent_id, params_diff, 1 AS lvl
   FROM   tree
   WHERE  id = 3

   UNION ALL
   SELECT t.id, t.parent_id, t.params_diff, c.lvl + 1
   FROM   cte  c
   JOIN   tree t ON t.id = c.parent_id
   )
SELECT id, parent_id, params_diff
    , (SELECT json_object(array_agg(key   ORDER BY lvl)
                        , array_agg(value ORDER BY lvl))::jsonb
        FROM  (
           SELECT key, value
           FROM (
                SELECT DISTINCT ON (key)
                       p.key, p.value, c.lvl
                FROM   cte c, jsonb_each_text(c.params_diff) p
                ORDER  BY p.key, c.lvl
                ) sub1
           ORDER  BY lvl
           ) sub2
       ) AS params

FROM   cte
WHERE  id = 3;

如何?

  1. 使用经典的递归 CTE 遍历树。
  2. LATERAL JOIN 中创建一个包含所有键和值 jsonb_each_text() 的派生 table,记住搜索路径中的级别 (lvl)。
  3. 使用 DISTINCT ON 得到每个 key 的 "first"(最低 lvlvalue。细节:
    • Select first row in each GROUP BY group?
  4. 对生成的键和值进行排序和聚合,并将数组提供给 json_object() 以构建最终的 params 值。

SQL Fiddle(只有第 9.3 页可以使用 json 而不是 jsonb)。