给定每日销售额,编写 SQL 计算 2025 年每天的 GMV,并计算截止到当天(年初至今)的累计销售额
面试题:2025年每日GMV及年初至今(YTD)累计销售额计算
1. 题目背景
在电商和零售业务分析中,计算每日GMV(商品交易总额)以及年初至今(Year-to-Date, 简称 YTD)的累计GMV是极其常见的需求。这有助于业务方实时监控销售目标的达成率。
现有一张订单明细表 user_orders,记录了用户的下单流水。请编写 SparkSQL 查询 2025 年每天的 GMV,并计算截止到当天(2025年内)的累计销售额。
2. 示例数据
输入数据表:user_orders
| order_id | user_id | order_date | amount |
|---|---|---|---|
| ord001 | 101 | 2024-12-31 | 500.00 |
| ord002 | 102 | 2025-01-01 | 1000.00 |
| ord003 | 103 | 2025-01-01 | 500.00 |
| ord004 | 101 | 2025-01-02 | 1500.00 |
| ord005 | 104 | 2025-01-03 | 800.00 |
| ord006 | 102 | 2025-01-03 | 1200.00 |
| ord007 | 105 | 2025-01-04 | 2000.00 |
3. 期望输出结果
| order_date | daily_gmv | ytd_gmv |
|---|---|---|
| 2025-01-01 | 1500.00 | 1500.00 |
| 2025-01-02 | 1500.00 | 3000.00 |
| 2025-01-03 | 2000.00 | 5000.00 |
| 2025-01-04 | 2000.00 | 7000.00 |
4. SparkSQL 解决方案
sql
WITH daily_summary AS (
-- Step 1: 过滤出2025年的数据,并按天聚合求出每日GMV
SELECT
order_date,
SUM(amount) AS daily_gmv
FROM
user_orders
WHERE
order_date >= '2025-01-01' AND order_date <= '2025-12-31'
GROUP BY
order_date
)
-- Step 2: 使用窗口函数计算年初至今的累计GMV
SELECT
order_date,
daily_gmv,
SUM(daily_gmv) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS ytd_gmv
FROM
daily_summary
ORDER BY
order_date;
5. SparkSQL 深度解析与面试加分项
① 窗口函数边界的性能考量(ROWS vs RANGE)
在 SQL 的窗口函数中,如果仅写 ORDER BY order_date,SparkSQL 默认会将其解析为:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。
RANGE(默认):会把相同日期的所有数据当成一个组来计算,在执行时需要额外的排序和临时内存,性能较差。ROWS(推荐):使用ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW是物理行上的定位,计算速度显著快于RANGE,在大数据集下能有效降低 CPU 消耗和执行时间。
② 核心优化:先聚合、后窗口
本题的解法采用了 CTE(公用表表达式) daily_summary 先对数据进行 GROUP BY 降维。
- 反面教材:直接在明细表上执行窗口函数,然后再进行去重或分组。这会导致 Spark 在 Shuffle 阶段传输海量明细数据,极易引发 数据倾斜(Data Skew) 和 OOM(内存溢出)。
- 标准答案:先
GROUP BY压缩数据量(一个日期只有一条数据),再对聚合后的结果集执行窗口计算,将 Shuffle 的数据量降到最低。
③ 过滤条件的谓词下推(Predicate Pushdown)
在 WHERE 子句中,我们避免了使用类似 YEAR(order_date) = 2025 的函数操作,而是采用了区间过滤 order_date >= '2025-01-01' AND order_date <= '2025-12-31'。
- 原理:如果
order_date是分区字段,采用区间过滤可以直接触发 分区裁剪(Partition Pruning),Spark 只会扫描 2025 年对应的数据目录。若使用函数包裹字段(如YEAR(order_date)),Spark 无法直接利用分区索引,会导致全表扫描。
④ Spark 执行计划(Physical Plan)层面的变化
- FileScan:读取
user_orders,利用分区裁剪和谓词下推过滤出 2025 年数据。 - HashAggregate:在 Map 端和 Reduce 端进行本地聚合,计算出
daily_gmv(极大地减少了网络传输)。 - Exchange (HashPartitioning):根据
order_date的 Hash 值重新分区,但由于数据已被聚合,此时的分区数据量极小。 - Window:在单节点或多节点上,利用内存中的高效指针移动,完成
ytd_gmv的累加。
右滑查看面试常问