GROUP BY本身不能识别连续性,必须用ROW_NUMBER()与日期作差构造恒定group_key来划分连续段,再按user_id和group_key分组统计天数。

GROUP BY 本身不能直接统计连续登录天数
这是最容易误解的一点:GROUP BY 只能按字段分组聚合,它不感知时间顺序或“连续性”。想算“连续登录X天”,必须先识别出连续的日期段,再对每一段计数——这需要窗口函数配合逻辑判断,GROUP BY 仅在最后一步用于归总(比如按用户统计最长连续天数)。
典型错误是写成这样:
SELECT user_id, COUNT(*) FROM login_log GROUP BY user_id, DATE(login_time)
这只能统计「登录频次」,不是「连续天数」。真正的连续性判断依赖 LAG() 或 ROW_NUMBER() 构造分组标识。
用 ROW_NUMBER() 差值法识别连续日期段
核心思路:对每个用户的登录日期排序,再用日期本身减去行号。同一连续段内,这个差值恒定。
实操建议:
- 先用
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY DATE(login_time))生成序号 - 把
DATE(login_time)转为天数(如TO_DAYS()in MySQL,login_time::date - '1970-01-01'::datein PostgreSQL) - 计算
date_part - row_num作为连续段 key - 再用
GROUP BY user_id, 连续段key统计该段天数
示例(PostgreSQL):
WITH ranked AS (
SELECT user_id, login_time::date AS d,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time::date) AS rn
FROM login_log
),
segments AS (
SELECT user_id, d, d - rn * INTERVAL '1 day' AS grp
FROM ranked
)
SELECT user_id, MIN(d) AS start_date, MAX(d) AS end_date, COUNT(*) AS days
FROM segments
GROUP BY user_id, grp;活跃度指标要区分定义再聚合
“活跃度”不是标准 SQL 概念,不同场景含义不同,GROUP BY 的作用取决于你定义什么:
- 若指「最近7天登录天数」:先过滤
WHERE login_time >= CURRENT_DATE - INTERVAL '6 days',再GROUP BY user_id+COUNT(DISTINCT DATE(login_time)) - 若指「平均单日登录次数」:按
user_id和DATE(login_time)两层GROUP BY,外层再AVG() - 若需加权(如夜间登录权重更高):必须先用
CASE WHEN算出单次权重,再SUM()后GROUP BY user_id
注意:MySQL 5.7 不支持窗口函数,得用变量模拟 ROW_NUMBER();而 SQLite 需升级到 3.25+ 才有 ROW_NUMBER()。别在旧环境硬套新语法。
性能和边界情况必须提前检查
连续登录统计在数据量大时容易慢,尤其没索引或日期格式混乱时:
- 确保
user_id和login_time有联合索引(顺序建议(user_id, login_time)) - 避免在
WHERE中对login_time用函数(如DATE(login_time)),会失效索引 - 空值、重复日期、跨午夜登录(如 23:59 和 00:01 算不算连续?业务要明确定义)
- 如果日志含时区信息,统一转为 UTC 或本地时区再截日期,否则同一天可能被拆成两天
最常被忽略的是:连续登录判定默认以“自然日”为单位,但有些产品要求“24小时内即算连续”。这时就不能用 DATE(),得用时间戳差值判断,整个逻辑就得重写——GROUP BY 依然只是最后收口工具,不是解题主力。

















