如何递归地对 SQL 中的所有子树求和?

How to SUM all subchildren tree in SQL recursively?

你好,我在这个问题上纠结了一段时间><

我在树结构中有 4 个类别

tenant_category_transaction_view:

我想要每个类别children“sumSubtotal”的总和

类似的东西:

我已经非常接近了...但是有些东西我不明白 >><"

with recursive cte (sumSubtotal, sumQuantity, id, idParentCategory, treeSum, depth) as (

        select  root.sumSubtotal, -- STEP 1
                root.sumQuantity, 
                root.id, 
                root.idParentCategory, 
                root.sumSubtotal as treeSum,
                0 as depth
        from    tenant_category_transaction_view as root

        union all -- LOOP THROUGH ALL ROOT ROWS AND ADD ROWS TO THE CTE WITH THE INNER JOIN

        select  child.sumSubtotal, -- STEP 3
                child.sumQuantity, 
                child.id, 
                child.idParentCategory, 
                (cte.treeSum + child.sumSubtotal) AS treeSum,
                (cte.depth + 1) AS depth
        from    tenant_category_transaction_view AS child

        inner join cte on child.idParentCategory = cte.id -- STEP 2
)
select sumSubtotal, sumQuantity, id, idParentCategory, treeSum, depth -- STEP 4
from cte

以上查询结果:

我似乎生成了正确的 treeSum 但只有一个分支颠倒了

你能帮我一把吗?

感谢您的宝贵时间:)

我更新了 fiddle 以包含问题中提供的确切架构/数据,包括空问题。它还包括我建议的更改示例。

该解决方案基本上采用给定数据并在内部对其进行转换(在 CTE 术语 nodes 中),以便 2 个顶级类别行 link 成为一个公共行,id 为 0,因此我提供的原始逻辑可用于将其视为一个类别的分层列表。

首先,我们递归地找到所有分支列表。每个分支都由相应的根标识。然后,我们汇总每个 root/branch.

的节点数量

The fiddle

WITH RECURSIVE nodes AS (
         SELECT id, COALESCE(idParentCategory, 0) AS idParentCategory
              , sumSubtotal, sumQuantity
           FROM tenant_category_transaction_view
          UNION
         SELECT 0, null, 0, 0
     )
   , cte AS (
        SELECT t.*, t.id AS root
             , idParentCategory AS idParentCategory0
             , sumSubtotal      AS sumSubtotal0
             , sumQuantity      AS sumQuantity0
          FROM nodes AS t
         UNION ALL
        SELECT t.* , t0.root
             , t0.idParentCategory0
             , t0.sumSubtotal0
             , t0.sumQuantity0
          FROM cte AS t0
          JOIN nodes AS t
            ON t.idParentCategory = t0.id
     )
SELECT root
     , MIN(idParentCategory0)   AS idParentCategory
     , MIN(sumSubtotal0)        AS sumSubtotal
     , MIN(sumQuantity0)        AS sumQuantity
     , SUM(t1.sumSubtotal)      AS total
  FROM cte AS t1
 GROUP BY root
 ORDER BY root
;

结果:

root idParentCategory sumSubtotal sumQuantity total
0 null 0 0 9890
1 0 9800 98 9800
4 0 20 1 90
5 4 30 1 70
6 5 40 1 40

设置:

CREATE TABLE tenant_category_transaction_view (
    id               int primary key
  , idParentCategory int
  , sumSubtotal      int
  , sumQuantity      int
);

INSERT INTO tenant_category_transaction_view VALUES
    (1, null, 9800, 98)
  , (4, null,   20,  1)
  , (5,    4,   30,  1)
  , (6,    5,   40,  1)
;

以下使用建议对原table和数据进行微调。而不是 id 为 1 和 4 的行的 2 个顶部空父引用,添加一个顶行(例如,id 为 99)并让 id 为 2 和 4 的行引用 parent = 99.

WITH RECURSIVE cte AS (
        SELECT t.*, t.id AS root
          FROM tenant_category_transaction_view AS t
         UNION ALL
        SELECT t.*, t0.root
          FROM cte AS t0
          JOIN tenant_category_transaction_view AS t
            ON t.idParentCategory = t0.id
     )
