基于本文回答
0
评论

找出用户输入取件码、柜门打开后但由于某种原因未取走快递(传感器检测到格口重新锁定,且状态为未取出),并在 2 小时内被快递员取出重新投递的记录

SparkSQL 面试题:异常取件与快速重投分析

1. 题目背景与业务场景

在智能快递柜业务中,存在一种特殊的“异常取件”场景:用户输入了正确的取件码,柜门也正常打开,但可能由于用户分神、临时有事离开或误操作,导致用户并未真正拿走快递。快递柜传感器检测到柜门在一定时间后被重新锁定,且系统状态判定为“未取出”。

为了保障快递安全,快递员通常会在收到异常报警后,在短时间内(本题限制为 2 小时内)将该快递取出并重新进行投递。

本题要求通过 SparkSQL 分析快递柜事件日志表投递记录表,找出所有满足“用户开门未取 -> 2小时内快递员取出重投”的异常记录。


2. 面试数据准备(样例数据)

表 1:快递柜操作事件日志表 (cabinet_event_log)

记录快递柜格口的所有物理事件(开门、锁门、传感器状态)。

event_id express_id (快递单号) event_type (事件类型) lock_status (锁状态) cargo_status (货物状态) event_time (事件时间)
E001 EXP1001 USER_OPEN (用户开门) OPEN UNKNOWN 2023-10-25 10:00:00
E002 EXP1001 AUTO_CLOSE (自动关闭) LOCKED NOT_TAKEN (未取出) 2023-10-25 10:02:00
E003 EXP1002 USER_OPEN (用户开门) OPEN UNKNOWN 2023-10-25 11:00:00
E004 EXP1002 AUTO_CLOSE (自动关闭) LOCKED TAKEN (已取出) 2023-10-25 11:01:00
E005 EXP1003 USER_OPEN (用户开门) OPEN UNKNOWN 2023-10-25 14:00:00
E006 EXP1003 AUTO_CLOSE (自动关闭) LOCKED NOT_TAKEN (未取出) 2023-10-25 14:03:00
表 2:快递投递及处理记录表 (delivery_record)

记录快递员的投递、取出重投等人工操作。

record_id express_id (快递单号) action_type (操作类型) operator_role (操作员角色) action_time (操作时间)
R001 EXP1001 DELIVERY (首次投递) COURIER (快递员) 2023-10-25 08:00:00
R002 EXP1001 RE_TAKE (异常取出) COURIER (快递员) 2023-10-25 11:30:00
R003 EXP1001 REDELIVERY (重新投递) COURIER (快递员) 2023-10-25 11:35:00
R004 EXP1003 RE_TAKE (异常取出) COURIER (快递员) 2023-10-25 17:30:00

3. 面试题答案 (SparkSQL 实现)

sql
WITH user_abort_events AS (
    -- 1. 步骤一:找出“用户开门后未取走、柜门重新锁定”的异常事件
    -- 使用窗口函数 LEAD 获取紧邻的下一个锁定事件,并确保货物状态为 'NOT_TAKEN'
    SELECT 
        express_id,
        event_time AS open_time,
        next_event_time AS lock_time
    FROM (
        SELECT 
            express_id,
            event_type,
            event_time,
            LEAD(event_type) OVER (PARTITION BY express_id ORDER BY event_time) AS next_event_type,
            LEAD(lock_status) OVER (PARTITION BY express_id ORDER BY event_time) AS next_lock_status,
            LEAD(cargo_status) OVER (PARTITION BY express_id ORDER BY event_time) AS next_cargo_status,
            LEAD(event_time) OVER (PARTITION BY express_id ORDER BY event_time) AS next_event_time
        FROM cabinet_event_log
    ) t
    WHERE t.event_type = 'USER_OPEN' 
      AND t.next_event_type = 'AUTO_CLOSE'
      AND t.next_lock_status = 'LOCKED'
      AND t.next_cargo_status = 'NOT_TAKEN'
),

courier_retakes AS (
    -- 2. 步骤二:筛选出快递员执行的“异常取出”记录
    SELECT 
        express_id,
        action_time AS courier_take_time
    FROM delivery_record
    WHERE action_type = 'RE_TAKE' 
      AND operator_role = 'COURIER'
)

-- 3. 步骤三:双表关联,限制快递员取出时间在用户锁定格口后的 2 小时内
SELECT 
    a.express_id,
    a.open_time AS user_open_time,
    a.lock_time AS cabinet_relock_time,
    c.courier_take_time,
    -- 计算快递员响应耗时(分钟)
    ROUND((CAST(c.courier_take_time AS LONG) - CAST(a.lock_time AS LONG)) / 60, 2) AS courier_response_minutes
FROM user_abort_events a
JOIN courier_retakes c 
  ON a.express_id = c.express_id
WHERE c.courier_take_time > a.lock_time 
  -- 限制在 2 小时(7200秒)内
  AND CAST(c.courier_take_time AS LONG) - CAST(a.lock_time AS LONG) <= 7200;
预期输出结果:
express_id user_open_time cabinet_relock_time courier_take_time courier_response_minutes
EXP1001 2023-10-25 10:00:00 2023-10-25 10:02:00 2023-10-25 11:30:00 88.0

注:EXP1003 虽然也发生了未取走事件(14:03锁定),但快递员在 17:30 才取出,间隔了 3 小时 27 分钟,超过了 2 小时限制,因此被过滤。


4. SparkSQL 深度分析与面试应对指南

① 考察的核心知识点
  1. 窗口函数 LEAD 的妙用
    • 业务要求“开门后重新锁定且未取出”。在日志流中,这是前后紧邻的两条记录。
    • 面试官希望看到候选人能够使用 LEAD(col, 1) OVER (PARTITION BY ... ORDER BY ...) 将相邻行的状态平铺到同一行,便于进行条件过滤,避免多次自关联(Self-Join)带来的性能开销。
  2. 时间差值的计算与性能
    • 在 SparkSQL 中,直接将 TIMESTAMP 转换为 LONG(Unix 时间戳,单位为秒)进行减法运算,是计算时间差最通用、最高效且不易出错的方法,避免了因不同数据库底层 DATEDIFF 颗粒度不同(有的只支持天)导致的兼容性问题。
  3. 复杂业务逻辑的拆解能力 (CTE 表达式)
    • 使用 WITH ... AS ... (Common Table Expressions) 结构。在面试中,清晰的代码结构(如将“用户异常行为”和“快递员响应行为”拆为两个子查询)能极大提升面试官的好感,体现出良好的工程素养。
② Spark 优化细节(加分项)

如果在面试中被问及“如何优化这个 SQL 的执行性能?”,可以从以下几个维度回答:

  • 避免大表 Join 的 Shuffle (Broadcast Join)
    • 在实际生产中,cabinet_event_log(事件日志表)数据量极大,而符合 RE_TAKE 且是快递员操作的 delivery_record 过滤后数据量通常很小。
    • 可以向面试官主动提出:如果 courier_retakes 过滤后的数据集小于 spark.sql.autoBroadcastJoinThreshold(默认 10MB),Spark 会自动采用 Broadcast Hash Join,从而避免了昂贵的 Shuffle 阶段。
  • 分区裁剪与谓词下推
    • 在实际的大数据表中,日志表通常会按 dt (日期) 进行分区。在子查询中应当首先加上分区过滤条件(例如 WHERE dt = '20231025'),利用 Spark 的 Partition Pruning 减少读取的数据量。
右滑查看面试常问