web-dev-qa-db-ja.com

PostgreSQL:テーブル内の各グループの一連の日付を生成します

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-022016-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_idingするのに苦労しています。

7
Shaun Scovil

1. _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今週だけで非常によく似たケースがありました:

そこに詳細な説明があります。

2. _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()を使用して、ダングリングギャップの最後のバランスをコピーします。

関連:

5

バランスが次のような単調増加の場合:

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

すべきです日付ごとの残高ではなく個々のトランザクションにアクセスできる場合、問題は一般的なケースではおそらく少し簡単になります。

1
Lennart