PostgreSQL:为表中的每个组生成一系列日期

Sha*_*vil 9 postgresql group-by

balances在 PostgreSQL 9.3 中有一个表,如下所示:

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');
Run Code Online (Sandbox Code Playgroud)

它只包含用户进行交易的日期的余额。我需要它为每个用户包含一行以及给定日期范围内每个日期的余额。

  • 如果用户在范围内的给定日期没有行,我需要使用他们前一天的余额。
  • 如果用户在范围内的给定日期之后创建了他们的帐户,我需要避免为该用户/日期组合创建一行。

我可以引用一个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');
Run Code Online (Sandbox Code Playgroud)

我想要的结果是这样的:

+---------+---------+--------------------------+
| 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 |
+---------+---------+--------------------------+
Run Code Online (Sandbox Code Playgroud)

请注意,已经为用户 2 添加了行2016-01-022016-01-03,结转了之前的余额2016-01-01;并且没有为在 上创建的用户 3 添加任何行2016-01-03

要在日期范围内生成一系列日期,我知道我可以使用:

SELECT d.date FROM GENERATE_SERIES('2016-01-01', '2016-01-03', '1 day'::INTERVAL) d
Run Code Online (Sandbox Code Playgroud)

...但我正在努力用LEFT JOIN每组由user_id.

Erw*_*ter 7

1 CROSS JOINLEFT 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;
Run Code Online (Sandbox Code Playgroud)

返回您想要的结果 - 除了在您的示例中as_of_date是一个实际的date,而不是timestamp类似的。那应该更合适。

已创建但尚未进行任何交易的用户以余额为 0 列出。您没有定义如何处理极端情况。

而是使用timestamp输入generate_series()

使用多列索引对此进行备份对性能至关重要:

CREATE INDEX balances_multi_idx ON balances (user_id, as_of_date DESC, balance);
Run Code Online (Sandbox Code Playgroud)

就在本周,我们在 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;
Run Code Online (Sandbox Code Playgroud)

结果一样。如果您有上面提到的多列索引并且可以从中获取仅索引扫描,那么第一个解决方案很可能更快。

主要特征是形成组的值的运行计数。由于 count() 不计算 NULL 值,因此所有没有余额的日期与grp最近的余额属于同一组 ( )。然后max()在相同的窗口框架上使用一个简单的扩展grp来复制悬空间隙的最后一个平衡。

有关的: