Spark 3.x 动态分区裁剪:为什么它能自动跳过 90% 的数据扫描 Spark 3.x 动态分区裁剪为什么它能自动跳过 90% 的数据扫描一、从一个真实场景说起大家好我是朱大喜。先问大家一个问题你们在 Spark 里做两张大表 JOIN 的时候有没有遇到过一个尴尬的场景——明明事实表按日期分了 365 个分区你只查最近 7 天的数据但 Spark 硬是把 365 个分区的文件列表全都扫了一遍这不是 bug而是 Spark 2.x 时代的设计局限。在 Spark 3.0 之前静态分区裁剪只能在查询编译阶段根据 WHERE 条件中直接写在 SQL 里的过滤条件来跳过分区。一旦过滤条件隐藏在另一张小表里比如 JOIN 之后的 ON 条件优化器就没办法在扫描阶段做裁剪了。打个生活化的比方你家里有 365 个抽屉每个抽屉标着日期。你要找最近 7 天老婆买过的所有东西。老婆买过什么记录在一个小本子上维度表具体买了啥记在一堆购物小票里事实表。聪明人会先翻小本子找出最近 7 天她确实买了东西的那几天然后只开那几个抽屉。但 Spark 2.x 就像个耿直的机器人它会老老实实把 365 个抽屉全打开看一眼然后再跟小本子对日期——白白开了 350 多个没用的抽屉。二、动态分区裁剪的魔法在哪里Spark 3.0 引入的动态分区裁剪Dynamic Partition Pruning简称 DPP核心思路非常朴素在运行时把维度表的过滤结果作为广播变量下推到事实表的扫描阶段让事实表在读取文件时就跳过不相干的分区。graph LR A[维度表br/dim_date] --|过滤: 最近7天| B[过滤结果: 7个日期] B --|Broadcast 广播| C{事实表扫描阶段} D[事实表br/fact_ordersbr/365个分区] -- C C --|只扫描7个分区| E[JOIN 结果] style B fill:#4CAF50,color:#fff style C fill:#FF9800,color:#fff这里的关键词是**运行时和下推**。静态裁剪发生在编译期只能处理常量过滤条件而动态裁剪发生在执行期维度的过滤结果已经算出来了可以直接拿来用。而且它是下推到文件扫描层面的不是在 JOIN 之后才过滤这就从根源上减少了数据读取量。实际效果有多明显呢我们团队有个订单分析场景事实表 365 天分区 × 每天 2 亿行维度表是一个几十 MB 的日期配置表。老版本 Spark 每次查询都要扫描全表再过滤耗时 40 多分钟。升级到 Spark 3.x 并确认 DPP 生效后同样的查询扫描数据量降到原来的 8%耗时缩短到 3 分钟以内。三、DPP 的触发条件不是你想用就能用DPP 虽然美好但不是所有 JOIN 都能享受这个福利。它有严格的触发条件理解这些条件才能写出能被优化的 SQL。条件一必须是等值 JOIN。ON 条件必须是而不能是、或LIKE。因为只有等值才能建立分区键之间的精确映射关系。条件二维度表必须足够小能被广播。DPP 的前提是把维度表的过滤结果广播到所有 Executor。Spark 默认spark.sql.autoBroadcastJoinThreshold是 10MB如果你的维度表超过这个大小Spark 会退化为普通的 SortMergeJoin不再触发 DPP。条件三事实表的分区键必须参与 JOIN 的 ON 条件。如果你的事实表按dt分区但 JOIN 的 ON 条件是a.user_id b.user_id那 DPP 跟分区没关系不会生效。只有 JOIN 键包含了分区键时动态裁剪才有意义。-- ✅ 会触发 DPPdim_date.date_id 过滤后广播下推到 fact_orders -- 的扫描阶段只读取 dt 在最近7天内的分区 SELECT /* BROADCAST(d) */ f.* FROM fact_orders f JOIN dim_date d ON f.dt d.date_id WHERE d.date_type workday AND d.date_id 2026-07-08; -- ❌ 不会触发 DPPJOIN 键是 order_id跟分区键 dt 没关系 SELECT /* BROADCAST(d) */ f.* FROM fact_orders f JOIN dim_product p ON f.order_id p.order_id -- 不是分区键 WHERE f.dt 2026-07-08; -- 这里只能走静态裁剪还有一个容易踩坑的地方DPP 和 Bucket Join 是互斥的。如果你的表做了分桶Bucket而 Spark 选择了 Bucket Join 策略DPP 就不会生效。这时需要在性能上做取舍——是分桶带来的 Shuffle 减少更香还是 DPP 的数据量削减更猛。四、如何验证 DPP 真的生效了写完了 SQL跑得也挺快但你心里犯嘀咕它到底走了 DPP 没有Spark UI 里的信息说实话不太好读我来教你怎么快速确认。最直接的方法是在 Spark SQL 里用EXPLAIN看执行计划。如果你在扫描阶段看到dynamicpruning字样恭喜你DPP 起作用了。# 在 PySpark 中验证 DPP 是否生效 spark.sql( EXPLAIN EXTENDED SELECT f.* FROM fact_orders f JOIN dim_date d ON f.dt d.date_id WHERE d.date_type workday ).show(truncateFalse) # 关键输出标志看到这些就说明DPP生效了 # - *(1) Project [order_id#123L, dt#124, ...] # - *(1) BroadcastHashJoin [dt#124], [date_id#200], # :- *(1) Filter dynamicpruningexpression(dt#124 IN dynamicpruning#300) # | - *(1) FileScan parquet fact_orders[dt#124,...] # | : - DynamicPruningExpression # - BroadcastExchange HashedRelationBroadcastMode(...) # - *(2) Filter (date_type#202 workday)另外可以通过 Spark UI 的 SQL 页签来看。如果 DPP 生效了你会看到 Scan 阶段的number of files read远小于总文件数。没有 DPP 时365 个分区的文件可能全被列出来有了 DPP可能只读到了 7 个分区的文件。经验之谈如果 DPP 没生效排查顺序是 1) 看维度表大小是否超过广播阈值 2) 看 JOIN 键是否就是分区键 3) 看有没有显式或隐式地禁用了 AQEAdaptive Query Execution因为 DPP 是 AQE 框架下的一个子功能。五、总结动态分区裁剪是 Spark 3.x 里投入产出比最高的优化之一。你不需要改代码不需要调参数只要你的 SQL 满足条件等值 JOIN 小维度表 JOIN 键是分区键Spark 就会自动帮你省掉大量无用数据扫描。但别因为它自动就完全不管。写 SQL 时心里要有这条线我的事实表分区键是哪个维度表能不能再缩小一点让它进广播阈值JOIN 条件能不能精确地包含分区键这三个问题想清楚了DPP 一般不会让你失望。回到抽屉的比喻Spark 2.x 是把所有抽屉打开再核对小本子Spark 3.x 是先翻小本子确定是哪几个抽屉然后只开那几个。这一前一后的差异就是 40 分钟和 3 分钟的距离。