SELECT root
     , MIN(t2.idParentCategory) AS idParentCategory
     , MIN(t2.sumSubtotal)      AS sumSubtotal
     , MIN(t2.sumQuantity)      AS sumQuantity
     , SUM(t1.sumSubtotal)      AS total
  FROM cte AS t1
  JOIN tenant_category_transaction_view AS t2
    ON t1.root = t2.id
 GROUP BY root
 ORDER BY root
;

结果:

root idParentCategory sumSubtotal sumQuantity total
99 null 0 0 9890
1 99 9800 98 9800
4 99 20 1 90
5 4 30 1 70
6 5 40 1 40

此外,这可以写成基于 t2.id 的聚合,这是主键,由于功能依赖,允许稍微简化。

WITH RECURSIVE cte AS (
        SELECT t.*, t.id AS root
          FROM tenant_category_transaction_view AS t
         UNION ALL
        SELECT t.*, t0.root
          FROM cte AS t0
          JOIN tenant_category_transaction_view AS t
            ON t.idParentCategory = t0.id
     )
SELECT t2.id
     , t2.idParentCategory
     , t2.sumSubtotal
     , t2.sumQuantity
     , SUM(t1.sumSubtotal)      AS total
  FROM cte AS t1
  JOIN tenant_category_transaction_view AS t2
    ON t1.root = t2.id
 GROUP BY t2.id
 ORDER BY t2.id
;

最后,我们可以通过在递归逻辑中携带其他根值来删除最后一个 JOIN:

WITH RECURSIVE cte AS (
        SELECT t.*, t.id AS root
             , idParentCategory AS idParentCategory0
             , sumSubtotal      AS sumSubtotal0
             , sumQuantity      AS sumQuantity0
          FROM tenant_category_transaction_view AS t
         UNION ALL
        SELECT t.* , t0.root
             , t0.idParentCategory0
             , t0.sumSubtotal0
             , t0.sumQuantity0
          FROM cte AS t0
          JOIN tenant_category_transaction_view AS t
            ON t.idParentCategory = t0.id
     )
SELECT root
     , MIN(idParentCategory0)   AS idParentCategory
     , MIN(sumSubtotal0)        AS sumSubtotal
     , MIN(sumQuantity0)        AS sumQuantity
     , SUM(t1.sumSubtotal)      AS total
  FROM cte AS t1
 GROUP BY root
 ORDER BY root
;

设置:

DROP TABLE IF EXISTS tenant_category_transaction_view;
CREATE TABLE tenant_category_transaction_view (
    id               int primary key
  , idParentCategory int
  , sumSubtotal      int
  , sumQuantity      int
);

INSERT INTO tenant_category_transaction_view VALUES
    (99, null,    0,  0)
  , ( 1,   99, 9800, 98)
  , ( 4,   99,   20,  1)
  , ( 5,    4,   30,  1)
  , ( 6,    5,   40,  1)
;

解决方法其实很简单:-)

  1. 在 CTE 中添加代表根节点的 hard-coded 行(其中 id 为 NULL)。请注意,我们可以在 CTE 中使用多个锚点查询(不引用 CTE 名称的查询)。
  2. 创建从所有节点到它们所有后代(包括它们自己)的所有路径。每条路径由一行表示,其中包含起始节点 ID、后代节点 ID 和后代节点的数量。请注意 null-safe 等于运算符 (<=>)
  3. 的使用
  4. 按起始节点 id 聚合,并为每个节点汇总其所有后代的数量

P.S.
“递归”查询没有任何递归。这是一个误导性的名称。这些实际上是迭代查询,而不是递归。


with recursive cte (root_id, id, sumSubtotal)  as 
(
  select     null
            ,null
            ,0
  
  union all
  
  select     id
            ,id
            ,sumSubtotal

  from      tenant_category_transaction_view as tctv
  
  union all
  
  select     cte.root_id
            ,tctv.id
            ,tctv.sumSubtotal

  from      cte 
            join    tenant_category_transaction_view as tctv 
            on      tctv.idParentCategory <=> cte.id
)
select   root_id
        ,sum(sumSubtotal)
from     cte
group by root_id

+---------+-------------+
| root_id | sumSubtotal |
+---------+-------------+
| null    |        9890 |
| 1       |        9800 |
| 4       |          90 |
| 5       |          70 |
| 6       |          40 |
+---------+-------------+

fiddle