订阅更新

分区怎么设计才合理?——分区策略最佳实践

数据湖手册 第 17 题 约 7 分钟读完

本文目录
  1. 先说结论
  2. 真实案例:过度分区导致查询变慢
  3. 分区设计原则
  4. 常见分区模式
  5. 分区策略对比
  6. 动态分区问题
  7. 面试高频追问
  8. 实战踩坑:动态分区爆炸
  9. 总结

先说结论

分区的作用:分区是数据裁剪的最重要手段,可以跳过不相关分区,大幅提升查询性能。但过度分区会导致小文件问题。

单级分区:大多数情况用dt单级分区足够,按天或按小时。数据湖支持partition pruning,按天查询性能好。

多级分区:需要同时过滤多个列时使用,如dt+hour、country+dt。但要注意分区数量爆炸,二级分区值少于10个为宜。

最佳实践:按时间分区dt优先、基数低的列适合做分区、分区数量控制在100万以内、避免高基数列分区.

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

这道题面试官问得很多,因为分区设计直接影响查询性能和存储成本。我在某出海硬件品牌遇到过因为分区设计不合理,查询慢10倍的问题。很多人只知道按天分区,但说不清楚什么时候该用多级分区、什么时候不该分区。面试官想听的是你能说清楚设计思路和实战经验.

真实案例:过度分区导致查询变慢

2022年在某知名出海硬件品牌,我们的订单表orders设计了三级分区:dt+country+status。

业务部门反馈查询越来越慢,一个简单查询花了30分钟。

-- 表结构
CREATE TABLE orders (
  order_id BIGINT,
  user_id BIGINT,
  amount DECIMAL(10,2),
  ...)
PARTITIONED BY (dt STRING, country STRING, status STRING);

问题:dt有365个值,country有50个值,status有5个值,总分区数365*50*5=91250个。每个分区只有很少数据,导致大量小文件。

解决方案:我们去掉country和status分区,只保留dt分区,把country和status作为普通列。

-- 重建表,只保留dt分区
CREATE TABLE orders_new (
  order_id BIGINT,
  user_id BIGINT,
  country STRING,
  status STRING,
  amount DECIMAL(10,2),
  ...)
PARTITIONED BY (dt STRING);

改进效果:查询时间从30分钟降到2分钟。分区数从9万降到365,小文件问题解决。

分区设计原则

原则1:用于过滤的列才分区分区列应该是查询WHERE条件中经常出现的列。如果某列很少用于过滤,不要作为分区列。

原则2:基数低的列适合分区基数是唯一值的数量。country有50个值适合分区,user_id有1000万个值不适合分区。高基数列分区会产生大量小分区。

原则3:分区数量控制总分区数控制在100万以内。单级分区1000个以内最佳,多级分区每级10个值以内。分区过多会导致NameNode压力大、小文件问题。

原则4:时间优先大多数查询按时间过滤,dt或date是首选分区列。时间分区也是数据生命周期管理的基础,方便删除过期数据。

原则5:避免过度分区3级以上的分区很少需要。超过3级分区会导致管理复杂、性能下降。

常见分区模式

模式示例适用场景注意
按天分区dt=2024-03-20大多数OLAP查询最常用,365个分区/年
按小时分区dt=2024-03-20/hour=15实时查询、近实时分析8760个分区/年,注意小文件
按月分区dt=2024-03历史数据、归档数据12个分区/年,每个分区较大
地域+时间country=US/dt=2024-03-20多地域业务地域值少于10个
业务类型+时间event_type=click/dt=2024-03-20多事件类型事件类型少于10个

分区策略对比

策略优点缺点适用场景
不分区简单、无小文件问题无法裁剪数据小数据量、全表扫描
单级时间分区简单、裁剪效果好历史分区可能很大大多数OLAP场景
多级分区裁剪更精细小文件风险、管理复杂明确的多维过滤
集群分区每个用户一个分区用户过多时不适用多租户SaaS场景

动态分区问题

什么是动态分区:Spark写入时根据数据自动创建分区。不需要预先定义分区值,灵活性高。

动态分区的问题:如果数据中有很多不同的分区值,会产生大量小分区小文件。极端情况下可能创建数万个分区,导致任务失败。

// 动态分区写入
df.write
  .mode("overwrite")
  .partitionBy("dt", "country")  // 如果country有100个值,会产生100个文件
  .saveAsTable("orders")

动态分区配置:hive.exec.max.dynamic.partitions默认1000、hive.exec.max.dynamic.partitions.pernode默认100。需要根据数据调整。

最佳实践:限制动态分区数量、先按静态分区再动态分区、使用INSERT OVERWRITE代替动态分区。

面试高频追问

追问一:怎么判断是否需要分区?

看查询模式。如果大部分查询都有时间过滤条件,按时间分区。如果经常按地域过滤,加地域分区。如果查询全表扫描多,不要分区。关键是看WHERE条件中哪些列经常出现。

追问二:分区越多越好吗?

不是。分区多可以更精细裁剪数据,但会导致小文件问题、NameNode压力大、元数据管理复杂。要平衡裁剪效果和管理成本。单级分区365个、多级分区几千个是合理的上限。

追问三:什么时候不该分区?

三种情况不该分区:1)数据量小,小于10GB不需要分区;2)查询模式多样,没有明显的过滤列;3)实时写入场景,分区会导致小文件。这种情况下可以用Clustering代替分区。

实战踩坑:动态分区爆炸

2023年在某出海硬件品牌,我们遇到过动态分区创建太多导致任务失败的问题。

用户画像表user_tags按user_id分区,数据有100万个不同的user_id。

问题:动态分区模式下,Spark尝试创建100万个分区,任务直接失败。

# 错误做法:按高基数列分区
df.write.partitionBy("user_id").saveAsTable("user_tags")
# 报错:Too many dynamic partitions

解决方案:我们改成按dt分区,user_id作为普通列,通过Z-Ordering排序优化查询。

# 正确做法:按dt分区+Z-Ordering
df.write
  .partitionBy("dt")
  .saveAsTable("user_tags")
 
# OPTIMIZE with Z-Ordering
OPTIMIZE user_tags ZORDER BY (user_id);

教训:高基数列不适合做分区,要通过Z-Ordering或Clustering优化查询。

总结

分区设计是数据湖的核心,直接影响查询性能和存储成本。设计原则是:按时间分区优先、基数低的列适合分区、分区数量控制在100万以内、避免高基数列分区。大多数场景单级dt分区足够,多级分区要谨慎使用。动态分区很方便但要注意小文件风险。面试时能说清楚分区设计原则、常见模式、动态分区问题,就很稳.

下一篇聊一个进阶话题:查询还能更快吗?——Z-Ordering和Clustering数据布局优化

你们生产环境的分区是怎么设计的?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器