在 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.