将一天 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 优化方案:
- 双重聚合:若数据量极大,可以先按照
time_interval和user_id进行GROUP BY去重,然后再进行COUNT计数。 - 近似算法:如果业务允许微小误差,可以使用
approx_count_distinct(user_id, rsd)代替COUNT(DISTINCT)。它利用 HyperLogLog 算法,在不进行全局 Shuffle 去重的情况下实现快速估算,能显著降低系统内存消耗并缩短运行时间。
- 双重聚合:若数据量极大,可以先按照