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()
が含まれます。たとえば、次の画像を参照してください。
緑の日付の強調表示は、クエリに含まれているものを示しています。黄色の列の強調表示は、セットの一部になりたいレコードを示しています。
以前の読み:
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
_
ローリングウィンドウは、ほとんどの日のデータで(パフォーマンスに関して)意味をなします。
これはnot_(trans_date, entity_id)
_の重複を1日あたり集計しますが、同じ日のすべての行は常に30日間のウィンドウに含まれます。
大きなテーブルの場合、このようなカバリングインデックスはかなり役立ちます。
_CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);
_
最後の列amount
は、そこからインデックスのみのスキャンを取得する場合にのみ役立ちます。そうでなければそれを落とす。
ただし、いずれにしてもテーブル全体を選択している間は使用されません。小さなサブセットのクエリをサポートします。