递归查询中的分组结果(SQL 服务器)

Grouped result in recursive query (SQL Server)

我有一个递归查询,它按预期工作,用于计算库存计算的加权平均成本。我的问题是我需要来自按不同列分组的同一查询的多个加权平均值。我知道我可以通过多次计算来解决这个问题,每个键列计算一次。但是出于查询性能的考虑,希望遍历一次。有时我有 1M+ 行。

我简化了数据并将加权平均值替换为简单的总和,以使我的问题更容易理解。

如何使用递归 cte 得到下面的结果?请记住,我必须使用递归查询来计算加权平均成本。我在 sql 服务器 2016.

示例数据(Id也是排序顺序,Id和Key是唯一的。)

Id  Key1  Key2  Key3  Value
1   1     1     1     10
2   1     1     1     10
3   1     2     1     10
4   2     2     1     10
5   1     2     1     10
6   1     1     2     10
7   1     1     1     10
8   3     3     1     10

预期结果

Id  Key1  Key2  Key3  Value  Key1Sum  Key2Sum  Key3Sum
1   1     1     1     10     10       10       10
2   1     1     1     10     20       20       20
3   1     2     1     10     30       10       30
4   2     2     1     10     10       20       40
5   1     2     1     10     40       30       50
6   1     1     2     10     50       30       10
7   1     1     1     10     60       40       60
8   3     3     1     10     10       10       70

编辑

经过一些当之无愧的批评后,我必须更好地提出问题。

这是一个示例以及为什么我需要递归查询。在示例中,我得到了 Key1 的结果,但我在同一个查询中也需要 Key2 和 Key3 的结果。我知道我可以将同一个查询重复三次,但这是不可取的。

DECLARE @InventoryItem AS TABLE (
    IntentoryItemId INT NULL,
    InventoryOrder INT,
    Key1 INT NULL,
    Key2 INT NULL,
    Key3 INT NULL,
    Quantity NUMERIC(22,9) NOT NULL,
    Price NUMERIC(16,9) NOT NULL
);

INSERT INTO @InventoryItem (
    IntentoryItemId,
    InventoryOrder,
    Key1,
    Key2,
    Key3,
    Quantity,
    Price
)
VALUES 
(1, NULL, 1, 1, 1, 10, 1),
(2, NULL, 1, 1, 1, 10, 2),
(3, NULL, 1, 2, 1, 10, 2),
(4, NULL, 2, 2, 1, 10, 1),
(5, NULL, 1, 2, 1, 10, 5),
(6, NULL, 1, 1, 2, 10, 3),
(7, NULL, 1, 1, 1, 10, 3),
(8, NULL, 3, 3, 1, 10, 1);


--The steps below will give me the cost "grouped" by Key1
WITH Key1RowNumber AS (
    SELECT 
        IntentoryItemId,
        ROW_NUMBER() OVER (PARTITION BY Key1 ORDER BY IntentoryItemId) AS RowNumber
    FROM @InventoryItem
)

UPDATE @InventoryItem
    SET InventoryOrder = Key1RowNumber.RowNumber
FROM @InventoryItem InventoryItem
INNER JOIN Key1RowNumber
ON Key1RowNumber.IntentoryItemId = InventoryItem.IntentoryItemId;

WITH cte AS (
    SELECT  
        IntentoryItemId,
        InventoryOrder,
        Key1,
        Quantity,
        Price,
        CONVERT(NUMERIC(22,9), InventoryItem.Quantity) AS CurrentQuantity,
        CONVERT(NUMERIC(22,9), (InventoryItem.Quantity * InventoryItem.Price) / NULLIF(InventoryItem.Quantity, 0)) AS AvgPrice
    FROM @InventoryItem InventoryItem
    WHERE InventoryItem.InventoryOrder = 1
    UNION ALL
    SELECT 
        Sub.IntentoryItemId,
        Sub.InventoryOrder,
        Sub.Key1,
        Sub.Quantity,
        Sub.Price,
        CONVERT(NUMERIC(22,9), Main.CurrentQuantity + Sub.Quantity) AS CurrentQuantity,
        CONVERT(NUMERIC(22,9),
                ((Main.CurrentQuantity) * Main.AvgPrice + Sub.Quantity * Sub.price)
                    / 
                NULLIF((Main.CurrentQuantity)  + Sub.Quantity, 0) 
        ) AS AvgPrice
    FROM CTE Main
    INNER JOIN @InventoryItem Sub 
    ON Main.Key1 = Sub.Key1
    AND Sub.InventoryOrder = main.InventoryOrder + 1
)

