web-dev-qa-db-ja.com

PostgreSQLでローリング合計に負でない下限を設定する

これは本当に楽しい質問です(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回以上クリックする場合があり、その結果、複数の負の値が記録されます。収益を計算する場合、「ゼロ」が売上の境界である必要があります。

彼らのソリューションはすべて再帰を使用しています。

3
Evan Carroll

これは、ネストされた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
10
dnoeth

カスタム集計関数の使用

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)
3
Evan Carroll

クエリシスウィンドウ関数

これは 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)に対するアクションをキャンセルします。単純な実行合計の最小の負(最大絶対数)は、これまでに合計する必要があるものです。計算がゼロを下回るたびに、単純な実行合計で新しい絶対最低値が得られます。それはすべて美しくシンプルです。

PL/pgSQL関数

ベース 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 ここ

2

まあ、これは醜いですが、新しい関数や集計を追加しなくても機能します。

SELECT *, 
  CASE
    WHEN numvalue > 0 
    THEN sum( greatest(numvalue,0) ) OVER (PARTITION BY groupid ORDER BY pkid)
    ELSE 0 
  END AS result
FROM foo;