大厂真题 / 拼多多
拼多多 7.19 笔试真题 - 数据分析岗
本场考试概述
考试时间:2026年7月19日
考试岗位:数据分析岗
考试方向:SQL(MySQL 8.0)
难度评级:中等偏难
考点分析:
- 第一题:
LEFT JOIN与分组聚合(难度中等) - 第二题:两级聚合与条件聚合(难度困难)
- 第三题:窗口函数与分组孤岛(难度困难)
建议策略:
- 第一题先抓住“未使用的优惠券也要展示”,以优惠券表为主表做左连接
- 第二题先把参团流水压缩到“一团一行”,再回到活动粒度统计成团率,避免混淆聚合层级
- 第三题先用
LAG判断相邻调价记录是否降价,再用双行号之差划分连续区间
本文 SQL 按 MySQL 8.0 编写。第三题使用 CTE 和窗口函数,MySQL 5.7 无法直接执行。
第 1 题:优惠券使用情况统计
题目描述
某电商平台需要复盘优惠券投放效果。统计每种优惠券的使用次数、相关订单实付总金额和总优惠金额。没有被使用过的优惠券也必须展示,三个统计值均显示为 $0$。
- 使用次数:订单表中使用该优惠券的订单数
- 订单总金额:这些订单的
pay_amount之和 - 总优惠金额:使用次数乘以优惠券面值
- 排序规则:使用次数降序,次数相同时按优惠券 ID 升序
返回字段:coupon_id、coupon_name、use_count、total_order_amount、total_discount。
表结构:
c21_pdd_coupon(coupon_id, coupon_name, face_value):全量优惠券表,coupon_id为主键c21_pdd_coupon_order(order_id, coupon_id, pay_amount):优惠券订单表,order_id为主键
样例
输入
c21_pdd_coupon
coupon_id | coupon_name | face_value
CP001 | 满100减10 | 10.00
CP002 | 满200减30 | 30.00
CP003 | 新人立减5 | 5.00
CP004 | 会员专享15 | 15.00
c21_pdd_coupon_order
order_id | coupon_id | pay_amount
OD001 | CP001 | 90.00
OD002 | CP001 | 190.00
OD003 | CP001 | 90.00
OD004 | CP002 | 170.00
OD005 | CP002 | 270.00
OD006 | CP002 | 170.00
OD007 | CP003 | 45.00
输出
coupon_id | coupon_name | use_count | total_order_amount | total_discount
CP001 | 满100减10 | 3 | 370.00 | 30.00
CP002 | 满200减30 | 3 | 610.00 | 90.00
CP003 | 新人立减5 | 1 | 45.00 | 5.00
CP004 | 会员专享15 | 0 | 0.00 | 0.00
思路分析
第一步:从必须保留的表出发
题目要求未使用的优惠券也出现。如果从订单表出发,或者使用普通内连接,像 CP004 这样没有关联订单的优惠券会直接消失。
因此必须以全量优惠券表为主表,使用 LEFT JOIN 连接订单表。没有订单时,优惠券这一行仍被保留,只是右表的字段为 NULL。
第二步:正确统计使用次数
左连接后,未使用优惠券也会产生一行结果,所以不能使用 COUNT(*)。它会把这行算进去,让使用次数错误地变成 $1$。
COUNT(o.order_id) 只统计非 NULL 的订单 ID。未使用优惠券对应的 order_id 为 NULL,因此计数自然为 $0$。
第三步:处理空分组金额
SUM(o.pay_amount) 对全是 NULL 的分组会返回 NULL。题目要求显示 $0$,需要使用 COALESCE 兜底。
总优惠金额直接用订单数乘以面值。查询最后按 use_count 降序、优惠券 ID 升序排序。
题解代码
SELECT c.coupon_id,
c.coupon_name,
COUNT(o.order_id) AS use_count,
COALESCE(SUM(o.pay_amount), 0.00) AS total_order_amount,
COUNT(o.order_id) * c.face_value AS total_discount
FROM c21_pdd_coupon AS c
LEFT JOIN c21_pdd_coupon_order AS o
ON o.coupon_id = c.coupon_id
GROUP BY c.coupon_id, c.coupon_name, c.face_value
ORDER BY use_count DESC, c.coupon_id ASC;
复杂度分析
时间复杂度:典型哈希连接与哈希聚合下为 $O(C+O)$,其中 $C$ 是优惠券数,$O$ 是订单数。
空间复杂度:$O(C)$,用于保存优惠券粒度的分组结果。
第 2 题:拼团活动成团率统计
题目描述
平台的每个拼团活动有一个成团门槛。一个活动可以发起多个团,每个团包含多条参团支付记录。团内去重用户数达到或超过活动门槛时,该团成团成功;否则拼团失败并退款。
统计成团率大于 $0$ 的前 $5$ 名活动:
- 发起团数:活动下不同
group_id的数量 - 成团团数:去重参与用户数达到门槛的团数
- 成团率:成团团数除以发起团数,四舍五入保留两位小数
- 成团支付总金额:只累加成功团的全部支付流水,失败团不计入
- 排序规则:成团率降序,成团率相同时按活动 ID 升序
同一用户在同一团中出现多条记录时,人数只计算一次,但支付金额仍按支付流水逐条累加。
返回字段:activity_id、activity_name、launch_count、success_count、success_rate、success_pay_amount。
表结构:
c21_pdd_gb_activity(activity_id, activity_name, threshold_num):活动表c21_pdd_gb_group(group_id, activity_id):团信息表c21_pdd_gb_record(record_id, group_id, user_id, pay_amount):参团支付记录表
样例
以下按“团号: 去重人数/支付合计”压缩展示样例中的 $40$ 条参团记录。
输入
activity_id | threshold | groups(member_count/pay_sum)
A01 | 2 | G01:2/39.80, G02:2/39.80, G03:1/19.90
A02 | 3 | G04:3/89.70, G05:3/89.70, G06:3/89.70
A03 | 2 | G07:2/19.80, G08:1/9.90
A04 | 5 | G09:3/149.70, G10:2/99.80
A05 | 2 | G11:2/30.00
A06 | 4 | G12:4/159.60, G13:2/79.80, G14:4/159.60, G15:2/79.80
A07 | 2 | G16:2/24.00, G17:1/12.00, G18:1/12.00
输出
activity_id | activity_name | launch_count | success_count | success_rate | success_pay_amount
A02 | 三人拼水果 | 3 | 3 | 1.00 | 269.10
A05 | 老带新拼团 | 1 | 1 | 1.00 | 30.00
A01 | 9.9元拼手机壳 | 3 | 2 | 0.67 | 79.60
A03 | 新人拼团 | 2 | 1 | 0.50 | 19.80
A06 | 尝鲜拼团 | 4 | 2 | 0.50 | 319.20
思路分析
第一步:先确定统计粒度
成团与否是在“团”这一层判断的,但原始记录是一条支付流水一行。如果直接在活动层连接三张表,同一个团会被展开成多行,发起团数、成团团数和金额很容易相互干扰。
先在 group_stats 中按 group_id 聚合,让每个团只保留一行,同时得到去重成员数和支付合计。这是整道题最关键的粒度转换。
第二步:在团粒度判定是否成团
团级中间表连接活动表后,每个团都能拿到对应的 threshold_num。使用 CASE WHEN member_count >= threshold_num THEN ... END 即可做条件聚合。发起团数直接用团级行数;满足门槛时给成团数贡献 $1$,并把该团的支付合计加入成功金额。
第三步:回到活动粒度汇总
按活动分组后计算发起团数、成团团数、成团率和成团金额。成团率显式转成 DECIMAL(5,2),保证 1 以 1.00 的形式展示。
最后排除 success_count = 0 的活动,排序后取前 $5$ 名。样例中的 A07 并非完全失败,它的成团率约为 $0.33$,只是排在第 $6$ 名而被 LIMIT 5 截掉。
题解代码
WITH group_stats AS (
SELECT g.group_id,
g.activity_id,
COUNT(DISTINCT r.user_id) AS member_count,
COALESCE(SUM(r.pay_amount), 0.00) AS pay_amount
FROM c21_pdd_gb_group AS g
LEFT JOIN c21_pdd_gb_record AS r
ON r.group_id = g.group_id
GROUP BY g.group_id, g.activity_id
),
activity_stats AS (
SELECT a.activity_id,
a.activity_name,
COUNT(*) AS launch_count,
SUM(
CASE WHEN gs.member_count >= a.threshold_num THEN 1 ELSE 0 END
) AS success_count,
CAST(
ROUND(
SUM(
CASE WHEN gs.member_count >= a.threshold_num THEN 1 ELSE 0 END
) * 1.0 / COUNT(*),
2
) AS DECIMAL(5,2)
) AS success_rate,
COALESCE(
SUM(
CASE
WHEN gs.member_count >= a.threshold_num THEN gs.pay_amount
ELSE 0.00
END
),
0.00
) AS success_pay_amount
FROM c21_pdd_gb_activity AS a
JOIN group_stats AS gs
ON gs.activity_id = a.activity_id
GROUP BY a.activity_id, a.activity_name, a.threshold_num
)
SELECT activity_id,
activity_name,
launch_count,
success_count,
success_rate,
success_pay_amount
FROM activity_stats
WHERE success_count > 0
ORDER BY success_rate DESC, activity_id ASC
LIMIT 5;
复杂度分析
时间复杂度:典型哈希聚合下为 $O(R+G+A\log A)$,其中 $R$ 是参团记录数,$G$ 是团数,$A$ 是活动数;排序发生在活动结果集上。
空间复杂度:$O(G+A)$,用于团级和活动级聚合结果。
第 3 题:商品连续降价预警
题目描述
平台需要监控商品价格异动。当某个商品连续降价达到 $3$ 次及以上时触发预警。一次降价指当前调价记录的价格严格小于该商品的上一条调价记录;持平或上涨都会打断连续区间。
这里的“连续”指相邻调价记录连续下降,不要求两个调价日期在自然日上相邻。
每个达标区间输出一行:
- 降价起始日:第一次下降前的上一调价日
- 降价结束日:该区间最后一次下降的调价日
- 降价次数:区间内连续下降的次数
- 总降价金额:起始价格减去结束价格
同一商品可能出现多个达标区间。结果按降价次数降序、商品 ID 升序排列;本文额外按起始日升序保证同商品同次数时结果稳定。
返回字段:goods_id、goods_name、decline_start_date、decline_end_date、decline_count、total_drop。
表结构:
c21_pdd_price_goods(goods_id, goods_name):商品信息表c21_pdd_price_track(goods_id, price_date, price):调价记录表,(goods_id, price_date)为联合主键
样例
输入
goods_id | 从 2026-06-01 起按调价日期排列的价格
G001 | 200, 180, 160, 150, 130
G002 | 300, 280, 260, 250, 270, 260, 250
G003 | 100, 90, 90, 80, 70, 60
G004 | 50, 55, 52, 51
G005 | 500, 480, 450, 420, 400, 430, 410, 390, 370
输出
goods_id | goods_name | decline_start_date | decline_end_date | decline_count | total_drop
G001 | 碎花连衣裙 | 2026-06-01 | 2026-06-05 | 4 | 70.00
G005 | 手持云台相机 | 2026-06-01 | 2026-06-05 | 4 | 100.00
G002 | 透气运动鞋 | 2026-06-01 | 2026-06-04 | 3 | 50.00
G003 | 降噪蓝牙耳机 | 2026-06-03 | 2026-06-06 | 3 | 30.00
G005 | 手持云台相机 | 2026-06-06 | 2026-06-09 | 3 | 60.00
思路分析
第一步:让每条记录看到上一条价格
使用 LAG 按商品分区、按调价日期排序,取出 previous_date 和 previous_price。当前价格严格小于上一价格时,这一行才是降价记录。
先保留全部记录并编号十分重要。如果一开始就过滤出降价行,中间的持平或上涨记录会消失,两个本应断开的区间可能被错误拼在一起。
第二步:用双行号之差划分连续区间
给全部调价记录编号 all_row_number,再只对降价记录编号。对于没有被持平或上涨打断的一段连续降价,两套行号每次都同步加 $1$,二者之差保持不变。
以 G005 为例:
price_date | price | all_row_number | decline_row_number | segment_id
06-02 | 480 | 2 | 1 | 1
06-03 | 450 | 3 | 2 | 1
06-04 | 420 | 4 | 3 | 1
06-05 | 400 | 5 | 4 | 1
06-07 | 410 | 7 | 5 | 2
06-08 | 390 | 8 | 6 | 2
06-09 | 370 | 9 | 7 | 2
06-06 的价格从 $400$ 回升到 $430$,它不是降价行,却让全量行号多前进了一步。因此之后的行号差从 $1$ 变成 $2$,新的连续段自然被分开。
第三步:按区间聚合首尾信息
按 (goods_id, segment_id) 分组:
COUNT(*)是降价次数MIN(previous_date)是区间起始日MAX(price_date)是区间结束日- 区间严格递减,所以
MAX(previous_price) - MIN(price)就是总降幅
最后使用 HAVING COUNT(*) >= 3 只保留达到预警门槛的区间。
题解代码
WITH compared AS (
SELECT goods_id,
price_date,
price,
LAG(price_date) OVER (
PARTITION BY goods_id ORDER BY price_date
) AS previous_date,
LAG(price) OVER (
PARTITION BY goods_id ORDER BY price_date
) AS previous_price
FROM c21_pdd_price_track
),
numbered AS (
SELECT goods_id,
price_date,
price,
previous_date,
previous_price,
ROW_NUMBER() OVER (
PARTITION BY goods_id ORDER BY price_date
) AS all_row_number
FROM compared
),
decline_rows AS (
SELECT goods_id,
price_date,
price,
previous_date,
previous_price,
all_row_number
- ROW_NUMBER() OVER (
PARTITION BY goods_id ORDER BY price_date
) AS segment_id
FROM numbered
WHERE price < previous_price
),
segments AS (
SELECT goods_id,
segment_id,
MIN(previous_date) AS decline_start_date,
MAX(price_date) AS decline_end_date,
COUNT(*) AS decline_count,
MAX(previous_price) - MIN(price) AS total_drop
FROM decline_rows
GROUP BY goods_id, segment_id
HAVING COUNT(*) >= 3
)
SELECT g.goods_id,
g.goods_name,
s.decline_start_date,
s.decline_end_date,
s.decline_count,
s.total_drop
FROM segments AS s
JOIN c21_pdd_price_goods AS g
ON g.goods_id = s.goods_id
ORDER BY s.decline_count DESC,
g.goods_id ASC,
s.decline_start_date ASC;
复杂度分析
时间复杂度:$O(P\log P)$,其中 $P$ 是调价记录数;窗口函数需要按商品和日期排序。
空间复杂度:$O(P)$,用于窗口排序和中间 CTE 结果。
小结
- 第一题用
LEFT JOIN保留未使用优惠券,COUNT(列名)与COALESCE分别处理空计数和空金额 - 第二题先把支付流水聚合到团粒度,再用条件聚合计算活动成团率,核心是始终明确当前统计粒度
- 第三题用
LAG构造相邻比较,再用全量行号与降价行号的差值识别连续区间