SELECT cte.IntentoryItemId, cte.AvgPrice
FROM cte
ORDER BY IntentoryItemId

以下是在 SQL Server 2012 及更高版本中的操作方法...

IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL 
DROP TABLE #TestData;

CREATE TABLE #TestData (
    Id INT,
    Key1 INT,
    Key2 INT,  
    Key3 INT,
    [Value] INT 
    );
INSERT #TestData(Id, Key1, Key2, Key3, Value) VALUES
    (1, 1, 1, 1, 10), 
    (2, 1, 1, 1, 10), 
    (3, 1, 2, 1, 10), 
    (4, 2, 2, 1, 10), 
    (5, 1, 2, 1, 10), 
    (6, 1, 1, 2, 10), 
    (7, 1, 1, 1, 10), 
    (8, 3, 3, 1, 10);

--=============================================================

SELECT 
    td.Id, td.Key1, td.Key2, td.Key3, td.Value,
    Key1Sum = SUM(td.[Value]) OVER (PARTITION BY td.Key1 ORDER BY td.Id ROWS UNBOUNDED PRECEDING),
    Key2Sum = SUM(td.[Value]) OVER (PARTITION BY td.Key2 ORDER BY td.Id ROWS UNBOUNDED PRECEDING),
    Key3Sum = SUM(td.[Value]) OVER (PARTITION BY td.Key3 ORDER BY td.Id ROWS UNBOUNDED PRECEDING)
FROM
    #TestData td
ORDER BY
    td.Id;

结果...

Id          Key1        Key2        Key3        Value       Key1Sum     Key2Sum     Key3Sum
----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
1           1           1           1           10          10          10          10
2           1           1           1           10          20          20          20
3           1           2           1           10          30          10          30
4           2           2           1           10          10          20          40
5           1           2           1           10          40          30          50
6           1           1           2           10          50          30          10
7           1           1           1           10          60          40          60
8           3           3           1           10          10          10          70

Why you will want to calculate on 1M+ rows ?

其次,我认为您的数据库设计有误? key1 ,key2,key3 应该是 unpivoted 和一个名为 Keys 的列和另外 1 个列来标识每个密钥组。

下面的例子你就一目了然了。

如果我能够优化我的查询,那么我可以考虑计算很多行,否则我会尝试限制行数。

此外,如果可能,您可以考虑保留 Avg Price.i.e 的计算列。当 table 被填充时,您可以计算并存储它。

首先让我们知道输出是否正确。

DECLARE @InventoryItem AS TABLE (
    IntentoryItemId INT NULL,
    InventoryOrder INT,
    Key1 INT NULL,
    Key2 INT NULL,
    Key3 INT NULL,
    Quantity NUMERIC(22,9) NOT NULL,
    Price NUMERIC(16,9) NOT NULL
);

