PostgreSQL 9.3には、次のようなbalances
テーブルがあります。
CREATE TABLE balances (
user_id INT
, balance INT
, as_of_date DATE
);
INSERT INTO balances (user_id, balance, as_of_date) VALUES
(1, 100, '2016-01-03')
, (1, 50, '2016-01-02')
, (1, 10, '2016-01-01')
, (2, 200, '2016-01-01')
, (3, 30, '2016-01-03');
ユーザーがトランザクションを行った日付の残高のみが含まれます。特定の日付範囲内の各日付の残高を持つ各ユーザーの行を含める必要があります。
accounts
テーブルを参照して、ユーザーのcreate_date
を取得できます。
CREATE TABLE accounts (
user_id INT
, create_date DATE
);
INSERT INTO accounts (user_id, create_date) VALUES
(1, '2015-12-01')
, (2, '2015-12-31')
, (3, '2016-01-03');
私の望ましい結果は次のようになります:
+---------+---------+--------------------------+
| user_id | balance | as_of_date |
+---------+---------+--------------------------+
| 1 | 100 | 2016-01-03T00:00:00.000Z |
| 1 | 50 | 2016-01-02T00:00:00.000Z |
| 1 | 10 | 2016-01-01T00:00:00.000Z |
| 2 | 200 | 2016-01-03T00:00:00.000Z |
| 2 | 200 | 2016-01-02T00:00:00.000Z |
| 2 | 200 | 2016-01-01T00:00:00.000Z |
| 3 | 30 | 2016-01-03T00:00:00.000Z |
+---------+---------+--------------------------+
ユーザー2の2016-01-02
と2016-01-03
の行が追加され、2016-01-01
の以前の残高が引き継がれていることに注意してください。また、2016-01-03
で作成されたユーザー3の行は追加されていません。
日付範囲内の一連の日付を生成するために、私は使用できることを知っています:
SELECT d.date FROM GENERATE_SERIES('2016-01-01', '2016-01-03', '1 day'::INTERVAL) d
...しかし、LEFT JOIN
でグループ化された各行セットを使用して、そのシリーズをuser_id
ingするのに苦労しています。
CROSS JOIN
_、_LEFT JOIN LATERAL
_からサブクエリ_SELECT a.user_id, COALESCE(b.balance, 0) AS balance, d.as_of_date
FROM (
SELECT d::date AS as_of_date -- cast to date right away
FROM generate_series(timestamp '2016-01-01', '2016-01-03', interval '1 day') d
) d
JOIN accounts a ON a.create_date <= d.as_of_date
LEFT JOIN LATERAL (
SELECT balance
FROM balances
WHERE user_id = a.user_id
AND as_of_date <= d.as_of_date
ORDER BY as_of_date DESC
LIMIT 1
) b ON true
ORDER BY a.user_id, d.as_of_date;
_
希望する結果を返します。ただし、_as_of_date
_は実際のdate
であり、例のtimestamp
ではありません。それはもっと適切なはずです。
すでに作成されているが、まだトランザクションがないユーザーは、残高0でリストされます。コーナーケースへの対処方法を定義しませんでした。
むしろtimestamp
入力をgenerate_series()
に使用します:
複数列のインデックスを使用してこれをバックアップすることは、パフォーマンスにとって重要です。
_CREATE INDEX balances_multi_idx ON balances (user_id, as_of_date DESC, balance);
_
SO今週だけで非常によく似たケースがありました:
そこに詳細な説明があります。
CROSS JOIN
_、_LEFT JOIN
_、ウィンドウ関数_SELECT user_id
, COALESCE(max(balance) OVER (PARTITION BY user_id, grp
ORDER BY as_of_date), 0) AS balance
, as_of_date
FROM (
SELECT a.user_id, b.balance, d.as_of_date
, count(b.user_id) OVER (PARTITION BY user_id ORDER BY as_of_date) AS grp
FROM (
SELECT d::date AS as_of_date -- cast to date right away
FROM generate_series(timestamp '2016-01-01', '2016-01-03', interval '1 day') d
) d
JOIN accounts a ON a.create_date <= d.as_of_date
LEFT JOIN balances b USING (user_id, as_of_date)
) sub
ORDER BY user_id, as_of_date;
_
同じ結果。上記のマルチカラムインデックスがあり、それからインデックスのみのスキャンを実行できる場合、最初のソリューションがおそらくより高速です。
主な機能は、グループを形成するための値の現在のカウントです。 count()はNULL値をカウントしないため、残高のないすべての日付は、最新の残高と同じグループ(grp
)に分類されます。次に、grp
によって拡張された同じウィンドウフレーム上で単純なmax()
を使用して、ダングリングギャップの最後のバランスをコピーします。
関連:
バランスが次のような単調増加の場合:
SELECT b.user_id, max(b.balance) as balance, d.as_of_date
FROM GENERATE_SERIES('2016-01-01', '2016-01-03', '1 day'::INTERVAL) d (as_of_date)
LEFT JOIN balances b
on b.as_of_date <= d.as_of_date
GROUP BY b.user_id, d.as_of_date
ORDER BY b.user_id, d.as_of_date desc
すべきです日付ごとの残高ではなく個々のトランザクションにアクセスできる場合、問題は一般的なケースではおそらく少し簡単になります。