订阅更新

分区表设计得好,查询快100倍不是梦

SQL手册 第 14 题 约 8 分钟读完

本文目录
  1. 总结
  2. 面试原题
  3. 什么是分区?
  4. 分区设计的4个原则
  5. 分区裁剪失效的5种常见情况
  6. 实战:动态分区 vs 静态分区
  7. 面试加分:Spark中的Bucket

总结

要点建议
------------
分区键选择高频过滤字段,首选日期
分区粒度每个分区128MB-1GB
二级分区取值不超过几十个的维度字段
裁剪失效不要对分区键做函数运算
动态分区设置最大分区数限制
Bucket大表JOIN大表时考虑

一句话记忆:分区是大数据查询优化的第一步,先确保分区裁剪生效,再考虑其他优化手段。

明天 DAY 15 聊日期函数全家桶,日期处理是SQL里最高频的操作之一,但不同引擎的日期函数差异巨大,面试时很容易写错。

你们公司的分区策略是怎么设计的?评论区交流~

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

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

详细讲解

在大数据场景下,分区表是性能优化的第一道防线。一张10TB的Hive表,如果按天分区,每天100GB,你查某一天的数据只需要扫100GB而不是10TB——这就是分区裁剪的威力。面试官考分区,考的不只是语法,而是你的设计思维。

面试原题

你有一张用户行为日志表,日均数据量5亿行,请设计分区方案,并说明查询时如何利用分区提升性能。

什么是分区?

分区的本质就是把一张大表按某个字段切成多个小文件夹。在HDFS上,每个分区对应一个目录:

/warehouse/user_event/
  ├── dt=2025-01-01/
  │   ├── part-00000
  │   ├── part-00001
  │   └── ...
  ├── dt=2025-01-02/
  │   └── ...
  └── dt=2025-01-03/
      └── ...

查询时指定WHERE dt = '2025-01-01',引擎只扫描这一个目录,其他99.9%的数据根本不读。这就叫分区裁剪(Partition Pruning)。

分区设计的4个原则

原则一:选高频过滤字段做分区键

分区键应该是查询中几乎必然出现的过滤条件。对于日志类数据,日期(dt)是最佳选择,因为几乎所有查询都会限定时间范围。

-- 建表语句(Hive)
CREATE TABLE user_event (
  user_id     STRING,
  event_name  STRING,
  event_time  TIMESTAMP,
  properties  MAP<STRING, STRING>
)
PARTITIONED BY (dt STRING)
STORED AS ORC;

原则二:控制分区粒度,避免过细

分区太细会产生大量小文件,反而拖慢查询:

-- ❌ 按小时分区,一天24个分区,一年8760个分区
PARTITIONED BY (dt STRING, hour STRING)
 
-- ❌ 按用户ID分区,百万级分区
PARTITIONED BY (user_id STRING)

小文件问题在Hive/Spark中非常严重——每个小文件对应一个Map Task,Task调度的开销可能比实际计算还大。NameNode也要为每个文件维护元数据,分区太多会给NameNode造成压力。

经验法则:每个分区的数据量在128MB-1GB之间比较合理。 如果按天分区每天只有几MB数据,可以考虑按周或按月分区。

原则三:二级分区要谨慎

有时候一级分区不够用,需要二级分区:

-- 按天+按渠道的二级分区
PARTITIONED BY (dt STRING, channel STRING)
 
/warehouse/user_event/
  ├── dt=2025-01-01/
  │   ├── channel=Facebook/
  │   ├── channel=TikTok/
  │   └── channel=Google/
  └── dt=2025-01-02/
      └── ...

二级分区的好处是进一步缩小扫描范围。但前提是:第二个分区键的取值不能太多(几十个以内),而且查询中经常会按这个字段过滤。

我在做广告数据的时候,表按dt + channel双分区,因为日常查询90%都带这两个条件。但用户行为日志我只用dt单分区,因为行为类型有上百种,做二级分区会产生太多小文件。

原则四:分区键不要参与计算

-- ❌ 分区裁剪失效
SELECT * FROM user_event
WHERE SUBSTR(dt, 1, 7) = '2025-01'
 
