0Pricing
SQL Interview Prep · 课时

检测连续日历日

使用日期运算和行号查找不间断的日期序列

检测连续日历日 是 CoddyKit 上的免费 SQL Interview Prep 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 SQL Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 SQL Interview Prep 课程共包含 4 节课。

面试题设

面试官很喜欢连续段问题,因为它们能体现您是否真正理解窗口函数和日期运算。一个典型题目是:“给定一张记录用户登录日期的表,请找出每一段连续且未中断的日历日期。”

直觉式做法是使用自连接,将每条记录与下一条记录进行比较,但在大表上这种做法会产生严重的性能开销,而且表达起来也很别扭。专业的答案会使用间断与连续区间技术。本课中,您将学习如何使用行号和日期相减,清晰地检测连续日期。

示例数据

在整个课程中,我们使用一张 logins 表,其中每行表示某位用户在某天处于活跃状态。假设重复记录已经被移除(每个日历日只有一次登录)。

  • user_id — 登录的用户
  • login_date — 一个 DATE 值

用户 1 的日期为 1 月 1 日、2 日、3 日,然后出现间隔,接着是 1 月 6 日、7 日。我们预计会得到两段连续记录:一段持续 3 天,另一段持续 2 天。

SELECT * FROM logins ORDER BY user_id, login_date;
-- user_id | login_date
--    1    | 2024-01-01
--    1    | 2024-01-02
--    1    | 2024-01-03
--    1    | 2024-01-06
--    1    | 2024-01-07

核心洞察

下面这个技巧可以解决所有连续日期问题。如果按日期对记录排序,并为每条记录分配一个连续的行号,那么在任何一段连续日期中,日期与行号之间的差值都会保持不变。

为什么?因为在每个连续日期上,日期和行号都会恰好增加 1,所以它们的差值不会改变。当出现间隔时,日期会跳跃,但行号不会,因此这个常量会被打破,并开始一个新的分组。

观察差值

让我们手动处理用户 1 的数据。ROW_NUMBER 依次计数为 1、2、3、4、5。从日期中减去行号(按天数计算),观察结果。

  • 1 月 1 日 − 1 = 12 月 31 日
  • 1 月 2 日 − 2 = 12 月 31 日
  • 1 月 3 日 − 3 = 12 月 31 日
  • 1 月 6 日 − 4 = 1 月 2 日
  • 1 月 7 日 − 5 = 1 月 2 日

前 3 条记录共享 12 月 31 日,后 2 条记录共享 1 月 2 日。这个共享的锚点值就是我们的分组键。

添加 ROW_NUMBER

第一个具体步骤是添加行号,并按用户进行分区,这样连续段就不会跨越用户边界;同时按日期排序。

PARTITION BY user_id 会为每位用户重新开始计数;ORDER BY login_date 保证序列遵循日历顺序。

SELECT
  user_id,
  login_date,
  ROW_NUMBER() OVER (
    PARTITION BY user_id
    ORDER BY login_date
  ) AS rn
FROM logins;

计算分组锚点

现在从 login_date 中减去 rn 天。在 PostgreSQL 中,可以直接从日期中减去整数天数。结果就是用于标识每个连续区间的固定锚点。

请注意,不能在定义别名 rn 的同一个 SELECT 中引用它,因此必须先将前一个查询包装在 CTE 或子查询中。

WITH numbered AS (
  SELECT
    user_id,
    login_date,
    ROW_NUMBER() OVER (
      PARTITION BY user_id ORDER BY login_date
    ) AS rn
  FROM logins
)
SELECT
  user_id,
  login_date,
  login_date - rn AS grp
FROM numbered;

对连续区间分组

有了锚点后,每一段连续记录都会共享相同的 grp 值。按 user_id 和 grp 分组,然后进行聚合,得到每段记录的开始日期、结束日期和长度。

  • MIN(login_date) — 连续段的第一天
  • MAX(login_date) — 连续段的最后一天
  • COUNT(*) — 连续段中的天数
