LAG() 识别断点需配合时间差计算与布尔标记,再通过SUM()累计求和生成session_id实现断续区间划分,关键在准确表达业务阈值而非简单日期差。

LAG() 是识别断续活跃区间的起点,但单靠它只能标记“断点”,不能直接输出区间。必须配合布尔差分和累计求和,把断点转化为连续段编号。
用 LAG() 算时间差并标记断点
断续的本质是时间间隔超过业务阈值(比如 24 小时),不是日期是否相等。LAG(event_time) 能拿到上一行时间,但必须显式计算差值:
- PostgreSQL:
EXTRACT(EPOCH FROM (event_time - LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time))) / 3600 - MySQL:
TIMESTAMPDIFF(HOUR, LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time), event_time) - SQL Server:
DATEDIFF(HOUR, LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time), event_time)
再用 CASE WHEN interval_hours > 24 THEN 1 ELSE 0 END AS is_break 标记断点。注意首行 LAG() 返回 NULL,必须加 AND LAG(event_time) IS NOT NULL 过滤,否则整列可能变 NULL。
用 SUM() OVER 构造连续段 ID
ROW_NUMBER() 或 RANK() 不适合做段编号——它们按行计数,不反映逻辑连续性。真正稳定的做法是把 is_break 累计求和:
SUM(is_break) OVER (PARTITION BY user_id ORDER BY event_time ROWS UNBOUNDED PRECEDING) AS session_id- 这个
session_id在每次断点后 +1,天然形成连续活跃段的唯一标识 - 别漏写
PARTITION BY user_id,否则用户 A 的末次事件会和用户 B 的首次事件混算
后续所有聚合(如每段起止时间、事件数)都基于 user_id 和 session_id 分组即可。
为什么不能直接用 login_date - ROW_NUMBER()?
这个技巧只适用于「日期连续且无重复」的干净场景。一旦数据含以下情况,结果就失真:
- 同一天多次活跃(
ROW_NUMBER()递增,但login_date不变,差值跳变) - 原始字段是
TIMESTAMP未截断为DATE,导致同日不同秒被拆成多段 - 业务定义的“断续”不是按自然日,而是按小时/分钟阈值(如 4 小时不操作即算中断)
此时 LAG() + 时间差才是可解释、可调试、可对齐业务定义的路径。
聚合前必须处理的三个细节
输出最终的断续区间(如 start_time, end_time, duration_hours)前,这几个点容易被跳过:
- 去重:同一用户同一秒内多条记录要先
DISTINCT ON (user_id, event_time)(PostgreSQL)或用GROUP BY user_id, DATE(event_time), HOUR(event_time)(MySQL)归并 - 时区:如果
event_time是 UTC,而业务要求按本地时区判断断续,得先用AT TIME ZONE 'Asia/Shanghai'转换 - 排序稳定性:当
event_time相同时,ORDER BY event_time, event_id比单ORDER BY event_time更可靠,避免窗口函数行为不可复现
断续分析真正的复杂点不在窗口函数本身,而在如何把业务定义的“什么是断续”准确翻译成 SQL 中可计算的时间差逻辑——差值算错,后面全错。











