自底向上递归求和(最低级别只有值)

Posted

技术标签:

【中文标题】自底向上递归求和(最低级别只有值)【英文标题】:Bottom Up Recursive SUM (lowest level only has values) 【发布时间】:2021-11-30 12:50:18 【问题描述】:

我在 SQL Server 的产品层次结构中有一个基于树的 SKU 结构。最低级别的 SKU 永远只有值(这些是消费值)。然后我想在每个级别上生成层次结构的聚合。

这是示例表结构:

Id ParentId Name Volume IsSku
1 -1 All 0 0
2 1 Cat A 0 0
3 1 Cat B 0 0
4 2 Cat A.1 0 0
5 2 Cat A.2 0 0
6 3 Cat B.1 0 0
7 3 Cat B.2 0 0
8 4 SKU1 10 1
9 4 SKU2 5 1
10 5 SKU3 7 1
11 5 SKU4 4 1
12 6 SKU1 10 1
13 6 SKU2 5 1
14 7 SKU3 9 1
15 7 SKU4 7 1

我需要一个从 sku 级别 (IsSku=1) 开始的查询,然后进行处理,对 SKU 求和,并将产品类别级别的总和进行汇总,以获得累计运行总计。

我已经看到几个查询,其中在层次结构中存在递归求和,其中每个级别都已经有值,但我需要一个从具有值的最低级别开始并在向上移动时递归计算总和的查询。

我正在尝试这些,但它们看起来主要是对分层数据进行求和,其中每个节点已经有一个值(在我的例子中是卷)。我需要从最低级别开始,随着层次结构的上升,将聚合向上进行。我试图用我的数据来模拟这些帖子中的答案,但到目前为止我的数据设置并不成功。

24394601 29127163 11408878

查询的输出应该是这样的:

Id ParentId Name Volume IsSku
1 -1 All 54 0
2 1 Cat A 26 0
3 1 Cat B 28 0
4 2 Cat A.1 15 0
5 2 Cat A.2 11 0
6 3 Cat B.1 12 0
7 3 Cat B.2 16 0
8 4 SKU1 10 1
9 4 SKU2 5 1
10 5 SKU3 7 1
11 5 SKU4 4 1
12 6 SKU1 10 1
13 6 SKU2 2 1
14 7 SKU3 9 1
15 7 SKU4 7 1

我从递归 CTE 开始.

这是我的 CTE 的开始:

DECLARE @tblData TABLE
(
    [ID] INT NOT NULL,
    [ParentId] INT NULL,
    [Name] varchar(50) NOT NULL,
    [Volume] int NOT NULL,
    [IsSku] bit
)

INSERT INTO @tblData
VALUES 
 (1,-1,'All',0,0)
,(2,1,'Cat A',0,0)  
,(3,1,'Cat B',0,0)  
,(4,2,'Cat A.1',0,0)  
,(5,2,'Cat A.2',0,0)  
,(6,3,'Cat B.1',0,0)  
,(7,3,'Cat B.2',0,0)  
,(8,4,'SKU1',10,1)  
,(9,4,'SKU2',5,1)  
,(10,5,'SKU3',7,1)  
,(11,5,'SKU4',4,1)  
,(12,6,'SKU1',10,1)  
,(13,6,'SKU2',5,1)  
,(14,7,'SKU3',7,1)  
,(15,7,'SKU4',4,1)  

;WITH cte AS (   
    SELECT
        a.ID
        ,a.ParentID
        ,a.Name
        ,a.Volume
        ,CAST('/' + cast(ID as varchar) + '/' as varchar) Node
        ,0 AS level
        ,IsSku
    FROM @tblData AS a
    WHERE a.ParentID = -1

    UNION ALL

    SELECT
        b.ID
        ,b.ParentID
        ,b.Name
        ,b.Volume
        ,CAST(c.Node + CAST(b.ID as varchar) + '/' as varchar)
        ,level = c.level + 1
        ,b.IsSku
    FROM @tblData AS b  
    INNER JOIN cte c
        ON b.ParentId = c.ID
)

SELECT c1.ID, c1.ParentID, c1.Name, c1.Node
    ,ISNULL(SUM(c2.Volume),0)
FROM cte c1
LEFT OUTER JOIN cte c2
    ON c1.Node <> c2.Node
    AND LEFT(c2.Node, LEN(c1.Node)) = c1.Node
GROUP BY c1.ID, c1.ParentID, c1.Name, c1.Node

感谢任何帮助!

【问题讨论】:

你能包括你目前的查询吗? 我不太明白预期的结果:例如,ID=15 在样本中有Volume=4,但在输出中有Volume=7 @Charlieface,我的错。我修复了源值。 @Stu,我用我的原始起始数据、cte 和查询进行了更新。 @DaleK,感谢您的反馈。我添加了一些我看过的其他帖子来尝试模仿。 【参考方案1】:

应该这样做:

DECLARE @tbl TABLE(Id INT, ParentId INT, Name NVARCHAR(255), Volume INTEGER, IsSku BIT)
 
INSERT INTO @tbl
VALUES 
 (1,-1,'All',0,0)
,(2,1,'Cat A',0,0)  
,(3,1,'Cat B',0,0)  
,(4,2,'Cat A.1',0,0)  
,(5,2,'Cat A.2',0,0)  
,(6,3,'Cat B.1',0,0)  
,(7,3,'Cat B.2',0,0)  
,(8,4,'SKU1',10,1)  
,(9,4,'SKU2',5,1)  
,(10,5,'SKU3',7,1)  
,(11,5,'SKU4',4,1)  
,(12,6,'SKU1',10,1)  
,(13,6,'SKU2',5,1)  
,(14,7,'SKU3',7,1)  
,(15,7,'SKU4',4,1)  
SELECT * FROM @tbl
;

