🕵️♂️后端面试题分享:请用 SQL 找出连续登录三天的用户?
❝
“请用 SQL 查出连续登录三天的用户。”
第一次看到这题,我的脑子一懵:「连续三天?这是要写日历程序吗?!🤯」❞
💥开场:看似简单,其实暗藏玄机
很多后端同学第一次看到这道题时,都会下意识地想写成:
SELECT user_id
FROM login_log
GROUP BY user_id
HAVING COUNT(DISTINCT login_date) >= 3;
✅ 结果?找出了登录过三天的用户。 ❌ 但不是“连续三天”!
举个例子👇
| user_id | login_date |
|---|---|
| 1 | 2025-10-01 |
| 1 | 2025-10-02 |
| 1 | 2025-10-04 |
虽然用户 1 登录了三天,但并不连续,10-03 他翘班了(大概是出去浪🌴)。
所以,这道题的重点在于:「“连续”」 二字。
🧩思路分析:连续登录怎么判断?
别急着写 SQL,先想一想逻辑。
- 我们要找到的,是那些登录日期之间**「相差 1 天」**的用户。
- 如果能算出用户每次登录的“天数差”,那连续性就有迹可循了。
思路图解 🧠
假设我们有这样的登录记录:
| user_id | login_date |
|---|---|
| 1 | 2025-10-01 |
| 1 | 2025-10-02 |
| 1 | 2025-10-03 |
| 2 | 2025-10-01 |
| 2 | 2025-10-04 |
我们希望得到👇
| user_id | 连续登录天数 |
|---|---|
| 1 | ✅3 |
| 2 | ❌1 |
⚙️实现方法一:利用窗口函数检测连续登录(MySQL 8.0+)
WITH ordered_log AS (
SELECT
user_id,
login_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS rn
FROM login_log
),
grouped_log AS (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL rn DAY) AS grp
FROM ordered_log
)
SELECT
user_id
FROM grouped_log
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;
💡原理解释:
这个 DATE_SUB(login_date, INTERVAL rn DAY) 是关键!
想象一下:
| login_date | rn | DATE_SUB(login_date, INTERVAL rn DAY) |
|---|---|---|
| 2025-10-01 | 1 | 2025-09-30 |
| 2025-10-02 | 2 | 2025-09-30 |
| 2025-10-03 | 3 | 2025-09-30 |
因为这三天是连续的,减去行号后,得到的日期相同(2025-09-30)! 于是它们自动被归为同一组 ✅
是不是有点小聪明😎? 这招可以检测任何连续天数,只需改 HAVING COUNT(*) >= N 即可。
🐘那如果是 PostgreSQL 呢?
PostgreSQL 支持更灵活的时间运算,写法几乎一模一样:
WITH ordered_log AS (
SELECT
user_id,
login_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS rn
FROM login_log
),
grouped_log AS (
SELECT
user_id,
login_date,
(login_date::date - rn * INTERVAL '1 day') AS grp
FROM ordered_log
)
SELECT
user_id
FROM grouped_log
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;
✨差异说明:
- PostgreSQL 用
login_date::date做类型转换。 - 日期减去间隔直接支持
(date - interval)语法。 - 整体逻辑和 MySQL 一样,但语义更自然。
✅ 结果完全一致,甚至更优雅。
⚙️实现方法二:自连接法(适合老版本 MySQL)
如果数据库不支持窗口函数,也别慌,我们还有“穷举法”。
SELECT DISTINCT a.user_id
FROM login_log a
JOIN login_log b
ON a.user_id = b.user_id
AND DATEDIFF(b.login_date, a.login_date) = 1
JOIN login_log c
ON b.user_id = c.user_id
AND DATEDIFF(c.login_date, b.login_date) = 1;
解释一下👇 这就是在找满足:
day2 = day1 + 1
day3 = day2 + 1
的三元组 (a, b, c)。
虽然写起来有点“暴力”,但胜在通俗易懂。 就像在日志表里当侦探🕵️♂️, 挨个找出那些三天连刷签到的铁粉💪。
🚀实战思考:那性能呢?PARTITION BY 会不会炸?
好问题!
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) 在百万、千万甚至亿级数据量下,「确实可能成为性能瓶颈」。
别慌,我们可以从几个层面优化👇
🪣 1. 数据分区表
如果是每日登录日志,可以将表按日期分区:
CREATE TABLE login_log_202510
PARTITION OF login_log FOR VALUES FROM ('2025-10-01') TO ('2025-11-01');
✅ 优点:PostgreSQL/MySQL 都支持分区裁剪,自动跳过无关数据。
🧱 2. 建立合适的索引
CREATE INDEX idx_login_user_date ON login_log(user_id, login_date);
窗口函数内部要按 user_id 分组、login_date 排序, 这个索引**「刚好能被完全利用」**,极大减少排序成本。
🧮 3. 限制分析范围
一般业务只关心最近 N 天的连续登录,比如 30 天:
WHERE login_date >= current_date - INTERVAL '30 day'
✅ 让窗口函数处理的数据量可控,性能直接上升一个台阶。
💾 4. 区分实时与离线统计
如果数据量真到上亿级别,这种分析最好变为**「离线任务」**:
- 实时统计 ➜ Redis + 定时任务维护连续天数
- 离线统计 ➜ PostgreSQL / ClickHouse 批量跑窗口函数
📊 真正的后端设计思维是:「不让数据库替我们背锅」。
🧠 5. PostgreSQL 进阶优化(可选)
PostgreSQL 14+ 开始支持 「incremental sort」:
SET enable_incremental_sort = on;
它会对相同 user_id 的数据块进行“渐进排序”, 性能相比传统窗口函数快非常多。
🔍发散一下:这个题其实很有延伸性
这种“连续性判断”的逻辑,不只用于登录记录。
你可以用在:
- ✅ 「用户连续签到系统」
- ✅ 「设备连续在线监控」
- ✅ 「股价连续上涨/下跌趋势分析」
- ✅ 「会员活跃天数奖励机制」
甚至可以结合 Redis + CronJob 做实时统计,比如:
❝
“连续登录 7 天送 VIP 一天”, 当某天断签,就从头开始计数。
❞
听起来是不是更像一个真实项目的功能?😉
🧠小结:别只写 SQL,更要想“连续”的本质
其实,这类题考的并不是你 SQL 有多熟, 而是考你能否用“「规律抽象」”的方式去思考数据。
连续登录,不是“登录了几次”, 而是“这些登录是否相邻”。
懂了这一层,你就能写出更优雅、更具扩展性的查询逻辑。
🧮 附加玩法:最长连续天数排行榜
你还可以扩展出“最长连续登录天数”查询👇
SELECT user_id, MAX(cnt) AS max_streak
FROM (
SELECT user_id, grp, COUNT(*) AS cnt
FROM grouped_log
GROUP BY user_id, grp
) t
GROUP BY user_id;
这样就能直接生成“连续登录榜单”了🔥
❝
❤️ 「最后一句话」 面试题只是入口, 真正的收获,是你理解背后的数据规律。
下次再遇到类似问题,你就能自信地说: “连续?这个我懂,SQL 都写顺手了~😎”
❞
📮 如果你觉得这篇文章对你有帮助, 点个 「“在看”」 或 「分享给朋友」, 让我知道你也在偷偷变强💪!
📚 「总结一句话:」
❝
MySQL 写得巧,PostgreSQL 跑得稳。 真正的高手,不止会写 SQL,更懂数据的“节奏感”。
❞