统计用户在线预约快递柜寄件后、投递前取消订单的记录,按取消原因(如:无可用空柜、运费太贵、自己送去驿站等)进行占比统计
SparkSQL 面试题:快递寄件取消原因占比统计
1. 题目背景
在智慧物流场景中,用户通过手机 APP 预约快递柜寄件后,在实际“投递入柜”前可能会取消订单。分析这些“预约后、投递前”取消订单的原因占比,能够帮助产品和运营团队发现服务痛点(例如:是否存在常态化满柜、运费定价是否偏高、驿站竞争分流等),从而优化快递柜的布局与定价策略。
2. 样例数据
表 1:寄件订单表 (express_orders)
记录用户寄件订单的生命周期状态。
| order_id (订单ID) | user_id (用户ID) | create_time (预约时间) | status (当前状态) | cancel_reason (取消原因) | cancel_time (取消时间) |
|---|---|---|---|---|---|
| ord_001 | usr_101 | 2023-10-25 08:30:00 | cancelled | 无可用空柜 | 2023-10-25 08:45:00 |
| ord_002 | usr_102 | 2023-10-25 09:00:00 | delivered | NULL | NULL |
| ord_003 | usr_103 | 2023-10-25 09:15:00 | cancelled | 运费太贵 | 2023-10-25 09:30:00 |
| ord_004 | usr_104 | 2023-10-25 10:00:00 | cancelled | 自己送去驿站 | 2023-10-25 10:20:00 |
| ord_005 | usr_105 | 2023-10-25 10:30:00 | cancelled | 无可用空柜 | 2023-10-25 10:35:00 |
| ord_006 | usr_106 | 2023-10-25 11:00:00 | ordered | NULL | NULL |
| ord_007 | usr_107 | 2023-10-25 11:15:00 | cancelled | 填错地址重填 | 2023-10-25 11:40:00 |
状态说明 (
status):ordered(已预约/待投递),delivered(已投递入柜/完成),cancelled(已取消)。
表 2:快递柜投递流水表 (cabinet_deliveries)
记录快递员或用户实际将包裹投递放入快递柜的动作。如果用户在投递前取消,则此表无对应记录。
| delivery_id (投递ID) | order_id (订单ID) | cabinet_id (快递柜ID) | action_time (投递时间) |
|---|---|---|---|
| del_501 | ord_002 | cab_99 | 2023-10-25 09:10:00 |
3. 面试要求
请编写一段 SparkSQL 语句,统计在投递前取消的订单中,各种取消原因的出现次数以及占总取消订单的比例(保留2位小数,带百分号),并按占比降序排列。
4. 参考答案 (SparkSQL)
sql
WITH cancelled_before_delivery AS (
-- 1. 筛选出:已预约、已取消,且在投递流水表中无记录的订单
SELECT
o.order_id,
COALESCE(o.cancel_reason, '未填写') AS cancel_reason
FROM
express_orders o
LEFT JOIN
cabinet_deliveries d
ON
o.order_id = d.order_id
WHERE
o.status = 'cancelled'
AND d.order_id IS NULL -- 确保投递前取消(无投递记录)
),
reason_counts AS (
-- 2. 统计每个原因的频数,并通过窗口函数计算总取消数
SELECT
cancel_reason,
COUNT(1) AS reason_count,
SUM(COUNT(1)) OVER() AS total_cancelled_count
FROM
cancelled_before_delivery
GROUP BY
cancel_reason
)
-- 3. 计算占比并格式化输出
SELECT
cancel_reason,
reason_count,
CONCAT(ROUND((reason_count / total_cancelled_count) * 100, 2), '%') AS proportion
FROM
reason_counts
ORDER BY
reason_count DESC;
预期输出结果:
| cancel_reason | reason_count | proportion |
|---|---|---|
| 无可用空柜 | 2 | 50.00% |
| 自己送去驿站 | 1 | 25.00% |
| 运费太贵 | 1 | 25.00% |
(注:ord_007 虽然也是取消状态,但因为样例数据中其取消原因为“填错地址重填”,在此处按逻辑一并计入,未在上述精简表格中体现,实际运行以全量合并为准)
5. SparkSQL 考点与深度分析
本题看似简单,但在实际大厂数据研发面试中,能够考察面试者在数据清洗、逻辑抽象及 Spark 执行优化等多个维度的功底。
1. 业务逻辑的精准翻译(左外连接排他)
- 考点:“投递前取消”的定义。
- 分析:不能仅筛选
status = 'cancelled'。在实际业务中,可能存在“用户已经把快递放入柜中,但由于系统故障或特殊原因后台退单”的情况。因此,必须通过LEFT JOIN投递流水表,并过滤d.order_id IS NULL(即 Anti-Join 逻辑),来确保该订单绝对没有发生过实物投递。
2. 窗口函数(Window Function)的高效应用
- 考点:如何在单条 SQL 中同时计算“明细/分组值”与“全局总和”。
- 分析:新手常使用
GROUP BY计算完各原因数量后,再CROSS JOIN一个SELECT COUNT(1) FROM table子查询来求总数,这会导致对表进行二次扫描(Scan)。 - SparkSQL 优化:使用
SUM(COUNT(1)) OVER()。Spark 在执行聚合(HashAggregate)后,直接在内存中通过窗口算子计算出全局总和,避免了重复读取底层 HDFS 数据的开销。
3. SparkSQL 执行计划优化(Join 倾斜与选择)
面试官通常会延伸提问:“如果 express_orders 表有百亿级数据,而 cabinet_deliveries 只有百万级,这个 Join 该如何优化?”
- BroadCastHashJoin (MAPJOIN):由于投递表相对较小,可以强制使用广播连接:这样可以将小表广播到各个 Executor,避免大表在大网内进行 Shuffle(Exchange),极大提升运行效率。sql
SELECT /*+ BROADCAST(d) */ ... FROM express_orders o LEFT JOIN cabinet_deliveries d ... - 空值(Null)过滤与倾斜:由于是
LEFT JOIN且关联键是order_id,如果order_id存在大量 NULL 值,会导致 Shuffle 倾斜。在 Join 之前应先通过WHERE order_id IS NOT NULL进行谓词下推(Predicate Pushdown)过滤。