要搞定数据库里的“孤岛数据”——也就是连续登录、连续签到这类场景——关键思路其实就一句话:用 ROW_NUMBER() 和日期做差,差值相同的记录就属于同一连续段。听起来简单,但实操中坑不少,尤其是跨数据库兼容性和分组逻辑。下面把核心要点拆开说。

用 ROW_NUMBER() 和日期做差生成岛 ID
这里有个前提:日期本身不能直接减去行号,得先转成整数天数。为什么?因为不同数据库对日期加减的处理方式不一样,直接做差会闹出奇怪的结果。MySQL 用 TO_DAYS(date),PostgreSQL 用 DATE_PART('day', date::timestamp - '1970-01-01'::date),总之目的就是让日期变成一个可运算的数字。
- 排序必须严格一致:
ORDER BY date在ROW_NUMBER()里和后续GROUP BY中要一模一样,否则差值对不上,分组全乱。 - 连续粒度要匹配:如果你看的是“连续 3 天登录”,那就用天数差;如果数据是小时级(比如每两小时一条记录),就得用秒级或小时级差值——拿
EXTRACT(EPOCH FROM date)转秒再除 3600 就行。 - 别弄混:差值本身不是结果,只是分组的依据。真正的孤岛起止点要靠
MIN(date)和MAX(date)来取。
按用户或设备分组时,必须加 PARTITION BY
这个很容易被忽略:孤岛一定是在某个业务维度内定义的。比如用户 A 和用户 B 的登录日期不能混在一起算连续性。如果漏掉 PARTITION BY user_id,那 ROW_NUMBER() 会对所有记录拉通排序,结果必然是一团糟。
- 标准写法:
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date)。 - 如果需要多维分组(比如同时按
user_id和device_type),PARTITION BY里字段顺序不影响结果,但一定要和业务语义对齐。 - 注意:用了
PARTITION BY后,千万别再对整个结果集来个全局ORDER BY——那会打乱每个分区内的行号逻辑,结果同样报废。
MySQL 5.7 或旧版 SQL Server 怎么办
这些老版本不支持窗口函数,不少人想到用变量模拟 @rn := @rn + 1。这招风险极高:执行顺序不一定按你想象的那样来,遇到 LIMIT 或优化器重写,行号可能跳变或重复。生产环境里就是一颗定时冲击波。
- 更稳妥的方案:用自连接统计“比当前日期小的记录数”来生成行号,但数据量过万后性能明显下降。
- 生产建议:优先补全日期序列——在应用层生成日期范围,然后用
LEFT JOIN原表,用聚合函数找孤岛。这比硬扛变量方案可靠得多。 - 如果你非得用变量,至少加显式
ORDER BY,并考虑禁用查询缓存和某些优化开关,降低意外风险。
最后提一个容易被忽略的点:粒度匹配。拿“连续 3 天登录”当孤岛,却用小时级时间戳做差,结果一定碎成渣。差值类型必须和你业务上的连续性定义完全一致——这才是整个方案不出错的前提。
