分析每个快递员投递快件的习惯,统计其投递的包裹在大、中、小三种规格格口中的占比分布
SparkSQL 面试题:快递员投递包裹规格占比分析
1. 题目背景与要求
在物流行业中,快递柜是最后 100 米投递的重要基础设施。快递柜的格口通常分为大(L)、中(M)、小(S)三种规格。分析快递员对不同规格格口的投递偏好,可以帮助运营团队进行快递柜格口配置的优化或对快递员进行画像。
分析需求:
请编写 SparkSQL 语句,计算每个快递员投递的包裹在大、中、小三种规格格口中的占比分布(结果保留 4 位小数,并以百分比形式展示,例如 50.00%)。
2. 示例数据
表 1:快递员基础信息表 (t_courier)
| 字段名 | 类型 | 说明 |
|---|---|---|
| courier_id | String | 快递员ID |
| courier_name | String | 快递员姓名 |
| company | String | 所属快递公司 |
t_courier 数据样例:
| courier_id | courier_name | company |
|---|---|---|
| C001 | 张三 | 顺丰 |
| C002 | 李四 | 美团 |
| C003 | 王五 | 圆通 |
表 2:快递柜投递记录表 (t_delivery_record)
| 字段名 | 类型 | 说明 |
|---|---|---|
| record_id | String | 投递记录ID |
| courier_id | String | 快递员ID |
| box_size | String | 投递格口规格 (L:大, M:中, S:小) |
| delivery_time | String | 投递时间 |
t_delivery_record 数据样例:
| record_id | courier_id | box_size | delivery_time |
|---|---|---|---|
| R001 | C001 | L | 2023-10-25 08:00:00 |
| R002 | C001 | L | 2023-10-25 08:30:00 |
| R003 | C001 | M | 2023-10-25 09:00:00 |
| R004 | C001 | S | 2023-10-25 09:10:00 |
| R005 | C002 | S | 2023-10-25 10:00:00 |
| R006 | C002 | S | 2023-10-25 10:15:00 |
| R007 | C003 | M | 2023-10-25 11:00:00 |
| R008 | C003 | L | 2023-10-25 11:30:00 |
3. 期望输出结果
| courier_id | courier_name | total_count | L_ratio | M_ratio | S_ratio |
|---|---|---|---|---|---|
| C001 | 张三 | 4 | 50.00% | 25.00% | 25.00% |
| C002 | 李四 | 2 | 0.00% | 0.00% | 100.00% |
| C003 | 王五 | 2 | 50.00% | 50.00% | 0.00% |
4. SparkSQL 核心解法
在 SparkSQL 中,推荐使用 条件聚合(Conditional Aggregation) 来进行行列转换(Pivot)。这种方式在 Spark 引擎中执行效率最高,避免了复杂的 Join 操作。
sql
SELECT
c.courier_id,
c.courier_name,
COUNT(r.record_id) AS total_count,
-- 计算大箱子占比
CONCAT(
ROUND(COALESCE(SUM(CASE WHEN r.box_size = 'L' THEN 1 ELSE 0 END), 0) * 100.0 / COUNT(r.record_id), 2),
'%'
) AS L_ratio,
-- 计算中箱子占比
CONCAT(
ROUND(COALESCE(SUM(CASE WHEN r.box_size = 'M' THEN 1 ELSE 0 END), 0) * 100.0 / COUNT(r.record_id), 2),
'%'
) AS M_ratio,
-- 计算小箱子占比
CONCAT(
ROUND(COALESCE(SUM(CASE WHEN r.box_size = 'S' THEN 1 ELSE 0 END), 0) * 100.0 / COUNT(r.record_id), 2),
'%'
) AS S_ratio
FROM
t_courier c
LEFT JOIN
t_delivery_record r ON c.courier_id = r.courier_id
GROUP BY
c.courier_id,
c.courier_name
HAVING
total_count > 0;
5. SparkSQL 深度分析与面试加分项
在面试中,仅仅写出 SQL 只能算及格。能够针对 Spark 引擎的底层原理进行优化分析,才能拿到高分:
① 避免数据倾斜(Data Skew)
- 痛点: 头部快递员(如大区劳模、网点核心人员)的投递量可能极高,导致在
GROUP BY courier_id时,这部分 key 分发到同一个 Reduce 任务中,引发数据倾斜。 - 解决方案:
- 如果数据量极大,可以采用两阶段聚合。第一阶段给
courier_id加随机前缀(如1_C001,2_C001)进行局部聚合,第二阶段去掉前缀进行全局聚合。 - 确保 Spark 的
spark.sql.adaptive.enabled(AQE 自动倾斜连接/聚合优化)处于开启状态。
- 如果数据量极大,可以采用两阶段聚合。第一阶段给
② 广播连接(Broadcast Join)的应用
- 场景分析: 快递员基础信息表(
t_courier)通常是维度表,数据量相对较小(万级以下);而投递记录表(t_delivery_record)是事实表,数据量极大(千万级甚至亿级)。 - Spark 优化:
- 应当显式使用广播连接,避免大表在 Shuffle 阶段带来的网络和磁盘 I/O 损耗:sql
SELECT /*+ BROADCAST(c) */ ... FROM t_courier c LEFT JOIN t_delivery_record r ...
- 应当显式使用广播连接,避免大表在 Shuffle 阶段带来的网络和磁盘 I/O 损耗:
③ 计算精度的处理
- 在 SparkSQL 中,整数除法(如
3 / 4)默认会返回 Double 类型。但在金融和精确统计场景下,为了防止 Double 的精度丢失,建议使用* 100.0(隐式转换为 Decimal)或者显式使用CAST(expr AS DECIMAL(10,4))进行精度控制。
④ 零值防范与外连接(Left Join)
- 使用
LEFT JOIN是为了防止漏掉没有投递记录的快递员(虽然在实际业务中没投递过的快递员可能不需要统计,所以加上了HAVING total_count > 0)。 - 在计算分子时,使用
COALESCE(..., 0)可以有效防止因数据缺失导致的NULL值传递,保证了报表数据的健壮性。
右滑查看面试常问