これは本当に楽しい質問です(SQL Serverに質問されます) そして、それをPostgreSQLでどのように実行されたかを確認するために試してみたかったです。他の誰かがもっとうまくできるかどうか見てみましょう。このデータを取って、
CREATE TABLE foo
AS
SELECT pkid::int, numvalue::int, groupid::int
FROM ( VALUES
( 1, -1 , 1 ),
( 2, -2 , 1 ),
( 3, 5 , 1 ),
( 4, -7 , 1 ),
( 5, 1 , 2 )
) AS t(pkid, numvalue, groupid);
これを生成しようとしています:
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回以上クリックする場合があり、その結果、複数の負の値が記録されます。収益を計算する場合、「ゼロ」が売上の境界である必要があります。
彼らのソリューションはすべて再帰を使用しています。
これは、ネストされたOLAP関数を使用してTeradataで同様の問題を解決した方法です。
SELECT dt.*,
-- find the lowest previous CumSum < 0
-- and adjust the current CumSum to zero
Max(CASE WHEN CumSum < 0 THEN -CumSum ELSE 0 end)
Over (PARTITION BY groupid
ORDER BY pkid
ROWS Unbounded Preceding)
+ CumSum AS AdjustedSum
FROM
(
SELECT pkid, numvalue, groupid,
-- calculate a standard cumulative sum
Sum(numvalue)
Over (PARTITION BY groupid
ORDER BY pkid
ROWS Unbounded Preceding) AS CumSum
FROM foo
) AS dt
CREATE FUNCTION
を使用して、数値を追加する関数int_add_pos_or_zero
を作成しますが、数値が0未満の場合は0を返します。
CREATE FUNCTION int_add_pos_or_zero(int, int)
RETURNS int
AS $$
BEGIN
RETURN greatest($1 + $2, 0);
END;
$$
LANGUAGE plpgsql
IMMUTABLE;
ここで CREATE AGGREGATE
を実行して、ウィンドウ関数で実行できるようにします。 INITCOND
を=0
に設定します。
CREATE AGGREGATE add_pos_or_zero(int) (
SFUNC = int_add_pos_or_zero,
STYPE = int,
INITCOND = 0
);
これで、他のようにクエリを実行します Window Function。
SELECT pkid,
groupid,
numvalue,
add_pos_or_zero(numvalue) OVER (PARTITION BY groupid ORDER BY pkid)
FROM foo;
pkid | groupid | numvalue | add_pos_or_zero
------+---------+----------+-----------------
1 | 1 | -1 | 0
2 | 1 | -2 | 0
3 | 1 | 5 | 5
4 | 1 | -7 | 0
5 | 2 | 1 | 1
(5 rows)
これは dnoethのスマートクエリ とよく似ています(同じ基本ロジック)。外側のクエリでのより単純な式を使用すると、わずかに短くて効率的です。
SELECT groupid, pkid
, simple_sum
- LEAST(MIN(simple_sum)
OVER (PARTITION BY groupid
ORDER BY pkid ROWS UNBOUNDED PRECEDING), 0) AS rolling_sum
FROM (
SELECT pkid, numvalue, groupid
, SUM(numvalue) OVER (PARTITION BY groupid
ORDER BY pkid ROWS UNBOUNDED PRECEDING) AS simple_sum
FROM foo
) sub;
どのように機能しますか?
リクエストに応じて特別なローリング合計を計算するには、単純なローリング合計が負になるすべての行について、同じ正の数を追加してゼロにします。それが正確に外側のSELECT
での計算です。負の数を引くと、対応する正の数が加算されます。
LEAST(MIN(simple_sum) OVER (PARTITION BY groupid
ORDER BY pkid ROWS UNBOUNDED PRECEDING), 0)
周囲のLEAST
は、正の数(または0)に対するアクションをキャンセルします。単純な実行合計の最小の負(最大絶対数)は、これまでに合計する必要があるものです。計算がゼロを下回るたびに、単純な実行合計で新しい絶対最低値が得られます。それはすべて美しくシンプルです。
ベース Abelistoの実装 、改善:
CREATE OR REPLACE FUNCTION f_special_rolling_sum()
RETURNS TABLE (groupid int, pkid int, numvalue int, rolling_sum int) AS
$func$
DECLARE
last_groupid int;
BEGIN
FOR groupid, pkid, numvalue IN
SELECT f.groupid, f.pkid, f.numvalue
FROM foo f
ORDER BY f.groupid, f.pkid
LOOP
IF last_groupid = groupid THEN -- same partition continues
rolling_sum := GREATEST(rolling_sum + numvalue, 0);
ELSE -- new partition
last_groupid := groupid;
rolling_sum := GREATEST(numvalue, 0);
END IF;
RETURN NEXT;
END LOOP;
END
$func$ LANGUAGE plpgsql;
コール:
SELECT * FROM f_special_rolling_sum();
これまでに提供されたすべてのソリューションは、カバリングインデックスを使用したインデックスのみのスキャンから利益を得ることができます。
CREATE INDEX idx_foo_covering ON foo(groupid, pkid, numvalue);
関連:
関数、クエリ、インデックス(およびテスト自体)を最適化した後、両方で同様のパフォーマンスが得られます。クエリは関数よりもわずかに高速です。 (集約関数は他の関数より少し遅いです。)広範なテストスイート(オフに基づいて Abelistoのフィドル ):
dbfiddle for pg 9.6 ここ
10ページ目のdbfiddle ここ
まあ、これは醜いですが、新しい関数や集計を追加しなくても機能します。
SELECT *,
CASE
WHEN numvalue > 0
THEN sum( greatest(numvalue,0) ) OVER (PARTITION BY groupid ORDER BY pkid)
ELSE 0
END AS result
FROM foo;