sql - 如何将具有树结构的表聚合到单个嵌套 JSON 对象?

标签 sql json postgresql recursive-query typeorm

我在 Postgres 11.4 数据库中有一个具有自引用树结构的表:

+------------+
| account    |  
+------------+
| id         |
| code       | 
| type       |
| parentId   | -- references account.id
+------------+

每个 child 可以有另一个 child ,嵌套级别没有限制。

我想从中生成一个 JSON 对象,嵌套所有子对象(resurivly)。

是否可以通过单个查询来解决这个问题?
或者任何其他使用 typeORM 和一张 table 的解决方案?
否则我将不得不在服务器端手动绑定(bind)数据。

我试过这个查询:
SELECT account.type, json_agg(account) as accounts
FROM account
-- LEFT JOIN "account" "child" ON "child"."parentId"="account"."id" -- tried to make one column child
GROUP BY account.type   

结果:
[
  ...
  {
    "type": "type03",
    "accounts": [
      {
        "id": 28,
        "code": "acc03.001",
        "type": "type03",
        "parentId": null
      },
      {
        "id": 29,
        "code": "acc03.001.001",
        "type": "type03",
        "parentId": 28
      },
      {
        "id": 30,
        "code": "acc03.001.002",
        "type": "type03",
        "parentId": 28
      }
    ]
  }
  ...
]

我希望这样:
[
  ...
  {
    "type": "type03",
    "accounts": [
      {
        "id": 28,
        "code": "acc03.001",
        "type": "type03",
        "parentId": null,
        "child": [
          {
            "id": 29,
            "code": "acc03.001.001",
            "type": "type03",
            "parentId": 28
          },
          {
            "id": 30,
            "code": "acc03.001.002",
            "type": "type03",
            "parentId": 28
          }
        ]
      }
    ]
  }
  ...
]

最佳答案

这是棘手 .
这是一个递归问题,但标准 recursive CTEs没有能力处理它,因为我们需要在每个级别上进行聚合,而 CTE 不允许在递归项中进行聚合。
我用 PL/pgSQL 函数解决了这个问题:

CREATE OR REPLACE FUNCTION f_build_jsonb_tree(_type text = NULL)
  RETURNS jsonb
  LANGUAGE plpgsql AS
$func$
DECLARE
   _nest_lvl int;

BEGIN
   -- add level of nesting recursively
   CREATE TEMP TABLE t ON COMMIT DROP AS
   WITH RECURSIVE t AS (
      SELECT *, 1 AS lvl
      FROM   account
      WHERE  "parentId" IS NULL
      AND   (type = _type OR _type IS NULL) -- default: whole table

      UNION ALL
      SELECT a.*, lvl + 1
      FROM   t
      JOIN   account a ON a."parentId" = t.id
      )
   TABLE t;
   
   -- optional idx for big tables with many levels of nesting
   -- CREATE INDEX ON t (lvl, id);

   _nest_lvl := (SELECT max(lvl) FROM t);

   -- no nesting found, return simple result
   IF _nest_lvl = 1 THEN 
      RETURN (  -- exits functions
      SELECT jsonb_agg(sub) -- AS result
      FROM  (
         SELECT type
              , jsonb_agg(sub) AS accounts
         FROM  (
            SELECT id, code, type, "parentId", NULL AS children
            FROM   t
            ORDER  BY type, id
            ) sub
         GROUP BY 1
         ) sub
      );
   END IF;

   -- start collapsing with leaves at highest level
   CREATE TEMP TABLE j ON COMMIT DROP AS
   SELECT "parentId" AS id
        , jsonb_agg (sub) AS children
   FROM  (
      SELECT id, code, type, "parentId"  -- type redundant?
      FROM   t
      WHERE  lvl = _nest_lvl
      ORDER  BY id
      ) sub
   GROUP  BY "parentId";

   -- optional idx for big tables with many levels of nesting
   -- CREATE INDEX ON j (id);

   -- iterate all the way down to lvl 2
   -- write to same table; ID is enough to identify
   WHILE _nest_lvl > 2
   LOOP
      _nest_lvl := _nest_lvl - 1;

      INSERT INTO j(id, children)
      SELECT "parentId"     -- AS id
           , jsonb_agg(sub) -- AS children
      FROM  (
         SELECT id, t.code, t.type, "parentId", j.children  -- type redundant?
         FROM   t
         LEFT   JOIN j USING (id)  -- may or may not have children
         WHERE  t.lvl = _nest_lvl
         ORDER  BY id
         ) sub
      GROUP  BY "parentId";
   END LOOP;

   -- nesting found, return nested result
   RETURN ( -- exits functions
   SELECT jsonb_agg(sub) -- AS result
   FROM  (
      SELECT type
           , jsonb_agg (sub) AS accounts
      FROM  (
         SELECT id, code, type, "parentId", j.children
         FROM   t
         LEFT   JOIN j USING (id)
         WHERE  t.lvl = 1
         ORDER  BY type, id
         ) sub
      GROUP  BY 1
      ) sub
   );
END
$func$;
调用(准确返回所需结果):
SELECT jsonb_pretty(f_build_jsonb_tree());
db<> fiddle here - 带有扩展测试用例
我选择了键名 children而不是 child ,因为可以嵌套多个。
jsonb_pretty() 美化显示是可选的。
这是假设参照完整性;应该使用 FK 约束来实现。
对于您的特定情况,使用 code 的解决方案可能会更简单。列 - 如果它表现出(未公开)有用的特性。就像我们可能会在没有 rCTE 的情况下导出嵌套级别并添加临时表 t .但我的目标是 一般解决方案仅基于 ID 引用。
函数中发生了很多事情。我添加了内联注释。基本上,它这样做:
  • 创建一个添加了嵌套级别 (lvl) 的临时表
  • 如果没有找到嵌套,返回简单结果
  • 如果找到嵌套,折叠到 jsonb从顶层嵌套层往下。
    将所有中间结果写入第二个临时表 j .
  • 一旦我们到达第二个嵌套级别,返回完整的结果。

  • 该函数采用_type作为参数只返回给定的类型。否则,将处理整个表。
    另外:避免使用大小写混合的标识符,如 "parentId"如果可能的话,在 Postgres 中。看:
  • Are PostgreSQL column names case-sensitive?

  • 稍后使用 进行相关回答递归函数 :
  • How to turn a set of flat trees into a single tree with multiple leaves?
  • 关于sql - 如何将具有树结构的表聚合到单个嵌套 JSON 对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62280978/

    相关文章:

    django - 如何在 django ORM 中按年和月对事件进行分组?

    postgresql - 看不到 Simple.Data 跟踪消息

    sql - 进行安全的 sql 查询

    SQL 仅选择缺失和更新的行

    mysql - 将 AVG 与 HAVING 一起使用

    javascript - Jasmine StubRequest 未发送传递的字符串

    json - 使用 spark-submit 从 google dataproc spark cluster 读取 GCP 中的 JSON(zipped .gz) 时,未使用所有执行程序

    java - java中JSON字符串到JSON对象

    python - 在 Python 中创建 Postgres 函数

    sql - 如何设计数据库模式以支持类别标记?