Hive 从入门到实战(四):分区表与分桶表------大数据查询提速的第一把钥匙
这是《Hive 从入门到实战》系列第 4 篇。本篇是面试 + 实战双料重点:分区表、分桶表,以及面试必考的"分区 vs 分桶"。
本篇学习路线图:为什么要分区(一个 26 秒的痛点)→ 分区怎么用(建表 → 灌数 → 裁剪 → 运维)→ 分区自动化(动态分区)→ 分区的副作用(小文件)→ 分区解决不了的问题(引出分桶)→ 两者对比与面试追问 → 工程经验与文献印证。
目录
- 一、为什么需要分区表:从一个 26 秒的查询说起
- 二、分区表:静态分区、分区运维、二级分区
- 三、动态分区:让 Hive 自动帮你分目录
- 四、分桶表:分区分不动的时候怎么办
- 五、面试加餐:分区 vs 分桶 + 高频追问
- 六、总结
- 参考文献(知网/百度学术收录)
一、为什么需要分区表:从一个 26 秒的查询说起
第 1 篇我们建过一张卡口车流大表 gateway_records,跑一个分组统计:
sql
select city, count(1) from gateway_records group by city;
-- Time taken: 26.306 seconds
现在换个需求,只查某一天的数据:
sql
select * from gateway_records where dt = '20260928';
问题来了:普通表里所有日期的数据混在同一个目录 ,即使你只要一天,Hive 也得把整个目录全扫一遍 ,再逐行过滤。这叫全表扫描。
数据小无所谓,数据上 TB 呢?99% 的时间都浪费在"扫不需要的数据"上。
解决思路也很朴素 :像图书馆分楼层放书一样,把数据按日期分目录存放 ,查哪天就只扫哪个目录。这就是分区表(Partitioned Table)。
类比记忆:分区 = 文件夹。按"日期"建文件夹,文件(数据)各回各家,查询时直奔目标文件夹。
二、分区表:静态分区、分区运维、二级分区
思路有了(按目录分),落地四步走:建分区表 → 灌数据 → 查询验证裁剪 → 学会运维。
2.1 创建分区表
分区表语法只有一处不同:把分区字段用 PARTITIONED BY 声明,并且分区字段不能出现在普通字段里。
sql
create table if not exists dept_partition(
deptno int comment '部门编号',
dname string comment '部门名称',
loc string comment '部门位置'
)
partitioned by (day string comment '天分区字段') -- 分区字段写在最后,单独声明
row format delimited fields terminated by '\t';
新手最常见的错:把
day又写进普通字段列表里,建表直接报错。记住:分区字段是"目录名",不是"表里的列"------虽然查询时它能像普通列一样出现在 select 和 where 里。
工程提示:分区键怎么选? 这在工程和学术圈有相当共识(分区实践类论文13都强调了这点),三条原则:
- 选高频过滤字段:分区键应当是查询 where 里最常出现的字段(如日期、地区)------分区裁剪只有查询带上分区键时才生效,建在没人查的字段上等于白建
- 粒度适中:优先选"天"这种基数适中、查询常用的层级;像"用户ID"这种几十万基数的字段,直接做分区会瞬间炸出海量目录
- 留好后手:单级分区粒度不够时再加一级(见 2.5 二级分区),而不是一开始就往细里切
2.2 静态分区:手动指定装到哪个分区
方式一:load 加载,指定分区
sql
load data local inpath '/root/dept.txt'
into table dept_partition partition(day='20260928');
方式二:insert values,指定分区
sql
insert into table dept_partition partition(day='20260929') values
(10,'财务部','10005'),
(20,'行政部','10006'),
(30,'法务部','10007');
看看 HDFS 上到底长什么样:
/user/hive/warehouse/bigdata.db/dept_partition/
├── day=20260928/ ← 一个分区就是一个子目录
│ └── dept.txt
└── day=20260929/
└── 000000_0
2.3 分区裁剪:提速的原理
sql
select deptno, dname, loc
from dept_partition
where day='20260928'; -- where 带上分区字段
OK
deptno dname loc
10 财务部 10001
...
Time taken: 3.2 seconds
执行计划里只会扫描 day=20260928 这一个目录 ------这就是分区裁剪(Partition Pruning)。查几天就扫几个目录,分区越多、单分区越小,提速越明显。
提醒:分区字段过滤要写在 where 里,别写在子查询里导致裁剪失效。
2.4 分区运维:查看 / 增加 / 删除 / 修复
查看所有分区:
sql
show partitions dept_partition;
OK
day=20260928
day=20260929
增加分区:
sql
-- 增加单个分区
alter table dept_partition add partition(day='20260930');
-- 增加多个分区:分区之间【不能】有逗号
alter table dept_partition add partition(day='20260928') partition(day='20260929');
删除分区:
sql
-- 删除单个分区
alter table dept_partition drop partition (day='20260930');
-- 删除多个分区:分区之间【必须】加逗号
alter table dept_partition drop partition(day='20260928'), partition(day='20260929');
记忆口诀:增不逗,删要逗。(add 多分区无逗号,drop 多分区有逗号------两处写法不一样,考试常挖坑)
恢复分区(MSCK 修复)------了解即可但要懂原理
背景知识:分区信息保存在元数据 里,分区数据目录在 HDFS 上,两者必须一致,表才能正常读写。有三种情况会导致不一致:
| 不一致场景 | 后果 | 修复手段 |
|---|---|---|
| 手动在 HDFS 上 mkdir 了分区目录 | Hive 不认识这个目录 | add partition 补登记 |
| 手动在 HDFS 上删了分区目录 | 元数据残留"僵尸分区" | drop partition 清登记 |
| 外部表 drop partition 后 | 元数据删了,HDFS 目录还在 | 同上 |
一条命令自动修复:
sql
msck repair table dept_partition; -- 默认 sync
msck repair table dept_partition add partitions; -- 补登 HDFS 有而元数据没有的
msck repair table dept_partition drop partitions; -- 清理 HDFS 没有而元数据有的
msck repair table dept_partition sync partitions; -- 双向同步 = add + drop
输出示例:
OK
Repair: Added partition to metastore dept_partition: day=20260930
Time taken: 0.5 seconds
2.5 二级分区:天 + 小时
一天数据还是太大?再往下切一层:
sql
create table if not exists dept_partition2(
deptno int comment '部门编号',
dname string comment '部门名称',
loc string comment '部门位置'
)
partitioned by (day string comment '一级分区天', hour string comment '二级分区小时')
row format delimited fields terminated by '\t';
-- 加载数据:两级分区都要指定
load data local inpath '/root/dept.txt'
into table dept_partition2 partition(day='20260928',hour='15');
load data local inpath '/root/dept.txt'
into table dept_partition2 partition(day='20260928',hour='16');
查询时两个分区字段都带上:
sql
select * from dept_partition2 where day='20260928' and hour='16';
HDFS 目录变成两层嵌套:
dept_partition2/
└── day=20260928/
├── hour=15/
└── hour=16/
分区不是越多越好:分区太碎会产生大量小文件,反而拖慢 NameNode 和 MR。单分区数据量建议控制在合理规模(如 GB 级),按"天"还是"天+小时"要看数据量。
2.6 小文件问题:分区过度的副作用(工程必知)
上面的提醒值得单独展开------"小文件"是 Hive 生产环境最著名的性能杀手之一,也是分区表绕不开的副作用。
成因:分区越细 → HDFS 目录越多 → 每个目录里的文件越小越碎。动态分区一条 SQL 灌全量数据时,最容易一次产生成千上万个小文件。
两大后果:
| 后果 | 原因 |
|---|---|
| NameNode 内存吃紧 | HDFS 每个文件/目录的元数据都要常驻 NameNode 内存(一条约 150 字节,乘以千万级文件就很可观) |
| 查询启动开销剧增 | 一个文件分片对应一个 Map 任务,1 万个小文件 = 1 万个 Map 任务排队"开机",数据没多大、光启动就几分钟 |
治理三板斧:
sql
-- ① 开启输出合并:输出前按目标大小把小文件拼成大文件
set hive.merge.mapfiles=true; -- Map-only 任务合并输出,默认 true
set hive.merge.mapredfiles=false; -- 带 Reduce 的任务合并输出,默认关
set hive.merge.size.per.task=256000000; -- 合并后每个文件的目标大小(256MB,可调)
set hive.merge.smallfiles.avgsize=16000000;-- 平均文件小于 16MB 时才触发合并
sql
-- ② 定期"重写"大表:把碎文件重读再写成大文件
insert overwrite table dept_partition partition(day='20260928')
select deptno, dname, loc
from dept_partition
where day='20260928';
text
-- ③ 从源头少产小文件:控制分区粒度 + 建表就用 ORC 列式格式(第 5 篇展开)
小文件治理也是学术圈的活跃问题:有专利专门研究"基于 Spark 的 Hive 小文件合并方法及系统"5,《面向 Hive 查询的存储优化技术》4也从存储层讨论了同类优化------工程和学术在这里是同一个问题。
三、动态分区:让 Hive 自动帮你分目录
3.1 为什么需要动态分区
静态分区的问题:分区值是你手写的。如果要从一张明细表往按天分区的表灌一年的数据,难道手写 365 条 insert?
动态分区:分区值不写死,从查询结果里自动取。Hive 按数据里分区字段的值,自动建目录、自动分目录。
3.2 参数配置:先明白再执行
sql
-- 开启动态分区功能(Hive3 默认已开,写上保险)
set hive.exec.dynamic.partition=true;
-- 模式:strict(严格,默认)要求至少一个静态分区;nonstrict 允许全部动态
set hive.exec.dynamic.partition.mode=nonstrict;
-- 一条 insert 最多能创建的分区总数,默认 1000
set hive.exec.max.dynamic.partition=1000;
-- 单个 mapper/reducer 最多能创建的分区数,默认 100
set hive.exec.max.dynamic.partitions.pernode=100;
-- 一条 insert 最多能创建的文件数,默认 100000
set hive.exec.max.created.files=100000;
-- 查询结果为空且动态分区时是否抛异常,默认 false
set hive.error.on.empty.partition=false;
为什么要 strict / nonstrict 两种模式? 防手滑。默认 strict 强制你至少写一个静态分区(比如 partition(sex='男') 里至少指定性别),避免一条 SQL 意外生成成千上万个分区把 NameNode 搞挂。学习/确定安全时才切 nonstrict。
3.3 实战:按性别自动分区
准备普通表并灌数:
sql
create table if not exists bigdata.students(
id string comment '学号',
name string comment '姓名',
age int comment '年龄',
sex string comment '性别',
clazz string comment '班级'
)
row format delimited fields terminated by ',';
load data local inpath '/root/sh_code/student.csv' into table students;
创建目标分区表(sex 作分区字段,注意普通字段里没有 sex):
sql
create table if not exists bigdata.students_partition(
id string comment '学号',
name string comment '姓名',
age int comment '年龄',
clazz string comment '班级'
)
partitioned by (sex string comment '性别')
row format delimited fields terminated by ','
location '/data/bigdata/students_partition';
动态分区插入------分区字段放 select 的最后一列 ,且 partition(sex) 只写字段名不写值:
sql
insert into bigdata.students_partition partition(sex)
SELECT
id,
name,
age,
clazz,
sex -- 必须是最后一列,Hive 按它自动分目录
from bigdata.students;
验证:
sql
select * from students_partition where sex = '男' limit 3;
OK
id name age clazz
1500100001 施笑槐 22 理科一班
1500100005 王五 22 理科二班
...
shell
hdfs dfs -ls /data/bigdata/students_partition
# sex=男/ sex=女/ ← 自动建好的两个分区目录
易错点:select 列的顺序必须与"普通字段在前、分区字段在后"的建表顺序对应,分区字段漏写或写错位置会直接报错。
四、分桶表:分区分不动的时候怎么办
4.1 为什么有了分区还要分桶
分区是"按字段值建目录",但它有两个解决不了的问题:
- 分区字段要求是离散值(天、省、性别),数据量倾斜时单分区过大,没法继续切
- 分区只能加速过滤,没法加速 join 和抽样
分桶换了个思路:按字段的 hash 值把数据切成固定数量的文件。
类比记忆:分区 = 按班级建文件夹;分桶 = 按学号尾号把学生分进 4 个班。同一个"尾号班"的学生散落在全国(各分区),但 hash 规则一致,永远能对上号。
好处(面试答这三条):
- join 提速:两张表按 join 键分桶后,桶和桶之间可以直接对接(bucket join),避免全量 shuffle
- 高效抽样 :
TABLESAMPLE只读指定桶,不用全表扫 - 数据更均匀:hash 天然打散,缓解按值分区导致的倾斜
4.2 创建分桶表
sql
create table stu_buck(
id string comment '学号',
name string comment '姓名',
age int comment '年龄',
sex string comment '性别',
clazz string comment '班级'
)
CLUSTERED BY(id) into 4 BUCKETS -- 按 id 的 hash 分 4 个桶
row format delimited fields terminated by ',';
分桶排序表:多一个 SORTED BY,桶内数据再按 id 排好序:
sql
create table stu_buck_sort(
id string comment '学号',
name string comment '姓名',
age int comment '年龄',
sex string comment '性别',
clazz string comment '班级'
)
CLUSTERED BY(id) sorted by(id) into 4 buckets
row format delimited fields terminated by ',';
4.3 灌数据的正确姿势(重要踩坑)
直接 load data 不会真正分桶! 文件会原样整个塞进表目录,桶是"假的"。分桶表必须用 insert ... select 灌数,Hive 才会按 hash 规则切文件:
sql
-- 正确姿势:从普通表 insert 进来
insert into stu_buck select * from students;
(Hive 2.x 起默认 hive.enforce.bucketing=true,会自动按桶数起 Reduce;老版本需手动 set hive.enforce.bucketing=true。)
分好桶后表目录下是 4 个文件:
stu_buck/
├── 000000_0 ← 桶1
├── 000001_0 ← 桶2
├── 000002_0 ← 桶3
└── 000003_0 ← 桶4
4.4 TABLESAMPLE:只读某一个桶
sql
-- 读取第 1 个桶
SELECT * FROM stu_buck_sort TABLESAMPLE(BUCKET 1 OUT OF 4 ON id);
-- 读取第 2、3、4 个桶同理,改数字即可
SELECT * FROM stu_buck_sort TABLESAMPLE(BUCKET 2 OUT OF 4 ON id);
语法解读:
OUT OF 4:总共 4 个桶(也可以写桶数的因子,比如 OUT OF 2 表示每 2 个桶抽 1 个)BUCKET 1:取第几桶,范围 1, OUT OF 值ON id:分桶依据字段,必须和建表时 CLUSTERED BY 的一致,否则抽样不保证桶内数据
适用场景:开发调试时"随便抽点数据看看",或统计抽样估计------只扫 1/4 数据,速度直接快 4 倍。
4.5 分桶的工程建议:分桶键、分桶数与 SMB Join
三条来自工程与文献的经验23:
- 分桶键选高频 join 键:分桶的全部魔法都建立在"两表按同一个键 hash"上,所以分桶键应该选最常用来 join 的业务键(如用户ID、车牌号),而不是随便挑一列
- 分桶数与集群对齐:建议取 Reduce 数或节点数的整数倍(如 4、8、16...),让每个桶恰好对应一批任务;桶数一旦定下就不要再改------hash 规则变了,老数据和新数据就对不上号了
- 排序分桶开启 SMB Join :两表都按 join 键 CLUSTERED + SORTED 且桶数成倍数关系时,Hive 可走 SMB Join(Sorted Merge Bucket Join) ------两边桶文件各自有序、归并对接,连 shuffle 都省了(需设置
hive.optimize.bucketmapjoin.sortedmerge=true等参数)
五、面试加餐:分区 vs 分桶 + 高频追问
| 对比项 | 分区表 | 分桶表 |
|---|---|---|
| 切分依据 | 字段的值 | 字段的 hash 值 % 桶数 |
| 物理形态 | 目录(day=20260928/) | 文件(000000_0) |
| 字段是否真实存在 | 分区字段是伪列,不在数据文件里 | 分桶字段是真实列 |
| 数量 | 值有多少种就可能多少个分区(需控制) | 固定 INTO n BUCKETS |
| 主要作用 | 过滤裁剪,减少扫描范围 | join 优化、抽样、打散数据 |
| 灌数据 | load / insert 都行 | 只能 insert(load 不分桶) |
| 组合使用 | 可以先分区再分桶:分区目录下再切桶文件 | 同左 |
一句话总结:分区是目录,分桶是文件;分区按值分家,分桶按 hash 分班。
除了这张对比表,面试官还常顺着往下追问三个问题:
Q1:分区键应该怎么选?
答三原则(见 2.1 工程提示):① 选 where 里高频出现的过滤字段------分区裁剪只在查询带分区键时才生效;② 粒度适中,优先"天"这类常用层级,避免高基数字段直接做分区;③ 粒度不够先加二级分区,而不是一开始就切细。
Q2:分区建太多会怎样?怎么治理?
答:会产生小文件问题 ------NameNode 元数据内存吃紧 + Map 任务启动开销剧增(见 2.6)。治理三板斧:开启
hive.merge.*输出合并、定期insert overwrite重写大表、从源头控制分区粒度并用 ORC 存储。
Q3:为什么分桶能加速 join?
答:两张表按同一个 join 键分桶,hash 规则一致 → 相同键值必然落在两边编号相同的桶里 → join 时桶和桶直接对接(bucket join),不用全量 shuffle;若两表还是排序分桶,还能进一步走 SMB Join,连桶内归并都省了(见 4.5)。
六、总结
- 分区表的提速原理 = 分区裁剪:where 带分区字段,只扫目标目录
- 分区运维口诀:增不逗,删要逗 ;元数据和 HDFS 目录不一致用
msck repair - 动态分区 :分区字段放 select 最后一列,
partition(sex)只写字段名;注意 strict / nonstrict - 分桶 = hash 取模分文件:好处是 join 提速 + 抽样 + 打散;灌数据必须 insert,load 不分桶
- 分区解决"扫太多",分桶解决"切不匀 + join 慢",实战常组合使用
- 小文件是分区过度的副作用:开启 merge、定期 overwrite 重写;分桶键选 join 键、桶数对齐集群,可进一步开启 SMB Join