4. 分区裁剪优化策略:倾斜键重分布、盐化分区键、复合分区设计、动态重分区、自适应分区裁剪

分区裁剪(Partition Pruning)是 Spark/Hive 等引擎最有效的优化手段之一。但一旦碰上数据倾斜,裁剪效果就会大打折扣——甚至比没分区还慢。我这些年踩过的坑,大多都跟「倾斜键 + 错误的分区策略」有关。

说白了,分区裁剪的核心就一句话:让引擎在扫描阶段就跳过无关分区。但倾斜数据会导致某些分区过大,裁剪了也白裁。下面这五种策略,是我在实际项目中反复验证过的。

核心思路: 不是所有倾斜都需要重写整个ETL。很多时候,换个分区键、加个盐、或者动态调整一下,就能把裁剪效率拉回来。

4.1 倾斜键重分布(Skew Key Redistribution)

先看最常见的情况。订单表按 order_date 分区,但某一天的订单量是其他天的 100 倍。这时候分区裁剪虽然能跳过其他日期,但扫描那个大分区本身就要跑半小时。

我的做法是:把倾斜键拆成多个子键。比如把 order_date 拆成 year + month + day 三级分区。这样大促那天的数据会被分散到 2024/11/11 这个叶子分区,但引擎在裁剪时依然能精确命中。

-- 改造前:单键分区,倾斜严重
CREATE TABLE orders (
  order_id STRING,
  order_date STRING,
  amount DOUBLE
) PARTITIONED BY (order_date);

-- 改造后:三级分区,分散倾斜
CREATE TABLE orders_v2 (
  order_id STRING,
  order_date STRING,
  amount DOUBLE
) PARTITIONED BY (year STRING, month STRING, day STRING);

-- 查询时依然能裁剪
SELECT * FROM orders_v2
WHERE year = '2024' AND month = '11' AND day = '11';
我的经验: 倾斜键重分布适合「少数分区极大、多数分区极小」的场景。如果所有分区都均匀倾斜,这招效果有限。

4.2 盐化分区键(Salting the Partition Key)

盐化(Salting)是我个人最喜欢的手法。原理很简单:给分区键加一个随机后缀,把一个大分区拆成 N 个小分区。

举个例子。用户表按 city_id 分区,但北京的用户占了 60%。这时候可以给 city_id 加一个 0~9 的随机数后缀:

-- 盐化分区键
CREATE TABLE users_salted (
  user_id STRING,
  city_id STRING,
  salt STRING
) PARTITIONED BY (city_salted STRING);

-- 写入时生成盐值
INSERT INTO users_salted PARTITION (city_salted)
SELECT
  user_id,
  city_id,
  CONCAT(city_id, '_', FLOOR(RAND() * 10)) AS city_salted
FROM raw_users;

-- 查询时也要带盐值(或者用 IN 条件)
SELECT * FROM users_salted
WHERE city_salted IN ('beijing_0', 'beijing_1', ..., 'beijing_9');
注意: 盐化后查询必须显式指定所有子分区,否则引擎无法裁剪。我见过有人忘了改查询代码,结果全表扫描,比原来还慢。

我曾经在一个日活 5000 万的项目里用盐化把倾斜分区从 1 个拆成 20 个,查询时间从 8 分钟降到了 40 秒。代价是元数据膨胀了 20 倍,但 Hive Metastore 扛得住。

4.3 复合分区设计(Composite Partitioning)

复合分区就是「先按 A 分区,再按 B 子分区」。这招在 Hive 和 Spark 里都支持,但很多人用错了。

正确的姿势是:把倾斜度高的键放在第一级,把倾斜度低的键放在第二级。为什么?因为第一级分区决定了裁剪的粒度。

分区层级 推荐键类型 示例 裁剪效果
第一级 高基数、低倾斜 date 快速过滤大部分数据
第二级 低基数、高倾斜 city_id 在已裁剪范围内精细过滤
-- 复合分区设计示例
CREATE TABLE events_composite (
  event_id STRING,
  event_time TIMESTAMP,
  city_id STRING,
  event_type STRING
) PARTITIONED BY (event_date STRING, city_id STRING);

