在 2025 年第一季度主要购买“母婴”品类(该品类消费金额占比超 50%),但在第二季度主要购买“玩具”品类的用户
面试真题:跨季度消费行为转变用户分析
题目背景:
在电商数据分析中,分析用户的生命周期和消费偏好转移是非常常见的需求。本题要求找出在 2025 年第一季度(Q1)偏好“母婴”品类(消费占比 > 50%),但在第二季度(Q2)偏好转移到“玩具”品类(消费占比 > 50%)的高价值流转用户。
1. 示例数据
订单明细表:user_orders
| order_id | user_id | category | amount | order_date |
|---|---|---|---|---|
| o001 | 101 | 母婴 | 600.00 | 2025-02-15 |
| o002 | 101 | 美妆 | 400.00 | 2025-03-10 |
| o003 | 101 | 玩具 | 800.00 | 2025-05-20 |
| o004 | 101 | 数码 | 200.00 | 2025-06-01 |
| o005 | 102 | 母婴 | 400.00 | 2025-01-20 |
| o006 | 102 | 食品 | 600.00 | 2025-02-10 |
| o007 | 102 | 玩具 | 900.00 | 2025-04-15 |
| o008 | 102 | 服饰 | 100.00 | 2025-05-18 |
| o009 | 103 | 母婴 | 800.00 | 2025-03-05 |
| o010 | 103 | 图书 | 200.00 | 2025-03-25 |
| o011 | 103 | 玩具 | 300.00 | 2025-04-10 |
| o012 | 103 | 食品 | 700.00 | 2025-06-12 |
| o013 | 104 | 母婴 | 900.00 | 2025-02-28 |
数据逻辑说明:
- 用户 101:Q1 总消费 1000,母婴 600(占比 60% > 50%);Q2 总消费 1000,玩具 800(占比 80% > 50%)。(符合条件)
- 用户 102:Q1 总消费 1000,母婴 400(占比 40% <= 50%);Q2 总消费 1000,玩具 900(占比 90%)。(不符合条件:Q1母婴未过半)
- 用户 103:Q1 总消费 1000,母婴 800(占比 80%);Q2 总消费 1000,玩具 300(占比 30% <= 50%)。(不符合条件:Q2玩具未过半)
- 用户 104:Q1 总消费 900,母婴 900(占比 100%);Q2 无消费。(不符合条件)
2. SparkSQL 核心写法
sql
WITH user_quarterly_metrics AS (
SELECT
user_id,
-- Q1 消费统计
SUM(CASE WHEN order_date >= '2025-01-01' AND order_date <= '2025-03-31' THEN amount ELSE 0 END) AS q1_total_amount,
SUM(CASE WHEN order_date >= '2025-01-01' AND order_date <= '2025-03-31' AND category = '母婴' THEN amount ELSE 0 END) AS q1_baby_amount,
-- Q2 消费统计
SUM(CASE WHEN order_date >= '2025-04-01' AND order_date <= '2025-06-30' THEN amount ELSE 0 END) AS q2_total_amount,
SUM(CASE WHEN order_date >= '2025-04-01' AND order_date <= '2025-06-30' AND category = '玩具' THEN amount ELSE 0 END) AS q2_toy_amount
FROM user_orders
-- 过滤出 2025 上半年的数据,充分利用分区裁剪或索引
WHERE order_date >= '2025-01-01' AND order_date <= '2025-06-30'
GROUP BY user_id
)
SELECT
user_id,
q1_total_amount,
q1_baby_amount,
ROUND(q1_baby_amount / q1_total_amount, 4) AS q1_baby_ratio,
q2_total_amount,
q2_toy_amount,
ROUND(q2_toy_amount / q2_total_amount, 4) AS q2_toy_ratio
FROM user_quarterly_metrics
WHERE q1_total_amount > 0
AND (q1_baby_amount / q1_total_amount) > 0.5
AND q2_total_amount > 0
AND (q2_toy_amount / q2_total_amount) > 0.5;
3. 预期输出结果
| user_id | q1_total_amount | q1_baby_amount | q1_baby_ratio | q2_total_amount | q2_toy_amount | q2_toy_ratio |
|---|---|---|---|---|---|---|
| 101 | 1000.00 | 600.00 | 0.6000 | 1000.00 | 800.00 | 0.8000 |
4. 经典面试深度剖析与 SparkSQL 优化策略
面试官在考察这道题时,不仅看你能不能写出正确的 SQL,更看重你对 Spark 运行机制、Shuffle 优化和边界条件 的理解。
剖析一:为什么使用【条件聚合】而不是【JOIN 关联】?
- 初级开发做法:先过滤出 Q1 母婴占比过半的临时表 A,再过滤出 Q2 玩具占比过半的临时表 B,最后用
A JOIN B ON A.user_id = B.user_id。 - 高级开发做法(如上文答案):使用一阶段条件聚合(Conditional Aggregation)。
- Spark 深度解释:
- Join 方案:会导致两次
GROUP BY user_id产生两次 Shuffle,最后的JOIN还要再产生一次 Shuffle(涉及三次 Shuffle Stage)。 - 条件聚合方案:只需在
WHERE子句中过滤 2025 上半年的数据,对全表进行一次扫描(Single Pass Scan)和一次 Shuffle(聚合),大大节约了网络 I/O 与磁盘溢写。
- Join 方案:会导致两次
剖析二:如何避免 Double 类型计算中的【零除异常】(Division by Zero)?
- 在 Spark 中,如果某个用户在 Q1 消费了,但在 Q2 完全没有消费,其
q2_total_amount为0。 - 如果在
WHERE子句中直接进行q2_toy_amount / q2_total_amount计算,会导致计算报错或者返回NaN/Null。 - 解决方案:
- 在
WHERE条件中前置过滤q1_total_amount > 0 AND q2_total_amount > 0。 - 或者在计算比例时,使用
nullif(q2_total_amount, 0):q2_toy_amount / nullif(q2_total_amount, 0)。当分母为 0 时,nullif返回NULL,相除结果也是NULL,从而避免报错。
- 在
剖析三:Spark 物理执行层面的优化点(Predicate Pushdown & Partition Pruning)
- 分区裁剪(Partition Pruning):如果
user_orders表在 Hive/Spark 中是以order_date的年份/季度(如dt)作为分区键,那么WHERE order_date >= '2025-01-01' AND order_date <= '2025-06-30'能够让 Spark 只扫描对应的分区数据,避免全表扫描。 - 本地 Map 端预聚合(Map-side Aggregation):Spark SQL 默认开启
spark.sql.mapOutputs.minSize相关优化。在使用SUM(CASE...)时,Spark 会在 Map 端进行HashAggregate(预聚合),大大减少发送到 Reduce 端(Shuffle Read)的数据量。
右滑查看面试常问