这批题都来自真实笔试,每道题都有一个容易踩的坑。建议先自己写,再对照答案看哪里出了问题。
留存分析
留存率是数据分析岗最高频的指标之一。核心公式:
留存率 = 第 N 天仍活跃的用户数 / 注册用户总数 常见坑:分子分母写反。注册用户是分母(基数),留存用户是分子。
7 日留存率
reg(注册表)和 dau(活跃表),计算 2023-06-01 新增用户的第 7 日留存率。提示:LEFT JOIN 保留所有注册用户,第 7 天有活跃记录的才算留存
点击「运行」查看查询结果
查看答案
SELECT
COUNT(DISTINCT a1.user_id) AS total,
COUNT(DISTINCT a2.user_id) AS retained,
ROUND(COUNT(DISTINCT a2.user_id) * 1.0 / COUNT(DISTINCT a1.user_id), 4) AS retain_rate
FROM reg a1
LEFT JOIN dau a2
ON a1.user_id = a2.user_id
AND a2.active_date = DATE_ADD(a1.reg_date, INTERVAL 7 DAY)
WHERE a1.reg_date = '2023-06-01'; 注册用户 4 人(user 5 是 6/2 注册的,被 WHERE 过滤)。第 7 天(6/8)活跃的有 user 1 和 user 2,留存率 = 2/4 = 0.5。分子是留存用户,分母是注册用户,不要写反。
漏斗转化分析
漏斗分析的标准写法:按 session 聚合,用 MAX(CASE WHEN ...) 标记每步是否完成,再统计各步的 session 数。
常见坑:COUNT(CASE WHEN ... THEN 1 ELSE 0 END) 永远等于 COUNT(*),因为 ELSE 0 不是 NULL。应该用 SUM 或让 ELSE 返回 NULL。
首页→详情页→支付页完整转化率
logs,统计首页→详情页→支付页的完整转化率(走完全部三步的 session 占进入首页的 session 比例)。点击「运行」查看查询结果
查看答案
SELECT
SUM(step1) AS home_sessions,
SUM(step2) AS detail_sessions,
SUM(step3) AS pay_sessions,
ROUND(SUM(CASE WHEN step1=1 AND step2=1 AND step3=1 THEN 1 ELSE 0 END) * 1.0 / NULLIF(SUM(step1), 0), 4) AS full_cvr
FROM (
SELECT session_id,
MAX(CASE WHEN action = 'home' THEN 1 ELSE 0 END) AS step1,
MAX(CASE WHEN action = 'detail' THEN 1 ELSE 0 END) AS step2,
MAX(CASE WHEN action = 'pay' THEN 1 ELSE 0 END) AS step3
FROM logs
GROUP BY session_id
) t; session 1 走完三步,session 2 走了两步,session 3 只有首页,session 4 没有首页(不算进入漏斗)。进入首页的 session 有 3 个,完成支付的有 1 个,转化率 = 1/3 ≈ 0.333。NULLIF(SUM(step1), 0) 防止除以零。
滚动活跃用户数
计算每日最近 N 天的活跃用户数(去重),不能用窗口函数直接做,因为 COUNT(DISTINCT) 不支持 RANGE INTERVAL 帧。标准做法是自 JOIN:
-- 错误写法(MySQL 不支持)
COUNT(DISTINCT user_id) OVER(
ORDER BY active_date
RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW
)
-- 正确写法:自 JOIN
SELECT a1.active_date,
COUNT(DISTINCT a2.user_id) AS dau_7d
FROM (SELECT DISTINCT active_date FROM dau) a1
JOIN dau a2
ON a2.active_date BETWEEN DATE_SUB(a1.active_date, INTERVAL 6 DAY) AND a1.active_date
GROUP BY a1.active_date; 每日最近 7 天活跃用户数
dau 表,计算每天的最近 7 天去重活跃用户数(含当天)。点击「运行」查看查询结果
查看答案
SELECT a1.active_date,
COUNT(DISTINCT a2.user_id) AS dau_7d
FROM (SELECT DISTINCT active_date FROM dau) a1
JOIN dau a2
ON a2.active_date BETWEEN DATE_SUB(a1.active_date, INTERVAL 6 DAY) AND a1.active_date
GROUP BY a1.active_date
ORDER BY a1.active_date; 自 JOIN 让每个日期和过去 7 天内的所有活跃记录关联,COUNT(DISTINCT user_id) 去重计数。DATE_SUB(date, INTERVAL 6 DAY) 是 MySQL 的日期减法。
商品共购分析
找最常被一起购买的商品对,核心是自 JOIN + 去重。用 a1.product_id > a2.product_id 保证每对只出现一次(避免 (A,B) 和 (B,A) 重复计数)。
常见坑:COUNT(col1, col2) 语法错误,MySQL 的 COUNT 只接受一个参数,应该用 COUNT(*)。
最常被一起购买的商品对
order_items 表,找出最常被同一订单一起购买的商品对(商品 A 和商品 B 不区分顺序)。点击「运行」查看查询结果
查看答案
SELECT a1.product_id AS product_a, a2.product_id AS product_b,
COUNT(*) AS co_purchase_cnt
FROM order_items a1
JOIN order_items a2
ON a1.order_id = a2.order_id AND a1.product_id > a2.product_id
GROUP BY a1.product_id, a2.product_id
ORDER BY co_purchase_cnt DESC
LIMIT 1; a1.product_id > a2.product_id 保证每对只统计一次。101 和 102 在订单 1、2、4 中同时出现,共购次数 3 次,是最高的。
促销效果对比
对比促销日和非促销日的高价值订单占比。
常见坑:COUNT(CASE WHEN amount > 500 THEN 1 ELSE 0 END) 结果永远等于 COUNT(*),因为 ELSE 0 不是 NULL,COUNT 会把 0 也计入。应该用 SUM。
促销 vs 非促销高价值订单占比
orders 表(含 is_promotion 字段),对比促销日和非促销日中金额超过 500 元的订单占比。点击「运行」查看查询结果
查看答案
SELECT is_promotion,
COUNT(*) AS total_orders,
SUM(CASE WHEN amount > 500 THEN 1 ELSE 0 END) AS high_value_cnt,
ROUND(SUM(CASE WHEN amount > 500 THEN 1 ELSE 0 END) * 1.0 / COUNT(*), 4) AS high_value_rate
FROM orders
GROUP BY is_promotion; 促销日(is_promotion=1):4 单中 2 单超 500,占比 0.5。非促销日(is_promotion=0):5 单中 2 单超 500,占比 0.4。用 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 而不是 COUNT(CASE ...)。
新用户首单与次月复购
这道题综合了多个知识点:新用户筛选、首单识别、次月时间窗口、复购标记。
面试时要主动确认:
- 「次月」是首单后的第 2 个自然月,还是首单后 30-60 天?
- 「复购」是次月内有任意一笔订单,还是有多笔?
- 首单金额是下单金额还是支付金额?
2023 年新用户首单均值 + 次月复购率
users(注册表)和 orders(订单表),计算 2023 年新注册用户的首单平均金额,以及次月(首单后 30-60 天内)复购率。点击「运行」查看查询结果
查看答案
WITH new_users AS (
SELECT user_id FROM users WHERE YEAR(reg_date) = 2023
),
first_orders AS (
SELECT o.user_id, o.order_date AS first_date, o.amount AS first_amount
FROM (
SELECT o.user_id, o.order_date, o.amount,
ROW_NUMBER() OVER(PARTITION BY o.user_id ORDER BY o.order_date) AS rn
FROM orders o
JOIN new_users u ON o.user_id = u.user_id
) o
WHERE rn = 1
),
repurchase AS (
SELECT f.user_id,
MAX(CASE WHEN o.order_date > f.first_date
AND DATEDIFF(o.order_date, f.first_date) BETWEEN 30 AND 60
THEN 1 ELSE 0 END) AS is_repurchase
FROM first_orders f
LEFT JOIN orders o ON f.user_id = o.user_id
GROUP BY f.user_id
)
SELECT
ROUND(AVG(f.first_amount), 2) AS avg_first_order,
ROUND(AVG(r.is_repurchase), 4) AS repurchase_rate
FROM first_orders f
JOIN repurchase r ON f.user_id = r.user_id; 用 ROW_NUMBER() 按下单时间排序取第一行,得到真正的首单金额(而非 MIN(amount) 最低价订单)。YEAR(reg_date) = 2023 过滤新用户,DATEDIFF(o.order_date, f.first_date) 计算天数差判断次月窗口。user 1 首单 500 元,user 2 首单 800 元,user 3 首单 400 元,均值 ≈ 566.67,复购率 = 2/3 ≈ 0.667。
年度订单汇总(含零订单用户)
LEFT JOIN 保留无匹配行的技巧,核心是把过滤条件放在 JOIN ON 子句里,而不是 WHERE 或 HAVING。
常见坑:用 HAVING YEAR(order_date) = 2023 或 WHERE YEAR(order_date) = 2023 都会把未下单用户(order_date IS NULL)过滤掉,违背了「含未下单用户」的要求。正确做法是把年份过滤移进 JOIN 条件,让 LEFT JOIN 保留所有用户。
2023 年每用户订单总金额(含未下单用户)
users(user_id, name)和 orders(order_id, user_id, amount, order_date),统计每个用户 2023 年的订单总金额,未下单用户显示 0。提示:把年份过滤放在 JOIN ON 条件里,而非 WHERE/HAVING
点击「运行」查看查询结果
查看答案
SELECT u.user_id, u.name,
COALESCE(SUM(o.amount), 0) AS year_amount
FROM users u
LEFT JOIN orders o
ON u.user_id = o.user_id
AND YEAR(o.order_date) = 2023
GROUP BY u.user_id, u.name
ORDER BY u.user_id; Bob(user 2)在 2022 年有一笔订单,过滤条件放在 JOIN ON 里,LEFT JOIN 仍保留 Bob 这行,SUM 为 NULL,COALESCE 转成 0。Carol(user 3)有 2023 年订单 400 元。Alice(user 1)两笔 2023 年订单合计 800 元。如果把 YEAR(...) = 2023 放进 WHERE,Bob 会被整行丢掉。
日购买用户登录次数过滤
多层聚合 + 条件占比的经典写法:先按粒度聚合出中间结果,再 JOIN 到明细表上做计算。
常见坑:将「月均登录次数」写成 总登录次数 / 31,导致阈值被放大 31 倍——若要求「>3 次」实际变成「月登录 > 93 次」,几乎没有用户能达到。绝大多数业务场景里「平均登录次数」指的是该月总登录次数,不除以天数。
7 月每日购买用户中月登录次数超 3 次的占比
user_activity(user_id, date, action),action 取值为 login 或 purchase。计算 7 月每天购买用户中,7 月总登录次数超过 3 次的用户占比。提示:先把 7 月每用户总登录次数聚合成子查询,再 JOIN 到「每日购买用户」明细上
点击「运行」查看查询结果
查看答案
SELECT a1.date,
SUM(CASE WHEN a2.login_cnt > 3 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS rate
FROM (
SELECT DISTINCT user_id, date
FROM user_activity
WHERE action = 'purchase'
AND MONTH(date) = 7
) a1
LEFT JOIN (
SELECT user_id,
SUM(CASE WHEN action = 'login' THEN 1 ELSE 0 END) AS login_cnt
FROM user_activity
WHERE MONTH(date) = 7
GROUP BY user_id
) a2 ON a1.user_id = a2.user_id
GROUP BY a1.date
ORDER BY a1.date; user 1 在 7 月共登录 4 次(>3),user 2 登录 1 次,user 3 登录 0 次。7/1 有 3 名购买用户,其中只有 user 1 超过 3 次,占比 1/3 ≈ 0.333。7/2 只有 user 2 购买,登录 1 次不超 3,占比 0。
历史在职员工快照
查询「截至某一天在职的员工」是 SCD(缓慢变化维)类问题的基础写法:start_date <= 目标日期 AND end_date > 目标日期,其中 end_date 为 NULL 表示仍在职,用 COALESCE 替换成远未来日期。
2023-12-31 各部门在职员工数
history(emp_id, dept_id, start_date, end_date),end_date 为 NULL 表示仍在职。统计 2023-12-31 时各部门在职员工数。提示:COALESCE(end_date, '9999-01-01') 将 NULL 转为远未来日期,再用 > 目标日期筛选
点击「运行」查看查询结果
查看答案
SELECT dept_id,
COUNT(DISTINCT emp_id) AS employee_count
FROM history
WHERE start_date <= '2023-12-31'
AND COALESCE(end_date, '9999-01-01') > '2023-12-31'
GROUP BY dept_id; emp1 已于 6/30 离职,emp4 于 12/30 离职(end_date '2023-12-30' 不大于 '2023-12-31'),emp5 于 2024 年才入职。最终在职:dept 101 → emp2,dept 102 → emp3,各 1 人。dept 103 无在职员工不出现在结果中。