【问题标题】:Select data within one month prior to each user's last record选择每个用户最后一条记录前一个月内的数据
【发布时间】:2025-11-23 18:15:01
【问题描述】:

假设我有一个这样的名为“日记”的表:

| id | user_id |        recorded_at       | record |
|----|---------|--------------------------|--------|
| 20 |  50245  |2017-10-01 23:00:14.765366|   89   |
| 21 |  50245  |2017-12-05 10:00:33.135331|   97   |
| 22 |  50245  |2017-12-31 11:50:23.965134|   80   |
| 23 |  76766  |2015-10-06 11:00:14.902452|   70   |
| 24 |  76766  |2015-10-07 22:40:59.124553|   81   |

对于每个用户,我想检索最新的行和之前一个月内的所有行。

换句话说,对于user_id 50245,我想要他/她的数据从“2017-12-01 11:50:23.965134”到“2017-12-31 11:50:23.965134”;对于 user_id 76766,我希望他/她的数据从“2015-09-07 22:40:59.124553”到“2015-10-07 22:40:59.124553”。

因此期望的结果如下所示:

| id | user_id |        recorded_at       | record |
|----|---------|--------------------------|--------|
| 21 |  50245  |2017-12-05 10:00:33.135331|   97   |
| 22 |  50245  |2017-12-31 11:50:23.965134|   80   |
| 23 |  76766  |2015-10-06 11:00:14.902452|   70   |
| 24 |  76766  |2015-10-07 22:40:59.124553|   81   |

请注意,id 20 的记录包括在内,因为它比 user_id 50245 的最后一条记录早了一个多月。

有什么方法可以编写 SQL 查询来实现这一点?

【问题讨论】:

  • @ThorstenKettner 我真的不知道。如果您有提示或关键字要搜索,请提供一些。很抱歉成为 SQL 的新手。
  • 最佳查询技术取决于您的设置细节:Postgres 版本 (SELECT version();)、表定义(user_idrecorded_at 定义了 NOT NULL?)、基数(多少行?),值频率(每个用户/月有多少行;最小/最大/平均)。是否有一个单独的表 users 为(至少)每个相关用户提供 1 行?
  • @ErwinBrandstetter 为迟到的回复道歉。你的帖子很棒。请在您的答案下方查看我的评论并启发我。谢谢。

标签: sql postgresql date datetime greatest-n-per-group


【解决方案1】:

我会倾向于使用窗口函数:

select d.*
from (select d.*, max(d.recorded_at) over (partition by d.user_id) as max_recorded_at
      from diary d
     ) d
where recorded_at >= max_recorded_at - interval '1 month';

【讨论】:

    【解决方案2】:

    直接的方法是使用子查询来获取每个user_id 的最大值recorded_at,然后加入:

    select d.*
      from diary d
           join ( select user_id, max(recorderd_at) mra
                    from diary
                   group by user_id ) m on d.user_id = m.user_id
     where m.mra <= d.recorded_at + interval '1 month'
    

    这有两次访问表的缺点(在不同的 RDBMS 中可能不同 - 使用 explain 来查看执行计划)。

    更好的选择是使用窗口函数一次性完成所有操作:

    select id, user_id, recorderd_at
      from ( select *, max(recorderd_at) over (partition by user_id) as mra
               from diary ) x
     where mra <= recorderd_at + interval '1 months'
    

    免责声明我没有测试上面的查询,但无论如何你应该明白 - 请参阅 http://sqlfiddle.com/#!17/e90000/9 以获得具有类似架构的工作示例

    【讨论】:

      【解决方案3】:

      对于小表,任何(有效的)查询技术都是好的。

      对于表,细节很重要。假设:

      • 还有一个users 表,其中user_id 作为PK 包含所有相关用户(或可能更多)。这是典型的设置。

      • 您拥有(或可以创建)diary (user_id, recorded_at DESC NULLS LAST) 上的索引。如果recorded_at 定义为NOT NULLNULLS LAST 是可选的。但请确保查询与索引匹配。

      • 每个用户多于几行 - 典型用例。

      这应该是最快的选项之一:

      SELECT d.*
      FROM   users u
      CROSS  JOIN LATERAL (
         SELECT recorded_at
         FROM   diary
         WHERE  user_id = u.user_id
         ORDER  BY recorded_at DESC NULLS LAST
         LIMIT 1
         ) d1
      JOIN   diary d ON d.user_id = u.user_id
                    AND d.recorded_at >= d1.recorded_at - interval '1 month'
      ORDER  BY d.user_id, d.recorded_at;
      

      准确地产生您想要的结果。

      对于每个用户只有 几行 行,子查询中的 max()DISTINCT ON () 通常更快。

      相关(附详细说明):

      关于FROM 子句:

      【讨论】:

      • 我有以下几个问题:首先,为什么前两行中的SELECT d.* FROM users u 有效?通常我会做类似SELECT * FROM diary d INNER JOIN users u ON d.user_id = u.user_id 的事情。二、为什么用CROSS JOIN LATERAL而不是LEFT JOIN LATERAL?是不是因为前者不需要ON 声明?第三,也是最后,您演示中的最后一个JOIN 是什么类型的连接?
      • @ytu: d 是我们稍后加入的diary 的表别名。 CROSS JOIN 是一个无条件连接,它只需要每边有一行 - 而 LEFT [OUTER] JOIN 需要一个连接条件,但保留左侧的所有行,即使右侧没有匹配。 INNEROUTER 是可选的干扰词。手册中添加的链接和基础知识中有更多解释:postgresql.org/docs/current/static/sql-select.html#SQL-FROM.
      • 我知道diary 的别名设置d。但是,diary from users 中的 selecting 列对我来说并不直观,而且我几乎没有找到关于 FROM 子句在这种情况下如何工作的信息。 FROM 子句的顺序不重要吗?比SELECT d.* FROM diary d CROSS JOIN LATERAL...JOIN users u ON d.user_id = u.user_id好在哪里?
      • @ytu:FROM 子句中列出的所有表表达式的列(可选地与显式 JOIN 语法连接)在 SELECT 列表中可见。通常,FROM 子句中的项目顺序无关紧要。但也有各种例外,LATERAL 加入是一个值得注意的例外。我在上面添加了一些更多的指针。
      【解决方案4】:

      未测试,但这样的东西应该可以工作。

      我会使用子查询来获取 last_record 然后过滤掉那些在日期和上个月的,例如:

      select d.* from diary d,
      (select max(recorded_at) l from diary group by user_id) as last_record 
      where  d.recorded_at = last_record.l
      or
        ( 
         d.recorded_at  >= date_trunc('month', last_record.l - interval '1' month)
         and d.recorded_at  < last_record.l
        )
      

      【讨论】:

      • 谢谢!这些天我一定会尝试的。但是有一个问题:对于select d.* from diary d 之后的,,这是一种创建子查询并稍后使用结果的方法吗?我认为子查询只能写在**** JOIN 子句中。