按网点(Station)统计用户取件等待时间(小时数)的 50% 分位数(中位数)、90% 分位数和 95% 分位数
SparkSQL 面试题:网点用户取件等待时间分位数统计
1. 题目背景与要求
在物流与快递行业中,用户在网点的取件等待时间是衡量服务质量(SLA)的核心指标。为了评估整体服务水平并识别异常长等待的极端情况,需要按网点(Station)统计用户取件等待时间(小时数)的 50% 分位数(中位数)、90% 分位数和 95% 分位数。
2. 示例数据
表名:user_pickup_records(用户取件记录表)
记录ID (record_id) |
网点名称 (station) |
用户ID (user_id) |
到达网点时间 (arrival_time) |
完成取件时间 (pickup_time) |
|---|---|---|---|---|
| 101 | Station_A | U001 | 2023-10-25 09:00:00 | 2023-10-25 09:12:00 |
| 102 | Station_A | U002 | 2023-10-25 09:00:00 | 2023-10-25 09:30:00 |
| 103 | Station_A | U003 | 2023-10-25 10:00:00 | 2023-10-25 11:00:00 |
| 104 | Station_A | U004 | 2023-10-25 14:00:00 | 2023-10-25 16:00:00 |
| 105 | Station_A | U005 | 2023-10-25 15:00:00 | 2023-10-25 15:06:00 |
| 106 | Station_B | U006 | 2023-10-25 09:00:00 | 2023-10-25 09:30:00 |
| 107 | Station_B | U007 | 2023-10-25 10:00:00 | 2023-10-25 11:30:00 |
| 108 | Station_B | U008 | 2023-10-25 11:00:00 | 2023-10-25 11:15:00 |
面试题标准答案 (SparkSQL)
sql
WITH duration_calc AS (
SELECT
station,
-- 计算等待时间(小时数),保留4位小数
ROUND(
(CAST(UNIX_TIMESTAMP(pickup_time) AS DOUBLE) - CAST(UNIX_TIMESTAMP(arrival_time) AS DOUBLE)) / 3600.0,
4
) AS wait_hours
FROM
user_pickup_records
)
SELECT
station,
-- percentile_approx 允许传入数组一次性计算多个分位数
percentile_approx(wait_hours, 0.5) AS pct_50,
percentile_approx(wait_hours, 0.9) AS pct_90,
percentile_approx(wait_hours, 0.95) AS pct_95
FROM
duration_calc
GROUP BY
station;
预期输出结果
| station | pct_50 | pct_90 | pct_95 |
|---|---|---|---|
| Station_A | 0.5000 | 2.0000 | 2.0000 |
| Station_B | 0.5000 | 1.5000 | 1.5000 |
SparkSQL 深度分析与面试应对策略
在面试中解答此题,不仅要写出 SQL,还要能够向面试官阐述底层原理、性能瓶颈及优化方案。以下是针对该题目的深度剖析:
一、 核心函数选择:percentile vs percentile_approx
percentile函数:- 原理:精确计算分位数。它需要将组内的所有数据收集到内存中并进行全局排序。
- 缺点:如果某个网点(如大型转运中心)的数据量极大,全局排序会导致 OOM (Out Of Memory),且无法在分布式集群中高效并行处理。
- 限制:在 SparkSQL 中,
percentile的输入参数必须是整数/非浮点型。由于本题计算的是小时数(带小数),直接使用percentile会报错。
percentile_approx函数 (推荐):- 原理:近似计算分位数。它基于 T-Digest 算法,通过构建缓冲区对数据流进行聚类和压缩,只占用极小的内存( 级别)即可在超大规模数据集上估算出极度精确的分位数。
- 支持类型:支持
Double等数值类型。 - 第三参数 (精度控制):语法为
percentile_approx(col, p, [B])。B控制参数精度(默认值是 10,000)。B越大,精度越高,但消耗内存稍多。面试中提到这个参数是加分项。
二、 计算逻辑剖析
- 时间差转换:
- SparkSQL 中不能直接对
Timestamp进行减法,标准做法是使用UNIX_TIMESTAMP(col)将其转换为秒级时间戳(Long),相减后再除以3600.0转换为小时(Double)。
- SparkSQL 中不能直接对
- 多值聚合优化:
- 避免写三次
percentile_approx(wait_hours, 0.5)、percentile_approx(wait_hours, 0.9)导致的三次独立计算。SparkSQL 的percentile_approx支持传入数组参数:percentile_approx(wait_hours, array(0.5, 0.9, 0.95)),这在底层只会对数据做一次扫描(One-pass Scan),能显著提升性能。
- 避免写三次
三、 面试官可能追问的优化与边界问题
追问 1:如果数据存在倾斜(某个大网点数据量占 90%),如何优化?
- 解答:分位数计算天然对数据倾斜敏感,因为必须按
station进行GROUP BY(引发 Shuffle)。 - 优化方案:
- 两阶段聚合(针对近似值):先对网点加随机前缀进行局部聚合,再去掉前缀进行全局聚合(此方案对分位数精度有损,需谨慎)。
- 过滤异常值:在
WHERE条件中提前过滤掉arrival_time或pickup_time为空、或等待时间小于 0 的异常脏数据,减少参与 Shuffle 的数据量。
- 解答:分位数计算天然对数据倾斜敏感,因为必须按
追问 2:如果必须要精准分位数,且不能 OOM,怎么做?
- 解答:
- 如果数据量可以接受,使用窗口函数
row_number() over (partition by station order by wait_hours)标记序号 ,结合count(1) over (partition by station)计算总数 。 - 通过过滤条件 (或相邻两数取平均)来手动计算精准分位数。这种方式会将排序压力分散到各个 Executor 的磁盘和内存中,比直接
percentile鲁棒性更强。
- 如果数据量可以接受,使用窗口函数
- 解答: