横扫SQL面试
📌 用户留存率问题

📝 题目题干
某APP的用户登录明细表 login_detail
记录了用户的登录行为,需要计算以下两类指标:
📊 数据表结构
login_detail 用户登录表
字段名 | 类型 | 说明 |
---|---|---|
user_id | BIGINT | 用户ID(唯一标识) |
event_time | TIMESTAMP | 登录时间(精确到秒) |
- 标准留存率🌟 :用户在首次登录后第N天(精确日期)再次登录的比例
- 时间段活跃率🌟 :用户在首次登录后的N天内(任意一天)活跃的比例
编写SQL查询,返回以下指标(保留2位小数):💻🔍💻🔍
- 次日留存率(1_day_retention_rate)
- 3日留存率(3_day_retention_rate)
- 7日留存率(7_day_retention_rate)
- 3天窗口期活跃率(3_day_activity_rate)
- 7天窗口期活跃率(7_day_activity_rate)
精准定位问题时间点(例如用户在第3天流失率高),指导针对性改进(如第2天推送优惠券)。
user_id | event_time |
---|---|
1001 | 2023-08-01 09:30:15 |
1001 | 2023-08-02 10:15:00 |
1002 | 2023-08-01 14:20:30 |
1003 | 2023-08-01 18:45:00 |
1002 | 2023-08-03 11:00:00 |
1003 | 2023-08-05 19:30:00 |
-
用户1001 :
首次登录日期=2023-08-01
次日留存检查:2023-08-02 ✅(存在记录)
3日窗口活跃检查:2023-08-02~2023-08-04 ✅(有8月2日记录)
-
用户1003 :
首次登录日期=2023-08-01
7日留存检查:2023-08-08 ❌(无记录)
7天窗口活跃检查:2023-08-02~2023-08-08 ✅(有8月5日记录)
🎯 核心思路
一:标准留存率:首次登录后第N天(精确日期)
标准留存率 精准日期检查 DATE(event_time) = 首次登录 +N天
用户粘性分析、行业报告🌟🌟
📌 步骤1:计算用户首次登录日期
sql
with first_login as (
select
user_id,
min(date(event_time)) as first_login_date
from login_detail
group by user_id
)
- 从
login_detail
表中提取每个用户的最早登录日期 🍕. 🔍. 🔍 - 使用
MIN(date(event_time))
获取首次登录的日期(忽略时间部分) - 按
user_id
分组确保每个用户仅一条记录
得到的中间表 first_login
~🚀🚀🚀 ✅
user_id | first_login_date |
---|---|
1001 | 2023-08-01 |
1002 | 2023-08-01 |
1003 | 2023-08-01 |
📌 步骤2:验证用户留存状态
sql
retention_check as (
select
-- 从 first_login 中选取用户 ID
fl.user_id,
-- 检查次日留存情况
-- 使用 CASE WHEN EXISTS 语句来判断是否存在满足条件的记录
-- 如果用户在首次登录日期的次日有登录记录,则标记为 1,表示留存
-- 否则标记为 0,表示未留存
case when exists (
-- 子查询,用于检查是否存在满足条件的记录
select 1
-- 从用户登录明细表 login_detail 中查询数据
from login_detail ld
-- 确保子查询中的用户 ID 与 first_login 中的用户 ID 一致
where ld.user_id = fl.user_id
-- 确保登录日期为首次登录日期的次日
and date(ld.event_time) = fl.first_login_date + interval 1 day
) then 1 else 0 end as retained_1,
... -- 类似逻辑计算retained_3/retained_7
from first_login fl
)
处理过程 ✅
- 遍历
first_login
中每个用户 - 对每个用户执行三次子查询:
- 次日留存 :检查是否存在
first_login_date + 1天
的登录 ✅ - 3日留存 :检查是否存在
first_login_date + 3天
的登录 ✅ - 7日留存 :检查是否存在
first_login_date + 7天
的登录 ✅
- 次日留存 :检查是否存在
- 存在则标记1,否则标记0

中间表 retention_check
✅
user_id | retained_1 | retained_3 | retained_7 |
---|---|---|---|
1001 | 1 | 0 | 0 |
1002 | 0 | 1 | 0 |
1003 | 0 | 0 | 1 |
- 用户1001:次日在2023-08-02登录 ✅
- 用户1002:第3天在2023-08-04登录 ✅
- 用户1003:第7天在2023-08-08登录 ✅
📌 步骤3:计算留存率
sql
用户旅程示意图:
首次登录日 次日 第3天 第7天
│ │ │ │
├─────────────┼────────────┼────────────┤
2023-08-01 2023-08-02 2023-08-04 2023-08-08
(用户1001) ✅ ❌ ❌
(用户1002) ❌ ✅ ❌
(用户1003) ❌ ❌ ✅
sql
select
round(avg(retained_1)*100, 2) as retention_1_day_rate,
round(avg(retained_3)*100, 2) as retention_3_day_rate,
round(avg(retained_7)*100, 2) as retention_7_day_rate
from retention_check;
处理过程 ✅
- 对
retention_check
表的标记列取平均值avg(retained_1)
= (1+0+0)/3 = 0.3333- 转化为百分比:0.3333 × 100 = 33.33%
- 同理计算其他留存率
最终结果表 ✅
retention_1_day_rate | retention_3_day_rate | retention_7_day_rate |
---|---|---|
33.33 | 33.33 | 33.33 |
sql
-- 核心逻辑:精确匹配首次登录+N天的日期
WHERE date(ld.event_time) = fl.first_login_date + interval N day
Tips:Avg函数求比率🤣 🤣 🤣 🤣📚 给新手的AVG函数计算比率小课堂
💡 核心原理✅
AVG( [1,0,1,1,0] ) = (1+0+1+1+0)/5 = 3/5 = 0.6 → 60%
🌰 举个栗子
假设有3个用户:
sql
| user_id | retained_1 |
|---------|------------|
| A | 1 |
| B | 0 |
| C | 1 |
计算过程:
(1+0+1)/3 = 0.6666...
0.6666 × 100 = 66.6666...
ROUND后 → 66.67%
二:时间段活跃率 :首次登录后的N天内(任意一天)
时间段活跃率 时间段检查(如3天内) BETWEEN 首次登录+1天 AND +N天
短期行为分析、运营活动效果验证🌟🌟
场景类型 | 典型案例 |
---|---|
运营活动效果验证🧩 | 双11促销期的3天转化跟踪 |
新手引导期监测 🧩 | 注册后7天功能使用率统计 |
短期行为模式分析🧩 | 用户领券后3天核销率追踪 |
📚 时间段活跃率 = 在首次登录后N天内至少活跃一次 的用户比例
(如7天窗口期活跃率 = 首次后7天内任意一天登录过的用户数 / 总用户数)
📌步骤1:获取首次登录日期(同方案一)
sql
with first_login as (
select
user_id,
min(date(event_time)) as first_login_date
from login_detail
group by user_id
)

📌步骤2:窗口期活跃验证
sql
activity_check as (
select
fl.user_id,
-- 次日单日活跃检查
case when exists (
select 1
from login_detail ld
where ld.user_id = fl.user_id
and date(ld.event_time) = fl.first_login_date + 1
) then 1 else 0 end as active_1,
-- 3天窗口期活跃检查
case when exists (
select 1
from login_detail ld
where ld.user_id = fl.user_id
and date(ld.event_time) between fl.first_login_date + 1
and fl.first_login_date + 3
) then 1 else 0 end as active_3_window,
-- 同理:
-- 7天窗口期活跃检查
from first_login fl
)
中间表 activity_check
user_id | active_1 | active_3_window | active_7_window |
---|---|---|---|
1001 | 1 | 1 | 0 |
1002 | 0 | 1 | 1 |
1003 | 0 | 0 | 1 |
- 用户1001:
✅ 次日活跃(8月2日)
✅ 3天窗口期(8月2-4日)有登录
❌ 7天窗口期(8月2-8日)无后续登录
📌步骤3:计算窗口期活跃率
sql
select
round(avg(active_1)*100, 2) as active_1_day_rate,
round(avg(active_3_window)*100, 2) as active_3_day_window_rate,
round(avg(active_7_window)*100, 2) as active_7_day_window_rate
from activity_check;
最终结果表
active_1_day_rate | active_3_day_window_rate | active_7_day_window_rate |
---|---|---|
33.33 | 66.67 | 66.67 |
Tips:🎈🎈🎈
sql
-- 包含首日后第1天到第N天(闭区间)
BETWEEN first_day+1 AND first_day+N
-- 等效写法(跨数据库兼容)
date(ld.event_time) >= first_day+1
AND date(ld.event_time) <= first_day+N
🚨 常见误区
误区: 将窗口期误算为N个自然日
正解: 实际计算的是首日后的连续N天
sql
-- 错误示范(可能跨月错误)
between first_day and first_day + interval '3 days'
-- 正确写法(首日+1到首日+N)
between first_day + 1 and first_day + N
场景 | 适用逻辑 | 核心SQL片段 | 适用场景 |
---|---|---|---|
标准留存率 | 精准日期检查 | DATE(event_time) = 首次登录 +N天 |
用户粘性分析、行业报告 |
时间段活跃率 | 时间段检查(如3天内) | BETWEEN 首次登录+1天 AND +N天 |
短期行为分析、运营活动效果验证 |
给大家留一个作业~🤣 🤣 🤣 🤣
复购用户人数🚀🚀🚀
已知 order
表,表中记录了订单的相关信息,order_id
是唯一的。请通过 SQL 语句统计在 2024 年 1 月 1 日到 2024 年 3 月 10 日期间,有 30 日复购行为的人数。
表名 | 字段名 | 数据类型 | 说明 |
---|---|---|---|
order | order_id | 未知(唯一标识) | 订单的唯一编号 |
order | user_id | 未知 | 用户的编号 |
order | pay_day | 字符串(格式为 YYYYMMDD) | 订单的支付日期 |
其中,30 日复购的定义为:用户在 2024 年 1 月 1 日到 2024 年 3 月 10 日这个范围内首次支付后的 30 天内又再次进行了支付。
order_id | user_id | pay_day |
---|---|---|
10001 | user_01 | 20240101 |
10002 | user_02 | 20240105 |
10003 | user_03 | 20240110 |
10004 | user_01 | 20240115 |
10005 | user_04 | 20240120 |
10006 | user_02 | 20240201 |
10007 | user_05 | 20240208 |
10008 | user_03 | 20240215 |
10009 | user_06 | 20240220 |
10010 | user_01 | 20240301 |
10011 | user_07 | 20240305 |
10012 | user_04 | 20240310 |
复购用户人数SQL题解
📝 题目题干
表名 | 字段名 | 数据类型 | 说明 |
---|---|---|---|
order |
order_id | 唯一标识 | 订单的唯一编号 |
order |
user_id | 字符串或数值 | 用户的编号 |
order |
pay_day | 字符串(格式为 YYYYMMDD ) |
订单的支付日期 |
题目要求
统计在 2024年1月1日到2024年3月10日 期间,有 30日复购行为 的用户人数。
复购定义:用户在首次支付后的30天内(含当天)再次支付。
📌步骤1:获取首次支付日期✅
sql
WITH FirstOrder AS (
SELECT
user_id,
MIN(STR_TO_DATE(pay_day, '%Y%m%d')) AS first_pay_day
FROM `order`
WHERE
STR_TO_DATE(pay_day, '%Y%m%d') BETWEEN '2024-01-01' AND '2024-03-10'
GROUP BY user_id
)
- 使用
MIN(STR_TO_DATE(pay_day, '%Y%m%d'))
获取用户在时间段内的 首次支付日期。 - 筛选条件:仅统计首次支付在 2024-01-01 至 2024-03-10 的用户。
中间表示例 ✅
user_id | first_pay_day |
---|---|
user_01 | 2024-01-01 |
user_02 | 2024-01-05 |
📌步骤2:筛选复购订单✅
sql
Reorder AS (
SELECT
o.user_id,
STR_TO_DATE(o.pay_day, '%Y%m%d') AS reorder_day
FROM `order` o
JOIN FirstOrder fo
ON o.user_id = fo.user_id
WHERE
-- 订单日期在首次支付后的30天内(含当天)
STR_TO_DATE(o.pay_day, '%Y%m%d') BETWEEN fo.first_pay_day AND DATE_ADD(fo.first_pay_day, INTERVAL 30 DAY)
-- 排除首次支付当天的订单(仅保留复购订单)
AND STR_TO_DATE(o.pay_day, '%Y%m%d') > fo.first_pay_day
)
- 通过
BETWEEN
筛选首次支付后 30天内 的订单。 STR_TO_DATE(o.pay_day, '%Y%m%d') > fo.first_pay_day
排除首次支付当天的订单,确保复购是 再次支付。
中间表示例 ✅
user_id | reorder_day |
---|---|
user_01 | 2024-01-15 |
user_02 | 2024-02-01 |
📌步骤3:统计复购用户数✅
sql
SELECT COUNT(DISTINCT user_id) AS repurchase_count
FROM Reorder;
最终结果 ✅
repurchase_count |
---|
3 |
pay_day
为字符串时,需用 STR_TO_DATE(pay_day, '%Y%m%d')
显式转换。
📌 最终答案
sql
WITH FirstOrder AS (
SELECT
user_id,
MIN(STR_TO_DATE(pay_day, '%Y%m%d')) AS first_pay_day
FROM `order`
WHERE
STR_TO_DATE(pay_day, '%Y%m%d') BETWEEN '2024-01-01' AND '2024-03-10'
GROUP BY user_id
),
Reorder AS (
SELECT
o.user_id
FROM `order` o
JOIN FirstOrder fo
ON o.user_id = fo.user_id
WHERE
STR_TO_DATE(o.pay_day, '%Y%m%d') BETWEEN fo.first_pay_day
AND DATE_ADD(fo.first_pay_day, INTERVAL 30 DAY)
AND STR_TO_DATE(o.pay_day, '%Y%m%d') > fo.first_pay_day
)
SELECT COUNT(DISTINCT user_id) AS repurchase_count
FROM Reorder;
Tips:如果题目允许首次当天后续订单算复购
sql
-- 修改条件为 >=
AND STR_TO_DATE(o.pay_day, '%Y%m%d') >= fo.first_pay_day
-- 但需要去重首次订单本身:
AND o.order_id != (
SELECT MIN(order_id)
FROM `order`
WHERE user_id = o.user_id
AND STR_TO_DATE(pay_day, '%Y%m%d') = fo.first_pay_day
)
sql
过滤条件双重保险:
┌──────────────────────────────┐
│ 30天时间窗口 │
│ [first_day ~ first_day+30] │
└───────────────────┬──────────┘
▼
排除首日当天的订单
(只保留右侧箭头部分)