-- 查询时两层裁剪
SELECT * FROM events_composite
WHERE event_date = '2024-11-11'
  AND city_id IN ('beijing', 'shanghai');
避坑指南: 我曾经把 city_id 放在第一级、date 放在第二级,结果查询时只能按城市裁剪,日期过滤变成了全量扫描。嗯,后来花了三天才把数据重刷回来。

4.4 动态重分区(Dynamic Repartitioning)

动态重分区不是建表时的策略,而是运行时的优化。说白了,就是在执行计划里动态判断哪些分区倾斜,然后自动拆开。

Spark 3.0+ 的 Adaptive Query Execution (AQE) 就支持这个能力。它会根据 Shuffle 输出的统计信息,自动把倾斜的分区拆成多个小分区。

-- 开启 AQE 动态重分区
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")

-- 倾斜阈值配置(默认 5 倍)
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "256MB")

我个人习惯把 skewedPartitionFactor 设成 3 而不是默认的 5。因为 5 倍太宽松了,很多中等倾斜的场景检测不到。设成 3 倍虽然会多拆一些分区,但整体稳定性更好。

注意: 动态重分区只对 Shuffle 阶段有效,对文件扫描阶段的分区裁剪没有帮助。如果倾斜发生在源表分区上,还是得用前三种策略。

4.5 自适应分区裁剪(Adaptive Partition Pruning)

这是最「智能」的一种策略。它不是在建表时固定分区方案,而是根据查询模式动态调整裁剪逻辑。

举个例子。你的表按 user_id 哈希分区成 100 个分区。但某个查询只涉及 3 个用户,引擎如果能提前知道这 3 个用户落在哪几个分区,就能精准裁剪。

Spark 的 Dynamic Partition Pruning (DPP) 就是干这个的。它会在运行时先执行子查询,拿到过滤条件,再用这些条件去裁剪主表的分区。

-- 开启 DPP
spark.conf.set("spark.sql.optimizer.dynamicPartitionPruning.enabled", "true")

-- 示例:子查询结果用于裁剪主表分区
SELECT /*+ BROADCAST(s) */ o.*
FROM orders o
JOIN small_users s ON o.user_id = s.user_id
WHERE s.city = 'beijing';
-- DPP 会自动将 s.city = 'beijing' 转化为 o 表的分区过滤条件
我的经验: DPP 在「大表 Join 小表」时效果最好。如果两个表都很大,DPP 反而可能因为子查询执行时间过长而得不偿失。这时候我会手动用 IN 子查询代替 Join。

策略对比与选型指南

这五种策略不是互斥的,实际项目中经常组合使用。下面这张表是我自己总结的选型参考:

策略 适用场景 改造成本 运行时开销 推荐指数
倾斜键重分布 少数分区极大 中(需改分区键) 低 ★★★★☆
盐化分区键 单键严重倾斜 高(需改写入+查询) 低 ★★★★★
复合分区设计 多维度过滤 中(需重新建表) 低 ★★★★☆
动态重分区 Shuffle 倾斜 低(仅配置) 中(需 AQE 开销) ★★★☆☆
自适应分区裁剪 大表 Join 小表 低(仅配置) 中(子查询开销) ★★★★☆
分区裁剪优化策略全景图 数据倾斜场景 倾斜键重分布 拆分子键 盐化分区键 加随机后缀 复合分区设计 多级分区 动态重分区 AQE 运行时 自适应分区裁剪 DPP 动态裁剪 选型建议 • 倾斜键重分布 + 复合分区设计:适合建表阶段就预知倾斜的场景 • 盐化分区键:适合单键严重倾斜且查询模式固定的场景 • 动态重分区 + 自适应分区裁剪:适合运行时倾斜不确定、需要自动调优的场景

最后说一句:没有银弹。我见过有人把所有策略都试了一遍,最后发现只是数据质量有问题——空值太多导致分区倾斜。所以遇到倾斜问题,先查数据,再调策略,顺序别搞反了。