从递归树结构创建 JSON 对象

Ben*_*Ben 5 postgresql tree cte json recursive

具有这种简单的多对多自引用结构。
一个项目通过表拥有其他项目joins

CREATE TABLE items (
  item_id   serial PRIMARY KEY
, title     text
);

CREATE TABLE joins (
  id        serial PRIMARY KEY
, item_id   int
, child_id  int
);

INSERT INTO items (item_id, title) VALUES
  (1, 'PARENT')
, (2, 'LEVEL 2')
, (3, 'LEVEL 3.1')
, (4, 'LEVEL 4.1')
, (5, 'LEVEL 4.2')
, (6, 'LEVEL 3.2')
;

INSERT INTO joins (item_id, child_id) VALUES
  (1, 2)
, (2, 3)
, (3, 4)
, (3, 5)
, (2, 6)
;
Run Code Online (Sandbox Code Playgroud)

db<>在这里摆弄

我正在尝试以 JSON 形式检索给定项目的整个树结构。
例如查询值为item_id1的项目(伪代码):

SELECT i.*, fulltree from items i where item_id = 1;
Run Code Online (Sandbox Code Playgroud)

所需的输出fulltree

{
  id: 1,
  title: "PARENT",
  children: [
   {
     id: 2,
     title: "LEVEL 2",
     children: [
      {
        id: 3,
        title: "LEVEL 3.1",
        children: [
          {
            id: 4,
            title: "LEVEL 4.1"
          },
          {
            id: 5,
            title: "LEVEL 4.2"
          }
        ]
      },
      {
        id: 6,
        title: "LEVEL 3.2"
      }
     ]
   }
  ]
}
Run Code Online (Sandbox Code Playgroud)

在深入研究 Postgres 提供的 JSON 功能后,我通过重复嵌套查询来管理此类输出。简单但丑陋,并且限制重复次数。:/

我发现了递归查询。这里和那里找到的例子并不那么简单。很难找到一个切入点来理解该技术并使其适应我的需求。

我希望这里的示例足够简单,以便能够从有经验的用户那里获得帮助。

Erw*_*ter 3

递归CTE (rCTE)不允许在递归项中进行聚合。所以没有简单的解决方案。

我建议使用递归函数来实现优雅的解决方案:

CREATE OR REPLACE FUNCTION f_item_tree(_item_id int)
  RETURNS jsonb
  LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT jsonb_agg(sub)
FROM  (
   SELECT i.*, f_item_tree(i.item_id) AS children
   FROM   joins j
   JOIN   items i ON i.item_id = j.child_id
   WHERE  j.item_id = _item_id
   ORDER  BY i.item_id
   ) sub
$func$;
Run Code Online (Sandbox Code Playgroud)

小提琴

裸调用:

SELECT to_jsonb(sub) AS tree
FROM  (
   SELECT *, f_item_tree(item_id) AS children
   FROM   items
   WHERE  item_id = 1  -- root item_id HERE
   ) sub;
Run Code Online (Sandbox Code Playgroud)

删除具有 NULL 值的对象(无子对象)并进行美化:

SELECT jsonb_pretty(jsonb_strip_nulls(to_jsonb(sub))) AS tree
FROM  (
   SELECT *, f_item_tree(item_id) AS children
   FROM   items
   WHERE  item_id = 1  -- root item_id HERE
   ) sub;
Run Code Online (Sandbox Code Playgroud)

准确地产生您想要的输出(一棵完整的树):

CREATE OR REPLACE FUNCTION f_item_tree(_item_id int)
  RETURNS jsonb
  LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT jsonb_agg(sub)
FROM  (
   SELECT i.*, f_item_tree(i.item_id) AS children
   FROM   joins j
   JOIN   items i ON i.item_id = j.child_id
   WHERE  j.item_id = _item_id
   ORDER  BY i.item_id
   ) sub
$func$;
Run Code Online (Sandbox Code Playgroud)

jsonb_strip_nulls()...

递归地从给定 JSON 值中删除具有 null 值的所有对象字段。

如果null您想保留其他字段的值,则必须执行更多操作。喜欢:

CREATE OR REPLACE FUNCTION f_item_tree(_item_id int)
  RETURNS jsonb
  LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT jsonb_agg(
          CASE WHEN children IS NULL
               THEN to_jsonb(sub) - 'children'
            -- THEN jsonb_build_object('title', title, 'item_id', item_id)  -- alt: spell out
               ELSE to_jsonb(sub) END
       )
FROM  (
   SELECT i.*, f_item_tree(i.item_id) AS children
   FROM   joins j
   JOIN   items i ON i.item_id = j.child_id
   WHERE  j.item_id = _item_id
   ORDER  BY i.item_id
   ) sub
$func$;
Run Code Online (Sandbox Code Playgroud)

小提琴

后来,与替代方案密切相关的答案(最明显的是最大递归级别):