WITH cte AS (
    SELECT       
        Id,ParentId, Name, Volume, IsSku, CAST(Id AS VARCHAR(MAX)) AS Hierarchy
    FROM       
        @tbl
    WHERE ParentId=-1
    UNION ALL
    SELECT 
        t.Id,t.ParentId, t.Name, t.Volume, t.IsSku, CAST(c.Hierarchy + '|' + CAST(t.Id AS VARCHAR(MAX)) AS VARCHAR(MAX)) 
    FROM 
        cte c 
        INNER JOIN @tbl t
            ON c.Id = t.ParentId

)
SELECT Id,ParentId, Name, ChildVolume AS Volume, IsSku
FROM (
    SELECT c1.Id, c1.ParentId, c1.Name, c1. Volume, c1.IsSku, SUM(c2.Volume) AS ChildVolume
    FROM cte c1
        LEFT JOIN cte c2 ON c2.Hierarchy LIKE c1.Hierarchy + '%'
    GROUP BY c1.Id, c1.ParentId, c1.Name, c1. Volume, c1.IsSku
) x

基本上,计算分三个步骤进行:

    通过连接 Id 递归地为每个后代捕获层次结构:CAST(c.Hierarchy + '|' + CAST(t.Id AS VARCHAR(MAX)) AS VARCHAR(MAX))

    将结果表与其自身连接,这样每条记录都与其自身及其所有后代连接:FROM cte c1 LEFT JOIN cte c2 ON c2.Hierarchy LIKE c1.Hierarchy + '%'

    最后通过分组聚合每个层次的Volume:SUM(c2.Volume) AS ChildVolume

这里参考了 Ed Harper 对类似问题的回答:Hierarchy based aggregation

【讨论】:

太棒了!我有正确的 cte 并以类似的方式将层次结构捕获到一列中,但试图找出您在最终查询中的下一部分。谢谢!【参考方案2】:

由于递归 CTE 在 SQL Server 中的工作方式,很难让这种逻辑有效地工作。它通常要么需要自连接整个结果集,要么使用 JSON 或 XML 之类的东西。

问题在于,在 CTE 的每次递归中,虽然它看起来您正在同时处理整个集合,但它实际上一次只反馈一行。因此,在递归中不允许分组。

相反,最好简单地使用WHILE 循环进行递归并插入到临时表或表变量中,然后将其读回聚合

使用OUTPUT子句查看中间结果

DECLARE @tmp TABLE (
  Id INTEGER,
  ParentId INTEGER,
  Name VARCHAR(7),
  Volume INTEGER,
  IsSku INTEGER,
  Level INT,
  INDEX ix CLUSTERED (Level, ParentId, Id)
);

INSERT INTO @tmp
  (Id, ParentId, Name, Volume, IsSku, Level)
-- OUTPUT inserted.Id, inserted.ParentId, inserted.Name, inserted.Volume, inserted.IsSku, inserted.Level
SELECT
  p.Id,
  p.ParentId,
  p.Name,
  p.Volume,
  p.IsSku,
  1
FROM Product p
WHERE p.IsSku = 1;

DECLARE @level int = 1;
WHILE (1=1)
BEGIN
    INSERT INTO @tmp
      (Id, ParentId, Name, Volume, IsSku, Level)
    -- OUTPUT inserted.Id, inserted.ParentId, inserted.Name, inserted.Volume, inserted.IsSku, inserted.Level
    SELECT
      p.Id,
      p.ParentId,
      p.Name,
      t.Volume,
      p.IsSku,
      @level + 1
    FROM (
        SELECT
          t.ParentID,
          Volume = SUM(t.Volume)
        FROM @tmp t
        WHERE t.Level = @level
        GROUP BY
          t.ParentID
    ) t
    JOIN Product p ON p.Id = t.ParentID;

    IF (@@ROWCOUNT = 0)
        BREAK;
        
    SET @level += 1;
END;

SELECT *
FROM @tmp
ORDER BY Id;

db<>fiddle

由于万圣节保护,此解决方案确实涉及阻塞运算符(在我的情况下,我看到了“不必要的”排序)。您可以通过使用Itzik Ben-Gan's Divide and Conquer method 来避免它,利用两个表变量并在它们之间进行翻转。

【讨论】:

“相反,用 WHILE 循环简单地递归要好得多”——我从没想过我会听到你说这些话 :) 是的,SQL Server 不能很好地进行递归。另请参阅 Dwain Camps 的这篇关于性能特征的优秀文章 red-gate.com/simple-talk/databases/sql-server/… 哦,我完全理解它在某些情况下是最适合这项工作的工具 :) 考虑到我们经常评论“使用基于集合的操作”,这很有趣,哈哈 奇怪的是,rCTE 实际上比这更少基于集合,因为它一次只工作一行。请注意我如何一次插入一个完整级别,并且表变量已正确索引。 rCTE 还在内部使用表变量,只是效率较低。 那将是因为您一次处理一行,而这一次处理整个级别。它实际上不会在现实生活场景中循环那么多次

以上是关于自底向上递归求和(最低级别只有值)的主要内容,如果未能解决你的问题,请参考以下文章

编译器实现

自顶向下和自底向上编程

自顶向下和自底向上的估算方式

自底向上的归并排序算法

S5-自底向上的语法分析

干货好文!自底向上——知识图谱构建技术初探