跳到主要内容

字节跳动大数据面试SQL-用户的第二天的留存率

一、题目背景

这道题来自字节跳动用户增长部门的数据分析岗面试。留存率是衡量产品"粘性"的核心指标——用户今天来了,明天还会不会再来?对于抖音、今日头条这样的内容产品,次日留存率直接反映了推荐算法的质量和内容的吸引力。

业务场景:DAU 只能告诉你有多少人来过,留存率才能告诉你有多少人留下来了。投资人看留存,产品经理盯留存,面试官也爱考留存。

二、题目

现有一张用户登录日志表 t9_zj_user_login,记录了用户每天的登录情况(每天每个用户至多一条记录)。请计算每天的新增用户的次日留存率。

t9_zj_user_login 表

+----------+------------+
| user_id | login_date |
+----------+------------+
| 1 | 2025-01-01 |
| 2 | 2025-01-01 |
| 3 | 2025-01-01 |
| 1 | 2025-01-02 |
| 2 | 2025-01-02 |
| 4 | 2025-01-02 |
| 5 | 2025-01-02 |
| 1 | 2025-01-03 |
| 3 | 2025-01-03 |
| 4 | 2025-01-03 |
+----------+------------+

定义

  • 新增用户:某天首次登录的用户(之前从未出现过)
  • 次日留存:新增用户在第二天再次登录
  • 次日留存率 = 次日留存的用户数 / 新增用户数

三、思路分析

核心是"锚定首次登录日期" + "判断次日是否登录",分三步:

  1. 找首次登录日MIN(login_date) GROUP BY user_id 确定每个用户是哪天新增的
  2. 关联次日登录:LEFT JOIN 原表,关联条件为 同用户 AND 登录日 = 首次登录日 + 1
  3. 计算留存率:按首次登录日分组,COUNT(次日user_id) / COUNT(新增user_id)
维度评分
题目难度⭐️⭐️⭐️
题目清晰度⭐️⭐️⭐️⭐️
业务常见度⭐️⭐️⭐️⭐️⭐️

四、逐步推导

步骤1:找出每个用户的首次登录日期

SELECT
user_id,
MIN(login_date) AS first_date
FROM t9_zj_user_login
GROUP BY user_id;
+----------+-------------+
| user_id | first_date |
+----------+-------------+
| 1 | 2025-01-01 |
| 2 | 2025-01-01 |
| 3 | 2025-01-01 |
| 4 | 2025-01-02 |
| 5 | 2025-01-02 |
+----------+-------------+
5 rows selected (0.701 seconds)

1月1日新增用户1/2/3,1月2日新增用户4/5。

步骤2:关联次日登录记录

用首次登录表 LEFT JOIN 登录日志表,关联条件为同用户且登录日为首次登录日+1。

WITH first_login AS (
SELECT user_id, MIN(login_date) AS first_date
FROM t9_zj_user_login
GROUP BY user_id
)
SELECT
f.first_date,
f.user_id AS new_user,
t.user_id AS retained_user
FROM first_login f
LEFT JOIN t9_zj_user_login t
ON f.user_id = t.user_id
AND t.login_date = DATE_ADD(f.first_date, 1);

执行结果

+-------------+-----------+----------------+
| first_date | new_user | retained_user |
+-------------+-----------+----------------+
| 2025-01-01 | 1 | 1 |
| 2025-01-01 | 2 | 2 |
| 2025-01-01 | 3 | NULL |
| 2025-01-02 | 4 | 4 |
| 2025-01-02 | 5 | NULL |
+-------------+-----------+----------------+
5 rows selected (0.625 seconds)

retained_user 为 NULL 表示该用户次日未登录。

步骤3:聚合计算留存率

WITH first_login AS (
SELECT user_id, MIN(login_date) AS first_date
FROM t9_zj_user_login
GROUP BY user_id
)
SELECT
f.first_date AS date,
COUNT(DISTINCT f.user_id) AS new_users,
COUNT(DISTINCT t.user_id) AS day1_retained,
ROUND(COUNT(DISTINCT t.user_id) * 100.0 / COUNT(DISTINCT f.user_id), 1) AS day1_retention_pct
FROM first_login f
LEFT JOIN t9_zj_user_login t
ON f.user_id = t.user_id
AND t.login_date = DATE_ADD(f.first_date, 1)
GROUP BY f.first_date
ORDER BY f.first_date;

最终结果

+-------------+------------+----------------+---------------------+
| date | new_users | day1_retained | day1_retention_pct |
+-------------+------------+----------------+---------------------+
| 2025-01-01 | 3 | 2 | 66.7 |
| 2025-01-02 | 2 | 1 | 50.0 |
+-------------+------------+----------------+---------------------+
2 rows selected (1.419 seconds)

1月1日新增3人,次日回来2人,留存率 66.7%。用户3虽然在1月3日又登录了,但不满足"次日"条件,不算次日留存。

五、常见坑点

坑1:LEAD 方案的隐蔽陷阱

有人用 LEAD(login_date) OVER (PARTITION BY user_id ORDER BY login_date) 判断次日是否登录。这个方法的问题是:如果用户只登录过一次,LEAD 返回 NULL,看起来像是"没留存"——这倒没错。但 LEAD 方案需要先获取全量登录记录再做窗口,代码更复杂,不如 JOIN 方案直观。

坑2:不能忘记 COUNT DISTINCT

虽然本例每天每用户只有一条记录,但实际数据可能有多条(如多设备登录)。用 COUNT(DISTINCT user_id) 更安全。

坑3:最近日期无法计算留存

最后一天(如1月3日)新增的用户,还没有"次日"数据,留存率会是 0 或 NULL。实际业务中通常排除最近N天的新增用户。

六、举一反三

  1. 3日/7日留存:将 DATE_ADD(first_date, 1) 改为 37,可一次算出多日留存
  2. 按渠道维度:如果登录表有 channel 字段,GROUP BY 加上渠道,可对比不同渠道的留存质量
  3. 留存曲线:写一个循环生成 DATE_ADD 1-30 天的 LEFT JOIN,画出完整的留存衰减趋势图

七、知识点总结

考点说明
MIN + GROUP BY锚定首次登录日期作为"新增日"
LEFT JOIN + DATE_ADD判断次日是否留存,NULL = 未留存
COUNT DISTINCT去重避免多次登录干扰
留存率公式COUNT(次日user_id) * 100.0 / COUNT(新增user_id)

八、建表语句和数据插入

点击展开 DDL & DML
CREATE TABLE IF NOT EXISTS t9_zj_user_login (
user_id INT,
login_date STRING
);

INSERT INTO t9_zj_user_login VALUES
(1, '2025-01-01'),
(2, '2025-01-01'),
(3, '2025-01-01'),
(1, '2025-01-02'),
(2, '2025-01-02'),
(4, '2025-01-02'),
(5, '2025-01-02'),
(1, '2025-01-03'),
(3, '2025-01-03'),
(4, '2025-01-03');
📱关注公众号

「数据仓库技术」文章同步更新,不错过每一篇干货

微信公众号二维码
💬加群交流

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

交流微信二维码

你可能还想看