如何用ROW_NUMBER快速计算SQL表中连续登录天数,构造等差数列?
- 内容介绍
- 相关推荐
本文共计738个文字,预计阅读时间需要3分钟。
由于用户登录日期可能不连续、存在重复、跨月/跨年跳跃,使用`DATEDIFF()`函数计算天数差会将中间缺失的天数也计入,无法真实反映连续登录的情况。而使用`ROW_NUMBER()`按登录时间排序后生成的递增序号,与日期本身结合构成可排序序列,只要`login_date + ROW_NUMBER()`的结果相同,即可说明这些日期是连续的(即差分数列为1)。
关键点在于:同一连续段内,login_date - ROW_NUMBER() 是常量;段与段之间这个值会跳变。
构造分组标识:用 DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (...) DAY)
MySQL 8.0+ 或 PostgreSQL 中需把日期转为可运算类型。以 MySQL 为例,核心是构造一个稳定的“连续段锚点”:
SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp FROM user_login;
这个 grp 就是每个连续登录段的唯一标识。之后按 user_id 和 grp 分组,COUNT(*) 就是该段连续天数。
注意:ROW_NUMBER() 必须加 PARTITION BY user_id,否则全表编号会混掉不同用户;ORDER BY login_date 缺失会导致分组错乱。
查最长连续天数:嵌套查询 + MAX(COUNT(*)) 不行,得用两层 GROUP BY
不能直接 GROUP BY user_id, grp 后再 MAX(COUNT(*))——SQL 不允许在聚合函数里嵌套聚合。正确写法是两步:
- 子查询先算出每个用户的每一段连续天数
- 外层按
user_id取MAX(segment_days)
SELECT user_id, MAX(segment_days) AS max_consecutive_days FROM ( SELECT user_id, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp, COUNT(*) AS segment_days FROM user_login GROUP BY user_id, grp ) t GROUP BY user_id;
如果只要全局最长(不区分用户),去掉外层 GROUP BY,直接 SELECT MAX(segment_days) 即可。
容易被忽略的边界问题:重复登录、时区、日期精度
若同一天多次登录,ROW_NUMBER() 会为每条记录分配不同序号,导致本应合并的日期被拆成多个“伪连续段”。解决方法是先去重:
- 用
DISTINCT user_id, DATE(login_time)或GROUP BY user_id, DATE(login_time)预处理 - PostgreSQL 可用
date_trunc('day', login_time)统一精度 - MySQL 若用
DATETIME字段,务必用DATE(login_time)提取日期,避免时分秒干扰
另外,ROW_NUMBER() 在窗口函数中默认无 ORDER BY 会报错,且 PARTITION BY 漏写会导致整个逻辑失效——这种错误不会报语法错,但结果完全不对,调试时要优先核对这两项。
本文共计738个文字,预计阅读时间需要3分钟。
由于用户登录日期可能不连续、存在重复、跨月/跨年跳跃,使用`DATEDIFF()`函数计算天数差会将中间缺失的天数也计入,无法真实反映连续登录的情况。而使用`ROW_NUMBER()`按登录时间排序后生成的递增序号,与日期本身结合构成可排序序列,只要`login_date + ROW_NUMBER()`的结果相同,即可说明这些日期是连续的(即差分数列为1)。
关键点在于:同一连续段内,login_date - ROW_NUMBER() 是常量;段与段之间这个值会跳变。
构造分组标识:用 DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (...) DAY)
MySQL 8.0+ 或 PostgreSQL 中需把日期转为可运算类型。以 MySQL 为例,核心是构造一个稳定的“连续段锚点”:
SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp FROM user_login;
这个 grp 就是每个连续登录段的唯一标识。之后按 user_id 和 grp 分组,COUNT(*) 就是该段连续天数。
注意:ROW_NUMBER() 必须加 PARTITION BY user_id,否则全表编号会混掉不同用户;ORDER BY login_date 缺失会导致分组错乱。
查最长连续天数:嵌套查询 + MAX(COUNT(*)) 不行,得用两层 GROUP BY
不能直接 GROUP BY user_id, grp 后再 MAX(COUNT(*))——SQL 不允许在聚合函数里嵌套聚合。正确写法是两步:
- 子查询先算出每个用户的每一段连续天数
- 外层按
user_id取MAX(segment_days)
SELECT user_id, MAX(segment_days) AS max_consecutive_days FROM ( SELECT user_id, DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp, COUNT(*) AS segment_days FROM user_login GROUP BY user_id, grp ) t GROUP BY user_id;
如果只要全局最长(不区分用户),去掉外层 GROUP BY,直接 SELECT MAX(segment_days) 即可。
容易被忽略的边界问题:重复登录、时区、日期精度
若同一天多次登录,ROW_NUMBER() 会为每条记录分配不同序号,导致本应合并的日期被拆成多个“伪连续段”。解决方法是先去重:
- 用
DISTINCT user_id, DATE(login_time)或GROUP BY user_id, DATE(login_time)预处理 - PostgreSQL 可用
date_trunc('day', login_time)统一精度 - MySQL 若用
DATETIME字段,务必用DATE(login_time)提取日期,避免时分秒干扰
另外,ROW_NUMBER() 在窗口函数中默认无 ORDER BY 会报错,且 PARTITION BY 漏写会导致整个逻辑失效——这种错误不会报语法错,但结果完全不对,调试时要优先核对这两项。

