核心思路是先去重并提取日期,再用lag/lead判断相邻日期是否连续,或用“日期-行号”恒定值分组识别连续段;需注意null过滤、显式dateadd、时区定义等细节。

用 LAG 和 LEAD 判断相邻登录日期是否连续
核心思路是:对每个用户按登录时间排序,取前一条和后一条的日期,看是否构成“当天-前1天-前2天”或“当天-后1天-后2天”这样的连续三元组。注意必须先去重(同一用户同一天多次登录只算1次),再按 user_id 分区、login_date 排序。
常见错误是直接对原始日志用 LAG(login_date, 1),结果因重复记录导致偏移错位。正确做法是先 SELECT DISTINCT user_id, CAST(login_time AS DATE) AS login_date FROM login_log 构建干净日期集。
-
LAG(login_date, 1)返回前1行的日期,LAG(login_date, 2)返回前2行的日期 - 判断连续:
login_date = DATEADD(day, 1, LAG(login_date, 1)) AND LAG(login_date, 1) = DATEADD(day, 1, LAG(login_date, 2)) - SQL Server 不支持
LAG(..., 2)在同一表达式中嵌套调用,需拆成独立列或用 CTE 预计算
用 DATEDIFF + 行号差值法识别连续段
这是更健壮的做法:给每个用户的每日登录记录打一个递增序号(ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date)),再用 login_date 减去这个序号——连续日期的“日期 - 序号”值恒定,可作为分组依据。
例如用户 A 在 2024-05-01、05-02、05-03 登录,对应序号 1/2/3,则 DATEADD(day, -1, '2024-05-01')、DATEADD(day, -2, '2024-05-02')、DATEADD(day, -3, '2024-05-03') 全部等于 2024-04-30,于是能聚合成一组。
- 必须用
CAST(login_time AS DATE)或CONVERT(DATE, login_time)统一日期粒度 - 分组后用
HAVING COUNT(*) >= 3筛出至少3天的组 - 若需返回具体哪3天,可在外层再关联原表或用
STRING_AGG(SQL Server 2017+)拼接
处理跨月/跨年时 DATEADD 的边界问题
SQL Server 的 DATEADD(day, -n, date) 能正确处理月末(如 DATEADD(day, -1, '2024-03-01') 得到 '2024-02-29'),无需额外判断。但要注意:若原始数据含 NULL 登录时间,ROW_NUMBER() 仍会分配序号,导致差值异常;务必在 CTE 中先 WHERE login_time IS NOT NULL 过滤。
- 避免用
login_date - ROW_NUMBER()这种隐式转换(SQL Server 不允许 date 直接减 int) - 必须显式写成
DATEADD(day, -ROW_NUMBER() OVER (...), login_date) - 如果服务器兼容级别 OVER 子句中的
ORDER BY,此方案不可用
性能关键点:索引与数据量预判
当用户量超百万、日志表达亿级时,ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) 会触发全局排序,极易成为瓶颈。此时应优先考虑加复合索引:CREATE INDEX IX_user_login ON login_log (user_id, login_time),并确保 login_time 列有非空约束或过滤条件。
- 不要在大表上直接跑窗口函数查全量;先用
WHERE login_time >= DATEADD(day, -30, GETDATE())限定时间范围 - 如果只要“是否存在连续3天”,用
EXISTS+ 自连接(t1.login_date = DATEADD(day, 1, t2.login_date) AND t2.login_date = DATEADD(day, 1, t3.login_date))可能比窗口函数更快 - 临时表存中间结果(如去重后的用户-日期对)比反复扫描原表更可控
实际业务中,“连续3天”的定义常隐含时区、日切逻辑(比如按自然日还是按用户本地时间),这些细节往往比窗口函数本身更耗调试时间。










