自底向上递归 SUM(最低层只有值)

Bottom Up Recursive SUM (lowest level only has values)

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

这是示例 table 结构:

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 求和并将总和加到产品类别级别以获得累计 运行 总数。

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

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

查询的输出应如下所示:

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 开始,returns 如果该节点已经有卷,层次结构可以聚合卷,但似乎无法弄清楚如何从 SKU 级别开始并继续向上聚合层次结构。

这是我的 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

感谢任何帮助!

应该这样做:

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

基本上计算分三步进行:

  1. 通过连接 ID 以递归方式捕获每个后代的层次结构:CAST(c.Hierarchy + '|' + CAST(t.Id AS VARCHAR(MAX)) AS VARCHAR(MAX))

  2. 将生成的 table 与其自身相连接,以便每条记录与其自身及其所有后代相连接:FROM cte c1 LEFT JOIN cte c2 ON c2.Hierarchy LIKE c1.Hierarchy + '%'

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

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

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

之类的东西

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

相反,简单地使用 WHILE 循环 递归并插入临时 table 或 table 变量会更好,然后读回汇总

Use the OUTPUT clauses to view the intermediate results

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、利用两个 table 变量并在它们之间切换来避免它。