基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

统计每个快递柜每天“所有格口均处于占用状态(无空闲格口)”的累计持续时长(分钟数),找出最易爆柜的前 100 个快递柜

SparkSQL 面试题:快递柜爆柜(满载)累计持续时长统计

1. 题目背景与业务逻辑

在智能快递柜运营中,“爆柜”(即所有格口均被占用,无可用空闲格口)会导致快递员无法投递,严重影响用户体验和运营效率。
本题要求:统计每个快递柜每天“所有格口均处于占用状态”的累计持续时长(分钟数),并找出最易爆柜(即日均爆柜时长最长)的前 100 个快递柜。


2. 示例数据

表 1:快递柜主表 cabinet_info

记录快递柜的基本信息及总格口数。

字段名 类型 说明
cabinet_id String 快递柜ID
total_grids Integer 该快递柜的总格口数

cabinet_info 示例数据:

cabinet_id total_grids
CAB001 3
CAB002 4

表 2:格口状态变更流水表 grid_status_log

记录每个格口的占用与释放事件。每次状态变更生成一条记录。

  • status = 1 表示被占用(投递)
  • status = 0 表示变为空闲(取出)
字段名 类型 说明
cabinet_id String 快递柜ID
grid_id String 格口ID
status Integer 状态:1-占用,0-空闲
event_time Timestamp 事件发生时间

grid_status_log 示例数据:
(以 CAB001 拥有 3 个格口为例,展示某天状态变化)

cabinet_id grid_id status event_time
CAB001 G1 1 2023-10-25 08:00:00
CAB001 G2 1 2023-10-25 08:05:00
CAB001 G3 1 2023-10-25 08:10:00
CAB001 G1 0 2023-10-25 08:30:00
CAB001 G1 1 2023-10-25 09:00:00
CAB001 G2 0 2023-10-25 09:15:00

3. 核心解题思路与 SparkSQL 分析

这是一道典型且高频的时序状态分析(State Duration)面试题,考察窗口函数、状态重建以及时间段重叠计算。在 SparkSQL 中解决此问题分为四个核心步骤:

步骤一:计算每次事件发生后的“当前占用格口数”

利用窗口函数,对每个柜子按时间升序,使用累计求和(Cumulative Sum)计算出每次事件发生后,当前处于占用状态的格口总数。

  • 计算公式current_occupied = SUM(status_change)
  • 注意:如果是状态变更为 1(占用),则增量为 +1;若变更为 0(释放),则增量为 -1
步骤二:构建状态持续区间(生成生命周期)

我们需要计算“当前状态”持续了多久。利用 LEAD 窗口函数获取下一次状态变更的时间作为当前状态的结束时间。

  • 若下一次变更跨天或没有下一次变更,则结束时间截止到当天深夜 23:59:59
步骤三:筛选出“爆柜”区间并计算时长

当前占用格口数 = 总格口数 时,代表进入“爆柜”状态。

  • 爆柜持续时长 = 下一次变更时间 - 当前变更时间
步骤四:按柜子和日期聚合,并计算日均时长排序

cabinet_iddate 分组求和,再按 cabinet_id 求平均值,降序排列取前 100。


4. SparkSQL 满分标准答案

sql
WITH pv_status_change AS (
    -- 1. 标准化状态变更值:1表示+1,0表示-1
    SELECT 
        cabinet_id,
        event_time,
        CAST(TO_DATE(event_time) AS STRING) AS op_date,
        CASE WHEN status = 1 THEN 1 ELSE -1 END AS change_val
    FROM grid_status_log
),

pv_occupied_count AS (
    -- 2. 计算每次事件后的累计占用数
    SELECT 
        log.cabinet_id,
        log.op_date,
        log.event_time,
        SUM(log.change_val) OVER (
            PARTITION BY log.cabinet_id 
            ORDER BY log.event_time 
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS current_occupied,
        info.total_grids
    FROM pv_status_change log
    JOIN cabinet_info info ON log.cabinet_id = info.cabinet_id
),

pv_state_duration AS (
    -- 3. 使用 LEAD 函数获取当前状态的结束时间,构建时间区间
    SELECT 
        cabinet_id,
        op_date,
        current_occupied,
        total_grids,
        event_time AS start_time,
        -- 如果没有下一条记录,则默认截止到当天最后一秒
        COALESCE(
            LEAD(event_time) OVER (PARTITION BY cabinet_id, op_date ORDER BY event_time),
            CAST(CONCAT(op_date, ' 23:59:59') AS TIMESTAMP)
        ) AS end_time
    FROM pv_occupied_count
),

pv_full_load_minutes AS (
    -- 4. 筛选出“全部占满”的区间,并计算持续分钟数
    SELECT 
        cabinet_id,
        op_date,
        -- 计算差值并转换为分钟数(Spark中TIMESTAMP相减得到秒数,需除以60)
        SUM(CAST(unix_timestamp(end_time) - unix_timestamp(start_time) AS DOUBLE) / 60.0) AS daily_full_duration_minutes
    FROM pv_state_duration
    WHERE current_occupied = total_grids
    GROUP BY cabinet_id, op_date
),

pv_avg_full_load AS (
    -- 5. 计算每个快递柜的日均爆柜时长
    SELECT 
        cabinet_id,
        AVG(daily_full_duration_minutes) AS avg_daily_full_duration_minutes,
        COUNT(DISTINCT op_date) AS active_days
    FROM pv_full_load_minutes
    GROUP BY cabinet_id
)

-- 6. 输出最易爆柜的前 100 个快递柜
SELECT 
    cabinet_id,
    ROUND(avg_daily_full_duration_minutes, 2) AS avg_daily_full_duration_minutes,
    active_days
FROM pv_avg_full_load
ORDER BY avg_daily_full_duration_minutes DESC
LIMIT 100;

5. 针对面试官的 Spark 优化与深度回答建议

如果在面试中写出上述 SQL,可以主动提及以下 Spark 优化点,以极大提升面试评价:

  1. 数据倾斜应对(Data Skew)

    • 痛点:快递柜数量多,但某些“网红小区”的快递柜使用极度频繁,导致这些 cabinet_id 的流水数据远多于其他柜子。在 PARTITION BY cabinet_id 时会发生严重的数据倾斜。
    • 解决方案:如果出现倾斜,可以引入“随机前缀”将大快递柜拆分,或者在执行 Window 函数前,确保 Spark 的 spark.sql.shuffle.partitions 设置合理,并开启 Adaptive Query Execution (AQE) 自动合并和拆分倾斜分区。
  2. Jumping Window / 跨天边界处理的边界问题

    • 面试官可能会问:“如果一个柜子从昨天晚上 23:00 一直满载到今天早上 02:00,你的 SQL 怎么处理?”
    • 回答提示:本 SQL 采用 PARTITION BY cabinet_id, op_date 进行了按天截断(利用 23:59:59 封顶)。如果需要精确的跨天连续计算,应当去掉 op_date 分区,在全局时间轴上使用 LEAD,然后再通过 UDF 或 Spark 算子将跨天的区间拆分到两个自然日中。
  3. Join 优化(MapJoin / Broadcast Hash Join)

    • 快递柜基础信息表 cabinet_info 通常非常小(几十万行以内),而流水表 grid_status_log 非常大(数亿行)。
    • 优化方案:在对两表进行 JOIN 时,应使用 BROADCAST 提示强制将 cabinet_info 广播到各个 Executor,避免大表 Shuffle:
      sql
      JOIN /*+ BROADCAST(info) */ cabinet_info info ON log.cabinet_id = info.cabinet_id
00:00
00:00