4. 分区与分桶策略:按时间分区、按业务维度分桶、分区键选择技巧

分区和分桶,说白了就是给数据「分堆儿」。

我见过太多宽表跑不动的情况,十有八九是分区策略没想清楚。你想想看,一张几百亿行的宽表,没有合理分区,每次查询都是全表扫描,再好的硬件也扛不住。

这一章,我把这些年踩过的坑和总结出来的经验,一次性说清楚。

4.1 分区策略:先按时间切一刀

我个人习惯,宽表的第一级分区永远选时间。为什么?因为绝大多数业务查询都带时间范围。

核心原则:分区粒度 = 最频繁的查询时间跨度。

如果业务天天看「最近7天」,你就按天分区。如果看「月度报表」,按月分区就够了。

我在项目中遇到过这样一个案例:某电商订单宽表,每天新增 5000 万行。最初设计是按月分区,结果运营同学查「昨天未发货订单」时,每次都要扫描整个月的数据——30 亿行!

后来改成按天分区,同样的查询,只扫 5000 万行,速度提升了 60 倍。

4.1.1 时间分区的最佳实践

  • 按天分区:适合实时性要求高、查询粒度细的场景。缺点是分区数量增长快,Hive 等系统对上千个分区性能会下降。
  • 按月分区:适合历史归档、月度汇总场景。分区数量可控,但查询单日数据时仍需过滤。
  • 按周分区:折中方案。我一般只在数据量中等(日均千万级)且业务以周为单位分析时使用。
分区粒度 适用场景 分区数/年 查询性能
按天 实时分析、明细查询 365 ⭐⭐⭐⭐⭐
按周 周报、中等粒度分析 52 ⭐⭐⭐⭐
按月 月度汇总、历史归档 12 ⭐⭐⭐

注意:分区不是越细越好。我曾经见过一张表按小时分区,一年产生 8760 个分区。结果 Hive 的元数据服务直接被打挂了,查询时列出所有分区就要好几秒。

建议:单表分区数控制在 1000 以内,超过这个量就要考虑合并粒度或改用分桶。

4.2 分桶策略:按业务维度做散列

时间分区解决的是「范围扫描」的问题。但还有一类查询——比如「查某个用户的全部订单」——它不关心时间,只关心用户 ID。

这时候,分桶就派上用场了。

4.2.1 分桶的本质是哈希

分桶不是按值范围切分,而是对分桶键做哈希计算,然后模上桶数,决定数据去哪个桶。

-- Hive 示例:按用户ID分桶
CREATE TABLE order_wide (
    user_id      STRING,
    order_id     STRING,
    product_id   STRING,
    amount       DECIMAL(10,2),
    order_time   TIMESTAMP
)
PARTITIONED BY (dt STRING)
CLUSTERED BY (user_id) INTO 64 BUCKETS;

这样做的好处是:相同 user_id 的数据一定落在同一个桶里。查询时只需要扫描一个桶,而不是全表。

我的经验:分桶数一般设为 2 的 N 次方(16、32、64、128)。因为哈希算法对 2 的幂次取模效率最高,而且数据分布更均匀。

4.2.2 哪些字段适合做分桶键?

  • 高基数字段:用户 ID、设备 ID、订单号。这些字段取值多,哈希后分布均匀。
  • 查询频繁的等值条件:如果业务经常用「where user_id = xxx」,那 user_id 就是绝佳的分桶键。
  • Join 键:如果宽表经常和另一张表做 Join,且 Join 键相同,分桶可以避免 Shuffle。

避坑:不要用低基数字段做分桶键。比如「性别」只有男女两个值,分 64 个桶的话,数据只会落到 2 个桶里,剩下 62 个桶是空的。这叫「数据倾斜」,比不分桶还慢。

4.3 分区键选择的三个技巧

选分区键这件事,看着简单,其实门道不少。我总结了三招:

4.3.1 技巧一:分区键要「剪枝率高」

分区剪枝的意思是:查询条件能直接定位到具体分区,而不是扫描所有分区。

-- 好的分区键:查询能直接剪枝
SELECT * FROM order_wide WHERE dt = '2024-01-15';  -- 只扫一个分区

-- 坏的分区键:无法剪枝
SELECT * FROM order_wide WHERE year(dt) = 2024;     -- 扫所有分区

