在 2025 年第一季度主要购买“母婴”品类(该品类消费金额占比超 50%),但在第二季度主要购买“玩具”品类的用户
面试题目
题目背景:某电商平台希望分析 2025 年用户的跨品类消费行为转移情况。
具体要求:编写 SQL 查询,找出在 2025 年第一季度(Q1) 主要购买“母婴”品类(即该品类消费金额占该用户 Q1 总消费金额的 50% 以上),但在 2025 年第二季度(Q2) 主要购买“玩具”品类(即该品类消费金额占该用户 Q2 总消费金额的 50% 以上)的用户 user_id。
数据准备
用户订单表:user_orders
| 订单ID (order_id) | 用户ID (user_id) | 购买日期 (purchase_date) | 品类 (category) | 消费金额 (amount) |
|---|---|---|---|---|
| 1 | 101 | 2025-01-15 | 母婴 | 100.00 |
| 2 | 101 | 2025-02-20 | 美妆 | 30.00 |
| 3 | 101 | 2025-04-10 | 玩具 | 200.00 |
| 4 | 101 | 2025-05-12 | 数码 | 50.00 |
| 5 | 102 | 2025-03-01 | 母婴 | 200.00 |
| 6 | 102 | 2025-04-20 | 玩具 | 50.00 |
| 7 | 102 | 2025-06-15 | 服装 | 100.00 |
| 8 | 103 | 2025-01-10 | 母婴 | 50.00 |
| 9 | 103 | 2025-02-15 | 食品 | 100.00 |
| 10 | 103 | 2025-05-01 | 玩具 | 150.00 |
数据样例说明:
- 用户 101:
- Q1 总消费 ,母婴消费 (占比 ),满足 Q1 条件;
- Q2 总消费 ,玩具消费 (占比 ),满足 Q2 条件。
- 应被检索出。
- 用户 102:
- Q1 总消费 ,母婴占比 ,满足 Q1;
- Q2 总消费 ,玩具消费 (占比 ),不满足 Q2。
- 不应被检索出。
- 用户 103:
- Q1 总消费 ,母婴消费 (占比 ),不满足 Q1;
- Q2 总消费 ,玩具占比 ,满足 Q2。
- 不应被检索出。
期望输出
| 用户ID (user_id) |
|---|
| 101 |
SQL 解决方案
sql
WITH q1_mother_baby AS (
-- 步骤 1:筛选出 2025 年第一季度主要购买“母婴”的用户
SELECT
user_id
FROM user_orders
WHERE purchase_date >= '2025-01-01' AND purchase_date <= '2025-03-31'
GROUP BY user_id
HAVING SUM(CASE WHEN category = '母婴' THEN amount ELSE 0 END) > SUM(amount) * 0.5
),
q2_toys AS (
-- 步骤 2:筛选出 2025 年第二季度主要购买“玩具”的用户
SELECT
user_id
FROM user_orders
WHERE purchase_date >= '2025-04-01' AND purchase_date <= '2025-06-30'
GROUP BY user_id
HAVING SUM(CASE WHEN category = '玩具' THEN amount ELSE 0 END) > SUM(amount) * 0.5
)
-- 步骤 3:取两个季度的交集用户
SELECT
q1.user_id
FROM q1_mother_baby q1
INNER JOIN q2_toys q2 ON q1.user_id = q2.user_id;
SQL 解析与面试技巧
1. 解题思路拆解(面试时如何向面试官表达)
- 分而治之(Divide and Conquer):本题的关键是将复杂的跨季度条件拆解为两个独立的子问题。
- 子问题 A:找出 2025 年 Q1 “母婴”消费占比 > 50% 的用户。
- 子问题 B:找出 2025 年 Q2 “玩具”消费占比 > 50% 的用户。
- 条件求和(Conditional Aggregation):在计算占比时,使用
SUM(CASE WHEN...)来过滤特定品类的消费,再除以(或对比)SUM(amount)总消费。这比多次关联同一张表更优雅、性能更好。 - 求交集:最后通过
INNER JOIN(或者EXISTS、IN)将两个子查询的结果根据user_id关联,即可得到最终目标用户。
2. 关键语法解析
HAVING子句的妙用:WHERE用于在分组前过滤单行订单数据(限制时间范围在 Q1 或 Q2)。HAVING用于在GROUP BY user_id分组后,对聚合后的金额进行比例判断。SUM(CASE WHEN category = '母婴' THEN amount ELSE 0 END) > SUM(amount) * 0.5巧妙地规避了分母可能为 0 的风险,比直接用除法算比例更安全。
3. 面试加分项 / 性能优化讨论
如果面试官追问:“如果这张表有几亿条数据,该如何优化这个查询?”你可以从以下几个方面回答:
- 索引优化:确保在
purchase_date和user_id上建立了联合索引,例如(purchase_date, user_id, category, amount)。这样可以直接利用索引覆盖(Index Covering),无需回表查询物理行数据。 - 分区表:由于查询是严格基于时间季度的,如果
user_orders表按天或按月进行了分区(Partitioning),数据库引擎可以利用分区裁剪(Partition Pruning)只扫描 2025 年上半年的分区,极大减少 I/O 消耗。 - 替代语法
EXISTS:在最后的关联部分,如果不需要展示 Q2 的其他数据,使用EXISTS替代INNER JOIN有时在特定数据库引擎中能获得更好的半连接(Semi-Join)优化。