必须先去重再用ROW_NUMBER(),否则同天多次登录会导致行号错误;正确做法是先SELECT DISTINCT user_id, CAST(login_time AS DATE) AS login_date去重,再窗口计算。

SQL Server 2022 中必须先去重再用 ROW_NUMBER()
直接对原始日志表(含时间戳、重复记录)跑窗口函数,结果一定错。SQL Server 不会自动帮你按天聚合,同一天多次登录会导致 ROW_NUMBER() 多计,后续日期减行号的差值就乱了。
正确做法是:先用 SELECT DISTINCT user_id, CAST(login_time AS DATE) AS login_date 抽出每个用户每天唯一登录日,再进窗口计算。如果原始字段是 DATETIME2,CAST 比 CONVERT 更明确、不易受语言设置影响。
- 别用
DATEADD(DAY, DATEDIFF(DAY, 0, login_time), 0)—— 冗长且可读性差 - 如果表里有未来日期(比如测试数据或时区错配),必须加
WHERE login_date 过滤,否则分组会把“还没发生的连续”算进去 - 去重后建议加索引:
CREATE INDEX IX_user_login_date ON login_log (user_id, login_date),大幅提升PARTITION BY user_id ORDER BY login_date的执行效率
DATEADD 和 ROW_NUMBER 相减才是关键分组键
核心逻辑不是“找相邻差1”,而是构造一个在连续日期下恒定的值:DATEADD(DAY, -rn, login_date)。这个值就是“连续组标识”。只要日期连续,减去递增的行号后,结果日期就固定不变。
注意:SQL Server 不支持 MySQL 风格的 DATE_SUB(login_date, INTERVAL rn DAY),必须用 DATEADD;且 rn 是从 1 开始的,所以是 -rn,不是 -(rn-1)。
- 错误写法:
login_date - rn—— SQL Server 会尝试隐式转成数字,报错或得诡异结果 - 正确写法:
DATEADD(DAY, -rn, login_date),类型安全,语义清晰 - 这个表达式结果是
DATE类型,可直接用于GROUP BY,无需额外CAST
查“当前连续多少天”得倒序窗口 + LEAD 辅助判断
如果需求是“截至今天,用户最近连续登录了多少天”,就不能只分组聚合——你得从最新一条登录开始,逐条验证是否和前一天连续。这时候正序 ROW_NUMBER() 不够用。
方案是:先按 user_id 分组,ORDER BY login_date DESC 倒排,用 LEAD(login_date, 1) OVER (...) 取下一天(即更早一天)的日期,再比对差值是否为 1。
- 必须用
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW显式指定窗口帧,否则默认帧可能不包含所有前置行 - 若用户今天没登录,但你想知道“如果登了就是第 N 天”,得手动
UNION ALL一条GETDATE()虚拟记录,再跑倒序逻辑 - 别忘了
HAVING MAX(login_date) = GETDATE(),否则会把历史某段连续(非当前)也捞出来
SQLite 或 MySQL 5.7 用户别硬套这个写法
SQL Server 2022 支持完整窗口函数,但如果你实际连的是旧版 SQL Server(2012+)、PostgreSQL 或 MySQL 8.0+,这套逻辑基本通用;可一旦落到 SQLite 或 MySQL 5.7,ROW_NUMBER() 就不可用,变量模拟风险极高——执行计划稍一变,@rn 就乱序。
真要兼容,要么提前用应用层生成序列号,要么改用自连接 + DATEDIFF,但千万注意:自连接在百万级登录表上极易超时,且无法利用索引跳过中间行。
最易被忽略的一点:所有日期字段必须是 DATE 类型。哪怕只是 CAST(login_time AS DATE) 一次,也比在每层子查询里反复转换强——SQL Server 的表达式重用机制并不总如你所愿。

















