系统定义“用户投递后 4 小时内快递员未进行开柜揽收”为揽收超时。统计每个快递公司(如顺丰、极兔、中通等)的寄件揽收超时率
Spark SQL 面试题:快递寄件揽收超时率统计
1. 题目背景
在物流行业中,时效性是衡量服务质量的核心指标。系统定义:“用户投递后 4 小时内(即 分钟),快递员未进行开柜揽收”为揽收超时。
现在需要你编写一段 Spark SQL,统计每个快递公司的寄件揽收超时率。
超时率计算公式:
2. 示例数据
表 1:用户投递记录表 user_delivery
记录用户在快递柜的投递时间及对应的快递公司。
| delivery_id (投递ID) | company_name (快递公司) | drop_off_time (用户投递时间) |
|---|---|---|
| D001 | 顺丰速运 | 2023-10-25 08:00:00 |
| D002 | 顺丰速运 | 2023-10-25 09:00:00 |
| D003 | 极兔速递 | 2023-10-25 10:00:00 |
| D004 | 中通快递 | 2023-10-25 11:10:00 |
| D005 | 中通快递 | 2023-10-25 12:00:00 |
| D006 | 极兔速递 | 2023-10-25 14:00:00 |
表 2:快递员揽收记录表 courier_pickup
记录快递员开柜揽收的时间(若用户投递后,快递员一直未揽收,则此表无对应投递ID的记录)。
| pickup_id (揽收ID) | delivery_id (投递ID) | pickup_time (快递员揽收时间) |
|---|---|---|
| P001 | D001 | 2023-10-25 10:30:00 |
| P002 | D002 | 2023-10-25 14:00:00 |
| P003 | D003 | 2023-10-25 11:30:00 |
| P004 | D004 | 2023-10-25 17:00:00 |
| (未揽收) | D006 | - |
3. 期望输出结果
| company_name (快递公司) | total_delivery (总投递数) | timeout_count (超时数) | timeout_rate (超时率) |
|---|---|---|---|
| 顺丰速运 | 2 | 1 | 50.00% |
| 极兔速递 | 2 | 1 | 50.00% |
| 中通快递 | 2 | 1 | 50.00% |
4. Spark SQL 参考答案
sql
WITH joined_data AS (
SELECT
d.delivery_id,
d.company_name,
d.drop_off_time,
p.pickup_time,
-- 计算时间差(单位:秒),若未揽收则认为无限大(即超时)
-- Spark SQL 中可以直接用 unix_timestamp 转换后相减
(unix_timestamp(p.pickup_time) - unix_timestamp(d.drop_off_time)) / 3600 AS hours_diff
FROM
user_delivery d
LEFT JOIN
courier_pickup p ON d.delivery_id = p.delivery_id
),
timeout_flag_data AS (
SELECT
company_name,
CASE
-- 情况1:一直未揽收(hours_diff 为 NULL) -> 算作超时
WHEN hours_diff IS NULL THEN 1
-- 情况2:揽收时间差大于 4 小时 -> 算作超时
WHEN hours_diff > 4 THEN 1
-- 情况3:4 小时内揽收 -> 未超时
ELSE 0
END AS is_timeout
FROM
joined_data
)
SELECT
company_name,
COUNT(1) AS total_delivery,
SUM(is_timeout) AS timeout_count,
-- 格式化输出百分比,保留两位小数
CONCAT(ROUND(SUM(is_timeout) * 100.0 / COUNT(1), 2), '%') AS timeout_rate
FROM
timeout_flag_data
GROUP BY
company_name;
5. Spark SQL 核心考点与深度分析
考点一:表关联关系的选择(Left Join 还是 Inner Join?)
- 分析: 必须使用
LEFT JOIN。因为部分快件可能从未被揽收(如示例数据中的D006)。如果使用INNER JOIN,未揽收的快件会在关联时被过滤掉,导致“超时数”和“总投递数”的统计全部偏小,无法真实反映业务现状。
考点二:如何定义“未揽收”的边界条件?
- 分析: 这是一个典型的业务细节坑。在 SQL 中,
LEFT JOIN之后未揽收的记录其pickup_time值为NULL。在写CASE WHEN逻辑时,必须先将pickup_time IS NULL(或hours_diff IS NULL)判定为超时,再判断hours_diff > 4。 - Spark 优化点: 在 Spark SQL 中,处理
NULL值时要格外小心,因为任何数与NULL进行大小比较(如NULL > 4)结果都为NULL,不会走入ELSE,如果不显式处理NULL,会导致漏计。
考点三:时间差的计算函数选择
- 分析: Spark SQL 提供了多种计算时间差的方式:
unix_timestamp(t2) - unix_timestamp(t1):将时间转为秒级时间戳相减,最通用、精度最高,推荐用于需要精确到分/秒的场景。datediff(t2, t1):仅计算天数差,不适合本题的“4小时”高精度要求。cast(t2 as double) - cast(t1 as double):在 Spark 3.x 中,可以直接将 Timestamp 转换为 Double(单位为秒)进行减法运算。
考点四:数据倾斜与 Spark 性能调优
若在实际面试中面试官追问:“如果某家快递公司(如顺丰)的数据量极大,导致发生数据倾斜,该如何优化?”
- 解决方案:
- Broadcast Join(广播连接): 揽收表(
courier_pickup)和投递表(user_delivery)如果其中一个较小(默认 <10MB,可通过spark.sql.autoBroadcastJoinThreshold调整),可以使用broadcast(small_table),避免 Shuffle 产生的数据倾斜。 - 加盐两阶段聚合(Salted Aggregation): 如果是
GROUP BY company_name导致的聚合倾斜,可以给company_name加上随机前缀(如SF_1,SF_2),先进行局部聚合,再去掉前缀进行全局聚合。
- Broadcast Join(广播连接): 揽收表(
右滑查看面试常问