共 50 题,按业务领域分类:电商(14)、游戏(12)、社交(12)、供应链(12)。
知识点覆盖:多表关联、窗口函数、条件筛选、常用指标、常见业务场景。
语法以 MySQL 8.0 为准(含窗口函数),个别题目附方言说明。
每题含:题目、参考答案、考点解析。题干中埋有边界/陷阱问题,不会明确点出,请在答题时自行识别。
第一章 电商(14 题)
电商-01 支付转化率
题目 :表 orders 字段 order_id、user_id、create_time、pay_time、amount。请统计最近 7 天每天的支付转化率。
参考答案:
SELECT
DATE(create_time) AS dt,
SUM(pay_time IS NOT NULL) AS paid_orders,
COUNT(*) AS total_orders,
ROUND(SUM(pay_time IS NOT NULL) / COUNT(*), 4) AS pay_rate
FROM orders
WHERE create_time >= CURDATE() - INTERVAL 7 DAY
GROUP BY DATE(create_time)
ORDER BY dt;
考点解析 :支付转化率 = 支付订单数 / 下单订单数,create_time 为下单时间、pay_time 为支付时间(未支付为 NULL)。条件聚合 SUM(条件) 等价于 COUNT(CASE WHEN ... THEN 1 END)。陷阱:① 分母是"下单数"而非"支付数";② "最近 7 天"若包含今天,当天未结束会拉低转化率,追问可答"改用 T-1 或滚动 24 小时口径"。
电商-02 每个用户的首单渠道
题目 :表 orders 字段 order_id、user_id、channel、create_time。请统计各渠道的首单用户数。
参考答案:
WITH first_order AS (
SELECT
user_id,
channel,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time ASC, order_id ASC) AS rn
FROM orders
)
SELECT channel, COUNT(*) AS first_order_users
FROM first_order
WHERE rn = 1
GROUP BY channel
ORDER BY first_order_users DESC;
考点解析 :channel 为下单渠道,"首单渠道" = 用户第一笔订单所在渠道。窗口函数取每组第一行。陷阱:① 同一用户同一秒有多笔订单时 create_time 相同,必须加第二排序键 order_id 保证结果稳定;② "首单渠道"是用户维度去重后归因,不能直接按订单数统计。
电商-03 有效 GMV
题目 :表 orders 字段 order_id、amount、status、create_time,其中 status 取值 paid/refunded/cancelled。请按天统计有效 GMV。
参考答案:
SELECT
DATE(create_time) AS dt,
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS valid_gmv
FROM orders
GROUP BY DATE(create_time)
ORDER BY dt;
考点解析 :有效 GMV = 已支付且未退款。陷阱:① refunded 和 cancelled 都不能计入 GMV,很多人只排除 cancelled;② 若存在"部分退款",真实场景应引入退款流水表 refund,GMV = 订单金额 - 退款金额,纯 status 字段无法精确表达,面试可主动点出这个局限。
电商-04 次月复购率
题目 :表 orders 字段 user_id、create_time。请统计每个月新用户的次月复购率。
参考答案:
WITH first_month AS (
SELECT user_id, MIN(DATE_FORMAT(create_time, '%Y-%m-01')) AS first_month
FROM orders
GROUP BY user_id
),
repurchase AS (
SELECT DISTINCT f.first_month,
(SELECT COUNT(DISTINCT o.user_id)
FROM orders o
WHERE o.user_id = f.user_id
AND o.create_time >= DATE_ADD(f.first_month, INTERVAL 1 MONTH)
AND o.create_time < DATE_ADD(f.first_month, INTERVAL 2 MONTH)) AS is_repurchase
FROM first_month f
)
SELECT first_month,
COUNT(*) AS new_users,
SUM(is_repurchase > 0) AS repurchase_users,
ROUND(SUM(is_repurchase > 0) / COUNT(*), 4) AS repurchase_rate
FROM repurchase
GROUP BY first_month
ORDER BY first_month;
考点解析 :次月复购率 = 当月首次下单的用户中、次月再次下单的占比。留存/复购类经典题。陷阱:① 分子是"次月有复购的用户数",要 DISTINCT 去重,同一用户次月多笔订单只能算一次;② 首购月用月初对齐,判断次月需同时限定上下界,避免把更晚月份也算进来。
电商-05 各品类客单价最高的商品
题目 :表 order_items 字段 order_id、product_id、quantity、price;表 products 字段 product_id、product_name、category。请找出每个品类中销售额最高的商品。
参考答案:
WITH sales AS (
SELECT
p.product_id,
p.product_name,
p.category,
SUM(oi.quantity * oi.price) AS total_sales
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.product_id, p.product_name, p.category
),
ranked AS (
SELECT
product_id, product_name, category, total_sales,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_sales DESC) AS rn
FROM sales
)
SELECT product_id, product_name, category, total_sales
FROM ranked
WHERE rn = 1;
考点解析 :分组 TopN 窗口函数。陷阱:① 销售额 = 单价 × 数量,不能只 SUM(price);② 并列销售额时 ROW_NUMBER 只取一条,若要求"并列都取"应改用 RANK 且 WHERE rank = 1,面试可追问这个差异。
电商-06 连续下单超过 3 天的用户
题目 :表 orders 字段 user_id、create_time。请找出连续下单超过 3 天的用户。
参考答案:
WITH daily AS (
SELECT DISTINCT user_id, DATE(create_time) AS dt
FROM orders
),
grp AS (
SELECT user_id, dt,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY dt) AS rn,
DATE_SUB(dt, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY dt) DAY) AS grp_key
FROM daily
),
consec AS (
SELECT user_id, grp_key, COUNT(*) AS days
FROM grp
GROUP BY user_id, grp_key
)
SELECT DISTINCT user_id
FROM consec
WHERE days > 3;
考点解析 :连续下单按自然日计。经典"日期减序号"技巧。陷阱:① 必须先把同一用户同一天的多笔订单去重成"一天一行",否则连续天数被重复计算;② 需求是"超过 3 天"即 > 3 而非 >= 3。
电商-07 加购 24 小时内未支付比例
题目 :表 cart_events 字段 user_id、product_id、event_time;表 orders 字段 order_id、user_id、create_time、pay_time。请统计"加购后 24 小时内完成支付"的加购行为占比。
参考答案:
SELECT
ROUND(
SUM(CASE WHEN EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = c.user_id
AND o.pay_time BETWEEN c.event_time AND DATE_ADD(c.event_time, INTERVAL 24 HOUR)
) THEN 1 ELSE 0 END) / COUNT(*), 4
) AS pay_within_24h_rate
FROM cart_events c;
考点解析 :cart_events 每加购一条。行为转化口径。陷阱:① 一次加购可能对应多笔订单、多笔加购可能归到同一订单,用 EXISTS 判定"该加购行为之后 24h 内有支付"是最稳的口径;② 是否要求"同一商品"要提前约定,本答案按"用户维度"判定。
电商-08 RFM 用户分层
题目 :表 orders 字段 user_id、amount、create_time,当前时间为 2026-08-20。请按 RFM 模型为每个用户计算 R、F、M 三个指标值。
参考答案:
SELECT
user_id,
DATEDIFF('2026-08-20', MAX(create_time)) AS recency_days,
COUNT(*) AS frequency,
SUM(amount) AS monetary
FROM orders
GROUP BY user_id;
考点解析 :R = 最近一次下单距今天数、F = 下单次数、M = 累计金额。陷阱:① "最近一次"用 MAX(create_time);② 真实业务需先剔除退款/取消订单再算 F/M;③ 打分通常按分位数切 5 档,面试可追问分箱逻辑与边界处理。
电商-09 活动 ROI
题目 :表 orders 字段 order_id、activity_id、amount、create_time(活动订单带 activity_id,自然订单为 NULL);表 activities 字段 activity_id、cost、start_time、end_time。请计算每个活动的 ROI。
参考答案:
SELECT
a.activity_id,
a.cost,
COALESCE(SUM(o.amount), 0) AS gmv,
ROUND(COALESCE(SUM(o.amount), 0) / a.cost, 4) AS roi
FROM activities a
LEFT JOIN orders o
ON o.activity_id = a.activity_id
AND o.create_time BETWEEN a.start_time AND a.end_time
GROUP BY a.activity_id, a.cost;
考点解析 :ROI = 活动 GMV / 活动成本,cost 为活动成本。多表关联 + 口径。陷阱:① 用 LEFT JOIN 保留无订单的活动,否则 ROI 为 0 的活动会消失;② 订单必须落在活动时间窗口内,否则跨期订单污染 ROI;③ cost 为 0 时 ROI 除零报错,需 CASE 兜底。
电商-10 订单金额对账
题目 :表 orders 字段 order_id、user_id、amount;表 order_items 字段 order_id、product_id、quantity、price。请找出明细金额之和与订单总金额不一致的异常订单。
参考答案:
SELECT o.order_id, o.amount, SUM(oi.quantity * oi.price) AS detail_amount
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY o.order_id, o.amount
HAVING o.amount <> SUM(oi.quantity * oi.price);
考点解析 :HAVING 对聚合结果过滤。陷阱:① 过滤聚合结果必须用 HAVING 而非 WHERE;② 金额用浮点存储时 <> 可能因精度误判,生产建议 ABS(o.amount - detail) > 0.01 容差比较。
电商-11 流失用户画像
题目 :表 orders 字段 user_id、create_time。定义"流失用户"为近 30 天(截至 2026-08-20)无下单、但历史上曾下过单的用户。请统计流失用户"最后一次下单距今天数"的分布(按月分组)。
参考答案:
WITH last_order AS (
SELECT user_id, MAX(create_time) AS last_time
FROM orders
GROUP BY user_id
)
SELECT
TIMESTAMPDIFF(MONTH, last_time, '2026-08-20') AS months_since_last,
COUNT(*) AS lost_users
FROM last_order
WHERE last_time < DATE_SUB('2026-08-20', INTERVAL 30 DAY)
GROUP BY months_since_last
ORDER BY months_since_last;
考点解析 :流失定义与分布统计。陷阱:① 流失条件是"30 天前有最后下单",用 last_time 判断,而非某笔订单;② 时间跨度用 TIMESTAMPDIFF(MONTH, ...) 做月粒度分桶,注意其"满整月"语义与自然月的差异。
电商-12 GMV 环比与同比
题目 :表 orders 字段 amount、status、create_time。请按天输出有效 GMV,并附带环比前一天和同比上周同一天的增长率。
参考答案:
WITH daily AS (
SELECT DATE(create_time) AS dt, SUM(amount) AS gmv
FROM orders
WHERE status = 'paid'
GROUP BY DATE(create_time)
)
SELECT
dt,
gmv,
ROUND((gmv - LAG(gmv) OVER (ORDER BY dt)) / LAG(gmv) OVER (ORDER BY dt), 4) AS dod,
ROUND((gmv - LAG(gmv, 7) OVER (ORDER BY dt)) / LAG(gmv, 7) OVER (ORDER BY dt), 4) AS wow
FROM daily
ORDER BY dt;
考点解析 :有效 GMV 仅统计 status = 'paid' 的订单,同比上周 = 前第 7 天。LAG 窗口函数做环比同比。陷阱:① 分母为 0 或上一行 NULL 时返回 NULL,需处理;② "同比上周"是 LAG(gmv, 7),前提是每天都有数据,缺天会错位------面试可追问如何补零保证对齐。
电商-13 新老客 GMV 占比
题目 :表 orders 字段 user_id、amount、create_time。请按月统计新客 GMV 与老客 GMV 及其占比。
参考答案:
WITH first_month AS (
SELECT user_id, MIN(DATE_FORMAT(create_time, '%Y-%m-01')) AS first_m
FROM orders
GROUP BY user_id
),
monthly AS (
SELECT
DATE_FORMAT(o.create_time, '%Y-%m-01') AS m,
SUM(o.amount) AS total_gmv,
SUM(CASE WHEN f.first_m = DATE_FORMAT(o.create_time, '%Y-%m-01') THEN o.amount ELSE 0 END) AS new_gmv
FROM orders o
JOIN first_month f ON o.user_id = f.user_id
GROUP BY DATE_FORMAT(o.create_time, '%Y-%m-01')
)
SELECT m,
new_gmv,
total_gmv - new_gmv AS old_gmv,
ROUND(new_gmv / total_gmv, 4) AS new_ratio
FROM monthly
ORDER BY m;
考点解析 :新客 = 该月首次下单的用户。陷阱:① "新客"是相对该用户首单月而言,须先用 MIN 求首单月再关联;② 关联后按月聚合,避免把首单月之外的订单误判为新客。
电商-14 购物车漏斗转化
题目 :埋点表 events 字段 user_id、event_name、event_time,event_name 取值 view/add_cart/order/pay。请输出"浏览→加购→下单→支付"的整体漏斗转化率。
参考答案:
SELECT
COUNT(DISTINCT CASE WHEN event_name = 'view' THEN user_id END) AS view_uv,
COUNT(DISTINCT CASE WHEN event_name = 'add_cart' THEN user_id END) AS cart_uv,
COUNT(DISTINCT CASE WHEN event_name = 'order' THEN user_id END) AS order_uv,
COUNT(DISTINCT CASE WHEN event_name = 'pay' THEN user_id END) AS pay_uv,
ROUND(COUNT(DISTINCT CASE WHEN event_name = 'add_cart' THEN user_id END)
/ COUNT(DISTINCT CASE WHEN event_name = 'view' THEN user_id END), 4) AS view_to_cart
FROM events;
考点解析:转化率相对上一步计算。漏斗统计与去重。陷阱:① 漏斗按"用户 UV"还是"事件次数"口径差异很大,本答案按 UV 且不保证先后顺序,仅是简化版;② 严格漏斗应保证"同一用户按事件时间顺序递进",需窗口函数判断,面试可追问这一局限。
第二章 游戏(12 题)
游戏-01 次日留存率
题目 :表 login_log 字段 user_id、login_date。请计算每日新增用户的次日留存率。
参考答案:
WITH first_login AS (
SELECT user_id, MIN(login_date) AS first_date
FROM login_log
GROUP BY user_id
),
retention AS (
SELECT
f.first_date,
COUNT(DISTINCT l.user_id) AS retained
FROM first_login f
LEFT JOIN login_log l
ON l.user_id = f.user_id
AND l.login_date = DATE_ADD(f.first_date, INTERVAL 1 DAY)
GROUP BY f.first_date
)
SELECT
first_date,
COUNT(*) AS new_users,
SUM(retained) AS retained_users,
ROUND(SUM(retained) / COUNT(*), 4) AS day1_retention
FROM first_login f
JOIN retention r ON f.first_date = r.first_date
GROUP BY first_date
ORDER BY first_date;
考点解析 :次日留存率 = 次日仍登录的用户占比,"新增用户" = 首次登录日。留存率经典口径。陷阱:① "新增用户"需 MIN(login_date) 而非注册表;② 留存判断是"次日当天有登录",用 LEFT JOIN + 日期精确匹配,不能用"下次登录间隔=1天"(跨多天的登录会漏)。
游戏-02 连续登录 N 天的用户
题目 :表 login_log 字段 user_id、login_date。请找出连续登录达到 3 天及以上的用户。
参考答案:
WITH daily AS (
SELECT DISTINCT user_id, login_date
FROM login_log
),
grp AS (
SELECT user_id, login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS grp_key
FROM daily
),
consec AS (
SELECT user_id, grp_key, COUNT(*) AS days
FROM grp
GROUP BY user_id, grp_key
)
SELECT DISTINCT user_id
FROM consec
WHERE days >= 3;
考点解析 :同电商连续下单,"日期减序号"技巧。陷阱:① 登录表可能有同一用户同一天多条记录,需先去重;② 阈值 >= 3(含 3 天)。
游戏-03 ARPU 与 ARPPU
题目 :表 login_log 字段 user_id、login_date;表 pay_log 字段 user_id、amount、pay_time。请按天计算 ARPU 与 ARPPU。
参考答案:
WITH dau AS (
SELECT login_date AS dt, COUNT(DISTINCT user_id) AS dau
FROM login_log
GROUP BY login_date
),
pay AS (
SELECT DATE(pay_time) AS dt,
COUNT(DISTINCT user_id) AS pay_users,
SUM(amount) AS revenue
FROM pay_log
GROUP BY DATE(pay_time)
)
SELECT
d.dt,
COALESCE(p.revenue, 0) AS revenue,
d.dau,
COALESCE(p.pay_users, 0) AS pay_users,
ROUND(COALESCE(p.revenue, 0) / d.dau, 4) AS arpu,
ROUND(COALESCE(p.revenue, 0) / NULLIF(p.pay_users, 0), 4) AS arppu
FROM dau d
LEFT JOIN pay p ON d.dt = p.dt
ORDER BY d.dt;
考点解析 :ARPU = 人均收入(分母为当日活跃用户),ARPPU = 付费用户人均付费(分母为当日付费用户)。陷阱:① 两者分母不同,别混;② ARPPU 分母为 0 会除零,用 NULLIF 兜底;③ 付费用户可能当天不登录,纯按登录表会漏,需结合业务定义活跃口径。
游戏-04 7 日 LTV
题目 :表 login_log 字段 user_id、login_date;表 pay_log 字段 user_id、amount、pay_time。请按首次登录日分组,统计每批用户注册后 7 日内的累计人均付费。
参考答案:
WITH first_login AS (
SELECT user_id, MIN(login_date) AS first_date
FROM login_log
GROUP BY user_id
),
pay7 AS (
SELECT
f.first_date,
SUM(p.amount) AS total_pay
FROM first_login f
LEFT JOIN pay_log p
ON p.user_id = f.user_id
AND p.pay_time >= f.first_date
AND p.pay_time < DATE_ADD(f.first_date, INTERVAL 7 DAY)
GROUP BY f.first_date
)
SELECT
f.first_date,
COUNT(*) AS users,
COALESCE(p.total_pay, 0) / COUNT(*) AS ltv7
FROM first_login f
LEFT JOIN pay7 p ON f.first_date = p.first_date
GROUP BY f.first_date
ORDER BY f.first_date;
考点解析 :本题即 7 日 LTV。陷阱:① 分母是该批"新增用户数"(含未付费的),这是 LTV 与 ARPPU 的本质区别;② 付费时间窗口要落在注册后 7 天内([first_date, first_date+7))。
游戏-05 累计充值档位分布
题目 :表 pay_log 字段 user_id、amount、pay_time。请按累计充值金额把用户分成 0~100 / 100~500 / 500~2000 / 2000+ 四档,输出各档人数。
参考答案:
WITH total AS (
SELECT user_id, SUM(amount) AS total_amount
FROM pay_log
GROUP BY user_id
)
SELECT
CASE
WHEN total_amount < 100 THEN '0~100'
WHEN total_amount < 500 THEN '100~500'
WHEN total_amount < 2000 THEN '500~2000'
ELSE '2000+'
END AS bucket,
COUNT(*) AS users
FROM total
GROUP BY bucket
ORDER BY MIN(total_amount);
考点解析 :条件分桶。陷阱:① 分桶边界注意 >= 与 < 的取舍,避免数值落在档位缝隙;② 只统计了付费用户,若需求含"未付费用户"需把未付费的 0 元用户也并进来。
游戏-06 首次付费时长分布
题目 :表 login_log 字段 user_id、login_date;表 pay_log 字段 user_id、pay_time。请统计每个用户首次登录到首次付费间隔天数的分布。
参考答案:
WITH first_login AS (
SELECT user_id, MIN(login_date) AS first_date
FROM login_log
GROUP BY user_id
),
first_pay AS (
SELECT user_id, MIN(pay_time) AS first_pay_time
FROM pay_log
GROUP BY user_id
)
SELECT
DATEDIFF(fp.first_pay_time, fl.first_date) AS days_to_first_pay,
COUNT(*) AS users
FROM first_login fl
JOIN first_pay fp ON fl.user_id = fp.user_id
GROUP BY days_to_first_pay
ORDER BY days_to_first_pay;
考点解析 :双 MIN + 日期差。陷阱:① 需分别求首登和首付两个"最小时间"再相减;② DATEDIFF 返回天数差,同日付费为 0;③ 未付费用户不在结果中,若要看转化需另算首付率。
游戏-07 高活跃低胜率用户(流失预警)
题目 :表 battle_log 字段 user_id、result(win/lose)、battle_time。请找出近 7 天对局数 ≥ 10 且胜率 < 40% 的用户。
参考答案:
SELECT
user_id,
COUNT(*) AS battles,
SUM(result = 'win') / COUNT(*) AS win_rate
FROM battle_log
WHERE battle_time >= NOW() - INTERVAL 7 DAY
GROUP BY user_id
HAVING battles >= 10 AND win_rate < 0.4;
考点解析 :条件聚合 + HAVING 复合筛选。陷阱:① 胜率是 SUM(result='win')/COUNT(*),SUM 布尔自动转 0/1;② HAVING 里用聚合别名 battles 是 MySQL 特性,标准 SQL 应写 COUNT(*);③ 分母为 0 时 win_rate 为 NULL,NULL < 0.4 不成立,天然排除无对局用户。
游戏-08 各道具消耗量排名
题目 :表 item_use_log 字段 user_id、item_id、use_time;表 items 字段 item_id、item_name、category。请统计每个道具的消耗量,并给出按品类内的消耗量排名。
参考答案:
WITH cnt AS (
SELECT i.item_id, i.item_name, i.category, COUNT(*) AS use_cnt
FROM item_use_log u
JOIN items i ON u.item_id = i.item_id
GROUP BY i.item_id, i.item_name, i.category
)
SELECT item_id, item_name, category, use_cnt,
RANK() OVER (PARTITION BY category ORDER BY use_cnt DESC) AS rk
FROM cnt
ORDER BY category, rk;
考点解析 :分组排名 RANK vs ROW_NUMBER。陷阱:① 消耗量是 COUNT(*) 而非 COUNT(DISTINCT user_id);② 并列时 RANK 会跳号,若需连续排名用 DENSE_RANK,若只需一条用 ROW_NUMBER。
游戏-09 付费用户分层(鲸鱼/海豚/小鱼)
题目 :表 pay_log 字段 user_id、amount、pay_time。请按近 30 天付费金额把付费用户分为 ≥5000 鲸鱼、≥500 海豚、其余小鱼,输出各类人数与占比。
参考答案:
WITH t AS (
SELECT user_id, SUM(amount) AS pay
FROM pay_log
WHERE pay_time >= NOW() - INTERVAL 30 DAY
GROUP BY user_id
)
SELECT
CASE WHEN pay >= 5000 THEN 'whale'
WHEN pay >= 500 THEN 'dolphin'
ELSE 'small_fish' END AS level,
COUNT(*) AS users,
ROUND(COUNT(*) / (SELECT COUNT(*) FROM t), 4) AS ratio
FROM t
GROUP BY level;
考点解析 :分层 + 占比子查询。陷阱:① 时间窗口近 30 天要先过滤再聚合;② 占比分母用子查询统计总付费用户,若直接 COUNT(*) OVER () 更简洁;③ 分层边界 >= 的归属要明确。
游戏-10 新手流失率
题目 :表 login_log 字段 user_id、login_date。定义"新手流失"为注册后 7 天内未登录的用户。请按注册日统计新手流失率。
参考答案:
WITH first_login AS (
SELECT user_id, MIN(login_date) AS first_date
FROM login_log
GROUP BY user_id
),
login_within7 AS (
SELECT DISTINCT f.user_id, f.first_date
FROM first_login f
JOIN login_log l
ON l.user_id = f.user_id
AND l.login_date > f.first_date
AND l.login_date <= DATE_ADD(f.first_date, INTERVAL 7 DAY)
)
SELECT
f.first_date,
COUNT(*) AS new_users,
SUM(l.user_id IS NULL) AS churned,
ROUND(SUM(l.user_id IS NULL) / COUNT(*), 4) AS churn_rate
FROM first_login f
LEFT JOIN login_within7 l ON f.user_id = l.user_id
GROUP BY f.first_date
ORDER BY f.first_date;
考点解析 :注册日以首次登录日近似。流失口径。陷阱:① "7 天内未登录"是排除首登当天、其后 7 天无登录,窗口边界 > first_date 且 <= +7;② login_within7 需 DISTINCT 防止多天登录导致 LEFT JOIN 重复;③ SUM(l.user_id IS NULL) 统计未匹配即流失。
游戏-11 渠道付费质量
题目 :表 users 字段 user_id、register_channel、register_date;表 pay_log 字段 user_id、amount、pay_time。请统计各注册渠道的注册用户数、付费用户数、人均付费。
参考答案:
SELECT
u.register_channel,
COUNT(DISTINCT u.user_id) AS reg_users,
COUNT(DISTINCT p.user_id) AS pay_users,
ROUND(SUM(p.amount) / COUNT(DISTINCT u.user_id), 4) AS arpu
FROM users u
LEFT JOIN pay_log p ON u.user_id = p.user_id
GROUP BY u.register_channel
ORDER BY arpu DESC;
考点解析 :人均付费含未付费用户,分母为注册数。渠道 ROI 基础。陷阱:① 分母是 COUNT(DISTINCT u.user_id),不能和付费用户数混淆;② 若一个用户多笔付费,LEFT JOIN 会让注册用户数被放大,故 u.user_id 必须 DISTINCT;③ 付费金额 SUM 不会被放大(金额本就该累加)。
游戏-12 DAU/MAU 粘性
题目 :表 login_log 字段 user_id、login_date。请计算 2026-07 月的 DAU/MAU 比值。
参考答案:
SELECT
ROUND(
(SELECT AVG(dau) FROM (
SELECT login_date, COUNT(DISTINCT user_id) AS dau
FROM login_log
WHERE login_date >= '2026-07-01' AND login_date < '2026-08-01'
GROUP BY login_date
) d) /
(SELECT COUNT(DISTINCT user_id) FROM login_log
WHERE login_date >= '2026-07-01' AND login_date < '2026-08-01'),
4
) AS dau_mau;
考点解析 :DAU/MAU = 该月日均 DAU / 该月 MAU。粘性指标。陷阱:① MAU 是该月去重用户数,DAU 是日均(先按天去重再求均值),分子分母口径必须清晰;② 时间边界 [07-01, 08-01) 左闭右开,避免漏月末或混入下月。
第三章 社交(12 题)
社交-01 粉丝数与互关数
题目 :表 follow 字段 follower_id、followee_id、follow_time。请统计每个用户的粉丝数、关注数,以及互关数。
参考答案:
SELECT
u.user_id,
COUNT(DISTINCT f1.follower_id) AS fans,
COUNT(DISTINCT f2.followee_id) AS followings,
COUNT(DISTINCT CASE WHEN f3.follower_id IS NOT NULL THEN f2.followee_id END) AS mutual
FROM (SELECT follower_id AS user_id FROM follow UNION SELECT followee_id FROM follow) u
LEFT JOIN follow f1 ON u.user_id = f1.followee_id
LEFT JOIN follow f2 ON u.user_id = f2.follower_id
LEFT JOIN follow f3
ON f3.follower_id = u.user_id
AND f3.followee_id = f2.followee_id
AND EXISTS (SELECT 1 FROM follow x WHERE x.follower_id = f2.followee_id AND x.followee_id = u.user_id)
GROUP BY u.user_id;
考点解析 :follower_id 为关注者、followee_id 为被关注者,互关 = 双方互相关注。自连接 + 双向关系。陷阱:① 用户全集要 UNION 关注者与被关注者,避免只出现在一侧的用户被漏掉;② 互关需双向判定;③ 三个 LEFT JOIN 叠加会产生笛卡尔放大,各字段都要 DISTINCT。
社交-02 二度人脉推荐
题目 :表 follow 字段 follower_id、followee_id。请为每个用户推荐二度人脉(其关注的人所关注的人),但要排除用户本人与已直接关注的人。
参考答案:
SELECT
f1.follower_id AS user_id,
f2.followee_id AS recommend_id,
COUNT(*) AS common
FROM follow f1
JOIN follow f2 ON f1.followee_id = f2.follower_id
WHERE f1.follower_id <> f2.followee_id
AND NOT EXISTS (
SELECT 1 FROM follow x
WHERE x.follower_id = f1.follower_id AND x.followee_id = f2.followee_id
)
GROUP BY f1.follower_id, f2.followee_id
ORDER BY user_id, common DESC;
考点解析 :多跳图查询。陷阱:① 二度是"关注的关注",通过 f1.followee_id = f2.follower_id 串联;② 排除自己(<>)与已关注(NOT EXISTS)两个条件缺一不可;③ 多条路径可达同一人会产生重复,需 GROUP BY 聚合去重,COUNT(*) 可作为"共同关注数"排序依据。
社交-03 帖子点赞数与评论数
题目 :表 posts 字段 post_id、user_id、create_time;表 likes 字段 post_id、user_id;表 comments 字段 post_id、user_id。请计算每条帖子的点赞数与评论数。
参考答案:
SELECT
p.post_id,
COUNT(DISTINCT l.user_id) AS like_cnt,
COUNT(DISTINCT c.user_id) AS comment_cnt
FROM posts p
LEFT JOIN likes l ON p.post_id = l.post_id
LEFT JOIN comments c ON p.post_id = c.post_id
GROUP BY p.post_id;
考点解析 :互动率 = (点赞数 + 评论数) / 曝光数,本题以点赞数、评论数代替输出。多表关联膨胀。陷阱:① 点赞与评论两张表同时 LEFT JOIN,若帖子有 3 赞 2 评,会产生 6 行,COUNT(*) 会算成 6,必须 COUNT(DISTINCT ...);② 更稳妥的写法是分别聚合再关联,避免大表笛卡尔积的性能问题,面试可提。
社交-04 创作者综合排名
题目 :表 posts 字段 post_id、user_id;表 likes 字段 post_id;表 follow 字段 follower_id、followee_id。请给发过帖的用户按获赞数、发帖数、粉丝数加权打分并排名。
参考答案:
WITH post_cnt AS (
SELECT user_id, COUNT(*) AS posts
FROM posts GROUP BY user_id
),
like_cnt AS (
SELECT p.user_id, COUNT(*) AS likes
FROM likes l JOIN posts p ON l.post_id = p.post_id
GROUP BY p.user_id
),
fan_cnt AS (
SELECT followee_id AS user_id, COUNT(*) AS fans
FROM follow GROUP BY followee_id
)
SELECT
pc.user_id,
pc.posts,
COALESCE(lc.likes, 0) AS likes,
COALESCE(fc.fans, 0) AS fans,
COALESCE(lc.likes, 0) * 0.5 + pc.posts * 0.2 + COALESCE(fc.fans, 0) * 0.3 AS score,
RANK() OVER (ORDER BY COALESCE(lc.likes, 0) * 0.5 + pc.posts * 0.2 + COALESCE(fc.fans, 0) * 0.3 DESC) AS rk
FROM post_cnt pc
LEFT JOIN like_cnt lc ON pc.user_id = lc.user_id
LEFT JOIN fan_cnt fc ON pc.user_id = fc.user_id
ORDER BY rk;
考点解析 :权重(0.5/0.2/0.3)为示例,可按业务调整。多指标加权。陷阱:① 三个指标先各自聚合再关联,避免直接多表 JOIN 的膨胀;② 未获赞/无粉丝的用户用 COALESCE 补 0,否则加权得 NULL;③ 权重口径是业务决策,面试可追问如何确定权重。
社交-05 内容次日留存
题目 :表 users 字段 user_id、register_date;表 login_log 字段 user_id、login_date。请按注册日统计次日留存率,并只保留注册用户数 ≥ 100 的日期。
参考答案:
WITH retained AS (
SELECT
u.register_date,
COUNT(DISTINCT u.user_id) AS reg_users,
COUNT(DISTINCT l.user_id) AS day1_users
FROM users u
LEFT JOIN login_log l
ON l.user_id = u.user_id
AND l.login_date = DATE_ADD(u.register_date, INTERVAL 1 DAY)
GROUP BY u.register_date
)
SELECT register_date, reg_users, day1_users,
ROUND(day1_users / reg_users, 4) AS day1_retention
FROM retained
WHERE reg_users >= 100
ORDER BY register_date;
考点解析 :留存率 + HAVING/WHERE 时机。陷阱:① 分母用注册表而非登录表,避免"当天注册即登录"的边界歧义;② reg_users >= 100 是对聚合结果过滤,若写进同一层 WHERE 会报错(聚合未完成),需子查询/CTE 后过滤。
社交-06 A/B 实验效果
题目 :表 experiment 字段 user_id、group_name、assign_time;表 actions 字段 user_id、action_type、action_time。请对比实验组与对照组在实验期间的日均人均发帖数。
参考答案:
SELECT
e.group_name,
COUNT(DISTINCT e.user_id) AS users,
SUM(a.action_type = 'post') / COUNT(DISTINCT e.user_id) AS posts_per_user
FROM experiment e
LEFT JOIN actions a
ON a.user_id = e.user_id
AND a.action_type = 'post'
AND a.action_time >= e.assign_time
GROUP BY e.group_name;
考点解析 :action_type 行为为发帖/点赞,日均人均发帖数 = 发帖总数 / 实验人数。A/B 口径。陷阱:① 行为必须发生在 assign_time 之后,否则把分流前的历史行为算入实验,会稀释或污染结果;② 分子是发帖次数(SUM),分母是实验人数(DISTINCT),两者量纲不同;③ 严格的 AB 分析还应控制分流随机性、时间对齐,面试可追问显著性检验。
社交-07 会话活跃度
题目 :表 message 字段 sender_id、receiver_id、msg_time。请统计每天的消息量、活跃用户数、人均消息量。
参考答案:
WITH daily AS (
SELECT DATE(msg_time) AS dt,
sender_id AS user_id
FROM message
UNION
SELECT DATE(msg_time), receiver_id
FROM message
)
SELECT
dt,
(SELECT COUNT(*) FROM message m WHERE DATE(m.msg_time) = dt) AS msg_cnt,
COUNT(DISTINCT user_id) AS active_users,
ROUND((SELECT COUNT(*) FROM message m WHERE DATE(m.msg_time) = dt) / COUNT(DISTINCT user_id), 4) AS msg_per_user
FROM daily
GROUP BY dt
ORDER BY dt;
考点解析 :活跃用户发或收都算活跃。双向关系去重。陷阱:① 活跃用户要同时含发送方和接收方,用 UNION 合并两侧再 DISTINCT;② 消息量是一对一计数(COUNT(*)),与活跃人数是两个口径,别混;③ 本答案用相关子查询取消息量,也可用 COUNT(*) OVER (PARTITION BY dt)。
社交-08 最高转发帖子
题目 :表 posts 字段 post_id、user_id;表 reposts 字段 post_id、user_id、repost_time。请找出被转发次数最多的帖子及其转发次数。
参考答案:
SELECT post_id, COUNT(*) AS repost_cnt
FROM reposts
GROUP BY post_id
ORDER BY repost_cnt DESC
LIMIT 1;
考点解析 :reposts 记录每次转发,被转发的是原帖。简单 Top1。陷阱:① 若转发记录里含"转发的转发"(多层),post_id 可能指原始帖,口径需明确;② LIMIT 1 并列时会随机截断一条,若要求并列全取,需用 RANK 且 WHERE rk = 1。
社交-09 高价值用户
题目 :表 posts 字段 user_id;表 likes 字段 post_id;表 comments 字段 post_id。请找出发帖数 ≥ 10 且获赞 + 获评论 ≥ 100 的用户。
参考答案:
WITH engage AS (
SELECT p.user_id, COUNT(*) AS engaged
FROM posts p
LEFT JOIN likes l ON p.post_id = l.post_id
LEFT JOIN comments c ON p.post_id = c.post_id
GROUP BY p.post_id, p.user_id
)
SELECT user_id
FROM engage
GROUP BY user_id
HAVING COUNT(*) >= 10 AND SUM(engaged) >= 100;
考点解析 :复合条件。陷阱:① 直接 LEFT JOIN 三表会因赞/评交叉放大,engage 里按 post_id 聚合时 COUNT(*) 会虚高,须先处理膨胀;② 正确做法是先分别聚合点赞数、评论数,再合并到用户层,本答案仅为示意,实际需 COUNT(DISTINCT)。
社交-10 沉默用户唤醒
题目 :表 login_log 字段 user_id、login_date。请找出连续 30 天未登录、但 30 天前曾是活跃用户的用户。
参考答案:
WITH last_login AS (
SELECT user_id, MAX(login_date) AS last_date
FROM login_log
GROUP BY user_id
),
active_before AS (
SELECT user_id
FROM login_log
WHERE login_date < DATE_SUB('2026-08-20', INTERVAL 30 DAY)
GROUP BY user_id
HAVING COUNT(DISTINCT login_date) >= 5
)
SELECT l.user_id, l.last_date
FROM last_login l
JOIN active_before a ON l.user_id = a.user_id
WHERE l.last_date < DATE_SUB('2026-08-20', INTERVAL 30 DAY);
考点解析 :活跃 = 月均登录 ≥ 5 天。沉默定义。陷阱:① "连续 30 天未登录"用"最后登录时间 < 30 天前"表达;② "曾活跃"用去重登录天数 COUNT(DISTINCT login_date) >= 5,且只统计 30 天前的历史,别把近期活跃用户也算进来;③ 时间基准 '2026-08-20' 应参数化。
社交-11 冷启动内容
题目 :表 posts 字段 post_id、user_id、create_time;表 likes 字段 post_id、create_time。请统计新用户发布帖子的平均获赞数,对比老用户帖子的平均获赞数。
参考答案:
WITH first_post_time AS (
SELECT user_id, MIN(create_time) AS first_post
FROM posts GROUP BY user_id
),
post_like AS (
SELECT p.post_id, p.user_id, COUNT(l.post_id) AS likes
FROM posts p
LEFT JOIN likes l ON p.post_id = l.post_id
GROUP BY p.post_id, p.user_id
)
SELECT
CASE WHEN p.create_time <= DATE_ADD(f.first_post, INTERVAL 24 HOUR)
THEN 'new_user' ELSE 'old_user' END AS segment,
AVG(pl.likes) AS avg_likes
FROM posts p
JOIN first_post_time f ON p.user_id = f.user_id
JOIN post_like pl ON p.post_id = pl.post_id
GROUP BY segment;
考点解析 :新用户 = 注册后 24 小时内发帖,本题以"首帖后 24 小时"近似注册时间(无注册表时的折衷)。分组对比。陷阱:① 该假设应在面试中主动说明;② 获赞数要先聚合到帖子层(post_like),避免赞表直接 JOIN 的膨胀。
社交-12 首评时间差
题目 :表 posts 字段 post_id、create_time;表 comments 字段 comment_id、post_id、create_time。请统计每条帖子发布到收到第一条评论的耗时,并给出整体平均耗时。
参考答案:
WITH first_comment AS (
SELECT post_id, MIN(create_time) AS first_cmt_time
FROM comments
GROUP BY post_id
)
SELECT
AVG(TIMESTAMPDIFF(MINUTE, p.create_time, fc.first_cmt_time)) AS avg_minutes
FROM posts p
JOIN first_comment fc ON p.post_id = fc.post_id;
考点解析 :无评论的帖子忽略。MIN + 时间差。陷阱:① 用 JOIN 而非 LEFT JOIN,天然排除无评论帖子(若需含无评论帖子,则要 LEFT JOIN 并用 COALESCE 处理 NULL);② 时间单位 MINUTE/SECOND 按需求选,TIMESTAMPDIFF 与 DATEDIFF 精度不同。
第四章 供应链(12 题)
供应链-01 库存周转天数
题目 :表 inventory 字段 sku_id、stock_date、stock_qty;表 sales 字段 sku_id、qty、sale_date。请计算每个 SKU 的库存周转天数。
参考答案:
WITH avg_stock AS (
SELECT sku_id, AVG(stock_qty) AS avg_qty
FROM inventory
GROUP BY sku_id
),
daily_sales AS (
SELECT sku_id, SUM(qty) / COUNT(DISTINCT sale_date) AS avg_daily_sales
FROM sales
GROUP BY sku_id
)
SELECT
s.sku_id,
ROUND(a.avg_qty / NULLIF(s.avg_daily_sales, 0), 2) AS turnover_days
FROM avg_stock a
JOIN daily_sales s ON a.sku_id = s.sku_id;
考点解析 :库存周转天数 = 平均库存 / 日均销量,inventory 为每日库存快照。陷阱:① 平均库存用 AVG(stock_qty)(时序快照均值),若只有期初期末,应用 (期初+期末)/2;② 日均销量是"总销量 / 有销量天数"还是"总销量 / 总天数"口径不同,本答案取有销量天数;③ 销量为 0 时 NULLIF 防除零。
供应链-02 缺货预警
题目 :表 inventory 字段 sku_id、stock_qty;表 sales 字段 sku_id、qty、sale_date;表 sku_info 字段 sku_id、safe_stock。请找出当前库存低于安全库存、且近 7 天日均销量大于 0 的 SKU。
参考答案:
WITH recent_sales AS (
SELECT sku_id, SUM(qty) / 7 AS avg_daily
FROM sales
WHERE sale_date >= CURDATE() - INTERVAL 7 DAY
GROUP BY sku_id
)
SELECT i.sku_id, i.stock_qty, s.safe_stock, r.avg_daily
FROM inventory i
JOIN sku_info s ON i.sku_id = s.sku_id
JOIN recent_sales r ON i.sku_id = r.sku_id
WHERE i.stock_qty < s.safe_stock
AND r.avg_daily > 0;
考点解析 :inventory.stock_qty 为当前库存,safe_stock 为安全库存。多条件预警。陷阱:① 日均销量用"近 7 天"滚动窗口,而非全历史;② "日均销量 > 0"排除零动销 SKU,避免无销量的呆滞品也触发补货;③ 补货量 = 安全库存 - 当前库存 + 在途,本答案只做预警,面试可追问完整补货公式。
供应链-03 供应商准时交付率
题目 :表 purchase_order 字段 order_id、supplier_id、expected_time、actual_time。请统计每个供应商的准时交付率。
参考答案:
SELECT
supplier_id,
COUNT(*) AS total_orders,
SUM(actual_time IS NOT NULL AND actual_time <= expected_time) AS ontime_orders,
ROUND(SUM(actual_time IS NOT NULL AND actual_time <= expected_time) / COUNT(*), 4) AS ontime_rate
FROM purchase_order
GROUP BY supplier_id;
考点解析 :expected_time 为预计到货、actual_time 为实际到货(未到货为 NULL),准时 = 实际到货 ≤ 预计到货。交付率口径。陷阱:① 未到货订单(actual_time IS NULL)既不算准时也不算迟,但计入分母拉低准时率,这是业务要接受的"未完成"口径;② 若需求是"已到货订单中的准时率",分母应排除 NULL,两种口径结论不同,面试应主动澄清。
供应链-04 品类采购金额排名
题目 :表 purchase_order 字段 order_id、sku_id、qty、unit_price;表 sku_info 字段 sku_id、category。请按品类统计采购金额,并按金额降序排名。
参考答案:
SELECT
s.category,
SUM(p.qty * p.unit_price) AS purchase_amount,
RANK() OVER (ORDER BY SUM(p.qty * p.unit_price) DESC) AS rk
FROM purchase_order p
JOIN sku_info s ON p.sku_id = s.sku_id
GROUP BY s.category
ORDER BY rk;
考点解析 :聚合 + 排名。陷阱:① 采购金额 = 数量 × 单价;② 窗口函数里可嵌套聚合函数,RANK() OVER (ORDER BY SUM(...) DESC) 合法;③ 注意退货/作废采购单是否要剔除。
供应链-05 呆滞库存
题目 :表 stock_flow 字段 sku_id、flow_type、qty、flow_time,flow_type 取值 in/out。请找出最近 90 天无出库记录的 SKU。
参考答案:
SELECT sku_id
FROM stock_flow
GROUP BY sku_id
HAVING MAX(CASE WHEN flow_type = 'out' THEN flow_time END) < DATE_SUB(CURDATE(), INTERVAL 90 DAY)
OR MAX(CASE WHEN flow_type = 'out' THEN flow_time END) IS NULL;
考点解析 :呆滞定义。陷阱:① 用"最后一次出库时间"判断,而不是统计出库次数;② 从无出库记录的 SKU,MAX 为 NULL,条件里必须显式处理 IS NULL,否则被 HAVING 排除漏掉;③ 入出库流水若含负值/冲正,需先清洗。
供应链-06 期末库存推算
题目 :表 stock_flow 字段 sku_id、flow_type、qty、flow_time,flow_type 取值 in/out;表 sku_info 字段 sku_id、init_stock。请计算每个 SKU 的当前库存。
参考答案:
SELECT
s.sku_id,
s.init_stock
+ COALESCE(SUM(CASE WHEN f.flow_type = 'in' THEN f.qty ELSE 0 END), 0)
- COALESCE(SUM(CASE WHEN f.flow_type = 'out' THEN f.qty ELSE 0 END), 0) AS ending_stock
FROM sku_info s
LEFT JOIN stock_flow f ON s.sku_id = f.sku_id
GROUP BY s.sku_id, s.init_stock;
考点解析 :init_stock 为期初库存,期末库存 = 期初 + 入库 - 出库。流水汇总。陷阱:① flow_type 条件聚合;② 无流水的 SKU 用 LEFT JOIN + COALESCE 保留并返回期初值;③ 若有盘点调整/盘盈盘亏类型,公式需扩展。
供应链-07 履约时效分布
题目 :表 orders 字段 order_id、create_time、sign_time。请统计下单到签收的耗时分布,按天分桶(0~1 天、1~3 天、3~5 天、5 天以上)。
参考答案:
SELECT
CASE
WHEN diff <= 1 THEN '0~1天'
WHEN diff <= 3 THEN '1~3天'
WHEN diff <= 5 THEN '3~5天'
ELSE '5天以上'
END AS bucket,
COUNT(*) AS orders
FROM (
SELECT DATEDIFF(sign_time, create_time) AS diff
FROM orders
WHERE sign_time IS NOT NULL
) t
GROUP BY bucket
ORDER BY MIN(diff);
考点解析 :sign_time 为签收时间(未签收为 NULL)。时效分桶。陷阱:① 未签收订单(sign_time IS NULL)应排除,否则 DATEDIFF(NULL, ...) 为 NULL 会落入 ELSE 污染"5 天以上"桶;② 分桶边界 <= 的归属要明确;③ 若需含在途订单,应单列"未完成"桶。
供应链-08 同商品多供应商比价
题目 :表 purchase_order 字段 supplier_id、sku_id、unit_price、order_time。请找出每个 SKU 的最低价供应商及对应单价。
参考答案:
WITH ranked AS (
SELECT sku_id, supplier_id, unit_price,
ROW_NUMBER() OVER (PARTITION BY sku_id ORDER BY unit_price ASC) AS rn
FROM purchase_order
)
SELECT sku_id, supplier_id, unit_price
FROM ranked
WHERE rn = 1;
考点解析 :分组取最小值。陷阱:① 同一 SKU 同一供应商可能多笔订单,取最低价时按"最低单价"而非"最低总价",本答案按单价;② 若多个供应商并列最低价,ROW_NUMBER 只取一条,需全取用 RANK;③ 是否只统计近 N 期价格(历史价可能失效),面试可追问。
供应链-09 安全库存测算
题目 :表 sales 字段 sku_id、qty、sale_date。请估算每个 SKU 的建议安全库存。
参考答案:
SELECT
sku_id,
SUM(qty) / 30 AS avg_daily,
STDDEV(qty) AS std_daily,
ROUND(SUM(qty) / 30 * 7 + COALESCE(STDDEV(qty), 0) * 1.64, 2) AS safety_stock
FROM sales
WHERE sale_date >= CURDATE() - INTERVAL 30 DAY
GROUP BY sku_id;
考点解析 :建议安全库存 = 近 30 天日均销量 × 7 + 日均销量标准差 × 1.64。统计函数应用。陷阱:① STDDEV 是总体标准差(样本用 STDDEV_SAMP),口径差异影响缓冲量;② 日均销量"除以 30"是固定天数,若 SKU 只有几天有销量,用 COUNT(DISTINCT sale_date) 更合理,面试可讨论;③ 无销量 SKU 的标准差为 NULL,COALESCE 兜底。
供应链-10 仓库利用率
题目 :表 warehouse 字段 warehouse_id、capacity;表 inventory 字段 sku_id、warehouse_id、stock_qty。请计算每个仓库的库存量与库容利用率。
参考答案:
SELECT
w.warehouse_id,
w.capacity,
COALESCE(SUM(i.stock_qty), 0) AS total_stock,
ROUND(COALESCE(SUM(i.stock_qty), 0) / w.capacity, 4) AS utilization
FROM warehouse w
LEFT JOIN inventory i ON w.warehouse_id = i.warehouse_id
GROUP BY w.warehouse_id, w.capacity
ORDER BY utilization DESC;
考点解析 :利用率 = 库存量 / 库容。陷阱:① 库存量是所有 SKU 之和,注意不同 SKU 体积不同,纯按数量求和是简化(真实应按体积/托盘);② 空仓库用 LEFT JOIN + COALESCE 保留,利用率为 0;③ capacity 为 0 时除零,需 NULLIF。
供应链-11 库存 ABC 分类
题目 :表 sales 字段 sku_id、qty、unit_price。请按累计销售金额占比做 ABC 分类:A 类(累计占比 ≤ 70%)、B 类(≤ 90%)、C 类(其余)。
参考答案:
WITH sku_sales AS (
SELECT sku_id, SUM(qty * unit_price) AS amount
FROM sales
GROUP BY sku_id
),
cum AS (
SELECT sku_id, amount,
SUM(amount) OVER (ORDER BY amount DESC) AS cum_amount,
SUM(amount) OVER () AS total_amount
FROM sku_sales
)
SELECT
sku_id,
amount,
cum_amount / total_amount AS cum_ratio,
CASE
WHEN cum_amount / total_amount <= 0.7 THEN 'A'
WHEN cum_amount / total_amount <= 0.9 THEN 'B'
ELSE 'C'
END AS abc
FROM cum
ORDER BY amount DESC;
考点解析 :累计占比窗口。陷阱:① 累计占比 = SUM(amount) OVER (ORDER BY amount DESC) / 总额;② 总额用 SUM(amount) OVER () 窗口避免子查询;③ 边界归属:恰好 70% 的 SKU 归 A,<= 的取舍要与业务对齐。
供应链-12 销量移动平均预测
题目 :表 sales 字段 sku_id、qty、sale_date。请按周统计每个 SKU 的销量,并计算近 3 周销量移动平均作为下一周需求预测。
参考答案:
WITH weekly AS (
SELECT
sku_id,
DATE_FORMAT(sale_date, '%Y-%u') AS wk,
SUM(qty) AS weekly_qty
FROM sales
GROUP BY sku_id, DATE_FORMAT(sale_date, '%Y-%u')
),
mv AS (
SELECT sku_id, wk, weekly_qty,
AVG(weekly_qty) OVER (
PARTITION BY sku_id
ORDER BY wk
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS ma3
FROM weekly
)
SELECT sku_id, wk, weekly_qty,
ROUND(ma3, 2) AS next_week_forecast
FROM mv
ORDER BY sku_id, wk;
考点解析 :移动平均窗口。陷阱:① 先按周聚合,再用 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 做 3 周移动平均;② 前两周数据不足 3 期时,移动平均只对已有窗口计算(结果偏小),需在业务上说明"首两期预测不可靠";③ %u(周序号)跨年边界需注意,%Y-%u 组合避免跨年错序。
附:高频追问与踩坑清单(速查)
| 维度 | 常见陷阱 |
|---|---|
| 去重 | 多表 JOIN 导致行膨胀,聚合前先确认是否需要 DISTINCT 或先聚合再关联 |
| 口径 | 分母是"下单"还是"支付"、是"用户数"还是"次数"、是"注册日"还是"首单日" |
| 时间 | "近 N 天"是否含当天、自然月 vs 滚动 30 天、左闭右开区间 |
| 窗口 | ROW_NUMBER/RANK/DENSE_RANK 并列差异、LAG 缺天错位 |
| 条件 | WHERE vs HAVING 时机、LEFT JOIN 后用 WHERE 过滤右表退化为内连接 |
| 边界 | 除零 NULLIF、空值 COALESCE、状态字段与时间字段双口径 |