核心思路是用ROW_NUMBER()编号后日期减编号得相同锚点日期以标识连续段;需按user_id分组排序、统一日期类型、建联合索引优化性能,并注意去重和数据库语法差异。
核心思路是:把每个用户的登录日期按顺序编号,再用日期减去编号,同一连续段的结果会相同。比如 2024-01-01、02、03 编号为 1/2/3,相减都得 2023-12-31 —— 这个“锚点日期”就是连续段的起始基准。
实操时注意三点:
ROW_NUMBER() 必须按 user_id 分组、login_date 排序,否则编号错乱login_date 不能是 DATETIME 或带时分秒,否则需先 CAST(login_date AS DATE)
ROW_NUMBER();MySQL 5.7 及更早版本需用变量模拟,容易出错,建议升级或换方案构造出锚点列(比如叫 grp)后,对 user_id 和 grp 联合分组,再用 COUNT(*) 算连续天数。
典型写法:
SELECT user_idFROM ( 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 login_log) tGROUP BY user_id, grpHAVING COUNT(*) > 3;
注意:HAVING COUNT(*) > 3 表示“超过3天”,即至少4天连续;若要“≥3天”,改用 >= 3。
用户同一天多次登录会导致 COUNT(*) 偏大,必须先去重:
DISTINCT user_id, login_date,或用 GROUP BY user_id, login_date 预聚合DATE_SUB 在 MySQL 中能正确处理如 2023-12-31 - INTERVAL 1 DAY = 2023-12-30,无需额外适配DATE 运算语法不同:PostgreSQL 用 login_date - ROW_NUMBER() OVER (...)::INT,Oracle 用 login_date - ROW_NUMBER() OVER (...)
没索引时,PARTITION BY user_id ORDER BY login_date 会触发全表扫描,百万级数据可能卡住。
建索引命令:
CREATE INDEX idx_user_login ON login_log (user_id, login_date);
这个联合索引同时支撑分组、排序和去重,比单列索引有效得多。如果表已有主键含这两列,确认顺序是否匹配——user_id 必须在前。
连续登录分析本身不难,难点在数据质量:空值、时区混杂、日志漏采都会让 grp 计算失准,上线前务必用真实样本跑一遍中间结果。