核心是用row_number()与转为整数天数的日期做差构造连续组标识(grp),相同差值即属同一连续段;必须加partition by user_id并严格order by login_date。

直接用 GROUP BY 没法识别孤岛,必须靠窗口函数构造“连续性标识”,核心是 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中完全相同 - 如果数据按小时粒度连续,就别用天数差——该用
EXTRACT(EPOCH FROM date)转秒再除 3600 - 差值不是最终结果,只是分组依据;
MIN(date)和MAX(date)才是孤岛的起止点
按用户或设备分组时,必须加 PARTITION BY
孤岛判定永远是“在某个业务维度内连续”,比如用户 A 的登录日和用户 B 的不能混。漏掉 PARTITION BY user_id 就会把所有人的记录拉通排序,结果全错。
-
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 天登录”当孤岛,却用小时级时间戳做差,结果一定碎成一地。