-- ✅ 分区裁剪生效
SELECT * FROM user_event
WHERE dt >= '2025-01-01' AND dt <= '2025-01-31'
 
-- ✅ 更简洁
SELECT * FROM user_event
WHERE dt BETWEEN '2025-01-01' AND '2025-01-31'

如果WHERE条件中对分区键做了函数运算(SUBSTR、YEAR、DATE_FORMAT等),优化器无法识别出分区范围,会退化为全表扫描。这是最常见的分区裁剪失效场景。

分区裁剪失效的5种常见情况

面试官经常问"什么情况下分区裁剪会失效",把这5种记住:

1. 对分区键做函数运算(上面讲过)

WHERE YEAR(dt) = 2025  -- ❌ 失效
WHERE dt >= '2025-01-01' AND dt < '2026-01-01'  -- ✅

2. 分区键隐式类型转换

-- dt是STRING类型
WHERE dt = 20250101  -- ❌ 数字和字符串比较,可能失效
WHERE dt = '20250101'  -- ✅ 类型匹配

3. OR条件中包含非分区字段

WHERE dt = '2025-01-01' OR user_id = 'u001'  -- ❌ OR导致全表扫描
-- 改写为UNION ALL
SELECT * FROM user_event WHERE dt = '2025-01-01'
UNION ALL
SELECT * FROM user_event WHERE user_id = 'u001'  -- 这个还是全扫,但至少第一个查询能裁剪

4. JOIN条件中的分区键

-- 如果分区过滤条件在JOIN的ON里而不是WHERE里,可能失效
SELECT *
FROM user_event e
JOIN date_dim d ON e.dt = d.dt AND d.is_weekend = 1
-- 有些引擎不会把is_weekend=1的过滤下推到user_event的分区裁剪

5. 动态分区写入后未更新统计信息

Hive的CBO依赖统计信息做优化决策。如果用动态分区写入了数据但没有ANALYZE TABLE,优化器可能做出错误的执行计划。

-- 写入后更新统计信息
ANALYZE TABLE user_event PARTITION(dt='2025-01-01') COMPUTE STATISTICS;

实战:动态分区 vs 静态分区

写数据的时候有两种分区方式:

静态分区: 手动指定分区值

INSERT OVERWRITE TABLE user_event PARTITION(dt='2025-01-01')
SELECT user_id, event_name, event_time, properties
FROM staging_table
WHERE dt = '2025-01-01'

动态分区: 让Hive根据数据自动创建分区

SET hive.exec.dynamic.partition=true;
SET hive.exec.dynamic.partition.mode=nonstrict;
 
INSERT OVERWRITE TABLE user_event PARTITION(dt)
SELECT user_id, event_name, event_time, properties, dt
FROM staging_table

动态分区方便但有风险——如果源数据中dt的取值有几千种(比如脏数据导致日期格式不对),会创建几千个分区,直接打爆NameNode。我见过一次事故,有人用动态分区导入了一批日期格式错乱的数据,一次性创建了20000多个分区,HDFS的NameNode直接挂了,影响了整个集群。

生产环境建议:动态分区一定要设置最大分区数限制:

SET hive.exec.max.dynamic.partitions=1000;
SET hive.exec.max.dynamic.partitions.pernode=100;

面试加分:Spark中的Bucket

分区是按目录切分,Bucket是在分区内进一步按Hash切分文件:

CREATE TABLE user_event_bucketed (
  user_id STRING,
  event_name STRING,
  event_time TIMESTAMP
)
PARTITIONED BY (dt STRING)
CLUSTERED BY (user_id) INTO 256 BUCKETS
STORED AS ORC;

Bucket的好处:两张表如果按同一个字段做了相同数量的Bucket,JOIN时可以做Bucket Map Join——每个Bucket只和对应的Bucket匹配,不需要全量Shuffle。这在大表JOIN大表的场景下效果显著。

但Bucket在实际工作中用得没有分区多,因为Bucket数量一旦确定就不好改,而且很多团队对Bucket的了解不够深。面试的时候知道这个概念和适用场景就够了。

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