跳到主要内容

SQL 退款率Top10商品和品类:退款量/销量 排序(拼多多面试题)

一、题目背景

这道题来自拼多多品控与风控部门的数据分析岗面试。拼多多"极致性价比"的品牌心智建立在低价不低质的基础上——平台不能因为价格便宜就放任劣质商品流通。退款率是品控系统最核心的监控指标:一件商品的退款率异常偏高,意味着大量用户对商品质量不满意,轻则伤害用户体验、重则引发舆论危机和监管介入。

品控团队需要从海量订单中识别退款率最高的商品和品类,建立"预警→抽检→下架"的自动化流程。这个场景的数据挑战在于:退款是稀疏事件——大多数商品的退款率为0或极低,只有少数问题商品退款率偏高。如果直接按退款率排序,一件"只卖了2单、退了1单"的商品退款率50%,会压倒"卖了1000单、退了200单"(退款率20%)的真实问题商品——小样本偏差是这类分析的核心陷阱。

业务场景:品控运营每周跑一次"高退款率商品Top 10"报表,附带每个商品的订单总量(判断统计显著性),筛选条件通常是"订单数 ≥ 50 且退款率前10"。考察的是两表 left join 关联、count(distinct) 去重、比率计算以及小样本偏差的业务意识。

二、题目

计算每个商品的退款率(退款率 = 退款订单数 / 总订单数),并按退款率降序输出Top10商品及其所属品类。

假设有两张表:

  • t8_product_orders:商品订单表(含下单记录)
  • t8_refund_orders:退款订单表
-- t8_product_orders 商品订单表
+--------+----------+------------+----------+
| prod_id| order_id | order_time | category |
+--------+----------+------------+----------+
| P001 | 1001 | 2025-06-01 | 服装 |
| P002 | 1002 | 2025-06-01 | 服装 |
| P001 | 1003 | 2025-06-02 | 服装 |
| P003 | 1004 | 2025-06-02 | 食品 |
| P001 | 1005 | 2025-06-03 | 服装 |
| P003 | 1006 | 2025-06-03 | 食品 |
| P002 | 1007 | 2025-06-04 | 服装 |
+--------+----------+------------+----------+

-- t8_refund_orders 退款订单表
+----------+--------+-------------------+
| order_id | prod_id| refund_time |
+----------+--------+-------------------+
| 1001 | P001 | 2025-06-03 10:00 |
| 1003 | P001 | 2025-06-05 11:00 |
| 1005 | P001 | 2025-06-06 08:00 |
| 1006 | P003 | 2025-06-06 16:00 |
+----------+--------+-------------------+

三、思路分析

  1. 先统计每个商品的总订单数,再统计每个商品的退款订单数;
  2. 使用 left join 关联两张表,避免退款为0的商品被遗漏;
  3. 退款率 = 退款订单数 / 总订单数,按退款率降序取Top10。
维度评分
题目难度⭐️⭐️
题目清晰度⭐️⭐️⭐️⭐️⭐️
业务常见度⭐️⭐️⭐️⭐️⭐️

四、逐步推导

1.分别统计每个商品的总订单数和退款订单数

执行SQL

select p.prod_id,
p.category,
count(distinct p.order_id) as total_orders,
count(distinct r.order_id) as refund_orders
from t8_product_orders p
left join t8_refund_orders r
on p.prod_id = r.prod_id and p.order_id = r.order_id
group by p.prod_id, p.category

查询结果

+--------+----------+--------------+--------------+
| prod_id| category | total_orders | refund_orders|
+--------+----------+--------------+--------------+
| P001 | 服装 | 3 | 3 |
| P002 | 服装 | 2 | 0 |
| P003 | 食品 | 2 | 1 |
+--------+----------+--------------+--------------+

2.计算退款率并取Top10

执行SQL