我见过有人用「年」做分区键,但查询条件里写的是具体日期。每次都要把函数作用在分区键上,剪枝失效,全表扫描。

4.3.2 技巧二:避免分区键数据倾斜

如果某个分区的数据量是其他分区的 10 倍,那这个分区就是「热点分区」。查询这个分区时,性能会急剧下降。

举个例子:按「省份」分区。北京、上海的数据量可能是西藏、青海的几百倍。这就导致北京分区成为瓶颈。

解决方案:用组合分区键。比如「省份 + 日期」,先按省份粗分,再按日期细分。这样热点省份的数据也会被日期打散。

4.3.3 技巧三:分区键类型要简单

分区键的数据类型,尽量用 INT 或 STRING,别用 TIMESTAMP、DECIMAL 这些复杂类型。

为什么?因为分区键要参与目录结构、元数据存储和比较运算。复杂类型会带来额外的序列化开销,而且某些引擎对 TIMESTAMP 类型的分区剪枝支持不好。

-- 推荐:用 STRING 类型存储日期
PARTITIONED BY (dt STRING)

-- 不推荐:用 TIMESTAMP 做分区键
PARTITIONED BY (order_time TIMESTAMP)

4.4 分区 + 分桶:组合拳才是王道

单独用分区或分桶,都有局限性。真正的高手,是把两者结合起来。

我一般这样设计:

  • 第一层:按时间分区——解决范围查询,控制数据扫描量。
  • 第二层:按业务维度分桶——解决等值查询和 Join 优化。

举个例子,一张用户行为宽表:

CREATE TABLE user_behavior_wide (
    user_id      STRING,
    session_id   STRING,
    event_type   STRING,
    page_url     STRING,
    duration     INT,
    event_time   TIMESTAMP
)
PARTITIONED BY (dt STRING)           -- 按天分区
CLUSTERED BY (user_id) INTO 128 BUCKETS  -- 按用户分桶
SORTED BY (event_time);              -- 桶内按时间排序

这样设计后:

  • 查「某天所有用户行为」→ 只扫一个分区
  • 查「某个用户全部历史」→ 只扫一个桶
  • 查「某天某个用户的行为序列」→ 只扫一个分区里的一个桶,而且桶内数据按时间有序

三层过滤下来,扫描量从几百亿行降到几万行,你说快不快?

小技巧:分桶后加上 SORTED BY,可以让桶内数据按指定字段排序。这样查询时如果用到排序字段,还能利用索引加速。我在做用户行为序列分析时,这个设计让查询快了 3 倍。

4.5 一张图看懂分区与分桶策略

下面这张图,是我自己总结的分区分桶决策流程。每次设计新表时,我都会对照着走一遍:

分区与分桶策略决策流程 分区与分桶策略决策流程 开始设计宽表 查询是否经常带时间范围? (如:最近7天、本月) 是 按时间分区(天/月) 是否有高频等值查询? (如:WHERE user_id = xxx) 是 按业务维度分桶 分区 + 分桶组合设计 (时间分区 + 业务分桶) 否 考虑其他分区键 提示:分区键选高剪枝率字段,分桶键选高基数字段,两者结合效果最佳

4.6 避坑指南:我踩过的那些坑

最后,分享几个我亲身经历过的教训:

坑一:分区字段放在 WHERE 条件里用了函数

我曾经写过 WHERE to_date(dt) = '2024-01-15',结果分区剪枝完全失效。因为 Hive 对分区字段做函数运算后,无法直接定位分区。

正确写法:WHERE dt = '2024-01-15'。分区字段保持原样,不要套函数。

坑二:分桶数设置过大

有次我把一张小表分了 256 个桶,结果每个桶里只有几千行数据。查询时反而因为要打开 256 个文件,性能比不分桶还差。

经验值:每个桶的数据量在 100MB~1GB 之间比较合适。小表不分桶,大表再分。

坑三:动态分区写入导致小文件问题

用动态分区写入数据时,如果分区键的取值特别多(比如按秒级时间戳分区),会产生大量小文件。HDFS 上小文件一多,NameNode 内存就爆了。

解决方案:控制分区粒度,或者在写入后做一次小文件合并(如 ALTER TABLE ... CONCATENATE)。

嗯,分区和分桶就讲到这里。记住一句话:分区剪枝是性能的第一道防线,分桶是第二道。两道防线都守住了,你的宽表想慢都难。