大厂真题 / 拼多多

拼多多 8.2 笔试真题 - 数据分析岗

本场考试概述

考试时间:2026年8月2日

考试岗位:数据分析岗

考试方向:SQL(MySQL 8.0)

难度评级:中等偏难

考点分析

  • 第一题:ROW_NUMBER 分组 Top1(难度中等)
  • 第二题:月度聚合、LAG 与环比计算(难度困难)
  • 第三题:RFM 聚合、阈值打分与 CASE WHEN 分层(难度困难)

建议策略

  • 三道题都采用“先聚合或排名,再在外层加工”的两层结构,不要急于把所有逻辑塞进一层查询
  • 仔细处理并列时取较小 ID、缺失月份不补齐、第一个月返回 NULL、三个 RFM 阈值均包含等号等边界条件
  • 熟练掌握 ROW_NUMBERLAGCASE WHEN,它们是数据分析岗 SQL 笔试的高频工具

本文 SQL 按 MySQL 8.0 编写,使用窗口函数;MySQL 5.7 无法直接执行。


第 1 题:各品类销售额最高商品

题目描述

某电商平台有一张商品销售记录表,每行记录一笔商品销售交易。查询每个品类中销售额 sale_amount 最高的那条记录,输出品类名称、商品名称和销售额。

如果同一品类内有多条记录的 sale_amount 相同,取 id 较小的一条。结果按品类名称升序排列。

返回字段:category_nameproduct_namesale_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 BYMAX

MAX(sale_amount) 能得到每个品类的最高金额,却不能可靠地取出这个金额所属行的 product_name。题目需要返回完整记录,因此不能在聚合时把行信息丢失。

第二步:在每个品类内部排名

使用 ROW_NUMBER(),按 category_name 分区,并按以下顺序排列:

  1. sale_amount DESC:销售额更高的记录排在前面
  2. 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_mthmonthly_gmvprev_month_gmvgrowth_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_idlast_order_dateorder_counttotal_amountrfm_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 中不能直接在另一个表达式里引用刚定义的 rfm 别名,先在 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 打分和组合分层,重点是阈值边界与兜底标签