字节跳动大数据面试SQL-最大连续登录天数
一、题目背景
这道题是 SQL 面试中"连续问题"的终极形态,字节、阿里、腾讯的面试中反复出现。它的核心技巧——日期减 ROW_NUMBER 差值法——可以用来解决所有"连续XXX"类问题。
业务场景:用户运营需要找出"高活跃用户"(连续登录天数最长的用户),给他们推送 VIP 权益。同时也要找出"即将流失的用户"(连续登录天数突然下降)。
二、题目
现有一张用户登录日志表 t17_zj_user_login,记录了用户每天的登录情况(每天每用户至多一条记录)。请找出每个用户的最大连续登录天数。
t17_zj_user_login 表
+----------+-------------+
| user_id | login_date |
+----------+-------------+
| 1 | 2025-01-01 |
| 1 | 2025-01-02 |
| 1 | 2025-01-03 |
| 1 | 2025-01-05 |
| 1 | 2025-01-06 |
| 1 | 2025-01-07 |
| 2 | 2025-01-01 |
| 2 | 2025-01-02 |
| 2 | 2025-01-04 |
| 2 | 2025-01-05 |
| 3 | 2025-01-03 |
| 3 | 2025-01-04 |
+----------+-------------+
期望输出:每个用户的最大连续登录天数。
三、思路分析
核心是日期 - ROW_NUMBER 差值法:
- 按 user_id 分区,按 login_date 排序,生成 ROW_NUMBER
- 计算
login_date - ROW_NUMBER,连续的日期会得到相同的差值 - 按差值分组,COUNT 求每组连续天数,MAX 取最大值
原理图解:
用户1 的日期:
01-01 → ROW_NUMBER=1 → 差值=01-01 - 1 = 12-31
01-02 → ROW_NUMBER=2 → 差值=01-02 - 2 = 12-31 ← 相同的差值!
01-03 → ROW_NUMBER=3 → 差值=01-03 - 3 = 12-31 ← 相同的差值!
01-05 → ROW_NUMBER=4 → 差值=01-05 - 4 = 01-01 ← 断了一天,差值变了
01-06 → ROW_NUMBER=5 → 差值=01-06 - 5 = 01-01
01-07 → ROW_NUMBER=6 → 差值=01-07 - 6 = 01-01
分组1 (差值12-31): 3天 | 分组2 (差值01-01): 3天 → 最大3天
| 维度 | 评分 |
|---|---|
| 题目难度 | ⭐️⭐️⭐️⭐️ |
| 题目清晰度 | ⭐️⭐️⭐️ |
| 业务常见度 | ⭐️⭐️⭐️⭐️⭐️ |
四、逐步推导
步骤1:生成 ROW_NUMBER
Spark SQL
SELECT
user_id,
login_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS rn
FROM t17_zj_user_login;
执行结果
+----------+-------------+-----+
| user_id | login_date | rn |
+----------+-------------+-----+
| 1 | 2025-01-01 | 1 |
| 1 | 2025-01-02 | 2 |
| 1 | 2025-01-03 | 3 |
| 1 | 2025-01-05 | 4 |
| 1 | 2025-01-06 | 5 |
| 1 | 2025-01-07 | 6 |
| 2 | 2025-01-01 | 1 |
| 2 | 2025-01-02 | 2 |
| 2 | 2025-01-04 | 3 |
| 2 | 2025-01-05 | 4 |
| 3 | 2025-01-03 | 1 |
| 3 | 2025-01-04 | 2 |
+----------+-------------+-----+
12 rows selected (8.494 seconds)(https://www.dwsql.com)
步骤2:日期减 ROW_NUMBER
这次咱们直接使用减完的日期作为分组字段,之前都是使用差值作为分组字段,其实差值用什么都可以,序号、差值、连续的开始日期等等,只要保证用户内不重复就可以。
Spark SQL
SELECT
user_id,
login_date,
DATE_SUB(login_date, rn) AS grp
FROM (
SELECT user_id, login_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS rn
FROM t17_zj_user_login
) t;
执行结果
+----------+-------------+-------------+
| user_id | login_date | grp |
+----------+-------------+-------------+
| 1 | 2025-01-01 | 2024-12-31 |
| 1 | 2025-01-02 | 2024-12-31 |
| 1 | 2025-01-03 | 2024-12-31 |
| 1 | 2025-01-05 | 2025-01-01 |
| 1 | 2025-01-06 | 2025-01-01 |
| 1 | 2025-01-07 | 2025-01-01 |
| 2 | 2025-01-01 | 2024-12-31 |
| 2 | 2025-01-02 | 2024-12-31 |
| 2 | 2025-01-04 | 2025-01-01 |
| 2 | 2025-01-05 | 2025-01-01 |
| 3 | 2025-01-03 | 2025-01-02 |
| 3 | 2025-01-04 | 2025-01-02 |
+----------+-------------+-------------+
12 rows selected (0.409 seconds)(https://www.dwsql.com)
步骤3:按差值分组,求最长连续天数
Spark SQL
SELECT
user_id,
MAX(consecutive_days) AS max_consecutive_days
FROM (
SELECT
user_id,
grp,
COUNT(*) AS consecutive_days
FROM (
SELECT
user_id,
login_date,
DATE_SUB(login_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date)) AS grp
FROM t17_zj_user_login
) t
GROUP BY user_id, grp
) t2
GROUP BY user_id
ORDER BY user_id;
最终结果:
+----------+-----------------------+
| user_id | max_consecutive_days |
+----------+-----------------------+
| 1 | 3 |
| 2 | 2 |
| 3 | 2 |
+----------+-----------------------+
3 rows selected (0.805 seconds)(https://www.dwsql.com)
五、常见坑点
坑1:DATE_SUB 的参数顺序
DATE_SUB(date, days) —— 第一个是日期,第二个是天数。如果用错成 DATE_SUB(rn, login_date) 会得到错误结果或者报错。
坑2:Spark SQL 的 DATE_SUB
Spark SQL 中 DATE_SUB(login_date, rn) 正确处理整数 rn。但在 Hive 中可能需要 DATE_SUB(login_date, CAST(rn AS INT))。
坑3:同一天多条登录记录
本例假设每天每用户只有一条记录。如果有重复,需要先 SELECT DISTINCT user_id, login_date 去重,否则 ROW_NUMBER 会编号到同一天的重复记录,差值计算会出错。
六、举一反三
- 允许间隔1天:把
DATEDIFF = 1改为DATEDIFF <= 2,允许中间断1天也算连续 - 连续活跃N周:把日期先 trunc 到周
DATE_TRUNC('week', login_date),再套用差值法 - 当前连续天数:不只是历史最大,还要知道"截止今天正在连续的最近一段有多少天"
七、知识点总结
| 考点 | 说明 |
|---|---|
| ROW_NUMBER + PARTITION BY | 按用户分区编号 |
| 日期 - ROW_NUMBER | Gaps and Islands 差值法核心 |
| 相同差值 = 连续 | 连续日期的数学特性 |
| DATE_SUB | 日期减去整数天数 |
| MAX + GROUP BY | 取得每个用户的最长连续段 |
八、建表语句和数据插入
点击展开 DDL & DML
CREATE TABLE IF NOT EXISTS t17_zj_user_login (
user_id INT,
login_date STRING
);
INSERT INTO t17_zj_user_login VALUES
(1, '2025-01-01'),(1, '2025-01-02'),(1, '2025-01-03'),
(1, '2025-01-05'),(1, '2025-01-06'),(1, '2025-01-07'),
(2, '2025-01-01'),(2, '2025-01-02'),(2, '2025-01-04'),(2, '2025-01-05'),
(3, '2025-01-03'),(3, '2025-01-04');
「数据仓库技术」文章同步更新,不错过每一篇干货

备注「数据仓库技术」加入社群,每日一道大厂SQL真题
