在PostgreSQL中用每位用户的最新值填充缺失的日期

时间:2019-02-27 16:07:48

标签: sql postgresql window-functions generate-series

我有一个表 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     |

因此,该表仅跟踪更改。我想得到的是一个连续的日期序列,其中包含当前有效的小时数。

例如我想知道2018年1月1日至2019年2月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     |
           ...

正如我所描述的,我不知道如何填空。我曾考虑过创建一个仅包含1900年到2100年之间的日期的表,但是我无法提出如何使用日期表填充空白的想法。

我已经阅读了有关generate_series的信息,我试图以不同的方式连接数据,并且还尝试使用PostgresSQL的window函数。但是我不知道如何。

我是最接近 date表的人,但是问题是,如果用户的最新行的日期超出了我要查询的范围,则不会显示该日期在结果中。这是我尝试过的查询:

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;

我将用户表等加入了这种关系,但是当我将日期范围过滤应用于查询时,那些行的最晚日期超出该日期范围的行将被忽略。

3 个答案:

答案 0 :(得分:1)

我认为这可以满足您的要求

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()的“简单”应用程序。 lead()正在为用户获取下一个日期。减去一天的复杂性以及所有这些,因此这些天没有重叠。

Here是db <>小提琴。

答案 1 :(得分:0)

所以听起来这里的关键是要在实际日期和上次更改日期之间建立一种关系(我们称之为目标日期)。 我的两分钱正在建立一个有两列的帮助器表:实际日期和目标日期。 首先用实际日期填充辅助表,目标日期可以留为空白。然后使用更新查询来填充目标日期:

update HelperTable set TargetDate = 
(select Date from YourOriginalTable where 
HelperTable.ActualDate >= YourOriginalTable.Date 
order by YourOriginalTable.Date desc limit 1)

这样,您可以建立上述的日期关系。然后,您可以利用此帮助器表来构建目标表。或者,您也可以在目标表中添加TargetDate,并且可以选择以后删除该列。

答案 2 :(得分:0)

因此,做了一些工作,提出了以下查询,我认为它将满足您的要求:

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,或 c 常见的 t 可表达的 e 表达,对此的一种简单解释是,它基本上是一种内联临时表在这种情况下。请小心使用它们,因为它们专门存储在内存中,因此返回的大量数据可能会导致过多的分页,从而使数据库进入爬网状态。

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

这是获取我们在日加载中加入的当前行中的小时数。如果该值为null,则它将从前几行加入的日负载中获取最高的小时数。如果该值为null,则返回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。

交叉联接将把两个表中的每个记录联接在一起,而没有过滤器。它在情况上很有用,但请记住,它将产生每个表中的记录数相乘的结果。粗心的使用会导致记录多于服务器的存储空间。

一旦交叉连接(为我们提供每个日期和每个用户ID),我们就会将其加入日负载中。