将一天 24 小时划分为四个时段:深夜(0-6)、上午(6-12)、下午(12-18)、晚上(18-24)。统计各时段的下单用户数、下单订单数以及 GMV 的占比
面试题:全天时段消费行为分析
题目描述
在电商和本地生活业务中,分析用户在一天中不同时段的消费习惯对于精细化运营至关重要。请编写 SparkSQL 查询,将一天 24 小时划分为四个时段:
- 深夜([0, 6) 点)
- 上午([6, 12) 点)
- 下午([12, 18) 点)
- 晚上([18, 24) 点)
统计各时段的下单用户数(去重)、下单订单数以及 GMV 的占比。
1. 基础数据准备
订单表:t_order
| order_id | user_id | order_time | gmv |
|---|---|---|---|
| ord_001 | U1 | 2023-10-25 02:30:00 | 100.0 |
| ord_002 | U2 | 2023-10-25 05:15:00 | 150.0 |
| ord_003 | U1 | 2023-10-25 08:45:00 | 200.0 |
| ord_004 | U3 | 2023-10-25 10:30:00 | 300.0 |
| ord_005 | U4 | 2023-10-25 14:20:00 | 50.0 |
| ord_006 | U4 | 2023-10-25 15:40:00 | 250.0 |
| ord_007 | U5 | 2023-10-25 19:10:00 | 400.0 |
| ord_008 | U2 | 2023-10-25 21:00:00 | 150.0 |
2. SparkSQL 核心解法
sql
WITH temp_order_period AS (
SELECT
order_id,
user_id,
gmv,
-- 提取小时数
HOUR(order_time) AS order_hour
FROM t_order
),
temp_period_mapped AS (
SELECT
order_id,
user_id,
gmv,
CASE
WHEN order_hour >= 0 AND order_hour < 6 THEN '深夜'
WHEN order_hour >= 6 AND order_hour < 12 THEN '上午'
WHEN order_hour >= 12 AND order_hour < 18 THEN '下午'
WHEN order_hour >= 18 AND order_hour < 24 THEN '晚上'
ELSE '未知'
END AS time_period
FROM temp_order_period
)
SELECT
time_period,
-- 1. 下单用户数(去重)
COUNT(DISTINCT user_id) AS active_user_count,
-- 2. 下单订单数
COUNT(order_id) AS total_order_count,
-- 3. 各时段 GMV
SUM(gmv) AS period_gmv,
-- 4. 占比计算:当前时段GMV / 总GMV(利用窗口函数避免二次关联)
ROUND(SUM(gmv) / SUM(SUM(gmv)) OVER() * 100, 2) AS gmv_ratio_pct
FROM temp_period_mapped
GROUP BY time_period
ORDER BY
CASE time_period
WHEN '深夜' THEN 1
WHEN '上午' THEN 2
WHEN '下午' THEN 3
WHEN '晚上' THEN 4
ELSE 5
END;
3. 预期输出结果
| time_period | active_user_count | total_order_count | period_gmv | gmv_ratio_pct |
|---|---|---|---|---|
| 深夜 | 2 | 2 | 250.0 | 15.63 |
| 上午 | 2 | 2 | 500.0 | 31.25 |
| 下午 | 1 | 2 | 300.0 | 18.75 |
| 晚上 | 2 | 2 | 550.0 | 34.38 |
4. 面试必会:SparkSQL 深度解析与调优
核心考点 1:窗口函数与聚合函数的嵌套使用 SUM(SUM(x)) OVER()
- 原理解析:在计算 GMV 占比时,新手常会使用
子查询先求SUM(gmv),然后再用主查询JOIN起来。 - 高级写法:
SUM(SUM(gmv)) OVER()。- 内层的
SUM(gmv)是基于GROUP BY time_period聚合后的组内 GMV。 - 外层的
SUM(...) OVER()是一个无PARTITION BY的全局窗口函数,它将四个时段的 GMV 再次求和,得到全局总 GMV。 - 优势:避免了额外的
JOIN算子,减少了 Shuffle 阶段,执行效率大幅提升。
- 内层的
核心考点 2:时间函数的选择与物理执行
HOUR(order_time)是 SparkSQL 内置的高效时间提取函数。- 如果面试官追问:“如果
order_time是 String 类型而非 Timestamp,该如何处理?”- 答:可以使用
CAST(order_time AS TIMESTAMP)进行隐式转换,或者直接使用substring(order_time, 12, 2)提取小时字符并转为整型,后者在特定场景下甚至能省去时间解析的开销。
- 答:可以使用
核心考点 3:数据倾斜与优化(高频面试追问)
- 问题:如果数据量极大,且“晚上”时段的订单量占了 90%,如何优化此 SQL?
- 优化方案:
- Count(Distinct) 优化:
COUNT(DISTINCT user_id)会引入全局 Shuffle。如果数据量巨大,可以开启 Spark 的两阶段聚合参数spark.sql.adaptive.skewJoin.enabled,或者通过手动加盐法(给user_id加随机前缀进行局部聚合,再去掉前缀进行全局聚合)来分散热点。 - 过滤不必要的数据:在
WHERE条件中提前过滤掉无用的异常数据或未支付订单,减少参与计算的数据量。
- Count(Distinct) 优化:
右滑查看面试常问