找出在平台购买过至少 3 种不同商品品类,但从未购买过“数码配件”品类的活跃用户 ID
SparkSQL 经典面试题:特定消费行为活跃用户分析
一、 题目描述
在电商和平台用户画像分析中,筛选具有特定消费特征的用户是极其常见的需求。请根据给定的用户订单明细表,找出在平台购买过至少 3 种不同商品品类,但从未购买过“数码配件”品类的活跃用户 ID。
二、 示例数据
1. 用户订单明细表 (user_orders)
| user_id (用户ID) | order_id (订单ID) | category (商品品类) | order_date (购买日期) |
|---|---|---|---|
| U001 | O101 | 食品 | 2023-10-01 |
| U001 | O102 | 美妆 | 2023-10-02 |
| U001 | O103 | 服饰 | 2023-10-03 |
| U002 | O104 | 食品 | 2023-10-01 |
| U002 | O105 | 数码配件 | 2023-10-02 |
| U002 | O106 | 居家 | 2023-10-04 |
| U003 | O107 | 食品 | 2023-10-01 |
| U003 | O108 | 美妆 | 2023-10-05 |
| U004 | O109 | 图书 | 2023-10-01 |
| U004 | O110 | 运动 | 2023-10-02 |
| U004 | O111 | 生鲜 | 2023-10-03 |
| U004 | O112 | 母婴 | 2023-10-04 |
| U005 | O113 | 数码配件 | 2023-10-05 |
2. 期望输出结果
| user_id |
|---|
| U001 |
| U004 |
数据分析说明:
U001:购买了食品、美妆、服饰共 3 种品类,且没有数码配件,符合条件。U002:购买了 3 种品类,但包含“数码配件”,排除。U003:仅购买了 2 种品类,排除。U004:购买了 4 种品类,且没有数码配件,符合条件。U005:仅购买了“数码配件”,排除。
三、 SparkSQL 解决方案
解法一:单表聚合过滤(推荐,性能最优)
利用 GROUP BY 进行一次性聚合,结合 HAVING 子句中的条件表达式进行过滤。这种方法只需对数据进行一次扫描和一次 Shuffle,效率最高。
sql
SELECT
user_id
FROM
user_orders
GROUP BY
user_id
HAVING
COUNT(DISTINCT category) >= 3
AND SUM(CASE WHEN category = '数码配件' THEN 1 ELSE 0 END) = 0
解法二:子查询排除法(逻辑清晰,适合大表广播优化)
先找出购买过“数码配件”的用户,再通过 NOT IN 或 LEFT ANTI JOIN 进行排除,最后分组筛选出品类数大于等于3的用户。
sql
WITH digital_users AS (
SELECT DISTINCT user_id
FROM user_orders
WHERE category = '数码配件'
)
SELECT
o.user_id
FROM
user_orders o
LEFT ANTI JOIN
digital_users d
ON
o.user_id = d.user_id
GROUP BY
o.user_id
HAVING
COUNT(DISTINCT o.category) >= 3
四、 面试官视角:SparkSQL 深度分析与优化
在实际面试中,仅仅写出 SQL 只能拿到及格分。如果能主动向面试官分析 Spark 底层的执行计划、数据倾斜以及性能优化,将是巨大的加分项。
1. 解法一与解法二的执行计划对比 (Physical Plan)
- 解法一(单表聚合):
- 执行流程:
FileSourceScan->HashAggregate(局部聚合) ->Exchange(Shuffle) ->HashAggregate(全局聚合) ->Filter(Having条件)。 - 优势:只涉及单表操作。Spark 在 Shuffle 前会进行 Map 端局部聚合(Partial Aggregate),大大减少了 Shuffle 传输的数据量。
- 执行流程:
- 解法二(Anti Join):
- 执行流程:生成两个分支。分支 A 过滤出“数码配件”用户,分支 B 为全表。然后进行
BroadcastHashJoin(如果“数码配件”用户量小)或SortMergeJoin,最后再进行HashAggregate。 - 适用场景:如果“数码配件”的购买人数极少,且该表有索引或分区,第一步过滤能过滤掉 99% 的数据,那么利用 Spark 的 Broadcast Anti Join 性能会非常卓越。
- 执行流程:生成两个分支。分支 A 过滤出“数码配件”用户,分支 B 为全表。然后进行
2. COUNT(DISTINCT) 带来的数据倾斜风险与优化
在解法一中,使用了 COUNT(DISTINCT category)。如果某个热点用户(如爬虫或企业级采购账号)产生了数千万条订单,COUNT(DISTINCT) 会导致该 user_id 所在的 Reduce 任务处理时间极长,引发数据倾斜。
优化方案(双重聚合):
如果在大规模数据下发生倾斜,可将一条 SQL 拆分为两步聚合,先去重,再计数。sqlWITH uniq_user_category AS ( -- 第一步:按用户和品类去重,同时标记是否为数码配件 SELECT user_id, category, MAX(CASE WHEN category = '数码配件' THEN 1 ELSE 0 END) as is_digital FROM user_orders GROUP BY user_id, category ) -- 第二步:二次聚合,此时没有 COUNT(DISTINCT),只有普通的 COUNT SELECT user_id FROM uniq_user_category GROUP BY user_id HAVING COUNT(category) >= 3 AND SUM(is_digital) = 0- 原理:通过将
GROUP BY user_id改为GROUP BY user_id, category,把原本倾斜在某一个user_id上的数据,分散到了多个category的组合上,从而实现负载均衡。
- 原理:通过将
3. 核心知识点总结
- 条件计数技巧:
SUM(CASE WHEN ... THEN 1 ELSE 0 END) = 0是 SQL 中表达“从未有过某行为”的高效替代方案,避免了写复杂的子查询。 - Spark 算子选择:在处理排除逻辑时,SparkSQL 的
LEFT ANTI JOIN性能通常优于NOT IN(特别是NOT IN遇到 NULL 值时可能会产生非预期结果或退化为全表扫描)。
右滑查看面试常问