lag()判断断点+sum()累计分组id是最稳解法,因group by无法表达“当前行是否合并取决于上一行end_time”的动态依赖关系,而窗口函数一次扫描即可完成,逻辑清晰且适配多数据库。

直接用 LAG() 判断断点 + SUM() 累计分组 ID,是最稳、最通用的解法。自连接或递归 CTE 在大数据量下容易崩,而窗口函数一次扫描就能完成,且逻辑可读、适配 Hive/MySQL 8.0+/PostgreSQL。
为什么不能用 GROUP BY 直接合并区间
因为区间是否合并,取决于当前行的 start_time 是否 ≤ 上一行合并后的最大 end_time,这是一种动态依赖关系。GROUP BY 是静态分组,无法表达“前一行状态影响当前归属”这个逻辑。
-
GROUP BY start_time, end_time:完全不处理重叠,只是去重 -
GROUP BY FLOOR((start_time + end_time)/2):数值近似无业务意义,可能把 [1,3] 和 [5,7] 错合 - 原始数据若含 NULL,
LAG()返回 NULL 后,start_time 结果为 UNKNOWN,整行被过滤——必须先 <code>WHERE start_time IS NOT NULL AND end_time IS NOT NULL
LAG() + SUM() 分组 ID 的标准写法
核心是两步:先用 LAG(end_time) 拿到历史最大右边界,再用条件标记是否新开组,最后累加生成稳定组号。
- 排序必须是
ORDER BY start_time ASC, end_time ASC:当多个区间起始时间相同时,按end_time升序能确保LAG()取到的是“已知最远的结束点”,否则可能取到一个短区间,误判为断点 -
LAG(end_time, 1, '1970-01-01')中的默认值要足够小(日期类型用远古时间,数值用负大数),避免首行比较出错 - 判断逻辑写成
CASE WHEN start_time > LAG(end_time) THEN 1 ELSE 0 END,注意是>不是>=——业务若要求端点相接即合并(如 [1,5] 和 [5,8]),就用>= -
SUM(is_new_group) OVER (ORDER BY start_time, end_time ROWS UNBOUNDED PRECEDING)生成group_id,这个累计和就是分组标识
最终聚合与边界细节
有了 group_id,外层 GROUP BY user_id, group_id 就能安全聚合,但要注意几个易漏点:
- 必须保留
user_id在分组字段里,否则不同用户会被混在一起合并 -
MIN(start_time)和MAX(end_time)是标准做法,但若原始数据带毫秒级精度误差,建议先ROUND(start_time, 0)或用DATEADD(second, 1, LAG(end_time)) 容忍 1 秒偏差 - 如果需要保留原始记录中的其他字段(如来源表名、会话 ID),不能直接丢弃,得用
ARRAY_AGG(id)(PostgreSQL)或字符串拼接兜底;Hive 中可用COLLECT_LIST(id) - MySQL 8.0 支持该写法,但旧版不支持窗口帧
ROWS UNBOUNDED PRECEDING,需改用变量模拟;SQLite 完全不支持窗口函数,得换方案
真正难的不是写出语法正确的 SQL,而是当数据存在跨天未清空、类型标记缺失、时钟漂移等现实脏数据时,仍能让 LAG() 和 SUM() 的组合保持语义稳定——这要求你对排序键的业务含义有明确控制,而不是依赖数据“看起来整齐”。











