SQL 如何合并连续日期和连续区间?
面试怎么回答
我会先按业务粒度去重并明确连续规则。固定一天步长时,可以按用户排序生成 ROW_NUMBER,用日期减序号构造稳定分组键;如果允许自定义间隔,就用 LAG 判断断点,再累计断点生成组号。最后按用户和组号聚合开始日期、结束日期和持续长度。同时要注意重复日期、跨时区和多维分区键,否则区间会被错误拆分或合并。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
同一个用户有 8 条活跃日期,结果却要输出 3 段连续区间。
难点不是 GROUP BY,而是先找到“岛”的边界。
这类题通常被称为 Gaps and Islands。核心不是背某一段 SQL,而是把“连续”翻译成稳定的分组键。
第一步:先把数据粒度处理干净
假设原始数据包含 user_id 和 active_date,同一个用户同一天可能有多条行为记录。
如果直接排序,重复日期会占用多个 ROW_NUMBER,后面的分组键就会漂移。所以第一步通常是按“用户 + 日期”去重。
还要先确认业务定义:连续一天算一段,还是允许中间间隔一天?跨时区数据按哪个自然日归属?这些规则不同,最后的区间也不同。
方法一:日期减序号
对每个用户按日期排序,生成 rn。连续日期每次增加一天,rn 也每次增加一,因此 active_date 减去 rn 天得到的值保持不变。
WITH d AS (
SELECT DISTINCT user_id, active_date FROM user_activity
), x AS (
SELECT user_id, active_date,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY active_date) AS rn
FROM d
)
SELECT user_id,
MIN(active_date) AS start_date,
MAX(active_date) AS end_date,
COUNT(*) AS days
FROM x
GROUP BY user_id, active_date - rn * INTERVAL '1 day';
不同引擎的日期函数写法不一样,但思路相同:连续递增的业务值减去连续递增的序号,得到稳定分组键。

方法二:LAG 判断断点再累计
日期减序号适合固定步长。如果业务规定“相隔不超过 3 天仍算同一段”,更通用的做法是:
- 用 LAG 取上一条记录;
- 当前记录不满足连续条件时标记 break_flag = 1;
- 对 break_flag 做累计求和,生成 island_id;
- 按 user_id 和 island_id 聚合起止边界。
这种方法把连续规则集中在一处,适合时间间隔、状态变化、金额区间等更复杂场景。
一个典型场景
运营要统计用户连续付费周期。最初只按日期减序号分组,后来发现补单记录会把历史日期重复写入,同一天多条记录导致连续段被错误拆开。
先按用户和业务日期去重,再用订单发生时区统一自然日,区间才稳定。

这类题真正考察的不是窗口函数,而是你有没有先定义粒度、顺序和连续边界。