INSERT INTO @InventoryItem (
    IntentoryItemId,
    InventoryOrder,
    Key1,
    Key2,
    Key3,
    Quantity,
    Price
)
VALUES 
(1, NULL, 1, 1, 1, 10, 1),
(2, NULL, 1, 1, 1, 10, 2),
(3, NULL, 1, 2, 1, 10, 2),
(4, NULL, 2, 2, 1, 10, 1),
(5, NULL, 1, 2, 1, 10, 5),
(6, NULL, 1, 1, 2, 10, 3),
(7, NULL, 1, 1, 1, 10, 3),
(8, NULL, 3, 3, 1, 10, 1);
--select * from @InventoryItem
--return    
;with cte as
(
select * 
, ROW_NUMBER() OVER (PARTITION BY Key1 ORDER BY IntentoryItemId) AS rn1
, ROW_NUMBER() OVER (PARTITION BY Key2 ORDER BY IntentoryItemId) AS rn2
, ROW_NUMBER() OVER (PARTITION BY Key3 ORDER BY IntentoryItemId) AS rn3
from @InventoryItem
)
,cte1 AS (
       SELECT  
        IntentoryItemId,

        Key1 keys,
        Quantity,
        Price
        ,rn1 
        ,rn1 rn
        ,1 pk
    FROM cte c

   union ALL

   SELECT  
        IntentoryItemId,

        Key2 keys,
        Quantity,
        Price
        ,rn1 
        ,rn2 rn
        ,2 pk
    FROM cte c

     union ALL

   SELECT  
        IntentoryItemId,

        Key3 keys,
        Quantity,
        Price
        ,rn1 
        ,rn3 rn
        ,3 pk
    FROM cte c

)

, cte2 AS (
    SELECT  
        IntentoryItemId,
       rn,
        Keys,
        Quantity,
        Price,
        CONVERT(NUMERIC(22,9), InventoryItem.Quantity) AS CurrentQuantity,
         CONVERT(NUMERIC(22,9), (InventoryItem.Quantity * InventoryItem.Price)) a,
          CONVERT(NUMERIC(22,9),  InventoryItem.Price) b,

        CONVERT(NUMERIC(22,9), (InventoryItem.Quantity * InventoryItem.Price) / NULLIF(InventoryItem.Quantity, 0)) AS AvgPrice
        ,pk
    FROM cte1 InventoryItem
    WHERE InventoryItem.rn = 1
    UNION ALL
    SELECT 
        Sub.IntentoryItemId,
       sub.rn,
        Sub.Keys,
        Sub.Quantity,
        Sub.Price,
        CONVERT(NUMERIC(22,9), Main.CurrentQuantity + Sub.Quantity) AS CurrentQuantity,
         CONVERT(NUMERIC(22,9),Main.CurrentQuantity * Main.AvgPrice),
         CONVERT(NUMERIC(22,9),Sub.Quantity * Sub.price),

        CONVERT(NUMERIC(22,9),
                ((Main.CurrentQuantity * Main.AvgPrice) + (Sub.Quantity * Sub.price))
                    / 
                NULLIF(((Main.CurrentQuantity)  + Sub.Quantity), 0) 
        ) AS AvgPrice
        ,sub.pk
    FROM CTE2 Main
    INNER JOIN cte1 Sub 
    ON Main.Keys = Sub.Keys and main.pk=sub.pk
    AND Sub.rn = main.rn + 1
    --and Sub.InventoryOrder<=2
)
select * 
,(select AvgPrice from cte2 c1 where pk=2 and c1.IntentoryItemId=c.IntentoryItemId ) AvgPrice2
,(select AvgPrice from cte2 c1 where pk=2 and c1.IntentoryItemId=c.IntentoryItemId ) AvgPrice3
from cte2 c

where pk=1
ORDER BY pk,rn

Alternate Solution (for Sql 2012+) and many thanks to Jason,

SELECT *
,CONVERT(NUMERIC(22,9),avg((Quantity * Price) / NULLIF(Quantity, 0)) 
OVER(PARTITION BY Key1 ORDER by IntentoryItemId ROWS UNBOUNDED PRECEDING))AvgKey1Price
,CONVERT(NUMERIC(22,9),avg((Quantity * Price) / NULLIF(Quantity, 0)) 
OVER(PARTITION BY Key2 ORDER by IntentoryItemId ROWS UNBOUNDED PRECEDING))AvgKey2Price
,CONVERT(NUMERIC(22,9),avg((Quantity * Price) / NULLIF(Quantity, 0)) 
OVER(PARTITION BY Key3 ORDER by IntentoryItemId ROWS UNBOUNDED PRECEDING))AvgKey3Price
from @InventoryItem
order by IntentoryItemId