别再写 COUNT 了!这才是查“连续登录三天”的正确 SQL 姿势🔥

141 阅读6分钟

🕵️‍♂️后端面试题分享:请用 SQL 找出连续登录三天的用户?


“请用 SQL 查出连续登录三天的用户。”
第一次看到这题,我的脑子一懵:「连续三天?这是要写日历程序吗?!🤯」


💥开场:看似简单,其实暗藏玄机

很多后端同学第一次看到这道题时,都会下意识地想写成:

SELECT user_id
FROM login_log
GROUP BY user_id
HAVING COUNT(DISTINCT login_date) >= 3;

✅ 结果?找出了登录过三天的用户。 ❌ 但不是“连续三天”!

举个例子👇

user_idlogin_date
12025-10-01
12025-10-02
12025-10-04

虽然用户 1 登录了三天,但并不连续,10-03 他翘班了(大概是出去浪🌴)。

所以,这道题的重点在于:「“连续”」 二字。


🧩思路分析:连续登录怎么判断?

别急着写 SQL,先想一想逻辑。

  • 我们要找到的,是那些登录日期之间**「相差 1 天」**的用户。
  • 如果能算出用户每次登录的“天数差”,那连续性就有迹可循了。

思路图解 🧠

假设我们有这样的登录记录:

user_idlogin_date
12025-10-01
12025-10-02
12025-10-03
22025-10-01
22025-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_daternDATE_SUB(login_date, INTERVAL rn DAY)
2025-10-0112025-09-30
2025-10-0222025-09-30
2025-10-0332025-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,更懂数据的“节奏感”。