我有一个表保存每个帐户的每日余额,但仅限于帐户发生交易的日期。以以下帐户为例:
account_id | updated_at | balance
------------+-------------------------------+---------
7 | 2024-03-14 23:51:12.430866+00 | 400
7 | 2024-03-15 23:44:34.791627+00 | 400
7 | 2024-03-16 23:38:02.437022+00 | 400
7 | 2024-03-17 23:56:52.592801+00 | 400
7 | 2024-03-17 23:56:52.592801+00 | 400
7 | 2024-03-17 23:56:52.592801+00 | 400
7 | 2024-03-17 23:56:52.592801+00 | 400
7 | 2024-03-17 23:56:52.592801+00 | 400
7 | 2024-03-20 17:47:04.989205+00 | 400
7 | 2024-03-20 17:47:04.989205+00 | 400
7 | 2024-03-21 19:27:48.242602+00 | 357.27
7 | 2024-03-22 16:19:17.737126+00 | 325.83
7 | 2024-03-23 18:09:37.164425+00 | 325.83
7 | 2024-03-24 22:45:58.444256+00 | 325.83
7 | 2024-03-24 22:45:58.444256+00 | 325.83
7 | 2024-03-25 16:46:49.831235+00 | 221.22
7 | 2024-03-26 15:32:09.949961+00 | 150.39
7 | 2024-03-27 21:58:14.160818+00 | 150.39
7 | 2024-03-28 19:23:19.219855+00 | 127.08
7 | 2024-03-29 17:33:07.109838+00 | 160.47
7 | 2024-03-30 17:49:41.642925+00 | 210.47
7 | 2024-04-01 16:53:34.900447+00 | 300.47
我在 2024 年 3 月 18 日和 2024 年 3 月 19 日没有任何条目。
我正在尝试构建一个表,其中包含截至昨天的所有天数的余额,包括上一个表中缺少的日期。预期结果是:
account_id | updated_at | balance
------------+------------+---------
7 | 2024-03-14 | 400
7 | 2024-03-15 | 400
7 | 2024-03-16 | 400
7 | 2024-03-17 | 400
7 | 2024-03-18 | 400
7 | 2024-03-19 | 400
7 | 2024-03-20 | 400
7 | 2024-03-21 | 357.27
7 | 2024-03-22 | 325.83
7 | 2024-03-23 | 325.83
7 | 2024-03-24 | 325.83
7 | 2024-03-25 | 221.22
7 | 2024-03-26 | 150.39
7 | 2024-03-27 | 150.39
7 | 2024-03-28 | 127.08
7 | 2024-03-29 | 160.47
7 | 2024-03-30 | 210.47
7 | 2024-04-01 | 300.47
....
这个表格会一直持续到昨天。
基于这个线程,我编写了以下代码片段:
WITH days AS (
SELECT generate_series('2024-03-01'::DATE, (CURRENT_DATE - INTERVAL '1 DAY')::DATE, '1 day'::INTERVAL)::DATE AS date_
)
, discontinued_days AS (
SELECT DISTINCT
account_id
, balance
, days.date_
, updated_at
FROM days LEFT JOIN my_balance_table
ON days.date_ >= my_balance_table.updated_at::DATE
)
, grouped AS (
SELECT
account_id
, balance
, date_
, updated_at
, COUNT(account_id) OVER (PARTITION BY account_id ORDER BY date_) AS grp
FROM discontinued_days
)
SELECT DISTINCT
account_id
, COALESCE(MIN(balance) OVER (PARTITION BY account_id, grp ORDER BY date_), 0) AS bba_balance
, date_
FROM grouped
它在 3 月 28 日之前的日子里运行良好,但是在 3 月 29 日,它返回余额
127.08
,而预计会看到 160.47
,因为这是我们在输入表 my_balance_table
中看到的余额。 3 月 29 日之后的日子也是如此:
account_id | balance | date_
------------+---------+------------
7 | 400 | 2024-03-14
7 | 400 | 2024-03-15
7 | 400 | 2024-03-16
7 | 400 | 2024-03-17
7 | 400 | 2024-03-18
7 | 400 | 2024-03-19
7 | 400 | 2024-03-20
7 | 357.27 | 2024-03-21
7 | 325.83 | 2024-03-22
7 | 325.83 | 2024-03-23
7 | 325.83 | 2024-03-24
7 | 221.22 | 2024-03-25
7 | 150.39 | 2024-03-26
7 | 150.39 | 2024-03-27
7 | 127.08 | 2024-03-28
7 | 127.08 | 2024-03-29
7 | 127.08 | 2024-03-30
7 | 127.08 | 2024-03-31
7 | 127.08 | 2024-04-01
如何解决这个问题?
我正在尝试构建一个包含所有天数余额的表
让我们首先解决一个潜在的问题。您有一个
timestamptz
列,其中没有“天”的概念。您必须设置定义您的日子的时区。否则,查询取决于当前会话的timezone
设置——它“有效”,直到它不起作用。
有多种方法可以做到这一点。在下面的查询中,我假设时区为“UTC”并相应地生成日期边界。
您评论:
...即使设置索引后性能仍然很差[...] 在 my_balance_table 上创建索引 my_balance_table_multi_idx(account_id、updated_at DESC、余额);
索引无法使用。您的查询会筛选应用于表列的表达式
updated_at::DATE
。无论如何,这排除了上述索引。此外,它排除了 any 索引,因为该表达式 取决于当前会话 的
timezone
设置。所以它不是不可变的,也不能成为索引。 不要以这种方式表述您的查询。使用 sargable 过滤表达式。然后索引就会产生奇迹。甚至稍微好一点:
CREATE INDEX my_balance_table_multi_idx ON my_balance_table (account_id, updated_at DESC) INCLUDE (balance);
参见:
为了优化性能,基数很重要。总共有多少行,有多少个不同的
account_id
,有多少重复项,缺失了多少天?每天可以有多个不同行吗?表是否已分区?索引、资源等
假设许多不同的
account_id
,很少有重复(只有像您的示例中那样的完整重复),很少有缺失的一天,并且我们查询单个给定的account_id
:
SELECT 7 AS account_id -- same as filter !!!
, (d.the_day AT TIME ZONE 'utc')::date -- don't depend on timezone setting !!!
, b.updated_at
, b.balance
FROM generate_series(timestamptz '2024-03-01 0:0+0' -- start-of-day for UTC !!!
, date_trunc('day', now(), 'UTC') -- possibly off-by-1 !!!
, interval '1 day') AS d(the_day)
LEFT JOIN LATERAL (
SELECT updated_at, balance
FROM my_balance_table
WHERE account_id = 7 -- filter early !!!
AND updated_at < d.the_day + interval '1 day' -- sargable !!!
ORDER BY updated_at DESC
LIMIT 1
) b ON true
ORDER BY d.the_day;
如果这没有在 ms(而不是秒或分钟)内执行,则说明有问题。考虑聘请一名顾问来调查这一问题。 :)
date_trunc()
将时区作为第三个参数需要 Postgres 12+。
有很多细则可以确保一切正确。
相关:
要立即处理整个表,而不是单个帐户,我的相关答案中第二个查询的变体将表现更好,并且上述索引并不重要。