web-dev-qa-db-ja.com

ローリング合計/カウント/日付間隔の平均

18か月で1,000エンティティにまたがるトランザクションのデータベースで、クエリを実行して、可能な30日の期間を_entity_id_でグループ化し、トランザクション金額の合計とその30のトランザクションのCOUNTを指定します日の期間、そして私がそれに対してクエリできる方法でデータを返します。多くのテストの後、このコードは私が望むものの多くを達成します:

_SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;
_

そして、次のような構造のより大きなクエリで使用します。

_SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;
_

このクエリでカバーされないのは、トランザクション数が複数の月にまたがるが、互いに30日以内である場合です。このタイプのクエリはPostgresで可能ですか?もしそうなら、私はどんな入力でも歓迎します。他のトピックの多くでは、rollingではなく、「running」の集計について説明しています。

更新

_CREATE TABLE_スクリプト:

_CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);
_

サンプルデータはここにあります 。 PostgreSQL 9.1.16を実行しています。

理想的な出力には、ローリング30日間のすべてのトランザクションのSUM(amount)およびCOUNT()が含まれます。たとえば、次の画像を参照してください。

Example of rows that would ideally be included in a "set" but are not because my set is static by month.

緑の日付の強調表示は、クエリに含まれているものを示しています。黄色の列の強調表示は、セットの一部になりたいレコードを示しています。

以前の読み:

22
tufelkinder

あなたが持っているクエリ

WINDOW句を使用してクエリを簡略化することもできますが、これはクエリプランを変更するのではなく、構文を短くするだけです。

_SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
_
  • また、idは確かに_NOT NULL_と定義されているため、少し高速なcount(*)を使用します。
  • また、すでに_ORDER BY entity_id_を使用しているため、_PARTITION BY entity_id_を使用する必要はありません。

ただし、さらに簡略化できます。
_ORDER BY_をウィンドウ定義に追加しないでください。クエリとは関係ありません。次に、カスタムウィンドウフレームを定義する必要はありません。

_SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);
_

シンプルで高速ですが、haveの優れたバージョンであり、staticか月です。

あなたが望むかもしれないクエリ

...は明確に定義されていないため、以下の仮定に基づいて構築します。

_entity_id_の最初と最後のトランザクション内の30日ごとのトランザクションと金額をカウントします。活動のない先行期間と後続期間を除外しますが、これらの外側の境界内に可能なすべての30日間を含めます。

_SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);
_

これは、各__entity_id_の30日間のすべての期間をリストします。集計と_trans_date_は、期間の最初の日(含む)です。個々の行の値を取得するには、もう一度ベーステーブルに結合します...

基本的な難しさは、ここで説明したものと同じです。

ウィンドウのフレーム定義は、現在の行の値に依存できません。

そして、むしろtimestamp入力でgenerate_series()を呼び出します:

実際に必要なクエリ

質問の更新とディスカッションの後:
同じ_entity_id_の行を、実際のトランザクションごとに30日間のウィンドウで累積します。

データがまばらに分散されているため、Postgres 9.1にはLATERAL結合がないため、範囲条件を使用した自己結合を実行する方が効率的です。

_SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id
_

SQLフィドル。

ローリングウィンドウは、ほとんどの日のデータで(パフォーマンスに関して)意味をなします。

これはnot_(trans_date, entity_id)_の重複を1日あたり集計しますが、同じ日のすべての行は常に30日間のウィンドウに含まれます。

大きなテーブルの場合、このようなカバリングインデックスはかなり役立ちます。

_CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);
_

最後の列amountは、そこからインデックスのみのスキャンを取得する場合にのみ役立ちます。そうでなければそれを落とす。

ただし、いずれにしてもテーブル全体を選択している間は使用されません。小さなサブセットのクエリをサポートします。

29