SQL分组查询连续登录超3天用户
时间:2026-07-07 07:03
利用ROW_NUMBER()对登录日期按用户分组排序编号,日期减去编号得到连续段锚点,再按用户和锚点分组统计天数,即可筛选出连续登录超过3天的用户。需注意日期格式统一、去重同一天多次登录,并为user_id和login_date建立联合索引以优化性能。
处理连续登录问题,核心思路其实就一句话:用`ROW_NUMBER()`给登录日期编号,再拿日期减去这个编号。你会发现,同一个连续段里,减出来的结果完全一致。比如,如果某用户在2024-01-01、01-02、01-03都有登录记录,编号分别是1、2、3,那么用日期分别减去对应编号,都会得到2023-12-31——这个共同的结果,就是连续段的“锚点”,也是我们分组判断的依据。
这里有三个细节值得留意:
* `ROW_NUMBER()`必须按`user_id`分组、并按`login_date`排序,否则编号顺序会全乱套。
* 日期字段的格式要统一。如果`login_date`是`DATETIME`类型、带有时分秒,得先通过`CAST(login_date AS DATE)`把它转成纯日期,不然计算会出问题。
* MySQL 8.0+、PostgreSQL、SQL Server这些主流数据库都原生支持`ROW_NUMBER()`;但如果你还在用MySQL 5.7或更早版本,就只得用变量来模拟了——不仅容易出错,还影响性能,建议尽快升级或者换方案。
得到锚点列(通常命名为`grp`或`group_id`),接下来的操作就很直观了:对`user_id`和`grp`做联合分组,然后用`COUNT(*)`统计每组的天数。
一个典型的SQL写法如下:
SELECT user_id
FROM (
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
) t
GROUP BY user_id, grp
HA VING COUNT(*) > 3;
注意一下`HA VING`条件:`COUNT(*) > 3`表示“超过3天”,也就是至少连续登录4天;如果需要“大于等于3天”,那么把`> 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`,不会有边界问题。但如果你用的是Oracle或PostgreSQL,日期运算的语法会有所不同:PostgreSQL需要写成`login_date - ROW_NUMBER() OVER (...)::INT`,Oracle则用`login_date - ROW_NUMBER() OVER (...)`。
### 性能优化的关键:一个恰当的联合索引
如果没有合适的索引,`PARTITION BY user_id ORDER BY login_date`这类操作在百万级数据上几乎必然导致全表扫描,速度会非常慢。
解决方案很明确:建立一个覆盖`user_id`和`login_date`的联合索引。
CREATE INDEX idx_user_login ON login_log (user_id, login_date);
这个索引能同时支撑分组、排序和去重,比两个单列索引的组合有效得多。如果你的表已经有主键包含了这两列,请确认一下列的顺序是否一致——`user_id`必须在前。
连续登录分析本身不复杂,真正的难点往往在数据质量上。空值、时区混杂、日志漏采,任何一个问题都可能让锚点计算失准。上线之前,一定记得用真实的样本数据把中间结果跑一遍验证一下。