基于本文回答
0
评论

将一天 24 小时划分为四个时段:深夜(0-6)、上午(6-12)、下午(12-18)、晚上(18-24)。统计各时段的下单用户数、下单订单数以及 GMV 的占比

SparkSQL 面试题:24小时分时段订单指标占比统计

1. 题目背景与要求

在电商和外卖等业务场景中,分析用户在一天中不同时间段的消费习惯对于精准营销和库存管理至关重要。
请编写 SparkSQL 语句,将一天 24 小时划分为四个时段:深夜(0-6点,不含6点)、上午(6-12点,不含12点)、下午(12-18点,不含18点)、晚上(18-24点,不含24点)
统计各时段的下单用户数(需去重)、下单订单数、GMV(交易总额)以及各指标在全天的占比

2. 示例数据

订单表:t_order_detail
order_id (订单ID) user_id (用户ID) order_time (下单时间) gmv (订单金额)
o_001 u_01 2023-10-25 01:15:00 100.0
o_002 u_01 2023-10-25 03:30:00 50.0
o_003 u_02 2023-10-25 08:45:00 200.0
o_004 u_03 2023-10-25 10:00:00 150.0
o_005 u_02 2023-10-25 14:20:00 300.0
o_006 u_04 2023-10-25 17:55:00 80.0
o_007 u_01 2023-10-25 19:10:00 120.0
o_008 u_05 2023-10-25 21:00:00 400.0
o_009 u_05 2023-10-25 23:30:00 100.0

3. 期望输出结果

time_interval (时段) user_cnt (下单用户数) order_cnt (订单数) total_gmv (GMV) user_ratio (用户占比) order_ratio (订单占比) gmv_ratio (GMV占比)
深夜(0-6) 1 2 150.0 20.00% 22.22% 10.00%
上午(6-12) 2 2 350.0 40.00% 22.22% 23.33%
下午(12-18) 2 2 380.0 40.00% 22.22% 25.33%
晚上(18-24) 2 3 620.0 40.00% 33.33% 41.33%

注:全天去重总用户数为 5 人(u_01, u_02, u_03, u_04, u_05),总订单数为 9,总GMV为 1500.0。


4. SparkSQL 标准答案

sql
WITH base_data AS (
    -- 1. 提取小时并划分时段
    SELECT 
        user_id,
        order_id,
        gmv,
        HOUR(order_time) AS order_hour,
        CASE 
            WHEN HOUR(order_time) >= 0 AND HOUR(order_time) < 6 THEN '深夜(0-6)'
            WHEN HOUR(order_time) >= 6 AND HOUR(order_time) < 12 THEN '上午(6-12)'
            WHEN HOUR(order_time) >= 12 AND HOUR(order_time) < 18 THEN '下午(12-18)'
            WHEN HOUR(order_time) >= 18 AND HOUR(order_time) < 24 THEN '晚上(18-24)'
            ELSE '未知'
        END AS time_interval
    FROM t_order_detail
),
interval_summary AS (
    -- 2. 计算各时段的聚合指标
    SELECT 
        time_interval,
        COUNT(DISTINCT user_id) AS interval_user_cnt,
        COUNT(DISTINCT order_id) AS interval_order_cnt,
        SUM(gmv) AS interval_gmv
    FROM base_data
    GROUP BY time_interval
),
global_summary AS (
    -- 3. 计算全天的全局指标(注意:用户数需全局去重)
    SELECT 
        COUNT(DISTINCT user_id) AS global_user_cnt,
        COUNT(DISTINCT order_id) AS global_order_cnt,
        SUM(gmv) AS global_gmv
    FROM base_data
)
-- 4. 关联计算占比并格式化输出
SELECT 
    i.time_interval,
    i.interval_user_cnt AS user_cnt,
    i.interval_order_cnt AS order_cnt,
    i.interval_gmv AS total_gmv,
    -- 占比计算,转换为百分比格式
    CONCAT(ROUND(i.interval_user_cnt * 100.0 / g.global_user_cnt, 2), '%') AS user_ratio,
    CONCAT(ROUND(i.interval_order_cnt * 100.0 / g.global_order_cnt, 2), '%') AS order_ratio,
    CONCAT(ROUND(i.interval_gmv * 100.0 / g.global_gmv, 2), '%') AS gmv_ratio
FROM interval_summary i
CROSS JOIN global_summary g
ORDER BY 
    CASE i.time_interval
        WHEN '深夜(0-6)' THEN 1
        WHEN '上午(6-12)' THEN 2
        WHEN '下午(12-18)' THEN 3
        WHEN '晚上(18-24)' THEN 4
        ELSE 5
    END;

5. SparkSQL 核心考点与深度分析

考点一:时间函数与条件分支(HOUR & CASE WHEN
  • 面试官意图:考察候选人对时间戳字段的处理能力,以及逻辑区间的划分。
  • Spark 优化细节HOUR(order_time) 在 Spark SQL 中会直接映射为内置的表达式,避免了使用标准的正则表达式或字符串截取,执行效率极高。
考点二:全局占比的计算与“非等值连接”(CROSS JOIN
  • 面试官意图:如何优雅地在一行中结合“当前分组聚合值”和“全局汇总值”。
  • 难点突破
    • 计算占比需要除以全天的总数。常见错误是用窗口函数 SUM(COUNT(DISTINCT user_id)) OVER()注意:由于同一个用户可能在多个时段下单,各时段去重用户数之和不等于全局去重用户数。
    • 因此,必须通过 global_summary 重新计算全局去重的 global_user_cnt(本例中为5,而分时段累加为1+2+2+2=7)。
    • 采用 CROSS JOIN(笛卡尔积)连接只有一个单行单列的临时表 global_summary,在 Spark SQL 中,这种加入单行表的行为会自动触发 Broadcast Nested Loop Join (BNLJ),几乎没有性能损耗。
考点三:Spark SQL 的数据倾斜与内存控制(COUNT(DISTINCT)
  • 面试官追问:在大数据量下,你的这段 SQL 可能会有什么性能瓶颈?如何优化?
  • 深度剖析
    • COUNT(DISTINCT user_id) 会触发全局去重。在 Spark 中,这会导致所有相同的数据分发到同一个 Reducer 上,极易引发 数据倾斜(Data Skew)
    • Spark 优化方案
      1. 双重聚合:若数据量极大,可以先按照 time_intervaluser_id 进行 GROUP BY 去重,然后再进行 COUNT 计数。
      2. 近似算法:如果业务允许微小误差,可以使用 approx_count_distinct(user_id, rsd) 代替 COUNT(DISTINCT)。它利用 HyperLogLog 算法,在不进行全局 Shuffle 去重的情况下实现快速估算,能显著降低系统内存消耗并缩短运行时间。
右滑查看面试常问