大厂真题 / 拼多多
拼多多 8.2 笔试真题 - 数据分析岗
本场考试概述
考试时间:2026年8月2日
考试岗位:数据分析岗
考试方向:SQL(MySQL 8.0)
难度评级:中等偏难
考点分析:
- 第一题:
ROW_NUMBER分组 Top1(难度中等) - 第二题:月度聚合、
LAG与环比计算(难度困难) - 第三题:RFM 聚合、阈值打分与
CASE WHEN分层(难度困难)
建议策略:
- 三道题都采用“先聚合或排名,再在外层加工”的两层结构,不要急于把所有逻辑塞进一层查询
- 仔细处理并列时取较小 ID、缺失月份不补齐、第一个月返回
NULL、三个 RFM 阈值均包含等号等边界条件 - 熟练掌握
ROW_NUMBER、LAG和CASE WHEN,它们是数据分析岗 SQL 笔试的高频工具
本文 SQL 按 MySQL 8.0 编写,使用窗口函数;MySQL 5.7 无法直接执行。
第 1 题:各品类销售额最高商品
题目描述
某电商平台有一张商品销售记录表,每行记录一笔商品销售交易。查询每个品类中销售额 sale_amount 最高的那条记录,输出品类名称、商品名称和销售额。
如果同一品类内有多条记录的 sale_amount 相同,取 id 较小的一条。结果按品类名称升序排列。
返回字段:category_name、product_name、sale_amount。
表结构:
c21_pdd_product_sales(id, product_name, category_name, sale_date, sale_amount)id为主键,sale_amount为单笔销售额
样例
输入
CREATE TABLE c21_pdd_product_sales (
id INT PRIMARY KEY,
product_name VARCHAR(50) NOT NULL,
category_name VARCHAR(50) NOT NULL,
sale_date DATE NOT NULL,
sale_amount DECIMAL(10,2) NOT NULL
);
INSERT INTO c21_pdd_product_sales VALUES
(1, '手机A', '电子', '2024-01-01', 5000.00),
(2, '手机B', '电子', '2024-01-02', 7000.00),
(3, '耳机C', '电子', '2024-01-03', 7000.00),
(4, 'T恤A', '服饰', '2024-01-01', 300.00),
(5, '外套B', '服饰', '2024-01-05', 900.00),
(6, '零食A', '食品', '2024-02-01', 150.00),
(7, '零食B', '食品', '2024-02-02', 150.00);
输出
category_name | product_name | sale_amount
服饰 | 外套B | 900.00
电子 | 手机B | 7000.00
食品 | 零食A | 150.00
思路分析
第一步:为什么不能只使用 GROUP BY 和 MAX
MAX(sale_amount) 能得到每个品类的最高金额,却不能可靠地取出这个金额所属行的 product_name。题目需要返回完整记录,因此不能在聚合时把行信息丢失。
第二步:在每个品类内部排名
使用 ROW_NUMBER(),按 category_name 分区,并按以下顺序排列:
sale_amount DESC:销售额更高的记录排在前面id ASC:销售额相同时,ID 更小的记录排在前面
这样每个品类的 rn = 1 就是唯一答案。不能换成 RANK(),因为并列最高记录都会获得排名 1。
第三步:在外层过滤第一名
窗口函数在 WHERE 之后计算,不能在同一层直接写 WHERE ROW_NUMBER() ... = 1。先在子查询中计算排名,再在外层保留 rn = 1。
题解代码
SELECT ranked.category_name,
ranked.product_name,
ranked.sale_amount
FROM (
SELECT category_name,
product_name,
sale_amount,
ROW_NUMBER() OVER (
PARTITION BY category_name
ORDER BY sale_amount DESC, id ASC
) AS rn
FROM c21_pdd_product_sales
) AS ranked
WHERE ranked.rn = 1
ORDER BY ranked.category_name ASC;
复杂度分析
时间复杂度:$O(n\log n)$,窗口函数需要按品类和排序键处理记录。
空间复杂度:$O(n)$,用于窗口排序及中间结果。
易错点
- 使用
RANK()会在最高金额并列时返回多行 - 省略
id ASC会使并列记录的选择不稳定 - 不能在同一层
WHERE中过滤窗口函数结果
第 2 题:月度 GMV 环比增长率
题目描述
某电商平台有一张订单表,每行记录一笔订单。按月统计 GMV(订单金额之和),并计算环比增长率,结果保留两位小数。
环比增长率为:
\[\frac{\text{本月 GMV}-\text{上月 GMV}}{\text{上月 GMV}}\times 100\%.\]这里的“上月”以数据中实际出现的月份顺序为准。若数据中只有 1 月和 3 月,3 月就与 1 月比较,不补齐 2 月。第一个月的上月 GMV 和环比增长率均为 NULL。
结果按年月升序排列。返回字段:y_mth、monthly_gmv、prev_month_gmv、growth_rate。
表结构:
c21_pdd_gmv_orders(order_id, order_date, amount)order_id为主键,amount为订单金额
样例
输入
CREATE TABLE c21_pdd_gmv_orders (
order_id INT PRIMARY KEY,
order_date DATE NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
INSERT INTO c21_pdd_gmv_orders VALUES
(1, '2024-01-05', 500.00),
(2, '2024-01-20', 500.00),
(3, '2024-03-02', 900.00),
(4, '2024-03-18', 600.00),
(5, '2024-04-10', 1200.00),
(6, '2024-12-25', 1200.00),
(7, '2025-01-08', 1000.00),
(8, '2025-01-30', 800.00);
输出
y_mth | monthly_gmv | prev_month_gmv | growth_rate
2024-01| 1000.00 | NULL | NULL
2024-03| 1500.00 | 1000.00 | 50.00
2024-04| 1200.00 | 1500.00 | -20.00
2024-12| 1200.00 | 1200.00 | 0.00
2025-01| 1800.00 | 1200.00 | 50.00
思路分析
第一步:把订单明细聚合为月表
用 DATE_FORMAT(order_date, '%Y-%m') 提取年月,再按年月分组求和。只有在月粒度结果上,上一行才表示上一个实际出现的月份。
第二步:使用 LAG 取上一行 GMV
在月表上按 y_mth 排序,LAG(monthly_gmv) 会把上一行的 GMV 放到当前行。首行没有上一行,因此自动得到 NULL;缺失月份不会被补齐,正好符合题意。
'%Y-%m' 是定长且月份补零的格式,因此字符串升序与时间升序一致,跨年也不会错位。
第三步:计算增长率
外层按公式计算并使用 ROUND(..., 2) 保留两位小数。上一月 GMV 为 NULL 时,首行结果自然也是 NULL。
如果业务数据允许上一月 GMV 为 0,应使用 NULLIF(prev_month_gmv, 0) 避免除零;下面的写法已经包含该保护。
题解代码
WITH monthly AS (
SELECT DATE_FORMAT(order_date, '%Y-%m') AS y_mth,
SUM(amount) AS monthly_gmv
FROM c21_pdd_gmv_orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
),
with_previous AS (
SELECT y_mth,
monthly_gmv,
LAG(monthly_gmv) OVER (ORDER BY y_mth) AS prev_month_gmv
FROM monthly
)
SELECT y_mth,
monthly_gmv,
prev_month_gmv,
ROUND(
(monthly_gmv - prev_month_gmv)
/ NULLIF(prev_month_gmv, 0) * 100,
2
) AS growth_rate
FROM with_previous
ORDER BY y_mth ASC;
复杂度分析
时间复杂度:$O(n+m\log m)$,其中 $n$ 为订单数,$m$ 为实际出现的月份数;主要开销为月度聚合和窗口排序。
空间复杂度:$O(m)$,用于保存月度结果与窗口计算。
易错点
LAG必须作用在月度聚合结果上,不能直接作用在订单明细上- 不应自行补齐缺失月份,也不能通过“日期减一个自然月”关联上月
- 应使用补零的
'%Y-%m',不要使用月份不补零的'%Y-%c' - 环比结果可以为负数或 0,不要额外过滤
第 3 题:RFM 用户分层
题目描述
某电商平台有一张订单表,每行记录一笔订单。基于 RFM 模型对用户进行分层:
- R(Recency):最近一次下单日期不早于
2024-10-01,记 1 分,否则记 0 分 - F(Frequency):累计下单次数不少于 3 次,记 1 分,否则记 0 分
- M(Monetary):累计消费金额不低于 1000 元,记 1 分,否则记 0 分
分层规则:
1/1/1:核心用户1/0/0:新用户0/1/1:流失风险用户0/0/0:流失用户- 其余组合:普通用户
结果按 user_id 升序排列。返回字段:user_id、last_order_date、order_count、total_amount、rfm_segment。
表结构:
c21_pdd_rfm_orders(order_id, user_id, order_date, amount)order_id为主键,一行表示一笔订单
样例
输入
CREATE TABLE c21_pdd_rfm_orders (
order_id INT PRIMARY KEY,
user_id INT NOT NULL,
order_date DATE NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
INSERT INTO c21_pdd_rfm_orders VALUES
(1, 101, '2024-11-01', 500.00),
(2, 101, '2024-11-15', 600.00),
(3, 101, '2024-12-01', 400.00),
(4, 102, '2024-12-01', 200.00),
(5, 103, '2024-01-01', 800.00),
(6, 103, '2024-03-01', 500.00),
(7, 103, '2024-06-01', 600.00),
(8, 104, '2024-01-15', 100.00),
(9, 105, '2024-11-01', 800.00),
(10, 105, '2024-11-20', 900.00),
(11, 106, '2024-08-01', 200.00),
(12, 106, '2024-09-01', 200.00),
(13, 106, '2024-10-01', 300.00),
(14, 107, '2024-05-01', 1000.00);
输出
user_id | last_order_date | order_count | total_amount | rfm_segment
101 | 2024-12-01 | 3 | 1500.00 | 核心用户
102 | 2024-12-01 | 1 | 200.00 | 新用户
103 | 2024-06-01 | 3 | 1900.00 | 流失风险用户
104 | 2024-01-15 | 1 | 100.00 | 流失用户
105 | 2024-11-20 | 2 | 1700.00 | 普通用户
106 | 2024-10-01 | 3 | 700.00 | 普通用户
107 | 2024-05-01 | 1 | 1000.00 | 普通用户
思路分析
第一步:聚合到用户粒度
按 user_id 分组,分别计算:
MAX(order_date):最近一次下单日期COUNT(*):累计订单数SUM(amount):累计消费金额
题目明确一行就是一笔订单,因此不需要按日期去重。
第二步:按阈值计算 R、F、M 三项得分
三个阈值都包含等号:
- 日期
>= '2024-10-01' - 次数
>= 3 - 金额
>= 1000
样例中的用户 106 和 107 正好位于边界,若写成严格大于会得到错误结果。
第三步:按得分组合贴标签
三个二进制得分共有 8 种组合。题目只为其中 4 种指定了特殊标签,因此最后必须用 ELSE '普通用户' 覆盖其余组合。
由于同一层 SELECT 中不能直接在另一个表达式里引用刚定义的 r、f、m 别名,先在 scored CTE 中打分,再在外层组合判断。
题解代码
WITH user_stats AS (
SELECT user_id,
MAX(order_date) AS last_order_date,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM c21_pdd_rfm_orders
GROUP BY user_id
),
scored AS (
SELECT user_id,
last_order_date,
order_count,
total_amount,
CASE WHEN last_order_date >= '2024-10-01' THEN 1 ELSE 0 END AS r,
CASE WHEN order_count >= 3 THEN 1 ELSE 0 END AS f,
CASE WHEN total_amount >= 1000 THEN 1 ELSE 0 END AS m
FROM user_stats
)
SELECT user_id,
last_order_date,
order_count,
total_amount,
CASE
WHEN r = 1 AND f = 1 AND m = 1 THEN '核心用户'
WHEN r = 1 AND f = 0 AND m = 0 THEN '新用户'
WHEN r = 0 AND f = 1 AND m = 1 THEN '流失风险用户'
WHEN r = 0 AND f = 0 AND m = 0 THEN '流失用户'
ELSE '普通用户'
END AS rfm_segment
FROM scored
ORDER BY user_id ASC;
复杂度分析
时间复杂度:典型哈希聚合下为 $O(n+u\log u)$,其中 $n$ 为订单数,$u$ 为用户数;排序发生在用户结果集上。
空间复杂度:$O(u)$,用于用户级聚合和打分结果。
易错点
- 三个阈值都必须使用
>= ELSE '普通用户'不能省略,否则未明确枚举的组合会得到NULL- 下单次数应使用
COUNT(*),不要自行改成COUNT(DISTINCT order_date) - 需要分层查询或使用 CTE,避免在同一层引用刚定义的 R/F/M 别名
小结
- 第一题使用
ROW_NUMBER完成分组 Top1,并通过id ASC稳定解决并列问题 - 第二题先汇总月度 GMV,再用
LAG取得数据中实际出现的上一月份,最后计算环比 - 第三题先聚合用户指标,再完成 R/F/M 打分和组合分层,重点是阈值边界与兜底标签