基于本文回答
0
评论

给定每日销售额,编写 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)层面的变化
  1. FileScan:读取 user_orders,利用分区裁剪和谓词下推过滤出 2025 年数据。
  2. HashAggregate:在 Map 端和 Reduce 端进行本地聚合,计算出 daily_gmv(极大地减少了网络传输)。
  3. Exchange (HashPartitioning):根据 order_date 的 Hash 值重新分区,但由于数据已被聚合,此时的分区数据量极小。
  4. Window:在单节点或多节点上,利用内存中的高效指针移动,完成 ytd_gmv 的累加。
右滑查看面试常问