LEAD/LAG需按user_id分组、visit_date升序排序,且先去重并截断时间戳;连续段识别须用日期减行号构造组标识,WHERE不可直接引用窗口别名,须用子查询或CTE,性能依赖(user_id, visit_date)复合索引。

LEAD 和 LAG 函数怎么填参数才不漏掉连续段
窗口函数本身不判断“连续”,只负责把前后行的日期拎出来;漏掉连续段,往往是因为 ORDER BY 没对齐业务粒度,或者没处理好重复访问(同一天多次访问)。比如用户一天刷三次,LAG(visit_date) 默认按行取上一行,可能拉到同一日的另一条记录,差值算出来是 0,不是 1。
- 必须用
DISTINCT ON (user_id, visit_date)或先聚合去重,确保每用户每天最多一条记录 -
LAG(visit_date) OVER (PARTITION BY user_id ORDER BY visit_date)是标准写法:分组按人、排序按日期升序,才能保证“前一天”语义准确 - 如果日期字段含时间戳(如
2024-03-01 14:22:05),得先用DATE(visit_time)截断,否则跨天但时间靠后的记录可能被排在前面
怎么用差值识别“连续”并分组打标
连续的本质是:当前日期 - 前一日期 = 1。但直接算差值只能标记相邻对,无法把多日连访归为一组。常见错误是只加一列 is_consecutive 布尔值,结果查不出“3 天以上连续”的用户。
- 正确做法是构造“连续组标识”:用
visit_date - INTERVAL '1 day' * ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY visit_date),相同结果值即属同一连续段 - PostgreSQL 支持
visit_date - ROW_NUMBER() OVER (...) * INTERVAL '1 day';MySQL 8.0+ 要用DATE_SUB(visit_date, INTERVAL ROW_NUMBER() OVER (...) - 1 DAY) - 这个表达式本质是把等差序列“拉平”——连续 n 天的日期减去对应序号,结果恒为起始日,从而实现分组依据
WHERE 条件里不能直接用窗口函数别名
很多人写完 SELECT *, visit_date - LAG(visit_date) OVER (...) AS diff,接着在 WHERE diff = 1,报错 column "diff" does not exist。这是 SQL 执行顺序决定的:WHERE 在窗口函数计算前就执行了。
- 必须用子查询或 CTE 包一层,例如:
WITH dated_diff AS ( SELECT user_id, visit_date, visit_date - LAG(visit_date) OVER (PARTITION BY user_id ORDER BY visit_date) AS diff FROM visits ) SELECT * FROM dated_diff WHERE diff = INTERVAL '1 day'; - MySQL 用户注意:
INTERVAL 1 DAY不能直接和 DATE 相减得数字,要用DATEDIFF(visit_date, LAG(visit_date) OVER (...)) = 1 - SQLite 不支持窗口函数中的
INTERVAL,得用julianday()差值:julianday(visit_date) - julianday(LAG(visit_date) OVER (...)) = 1
性能差?很可能是没建对索引
LEAD/LAG 自身不慢,但如果没有索引支撑 PARTITION BY + ORDER BY 组合,全表扫描+排序会让查询从毫秒变分钟,尤其在千万级访问日志表上。
- 必须建复合索引:
CREATE INDEX idx_user_date ON visits (user_id, visit_date); - 如果常按时间范围筛选(如“近 30 天”),可考虑分区表或加
WHERE visit_date >= '2024-03-01'提前剪枝 - 避免在
ORDER BY中用函数,比如ORDER BY DATE(visit_time)会让索引失效;应提前物化日期字段(加一列visit_date DATE并索引)
连续段识别真正卡住人的地方,从来不是函数怎么写,而是数据是否干净、索引是否到位、以及 WHERE 和窗口函数的执行阶段是否被混淆。这些点一错,结果要么空,要么乱,要么慢得不敢跑。










