web-dev-qa-db-ja.com

SQL Serverで、ローリングサムに負でない下限を設定する

ローリング合計の計算に下限を設定する必要があります。たとえば、

PKID    NumValue    GroupID
----------------------------
1       -1          1
2       -2          1
3       5           1
4       -7          1
5       1           2

私はを頂きたい:

PKID   RollingSum    GroupID
----------------------------- ## Explanation: 
1      0             1        ## 0 - 1 < 0  => 0
2      0             1        ## 0 - 2 < 0  => 0
3      5             1        ## 0 + 5 > 0  => 5
4      0             1        ## 5 - 7 < 0  => 0

負の数を追加すると合計が負になる場合、制限がアクティブになり、結果がゼロに設定されます。その後の加算は、元のローリング合計ではなく、この調整された値に基づく必要があります。

期待される結果は加算を使用して達成する必要があります。4番目の数値が-7から-3に変更される場合、4番目の結果は0ではなく2になります

いくつかのローリング数ではなく単一の合計を提供できる場合は、それも許容されます。ストアドプロシージャを使用して非負の加算を実装できますが、それでは低レベルすぎます。

これの実際の問題は、注文をプラスの金額として記録し、キャンセルをマイナスとして記録することです。接続の問題により、お客様はcancelボタンを2回以上クリックする場合があり、その結果、複数の負の値が記録されます。収益を計算する場合、「ゼロ」が売上の境界である必要があります。


このビジネスアプリケーションは完全に愚かですが、それについて私ができることは何もありません。この質問については、DBAが使用できるソリューションのみを検討してください。

GroupIDあたり最大50行を期待しています。

5
Ryan

これが私が思いついた再帰的なCTEの例です(これはうまくいくようです)。 IsはRow_Number()OVERを使用して、ギャップのないシーケンス番号を作成します。あなたのデータでそれがどれほどうまく機能するかはわかりませんが、それは試してみる価値があります。

--Set up demo data
IF OBJECT_ID('tempdb..#temp') IS NOT NULL
    drop table #temp
go
create table #temp (PKID int, NumValue int, GroupID int)
insert into #temp values
(1,-1,1), (3,-2,1), (5,5,1), (7,-3,1), (9,1,2)

--here is the real code
;
with RowNumberAddedToTemp as 
(
SELECT 
  ROW_NUMBER() OVER(ORDER BY PKID ASC) AS rn,
  * from #temp
  )
,x
AS (
    SELECT PKID         --Anchor row
        ,NumValue
        ,RunningTotal = CASE 
            WHEN NumValue < 0   --if initial value less than zero, make zero
                THEN 0
            ELSE NumValue
            END
        ,GroupID
        ,rn
    FROM RowNumberAddedToTemp
    WHERE rn = 1      

    UNION ALL

    SELECT y.PKID
        ,y.NumValue
        ,CASE 
            WHEN x.GroupID <> y.groupid     --did GroupId change?
                THEN CASE 
                        WHEN y.NumValue < 0     --if value is less than zero, make zero
                            THEN 0
                        ELSE y.numvalue         --start new groupid totals
                        END
            WHEN x.RunningTotal + y.NumValue < 0    --If adding the current row makes the total < 0, make zero
                THEN 0
            ELSE x.RunningTotal + y.NumValue        --Add to the running total for the current groupid
            END
        ,y.Groupid
        ,y.rn
    FROM x
    INNER JOIN RowNumberAddedToTemp AS y ON y.rn = x.rn + 1
    )
SELECT PKID
    ,Numvalue
    ,RunningTotal
    ,GroupID
FROM x
ORDER BY PKID
OPTION (MAXRECURSION 10000);
3
Scott Hodgin

以下は、Scott Hodginの answer に類似した再帰的なソリューションですが、インデックスをより効果的に利用できるはずです。データがどのように見えるかに応じて、パフォーマンスが向上します。 20000グループで100万行のデータをモックアップしました。それぞれに50行が関連付けられています。

_DROP TABLE IF EXISTS biz_application_problems;
CREATE TABLE dbo.biz_application_problems (
    PKID BIGINT NOT NULL,
    NumValue INT NOT NULL,
    GroupID INT NOT NULL,
    PRIMARY KEY (PKID)
);

INSERT INTO dbo.biz_application_problems WITH (TABLOCK)
SELECT TOP (1000000) 
  2 * ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, CAST(CRYPT_GEN_RANDOM(1) AS INT) % 22 - 11
, 1 + (-1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))) / 50
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE INDEX gotta_go_fast ON dbo.biz_application_problems (GroupID, PKID)
    INCLUDE (NumValue);
_

テーブルに追加した非クラスター化インデックスにより、CTEの再帰的な部分で、1回のインデックスシークで次の行を取得できます。また、PKに応じたテーブル内のデータの順序は関係ありません。ここでのもう1つのトリックは、ROW_NUMBER()を使用して効率的に次の行を取得することです。 CTEの再帰部分ではTOPを使用できないため、これが必要です。これがクエリです:

_WITH rec_cte AS (
    SELECT TOP 1
      GroupID
    , PKID
    , CASE WHEN NumValue < 0 THEN 0 ELSE NumValue END RunningSum
    , NumValue -- for validation purposes only
    FROM dbo.biz_application_problems
    ORDER BY GroupId, PKID

    UNION ALL

    SELECT t.GroupID
    , t.PKID
    , t.RunningSum
    , t.NumValue
    FROM 
    (
        SELECT 
          b.GroupID
        , b.PKID
        , CASE WHEN ca.RunningSum < 0 THEN 0 ELSE ca.RunningSum END RunningSum
        , b.NumValue
        , ROW_NUMBER() OVER (ORDER BY b.GroupId, b.PKID) rn
        FROM dbo.biz_application_problems b
        INNER JOIN rec_cte r ON
            (b.GroupID = r.GroupID AND b.PKID > r.PKID) OR b.GroupID > r.GroupID
        CROSS APPLY (
            SELECT CASE WHEN b.GroupID <> r.GroupID THEN 0 ELSE r.RunningSum END + b.NumValue
        ) ca (RunningSum)
    ) t
    WHERE t.rn = 1
)
SELECT *
FROM rec_cte
OPTION (MAXRECURSION 0);
_

結果のサンプルは次のとおりです。

_╔═════════╦══════╦════════════╦══════════╗
║ GroupID ║ PKID ║ RunningSum ║ NumValue ║
╠═════════╬══════╬════════════╬══════════╣
║       7 ║  700 ║         13 ║       -4 ║
║       8 ║  702 ║          0 ║       -2 ║
║       8 ║  704 ║          7 ║        7 ║
║       8 ║  706 ║          8 ║        1 ║
║       8 ║  708 ║          3 ║       -5 ║
║       8 ║  710 ║          0 ║       -4 ║
║       8 ║  712 ║          0 ║       -7 ║
║       8 ║  714 ║          7 ║        7 ║
║       8 ║  716 ║          2 ║       -5 ║
║       8 ║  718 ║         10 ║        8 ║
║       8 ║  720 ║          0 ║      -11 ║
║       8 ║  722 ║          0 ║       -7 ║
╚═════════╩══════╩════════════╩══════════╝
_

私のマシンでは、コードは100万行を処理するのに約10秒かかります。反復ごとに1行だけを返すインデックスシークを確認できます。

seeks

実際のプランをアップロードしました ここ も同様です。

1
Joe Obbish