统计每日的销售总额中,新用户(当天注册且当天消费)与老用户(非当天注册但当天有消费)各自的金额占比
SparkSQL 面试题:新老用户每日销售额占比统计
1. 题目背景与要求
在电商和零售的数据分析中,区分新用户(当天注册且当天消费)与老用户(之前已注册且当天有消费)的消费贡献,对于评估获客质量和用户留存具有重要意义。
请编写一段 SparkSQL 代码,统计每日的销售总额中,新用户与老用户各自的金额占比。
2. 示例数据
表 1:用户注册表 user_reg
| user_id (用户ID) | reg_date (注册日期) |
|---|---|
| U01 | 2023-10-01 |
| U02 | 2023-10-01 |
| U03 | 2023-10-02 |
| U04 | 2023-10-03 |
表 2:订单消费表 user_orders
| order_id (订单ID) | user_id (用户ID) | order_date (订单日期) | amount (消费金额) |
|---|---|---|---|
| O101 | U01 | 2023-10-01 | 100.0 |
| O102 | U02 | 2023-10-01 | 200.0 |
| O103 | U01 | 2023-10-02 | 150.0 |
| O104 | U03 | 2023-10-02 | 300.0 |
| O105 | U04 | 2023-10-03 | 100.0 |
| O106 | U02 | 2023-10-03 | 50.0 |
3. 期望输出结果
| dt (日期) | total_amount (当天总额) | new_user_ratio (新用户占比) | old_user_ratio (老用户占比) |
|---|---|---|---|
| 2023-10-01 | 300.00 | 100.00% | 0.00% |
| 2023-10-02 | 450.00 | 66.67% | 33.33% |
| 2023-10-03 | 150.00 | 66.67% | 33.33% |
数据解析说明:
- 10-01:U01, U02 均为当天注册并消费,全是新用户。
- 10-02:U03(新用户)消费 300,U01(老用户)消费 150,总额 450。新占 300/450=66.67%,老占 150/450=33.33%。
- 10-03:U04(新用户)消费 100,U02(老用户)消费 50,总额 150。新占 100/150=66.67%,老占 50/150=33.33%。
4. SparkSQL 标准答案
sql
WITH user_order_detail AS (
-- 1. 关联订单表与注册表,判断每次消费时用户是新用户还是老用户
SELECT
o.order_date,
o.amount,
CASE WHEN o.order_date = r.reg_date THEN 'new' ELSE 'old' END AS user_type
FROM user_orders o
LEFT JOIN user_reg r
ON o.user_id = r.user_id
),
daily_summary AS (
-- 2. 按天聚合,利用条件聚合分别计算新老用户的消费总额及当天总销售额
SELECT
order_date AS dt,
SUM(amount) AS daily_total_amount,
SUM(CASE WHEN user_type = 'new' THEN amount ELSE 0 END) AS new_user_amount,
SUM(CASE WHEN user_type = 'old' THEN amount ELSE 0 END) AS old_user_amount
FROM user_order_detail
GROUP BY order_date
)
-- 3. 计算占比并格式化输出
SELECT
dt,
ROUND(daily_total_amount, 2) AS total_amount,
CONCAT(ROUND((new_user_amount / daily_total_amount) * 100, 2), '%') AS new_user_ratio,
CONCAT(ROUND((old_user_amount / daily_total_amount) * 100, 2), '%') AS old_user_ratio
FROM daily_summary
ORDER BY dt;
5. SparkSQL 深度解析与面试应对策略
面试官考察这道题,表面上是看你能不能写出 SQL 逻辑,实际上是在评估你的 Spark 性能调优意识、边界条件处理能力 以及 大数据计算思维。
核心解析一:JOIN 类型的选择与数据倾斜(Spark 优化重点)
- 大表 JOIN 小表(广播 Join):在实际业务中,
user_orders(订单表)通常是事实表(海量数据),而user_reg(注册表)是维度表。- 面试加分表达:“如果
user_reg表的数据量较小(例如在 Spark 端小于广播阈值,默认 10MB),我会显式使用广播连接/*+ BROADCAST(r) */,将user_reg广播到每个 Executor,从而避免 Shuffle,彻底解决由于用户 ID 分布不均可能导致的数据倾斜问题。”
- 面试加分表达:“如果
- 大表 JOIN 大表:如果两表都是百亿级大表,Spark 默认会采用 SortMergeJoin。此时需确保两表在
user_id上已经过预分区(Bucketing),以减少 Shuffle Read 的数据传输量。
核心解析二:指标计算技巧(条件聚合 vs 子查询)
- 避免多次扫描数据:初学者容易写出“先用一个 SQL 算新用户,再写一个 SQL 算老用户,最后 JOIN 起来”的低效代码(导致数据被重复扫描)。
- 推荐做法:使用
CASE WHEN+SUM的条件聚合(Conditional Aggregation)方式。在一次GROUP BY的 map-side 聚合中,同时完成新、老用户金额的分类累加,极大地减少了磁盘 I/O 和内存占用。
核心解析三:边界与容错处理
- 数据缺失(Null 值)处理:如果存在“有消费记录但在注册表里找不到”的异常用户(如脏数据),使用
LEFT JOIN后r.reg_date会为NULL。在上面的答案中,CASE WHEN o.order_date = r.reg_date的判断会将这些异常用户安全地归类为'old'(非当天注册),不会导致 SQL 报错或数据丢失。如果在严格业务场景下,应提前与面试官沟通是否需要将r.user_id IS NULL的数据进行过滤。