select prod_id,
category,
total_orders,
refund_orders,
round(refund_orders / total_orders, 4) as refund_rate
from (
select p.prod_id,
p.category,
count(distinct p.order_id) as total_orders,
count(distinct r.order_id) as refund_orders
from t8_product_orders p
left join t8_refund_orders r
on p.prod_id = r.prod_id and p.order_id = r.order_id
group by p.prod_id, p.category
) t
order by refund_rate desc
limit 10

查询结果

+--------+----------+--------------+--------------+-------------+
| prod_id| category | total_orders | refund_orders| refund_rate |
+--------+----------+--------------+--------------+-------------+
| P001 | 服装 | 3 | 3 | 1.0000 |
| P003 | 食品 | 2 | 1 | 0.5000 |
| P002 | 服装 | 2 | 0 | 0.0000 |
+--------+----------+--------------+--------------+-------------+

五、常见坑点

坑1:用 inner join 会丢掉退款数为0的商品 — 退款订单表中没有该商品的退款记录,inner join 会让该商品整行消失,最后的总订单数统计就不准了。必须用 left join,这样退款数为 0 的商品还能保留,count(distinct r.order_id) 对无匹配行会计为 0。

坑2:count(distinct order_id) vs count(*) — 同一个商品在订单表中可能有多行记录(proid + order_id 组合),直接 count(*) 会把所有行都算进去。正确做法是用 count(distinct p.order_id) 统计去重后的订单数,否则总订单数会被 join 放大。

坑3:小样本偏差导致误判 — 只卖了 1 单且退款 1 单的商品,退款率是 100%,但样本太小没有治理价值。实际业务中通常加 having total_orders >= N(如 ≥5)过滤掉低销量的噪音商品,否则 Top10 可能全是只卖一单的偏门商品。

六、举一反三

  1. 品类级退款率聚合:按 category 聚合,计算每个品类的整体退款率,识别是某个商品的问题还是整个品类的系统性问题,帮助品控团队做层级决策

  2. 退款率时间趋势:按月统计退款率走势,看某个商品或品类的退款率是否在持续上升——如果某商品从 5% 涨到 30%,说明近期批次可能出了质量问题,需要及时抽检

  3. 退款原因分析:如果系统中还有退款原因表(退货类型/原因描述),关联后可分析退款的根因——是质量问题、描述不符、还是物流破损,帮助品控从源头改进

七、知识点总结

考点说明
left join保留主表全部数据,右表无匹配时用 NULL 填充,确保退款为0的商品不丢失
count(distinct)对 join 结果去重计数,避免同一订单因多行关联被重复计算
round 函数控制退款率的小数位数,保留 4 位便于对比精度
order by + limit降序排序后取前 N 行,是 TopN 分析的标准写法

八、建表语句和数据插入

点击展开 DDL & DML
-- 建表语句
create table t8_product_orders (
prod_id string comment '商品ID',
order_id bigint comment '订单ID',
order_time string comment '下单时间',
category string comment '商品品类'
) comment '商品订单表';

create table t8_refund_orders (
order_id bigint comment '订单ID',
prod_id string comment '商品ID',
refund_time string comment '退款时间'
) comment '退款订单表';

-- 插入数据
insert into t8_product_orders(prod_id, order_id, order_time, category) values
('P001', 1001, '2025-06-01', '服装'),
('P002', 1002, '2025-06-01', '服装'),
('P001', 1003, '2025-06-02', '服装'),
('P003', 1004, '2025-06-02', '食品'),
('P001', 1005, '2025-06-03', '服装'),
('P003', 1006, '2025-06-03', '食品'),
('P002', 1007, '2025-06-04', '服装');

insert into t8_refund_orders(order_id, prod_id, refund_time) values
(1001, 'P001', '2025-06-03 10:00'),
(1003, 'P001', '2025-06-05 11:00'),
(1005, 'P001', '2025-06-06 08:00'),
(1006, 'P003', '2025-06-06 16:00');
📱关注公众号

「数据仓库技术」文章同步更新,不错过每一篇干货

微信公众号二维码
💬加群交流

备注「数据仓库技术」加入社群,每日一道大厂SQL真题

交流微信二维码

你可能还想看