在 PostgreSQL 中用每个用户的最新值填充缺失日期
Filling missing dates with latest value per user in PostgreSQL
我有一个 table dayload 标记用户每天的工作时间何时发生变化。
| id | date | user_id | hours |
| 1 | 2019-01-27 | 1 | 4 |
| 2 | 2019-02-01 | 1 | 8 |
| 3 | 2018-06-30 | 2 | 5 |
| 4 | 2018-07-02 | 2 | 8 |
因此 table 仅跟踪更改。我想要得到的是一系列连续的日期和当前有效的时间。
例如我想知道 2018-01-01 和 2019-02-28 之间每个用户和天的工作时间 ,即
| id | date | user_id | hours |
| .. | 2018-01-27 | 1 | 4 |
| .. | 2018-01-28 | 1 | 4 |
| .. | 2018-01-29 | 1 | 4 |
| .. | 2018-01-30 | 1 | 4 |
| .. | 2018-01-31 | 1 | 4 |
| .. | 2019-02-01 | 1 | 8 |
| .. | 2019-02-02 | 1 | 8 |
| .. | 2019-02-03 | 1 | 8 |
| .. | 2019-02-04 | 1 | 8 |
...
| .. | 2018-06-30 | 2 | 5 |
| .. | 2018-07-01 | 2 | 5 |
| .. | 2018-07-02 | 2 | 8 |
| .. | 2018-07-03 | 2 | 8 |
...
我不知道如何填空,正如我描述的那样。我想过创建一个 table 只是充满了 1900 到 2100 之间的日期,但我无法想出如何使用日期 table.[=13= 来填充空白]
我读过 generate_series,我尝试以不同的方式连接数据,我还尝试使用 PostgresSQL 的 window 函数。但是我不知道怎么办。
我最接近日期table,但问题是如果用户的最新行的日期超出了我想要的范围查询,不会显示在结果中。这是我试过的查询:
SELECT user_id, d.date, minutes
FROM day d
JOIN dayload dl
ON dl.date = (
SELECT MAX(date) from DAYLOAD where date <= d.date
)
order by d.date;
我将用户 table 等加入到此关系中,但是当我将日期范围过滤应用于查询时,那些具有日期范围之外的最新日负载的行被排除在外。
所以听起来这里的关键是在实际日期和之前更改的日期(我们称之为目标日期)之间建立关系。
我的两分钱是构建一个助手 table,它有两列:实际日期和目标日期。
从使用实际日期填充助手 table 开始,目标日期可以留空。然后使用更新查询来填充目标日期:
update HelperTable set TargetDate =
(select Date from YourOriginalTable where
HelperTable.ActualDate >= YourOriginalTable.Date
order by YourOriginalTable.Date desc limit 1)
这样你就建立了上面提到的日期关系。然后您可以利用这个助手 table 来建立您的目标 table。或者您可以只在您的目标中添加 TargetDate table,如果您愿意,您可以选择稍后删除该列。
所以,玩了一会儿,提出了以下查询,我认为它可以满足您的要求:
with
__users as(
select distinct
user_id
from
dayload
)
select
row_number() over(order by __users.user_id asc, gs.date asc) as id,
gs.date::date,
__users.user_id,
coalesce(dayload.hours, max(hours) over(partition by __users.user_id order by gs.date asc), 0) as hours
from
generate_series('2018-01-01'::date, '2019-02-28'::date, interval '1 day') as gs("date")
cross join __users
left join dayload using(date, user_id)
order by
__users.user_id asc,
gs.date asc;
查询说明:
with
__users as(
select distinct
user_id
from
dayload
)
这称为 CTE,或 common table expression,一个对它的简单解释是说在这种情况下它基本上是一个内联临时 table 。使用它们时要小心,因为它们专门存储在内存中,因此大数据 returns 可能会导致过度分页,从而使您的数据库陷入困境。
generate_series('2018-01-01'::date, '2019-02-28'::date, interval '1 day') as gs("date")
这会在传入的第一个和第二个参数之间生成空白日期。您可以在此处定义要查询的日期范围。
coalesce(dayload.hours, max(hours) over(partition by user_id order by date asc), 0) as hours
这是在当前行中获取我们在日负载中加入的小时数。如果该值为空,则它会从前几行已加入的日负荷中获取最高小时数。如果为空,则 returns 0.
generate_series('2018-01-01'::date, '2019-02-28'::date, interval '1 day') as gs("date")
cross join __users
left join dayload using(date, user_id)
这首先获取“2018-01-01”::date 和“2019-02-28”::date 之间的每个日期,然后交叉连接到我们之前的 CTE。
交叉连接会将来自两个 table 的每条记录连接在一起,没有过滤器。它在某些情况下很有用,但请记住,它将产生每个 table 中的记录数相乘的结果。使用不当可能导致记录多于服务器的内存。
一旦交叉连接(给我们每个日期和每个用户 ID),我们就离开连接到 dayload。
我认为这符合您的要求:
select generate_series(date,
lead(date, 1, current_date) over (partition by user_id order by date) - interval '1 day',
interval '1 day'
) as date,
user_id, hours
from (values (1, '2019-01-27'::date, 1, 4),
(2, '2019-02-01'::date, 1, 8),
(3, '2018-06-30'::date, 2, 5)
) v(id, date, user_id, hours);
它是 generate_series()
的 "simple" 应用程序。 lead()
正在获取用户的下一个日期。减去一天的复杂性就是这些天没有重叠。
Here 是一个 db<>fiddle.
我有一个 table dayload 标记用户每天的工作时间何时发生变化。
| id | date | user_id | hours |
| 1 | 2019-01-27 | 1 | 4 |
| 2 | 2019-02-01 | 1 | 8 |
| 3 | 2018-06-30 | 2 | 5 |
| 4 | 2018-07-02 | 2 | 8 |
因此 table 仅跟踪更改。我想要得到的是一系列连续的日期和当前有效的时间。
例如我想知道 2018-01-01 和 2019-02-28 之间每个用户和天的工作时间 ,即
| id | date | user_id | hours |
| .. | 2018-01-27 | 1 | 4 |
| .. | 2018-01-28 | 1 | 4 |
| .. | 2018-01-29 | 1 | 4 |
| .. | 2018-01-30 | 1 | 4 |
| .. | 2018-01-31 | 1 | 4 |
| .. | 2019-02-01 | 1 | 8 |
| .. | 2019-02-02 | 1 | 8 |
| .. | 2019-02-03 | 1 | 8 |
| .. | 2019-02-04 | 1 | 8 |
...
| .. | 2018-06-30 | 2 | 5 |
| .. | 2018-07-01 | 2 | 5 |
| .. | 2018-07-02 | 2 | 8 |
| .. | 2018-07-03 | 2 | 8 |
...
我不知道如何填空,正如我描述的那样。我想过创建一个 table 只是充满了 1900 到 2100 之间的日期,但我无法想出如何使用日期 table.[=13= 来填充空白]
我读过 generate_series,我尝试以不同的方式连接数据,我还尝试使用 PostgresSQL 的 window 函数。但是我不知道怎么办。
我最接近日期table,但问题是如果用户的最新行的日期超出了我想要的范围查询,不会显示在结果中。这是我试过的查询:
SELECT user_id, d.date, minutes
FROM day d
JOIN dayload dl
ON dl.date = (
SELECT MAX(date) from DAYLOAD where date <= d.date
)
order by d.date;
我将用户 table 等加入到此关系中,但是当我将日期范围过滤应用于查询时,那些具有日期范围之外的最新日负载的行被排除在外。
所以听起来这里的关键是在实际日期和之前更改的日期(我们称之为目标日期)之间建立关系。 我的两分钱是构建一个助手 table,它有两列:实际日期和目标日期。 从使用实际日期填充助手 table 开始,目标日期可以留空。然后使用更新查询来填充目标日期:
update HelperTable set TargetDate =
(select Date from YourOriginalTable where
HelperTable.ActualDate >= YourOriginalTable.Date
order by YourOriginalTable.Date desc limit 1)
这样你就建立了上面提到的日期关系。然后您可以利用这个助手 table 来建立您的目标 table。或者您可以只在您的目标中添加 TargetDate table,如果您愿意,您可以选择稍后删除该列。
所以,玩了一会儿,提出了以下查询,我认为它可以满足您的要求:
with
__users as(
select distinct
user_id
from
dayload
)
select
row_number() over(order by __users.user_id asc, gs.date asc) as id,
gs.date::date,
__users.user_id,
coalesce(dayload.hours, max(hours) over(partition by __users.user_id order by gs.date asc), 0) as hours
from
generate_series('2018-01-01'::date, '2019-02-28'::date, interval '1 day') as gs("date")
cross join __users
left join dayload using(date, user_id)
order by
__users.user_id asc,
gs.date asc;
查询说明:
with
__users as(
select distinct
user_id
from
dayload
)
这称为 CTE,或 common table expression,一个对它的简单解释是说在这种情况下它基本上是一个内联临时 table 。使用它们时要小心,因为它们专门存储在内存中,因此大数据 returns 可能会导致过度分页,从而使您的数据库陷入困境。
generate_series('2018-01-01'::date, '2019-02-28'::date, interval '1 day') as gs("date")
这会在传入的第一个和第二个参数之间生成空白日期。您可以在此处定义要查询的日期范围。
coalesce(dayload.hours, max(hours) over(partition by user_id order by date asc), 0) as hours
这是在当前行中获取我们在日负载中加入的小时数。如果该值为空,则它会从前几行已加入的日负荷中获取最高小时数。如果为空,则 returns 0.
generate_series('2018-01-01'::date, '2019-02-28'::date, interval '1 day') as gs("date")
cross join __users
left join dayload using(date, user_id)
这首先获取“2018-01-01”::date 和“2019-02-28”::date 之间的每个日期,然后交叉连接到我们之前的 CTE。
交叉连接会将来自两个 table 的每条记录连接在一起,没有过滤器。它在某些情况下很有用,但请记住,它将产生每个 table 中的记录数相乘的结果。使用不当可能导致记录多于服务器的内存。
一旦交叉连接(给我们每个日期和每个用户 ID),我们就离开连接到 dayload。
我认为这符合您的要求:
select generate_series(date,
lead(date, 1, current_date) over (partition by user_id order by date) - interval '1 day',
interval '1 day'
) as date,
user_id, hours
from (values (1, '2019-01-27'::date, 1, 4),
(2, '2019-02-01'::date, 1, 8),
(3, '2018-06-30'::date, 2, 5)
) v(id, date, user_id, hours);
它是 generate_series()
的 "simple" 应用程序。 lead()
正在获取用户的下一个日期。减去一天的复杂性就是这些天没有重叠。
Here 是一个 db<>fiddle.