WITH numbered AS (
  SELECT user_id, login_date,
    ROW_NUMBER() OVER (
      PARTITION BY user_id ORDER BY login_date
    ) AS rn
  FROM logins
)
SELECT
  user_id,
  MIN(login_date) AS streak_start,
  MAX(login_date) AS streak_end,
  COUNT(*)        AS streak_len
FROM numbered
GROUP BY user_id, login_date - rn
ORDER BY user_id, streak_start;

方言差异

日期运算的语法有所不同。在面试中提到这一点,可以展示您知识面的广度。

  • PostgreSQL: login_date - rn(日期减去整数天数)
  • MySQL: DATE_SUB(login_date, INTERVAL rn DAY)
  • SQL Server: DATEADD(day, -rn, login_date)

逻辑完全相同,只有函数名称发生变化。可移植的思维模型是“将每个日期按其位置向前平移,这样连续段就会归并为同一个常量”。

-- SQL Server version of the anchor
DATEADD(day, -1 * rn, login_date) AS grp

为什么不使用自连接

面试官可能会问,为什么不使用类似 l1.login_date = l2.login_date + 1 的自连接。您可以说明以下原因:

  • 自连接只能检测相邻性,而不是完整的连续段;要组装出完整的连续记录段,仍然需要分组。
  • 如果没有合适的索引,它可能产生组合膨胀,复杂度为 O(n²)。
  • 行号方法只需进行一次有序遍历,扩展性要好得多。

对于这类问题,窗口函数是现代且符合预期的答案。

防范重复记录

整个方法都假设每位用户每天只有一条记录。如果源数据中同一天有多次登录,同一日期的两条记录会获得不同的行号,从而破坏锚点。

可以先去重来避免这一问题:将时间戳转换为日期并使用 DISTINCT,或者在日期上使用 DENSE_RANK 代替 ROW_NUMBER,这样相同日期就会共享同一个编号。

WITH days AS (
  SELECT DISTINCT user_id, login_ts::date AS login_date
  FROM raw_logins
)
SELECT * FROM days;

完整解决方案

将所有部分组合起来,就能得到一个清晰且适合面试的答案,列出每一段连续日期记录的开始日期、结束日期和长度。

这个相同的骨架——去重、编号、相减、分组——几乎可以解决您遇到的所有“连续”问题。

WITH days AS (
  SELECT DISTINCT user_id, login_ts::date AS login_date
  FROM raw_logins
),
numbered AS (
  SELECT user_id, login_date,
    ROW_NUMBER() OVER (
      PARTITION BY user_id ORDER BY login_date
    ) AS rn
  FROM days
)
SELECT user_id,
  MIN(login_date) AS streak_start,
  MAX(login_date) AS streak_end,
  COUNT(*)        AS streak_len
FROM numbered
GROUP BY user_id, login_date - rn
ORDER BY user_id, streak_start;

快速检查

请检验您对核心技巧的理解。

回顾

您已经学会了连续日期问题的基础模式:

  • 去重,确保每位用户每天只有一条记录。
  • 按日期排序、按用户分区的 ROW_NUMBER。
  • 从日期中减去行号,为每段连续记录得到一个固定锚点。
  • 按锚点执行 GROUP BY 并进行聚合,得到开始日期、结束日期和长度。

这个间断与连续区间骨架只需一次遍历即可扩展,并且优于自连接。接下来,您将使用它计算每位用户的最长连续记录段。

常见问题解答

「检测连续日历日」课时是免费的吗?

是的 — 「检测连续日历日」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 SQL Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 SQL Interview Prep 课程共包含 4 节课。

「检测连续日历日」这节课中我会学到什么?

使用日期运算和行号查找不间断的日期序列 你通过在浏览器中直接运行的动手代码来练习 SQL Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 SQL Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 SQL Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「检测连续日历日」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 SQL Interview Prep 课中编写并运行代码吗?

能。每节 SQL Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 检测连续日历日
  2. 每位用户的最长连续记录
  3. 满足条件的连续 N 行
  4. 截至今天的当前连续记录
← 返回 SQL Interview Prep