(课堂笔记)Hive 分区、分桶与数据倾斜
本文系统介绍了Hive的核心技术与优化策略。
主要内容包括:
1)分区与分桶技术,详细讲解静态/动态分区实现方法和分桶原理;
2)数据倾斜问题及解决方案,涵盖JOIN、GROUPBY等场景的优化方法;
3)Hive高级功能,如存储格式选择、排序方式比较和自定义函数开发;
4)性能优化体系,从运维配置、模型设计到SQL编写的多维度优化建议;
5)典型面试问题解析,如大表关联处理方案和参数调优技巧。
文章通过大量实例演示和命令速查表,帮助读者掌握Hive的核心技术要点和实战优化方法。
(课堂笔记)Hive 分区、分桶与数据倾斜
一、Hive 分区
1.1 什么是分区?
分区是将表的数据按照某个字段的值,分成不同的目录存储。查询时可以只扫描特定分区,大大提高查询效率。
建表时语法中包含分区列,但分区列不是表中的真实字段,只作为目录层级存在。
1.2 静态分区
定义:使用分区时必须指定分区名(key=value),这就是静态分区。
关键点:
项目 说明 分区列在语法中 是的,在 partitioned by子句中分区列是真实字段吗 不是,数据文件中没有这一列 分区列存在哪里 作为 HDFS 的目录层级存在
插入数据时在
partition子句中手动指定分区值,SELECT 中不包含分区列。
示例场景
三位任课老师的成绩文件:
| 科目 | 文件名 | 内容 |
|---|---|---|
| 语文 | chinese | 101,zhangsan,100 102,lisi,88 103,wangwu,78 |
| 数学 | math | 101,zhangsan,89 102,lisi,56 103,wangwu,60 |
| 英语 | english | 101,zhangsan,77 102,lisi,67 103,wangwu,98 |
操作步骤
步骤1:构造数据文件
bash
cd /home/hadoop/2608/test touch chinese math english # 依次编辑文件,写入内容
步骤2:创建分区表
sql
create table t_class(
sno int,
sname string,
score int
)
partitioned by (subject string)
row format delimited fields terminated by ',';
步骤3:加载数据到指定分区
sql
-- 加载语文数据到 yuwen 分区 load data local inpath '/home/hadoop/2608/test/chinese' into table t_class partition (subject='yuwen'); -- 加载数学数据到 shuxue 分区 load data local inpath '/home/hadoop/2608/test/math' into table t_class partition (subject='shuxue'); -- 加载英语数据到 yingyu 分区 load data local inpath '/home/hadoop/2608/test/english' into table t_class partition (subject='yingyu');
HDFS 存储结构
/user/hive/warehouse/t_class/ ├── subject=yuwen/ │ └── 数据文件(只有 sno,sname,score) ├── subject=shuxue/ │ └── 数据文件 └── subject=yingyu/ └── 数据文件
步骤4:查询某个分区数据
sql
select * from t_class where subject = 'yuwen';
步骤5:新增分区
sql
alter table t_class add partition (subject='history');
步骤6:插入数据到分区
sql
load data ... -- 方式一:加载文件 -- 方式二:从其他表插入 insert overwrite table t_class partition (subject='history') select sno, sname, score from t_class where subject = 'yuwen';
静态分区:插入时无分区列
特点:
分区值在
partition (subject='yuwen')中手动指定SELECT 语句中不需要包含分区列
一次只能写入一个分区
步骤7:删除分区
| 方式 | 命令 |
|---|---|
| 页面删除 | 直接在 Web 界面操作 |
| HDFS 删除 | hadoop fs -rm -r 分区目录 |
| Hive 删除 | alter table t_class drop partition (subject='math'); |
1.3 静态分区练习
练习要求
构造三个月份的产品销售数据:
| 文件 | 内容 |
|---|---|
| 202001 | 111,apple,7788 112,huawei,8899 113,xiaomi,6666 |
| 202002 | 111,apple,656 112,huawei,77567 113,xiaomi,86786 |
| 202003 | 111,apple,546 112,huawei,768678 113,xiaomi,867876 |
练习答案
1. 创建分区表
sql
create table t_sales(
pro_id int,
pro_name string,
quantity int
)
partitioned by (month int)
row format delimited fields terminated by ',';
2. 加载数据到指定分区
sql
load data local inpath '/home/hadoop/2608/test/202001' into table t_sales partition (month=202001); load data local inpath '/home/hadoop/2608/test/202002' into table t_sales partition (month=202002); load data local inpath '/home/hadoop/2608/test/202003' into table t_sales partition (month=202003);
3. 添加分区
sql
alter table t_sales add partition (month=202004);
4. 从现有分区插入数据到新分区
sql
insert overwrite table t_sales partition (month=202004)
select
pro_id,
pro_name,
quantity + 1000
from t_sales
where month = 202002;
5. 删除分区
sql
alter table t_sales drop partition (month=202001);
1.4 动态分区
定义:分区名未知时,MR 自动读取数据,读取分区列,将数据写入对应的分区。
插入数据时 SELECT 的最后一列是分区列,自动识别分区值。
核心特点
| 特点 | 说明 |
|---|---|
| 自动分区 | MR 自动根据分区列的值创建分区 |
| 使用简单 | 可以把分区表当作普通表使用 |
| 位置要求 | 分区列必须在 SELECT 字段的最后面 |
示例:将 EMP 表数据写入分区表(按 deptno 分区)
步骤1:创建分区表
sql
create table fenqu_emp(
empno int,
ename string,
job string,
mgr int,
hiredate string,
sal int,
comm int
)
partitioned by (deptno int)
row format delimited fields terminated by '\t';
步骤2:加载数据
sql
-- 方式一:直接加载文件 load data local inpath '/home/hadoop/2608/test/emp' into table fenqu_emp; -- 方式二:动态分区插入(需要结果集提供分区值) insert into fenqu_emp select * from fenqu_emp where deptno is not null;
注意:动态分区时,SELECT 的最后一个字段必须匹配分区列(deptno)
动态分区:插入时有分区列
特点:
SELECT 的最后一列必须是分区列
不需要写
partition (deptno=?),Hive 自动识别一次可以写入多个分区(有多少个不同的 deptno 值,就创建多少个分区目录)
1.5 动态分区练习
练习要求
将 dept 表的数据,以 deptno 分区的方式存入 fenqu_dept。
目标结果:HDFS 的 fenqu_dept 目录下有 4 个子目录:
-
deptno=10
-
deptno=20
-
deptno=30
-
deptno=40
练习答案
sql
-- 1. 创建分区表
create table fenqu_dept(
dname string,
loc string
)
partitioned by (deptno int)
row format delimited fields terminated by '\t';
-- 2. 动态分区插入
insert into fenqu_dept
select dname, loc, deptno from dept;
静态分区和动态分区对比
对比项 静态分区 动态分区 分区值指定方式 手动在 partition子句中写从 SELECT 结果中自动获取 SELECT 是否包含分区列 ❌ 不包含 ✅ 包含,且必须在最后一列 一次写入分区数 1 个 多个 适用场景 分区值固定的场景 分区值不确定或较多时
总结:
建表时分区列在语法中声明,但不是表中的真实字段,只是目录层级。
-
数据文件中没有分区列
-
静态分区时 SELECT 不需要分区列
-
动态分区时 SELECT 必须包含分区列(作为最后一列)
二、Hive 分桶
2.1 什么是分桶?
当分区数据分布不均匀时,可以使用分桶将数据按照哈希值均匀分配到多个桶(文件)中。
2.2 分桶原理
text
每条数据写入时:MOD(哈希值, 桶数) → 决定放入哪个桶
| 桶数 | 每个桶数据量 |
|---|---|
| 5 个桶 | 1 亿条数据 → 每个桶约 2000 万条 |
2.3 分表示例:按 ENAME 分桶
步骤1:创建分桶表
sql
create table fentong_emp(
empno int,
ename string,
job string,
mgr int,
hiredate string,
sal int,
comm int,
deptno int
)
clustered by (ename) into 3 buckets
row format delimited fields terminated by '\t';
步骤2:写入数据
sql
-- 分桶表就是普通表,直接插入 insert into fentong_emp select * from emp; -- ❌ 不推荐用 load data,因为不会触发分桶 -- load data local inpath '/home/hadoop/2608/test/emp' into table fentong_emp;
2.4 分桶练习
练习要求
创建分桶表 fentong_dept,分桶键为 DNAME,分 2 个桶,并将 dept 表数据写入。
练习答案
sql
-- 1. 创建分桶表
create table fentong_dept(
deptno int,
dname string,
loc string
)
clustered by (dname) into 2 buckets
row format delimited fields terminated by '\t';
-- 2. 写入数据
insert into fentong_dept
select * from dept;
2.5 分区 vs 分桶对比
| 对比项 | 分区 | 分桶 |
|---|---|---|
| 存储方式 | 分目录存放 | 分文件存放 |
| 分配规则 | 有规律(相同分区值放一起) | 无规律(哈希取模) |
| 字段要求 | 分区字段在数据文件中不存在 | 分桶字段在数据文件中真实存在 |
| 用途 | 提高查询效率 | 便于数据采样 |
怎么理解 分区是分目录存放,分桶是分文件存放?
分区 = 分目录(在 HDFS 上创建子文件夹)
分桶 = 分文件(在同一个目录下创建多个文件)
分区表示例
HDFS 存储路径: /user/hive/warehouse/t_class/ ├── subject=yuwen/ ← 目录1 │ └── 000000_0 ← 数据文件 ├── subject=shuxue/ ← 目录2 │ └── 000000_0 ← 数据文件 └── subject=yingyu/ ← 目录3 └── 000000_0 ← 数据文件特点:分区值不同 → 创建不同的子目录
分桶表示例
HDFS 存储路径: /user/hive/warehouse/fentong_emp/ ├── 000000_0 ← 桶1(文件) ├── 000001_0 ← 桶2(文件) └── 000002_0 ← 桶3(文件)特点:分桶值不同 → 在同一个目录下创建不同的文件
本质区别对比
对比项 分区 分桶 存储方式 分目录 分文件 目录结构 多级子目录 单级目录 文件数量 每个分区至少1个文件 固定数量的文件(桶数) 查询优化 按分区列过滤,跳过不相关目录 按分桶列取模,快速定位到具体文件
为什么分区是"目录"?
因为分区值作为路径的一部分存储,例如:
/user/hive/warehouse/t_class/subject=yuwen/数据文件 ↑ 这里就是目录名为什么分桶是"文件"?
因为分桶值通过哈希取模决定写入哪个文件,文件名是固定的
000000_0、000001_0...,不包含分桶值信息。
2.6 分桶的好处
-
便于后期的数据采样
-
使数据分布更均匀
2.7 桶的个数定义规则
text
桶数 = (总文件大小 / 128M) × 2
说明:128M 是 HDFS 的 Block Size 默认存储大小
三、数据倾斜
3.1 什么是数据倾斜?
Hive 将 SQL 转换成 MR 程序运行时,Map 阶段生成的键值对分布不均,导致某些节点处理的数据量非常大,某些节点非常少。
3.2 数据倾斜示例
sql
SELECT 省份, SUM(金额) FROM 订单表 GROUP BY 省份;
| 省份 | 数据量 | 问题 |
|---|---|---|
| 广东 | 10 亿条 | ⚠️ 数据量大 |
| 浙江 | 2 亿条 | 正常 |
| 云南 | 1000 万条 | 正常 |
| 内蒙 | 1 万条 | 正常 |
总结:Map 阶段数据分布不均,导致单节点负载过高。
3.3 数据倾斜的解决方案
方案1:JOIN 导致的数据倾斜
问题场景:
sql
select * from 订单表 m left join 用户表 n on m.客户ID = n.客户ID
解决方案:
| 方法 | 说明 |
|---|---|
| Key 值打散 | case when m.客户ID is null then rand() else m.客户ID end = n.客户ID |
| Map Join | 把小表放到内存中,驱动大表,避免 shuffle 运算 |
Shuffle 就是数据在节点之间重新分配的过程。
比喻:快递分拣
快递站收到各地包裹: 北京来的、上海来的、广州来的...混在一起 Shuffle = 分拣过程: 把所有去北京的分到一堆,去上海的分到一堆 分拣后才能装车运输(才能执行后续计算)
sql
-- Map Join 写法 select /*+ mapjoin(n) */ * from 订单表 m left join 用户表 n on m.客户ID = n.客户ID
方案2:GROUP BY 导致的数据倾斜
解决方案:
| 方法 | 说明 |
|---|---|
| 缩小粒度 | GROUP BY 省份 → GROUP BY 城市 |
| 参数调优 | set hive.groupby.xxx = Trueset hive.map.xxx = True |
方案3:小文件过多
问题:一个目录有几万甚至几十万个小文件
解决方案:
| 方法 | 说明 |
|---|---|
| 合并小文件 | 设置 combine 参数为 True |
| Insert Overwrite | 用 insert overwrite 方式合并小文件 |
方案4:数据类型不一致
问题场景:
sql
select *
from aaa
left join bbb on aaa.id = bbb.id;
-- aaa.id 是 int (101)
-- bbb.id 是 string ('101')
解决方案:
sql
-- 用 CAST 统一数据类型 select * from aaa left join bbb on cast(aaa.id as string) = bbb.id;
3.4 面试题示例
Q:数据倾斜有碰到过吗?原因是什么?怎么解决的?
A: 之前有碰到过几次,主要原因是因为 key 值分布不均导致的。
具体案例1:大表 JOIN 小表
-
某些科目数据在大表里面体量非常大,有的比较少
-
导致 JOIN 时 Map 阶段分布不均
-
解决方法:使用 Map Join,把小表放到内存驱动大表,避免数据倾斜
具体案例2:小文件过多
-
开发监控脚本,监控 HDFS 上的分区目录文件个数
-
某个分区表子目录文件个数超过阈值(5000),有几万个文件
-
导致跑批任务时间非常久
-
解决方法:通过
insert overwrite方式合并小文件,解决数据倾斜
跑批任务 = 运行批量处理任务(运行一个处理海量数据的 SQL 或脚本)
批量(一次处理大量数据,不是一条一条处理)
每天凌晨执行一次 → 叫"定时跑批"
小文件过多为什么导致跑批任务变慢?
正常情况(文件数合理)
text
表目录: └── dt=2024-01-01/ ├── 000000_0 (200MB) └── 000001_0 (200MB) └── 000002_0 (200MB)
总共 3 个文件
Hive 启动 3 个 Map 任务去读
很快读完
小文件过多的情况
text
表目录: └── dt=2024-01-01/ ├── part-00001 (1KB) ├── part-00002 (1KB) ├── part-00003 (1KB) ├── ... (1万个文件,每个只有几 KB) └── part-10000 (1KB)
总共 1 万个文件
Hive 需要启动 1 万个 Map 任务
每个 Map 任务有启动开销(几秒)
总耗时 = 1万 × 几秒 = 几小时
对比图
text
正常情况(3个文件): [启动3个Map] → [读数据] → [处理] → [完成] 总耗时:30秒 小文件过多(10000个文件): [启动10000个Map] → [读数据] → [处理] → [完成] 总耗时:2小时
为什么会有小文件过多?
原因 说明 频繁的 insert 每次 insert 都会生成新文件,不会合并 Flume/Kafka 持续写入 流式写入会生成大量小文件 分区太多 每个分区数据量少,文件个数多 错误的任务设计 没有定期合并小文件
解决方案:合并小文件
方法1:INSERT OVERWRITE 合并
sql
-- 把分区数据读出来,再覆盖写回去(自动合并) insert overwrite table target partition(dt='2024-01-01') select * from target where dt='2024-01-01';原理:读取时可能有 1 万个文件,但写入时 Hive 会根据配置生成较少的大文件。
方法2:设置合并参数
sql
-- 设置合并相关参数 set hive.merge.mapfiles=true; -- map 任务结束时合并 set hive.merge.mapredfiles=true; -- reduce 任务结束时合并 set hive.merge.size.per.task=268435456; -- 合并后的文件大小(256MB) set hive.merge.smallfiles.avgsize=16777216; -- 平均文件小于16MB时触发合并 -- 然后执行覆盖写入 insert overwrite table target partition(dt='2024-01-01') select * from target where dt='2024-01-01';方法3:重建表
sql
-- 创建临时表 create table target_tmp like target; -- 设置合并参数 set hive.exec.dynamic.partition.mode=nonstrict; -- 插入数据(会自动合并) insert overwrite table target_tmp partition(dt) select * from target; -- 替换原表 drop table target; alter table target_tmp rename to target;
监控脚本的作用
课堂笔记中提到"开发监控脚本",意思是:
bash
# 伪代码:每天检查一次 for each partition in 表: file_count = count_files(partition) if file_count > 5000: 发送告警: "分区 ${partition} 小文件过多,请合并" 或 自动执行合并脚本
总结
问题 答案 跑批任务是什么? 处理批量数据的 SQL/脚本(不是一条条处理) 为什么小文件过多会慢? 每个文件至少启动一个 Map 任务,任务启动开销大 正常多少文件合适? 每个文件 128MB-256MB,文件数 = 总数据量 ÷ 256MB 阈值 5000 合理吗? 合理,超过 5000 个文件会影响性能 怎么解决? INSERT OVERWRITE合并小文件
四、命令速查表
| 操作 | 命令 |
|---|---|
| 创建分区表 | create table ... partitioned by (col type) |
| 静态分区加载 | load data ... into table ... partition (col='value') |
| 添加分区 | alter table ... add partition (col='value') |
| 删除分区 | alter table ... drop partition (col='value') |
| 创建分桶表 | create table ... clustered by (col) into n buckets |
| 插入分桶表 | insert into ... select ...(不能用 load data) |
| Map Join | select /*+ mapjoin(小表) */ ... |
五、关键概念总结
| 概念 | 核心要点 |
|---|---|
| 静态分区 | 指定分区名,手动加载 |
| 动态分区 | 自动识别分区,分区列在 SELECT 最后 |
| 分桶 | 哈希取模均匀分布,便于采样 |
| 数据倾斜 | key 分布不均导致,可用 Map Join、打散 key、合并小文件解决 |
补充:Hive 周边 & 面试常问问题
本笔记针对初学者,将课堂笔记中笼统的概念和理论进行详细解释,帮助理解。
一、Linux 环境执行 Hive 脚本
1.1 为什么需要在 Linux 中执行 Hive 脚本?
在实际工作中,我们通常不会手动在 Hive 窗口一条条输入 SQL,而是:
-
将写好的 SQL 保存成文件
-
通过 Linux 命令批量执行
-
配合调度工具(如 Azkaban)定时运行
1.2 方式一:hive -e
bash
hive -e "SQL语句"
作用:切换到 Hive 窗口执行 SQL,执行完后自动退出 Hive 窗口,返回到 Linux 命令行。
示例1:直接执行查询
bash
hive -e "select * from a2608.fenqu_emp"
注意:如果之前没有用 use 数据库名; 切换数据库,则表名需要带上数据库名,格式为 数据库名.表名。
示例2:多条 SQL 用分号隔开
bash
hive -e "use mydb; show tables; select * from emp limit 10;"
1.3 方式二:hive -f
bash
hive -f SQL文件路径
作用:将文件内容当作 Hive 命令执行,执行完后自动退出。
示例:
bash
# 1. 创建一个 SQL 文件 echo "select * from a2608.fenqu_emp;" > aaa.sql # 2. 执行这个文件 hive -f aaa.sql
1.4 两种方式对比
| 方式 | 适用场景 | 优点 |
|---|---|---|
hive -e |
SQL 较短,一次性执行 | 简单快捷 |
hive -f |
SQL 较长,需要保存记录 | 便于复用和版本管理 |
二、Hive 的文件存储格式
2.1 什么是存储格式?
存储格式决定了数据在 HDFS 上以什么方式组织和存放。
2.2 常见存储格式
| 格式 | 类型 | 特点 |
|---|---|---|
| TEXTFILE | 行式存储(默认) | 纯文本,可读性好,占用空间大 |
| ORC | 列式存储 | 压缩比高,查询快,占用空间小 |
| SEQUENCEFILE | 行式存储 | 二进制格式,支持压缩 |
2.3 行式存储 vs 列式存储
行式存储(如 TEXTFILE):
text
数据按行存放:101,张三,90 → 102,李四,85 → 103,王五,88 查询时即使只需要"姓名"列,也会读取整行数据
列式存储(如 ORC):
text
数据按列存放:101,102,103 → 张三,李四,王五 → 90,85,88 查询时只需要读取需要的列,大大减少 IO
2.4 如何选择存储格式?
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 数据量大(TB/PB 级) | ORC | 压缩比高(节省存储),查询快(列式存储) |
| 数据量小(几百 MB) | TEXTFILE | 简单直观,便于排查问题 |
| 需要与其他工具交互 | TEXTFILE | 通用性最好 |
2.5 建表时指定存储格式
sql
-- 不指定时,默认是 TEXTFILE create table aaa(id int) row format delimited fields terminated by ',' stored as orc; -- 指定 ORC 格式 -- 其他格式示例 stored as textfile; -- 文本格式(默认) stored as sequencefile; -- 序列文件格式
三、Hive 的特殊功能
3.1 存储过程
| 问题 | 答案 |
|---|---|
| Hive 有存储过程吗? | 没有 |
| 为什么没有? | Hive 定位是数据仓库工具,不是传统数据库,不支持过程式编程 |
3.2 自定义函数(UDF)
什么是 UDF?
当 Hive 自带的函数无法满足业务需求时,可以自己编写 Java 代码实现自定义函数。
开发步骤:
-
编写 Java 类,继承
UDF类 -
实现
evaluate方法 -
打包成 JAR 包
-
在 Hive 中注册并使用
sql
-- 注册自定义函数 add jar /path/to/myudf.jar; create temporary function my_func as 'com.example.MyUDF'; -- 使用自定义函数 select my_func(column) from table;
3.3 索引
| 问题 | 答案 |
|---|---|
| Hive 有索引吗? | 通常没有(3.0 版本后有,但不常用) |
| 为什么不用索引? | 分区和分桶已经提供了类似索引的查询优化效果 |
四、Hive 的四种排序
4.1 排序方式对比
| 排序方式 | 英文 | 说明 | 适用场景 |
|---|---|---|---|
| 全局排序 | ORDER BY | 对所有数据排序,只有一个 reducer | 数据量小时使用 |
| 分区排序 | SORT BY | 每个 reducer 内部排序,不保证全局有序 | 数据量大,只关心局部有序 |
| 分发排序 | DISTRIBUTE BY | 按指定字段分发到不同 reducer | 需要将相同 key 的数据发到同一 reducer |
| 分区+分发 | CLUSTER BY | = DISTRIBUTE BY + SORT BY | 同时完成分区和分发 |
CLUSTER BY(日常简写,完整 CLUSTERED BY)
CLUSTERED 聚合(cluster 的过去式)
4.2 详细解释
ORDER BY(全局排序)
sql
select * from emp order by sal desc;
-
特点:所有数据进入一个 reducer,保证全局有序
-
缺点:数据量大时非常慢
-
注意:需要设置
set hive.mapred.mode=nonstrict;(关闭严格模式)
SORT BY(分区排序)
sql
select * from emp sort by sal desc;
-
特点:每个 reducer 内部排序,但 reducer 之间无序
-
举例:如果有 3 个 reducer,每个 reducer 输出的数据是排序的,但 reducer1 的最后一个值可能大于 reducer2 的第一个值
DISTRIBUTE BY(分发)
sql
select * from emp distribute by deptno;
-
特点:将相同 deptno 的数据分发到同一个 reducer
-
用途:为后续的排序或聚合做准备
CLUSTER BY(分区+排序)
sql
select * from emp cluster by deptno;
-
等价于:
distribute by deptno sort by deptno -
特点:按相同字段分发和排序
4.3 图解排序方式
text
ORDER BY: [所有数据] → reducer1 → [全局有序结果]
SORT BY: [数据] → reducer1 → [有序子集1]
[数据] → reducer2 → [有序子集2]
[数据] → reducer3 → [有序子集3]
(子集之间无序)
DISTRIBUTE BY: [数据] → 按key分发 → reducer1 (所有key=10)
→ reducer2 (所有key=20)
→ reducer3 (所有key=30)
CLUSTER BY: [数据] → 按key分发 + 内部排序 → reducer1 (key=10, 有序)
→ reducer2 (key=20, 有序)
→ reducer3 (key=30, 有序)
Hive 四种排序(核心区别 + 用法 + 场景)
1. ORDER BY(全局排序)
- 定义:全局唯一排序,所有数据进入同一个 Reducer处理,最终输出一个有序文件。
- 特点
- 严格有序,全局唯一排序规则
- 数据量大时极慢(单 Reducer 瓶颈)
- 默认开启严格模式,必须搭配
LIMIT- 语法
sql
SELECT * FROM table ORDER BY id ASC;- 场景:小数据集、需要绝对全局有序的结果。
2. SORT BY(分区内排序)
- 定义:每个 Reducer 内部有序,全局无序,只保证分区内有序。
- 特点
- 多 Reducer 并行执行,速度比 ORDER BY 快
- 分区之间数据无序,分区内有序
- 语法
sql
SELECT * FROM table SORT BY id DESC;- 场景:大数据量、只需要分区内有序,不要求全局有序。
3. DISTRIBUTE BY(数据分区 / 分发)
- 定义:指定分区规则,按照指定字段将数据分发到不同 Reducer,只分区、不排序。
- 特点
- 控制数据流向:相同字段值一定进入同一个 Reducer
- 单纯分区,无排序功能
- 通常配合
SORT BY使用- 语法
sql
SELECT * FROM table DISTRIBUTE BY city SORT BY id;- 场景:需要按字段分组到同一 Reducer,再做分区内排序 / 聚合。
4. CLUSTER BY(分区 + 排序 二合一)
- 定义:DISTRIBUTE BY + SORT BY 的简写合体
- 先按指定字段 DISTRIBUTE BY 分区
- 再按同一字段 SORT BY 升序排序
- 特点
- 分区字段 = 排序字段(必须是同一个字段)
- 等价:
DISTRIBUTE BY col SORT BY col- 只能默认升序,不能指定排序方向
- 语法
sql
-- 等价 DISTRIBUTE BY id SORT BY id SELECT * FROM table CLUSTER BY id;- 场景:需要先分区、再分区内排序,且分区和排序字段相同。
一句话终极总结(最好记)
- ORDER BY:全局有序,单 Reducer,慢
- SORT BY:分区内有序,多 Reducer,快
- DISTRIBUTE BY:只分区不排序,控制数据流向
- CLUSTER BY:分区 + 排序(同字段)= DISTRIBUTE + SORT
总结
- ORDER BY 全局唯一有序(单 Reducer,慢)
- SORT BY 分区内有序(多 Reducer,快)
- DISTRIBUTE BY 按字段分区(不排序)
- CLUSTER BY = DISTRIBUTE BY + SORT BY(同字段,分区 + 排序)
五、HDFS 读写流程
5.1 Hive 查询数据时,底层 HDFS 是怎么工作的?
text
┌─────────────────────────────────────────────────────────────┐ │ 查询流程 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 客户端(Hive) 向 NameNode 发送读请求 │ │ 2. NameNode 验证权限,检查文件是否存在 │ │ 3. NameNode 返回文件元数据(文件位置、块信息) │ │ 4. 客户端根据元数据,直接与 DataNode 通信读取数据 │ │ 5. DataNode 返回数据块内容 │ │ 6. Hive 组装数据返回给用户 │ └─────────────────────────────────────────────────────────────┘ 简化版流程:Hive → NameNode(获取元数据)→ DataNode(读取数据)→ Hive
5.2 写入流程(插入数据)
text
┌─────────────────────────────────────────────────────────────┐ │ 写入流程 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 客户端向 NameNode 请求创建文件 │ │ 2. NameNode 检查权限和空间 │ │ 3. NameNode 返回可用的 DataNode 列表 │ │ 4. 客户端将数据分块写入 DataNode │ │ 5. DataNode 之间复制副本(默认3份) │ │ 6. 写入完成,NameNode 更新元数据 │ └─────────────────────────────────────────────────────────────┘
六、Hive 优化(面试重点)
6.1 优化可以从哪些角度入手?
| 角度 | 负责角色 | 主要内容 |
|---|---|---|
| 运维角度 | 运维工程师 | 管理集群、服务器配置、节点扩展 |
| 模型开发角度 | 数据模型师 | 表结构设计、分区/分桶、存储格式 |
| 数据开发角度 | 数据开发工程师 | SQL 写法、参数调优、代码规范 |
6.2 运维角度优化
| 优化项 | 说明 |
|---|---|
| 参数优化 | JVM 内存配置、动态分区严格模式、笛卡尔积严格模式 |
| 节点扩展 | 增加服务器节点,提升计算能力 |
| 硬件配套 | 更好的 CPU、更大内存、SSD 硬盘 |
6.3 模型开发角度优化
| 优化项 | 说明 |
|---|---|
| 分区/分桶设计 | 合理分区减少扫描量,分桶便于采样 |
| 列式存储 | 数据量大时使用 ORC 格式,压缩比高、查询快 |
| 代理键设计 | 使用 org_id、account_id 等整数类型作为关联键,比字符串快 |
6.4 数据开发角度优化(重点)
| 优化项 | 说明 | 示例 |
|---|---|---|
| 先过滤再关联 | 减少 JOIN 的数据量 | SELECT * FROM a JOIN b ON a.id=b.id WHERE a.date='2024-01-01' 不如先过滤 a 表 |
| 先过滤再分组 | 减少 GROUP BY 的数据量 | 先 WHERE 过滤,再 GROUP BY |
| 慎用全局排序 | ORDER BY 只有一个 reducer,数据量大时极慢 | 能用 SORT BY 就不用 ORDER BY |
| 慎用 DISTINCT | 去重操作消耗大量资源 | 能用 GROUP BY 代替就用 GROUP BY |
| 慎用 UNION | 需要合并去重,开销大 | 确定无重复时用 UNION ALL |
| Map Join | 大表 JOIN 小表时,把小表放入内存 | 小表不能超过内存大小 |
| 避免数据倾斜 | 解决 key 分布不均的问题 | 详见上一章的"数据倾斜"部分 |
| 合理设置参数 | 调整 map/reduce 相关参数 | set hive.map.aggr=true; |
6.5 Map Join 详解
什么是 Map Join?
传统 JOIN 需要经过 shuffle 阶段(Map → Shuffle → Reduce),Map Join 把小表加载到内存,在 Map 阶段直接完成 JOIN,避免 shuffle。
如何使用?
sql
-- 方式1:自动 Map Join(Hive 会自动判断) set hive.auto.convert.join=true; set hive.mapjoin.smalltable.filesize=25000000; -- 小表阈值 25MB -- 方式2:手动指定(Hive 的 map join 语法) select /*+ mapjoin(a) */ * from 小表 a left join 大表 b on a.id = b.id;
七、面试常见问题:大表关联大表跑不过怎么办?
问题场景
两个都是大表(TB 级别),执行 JOIN 时出现内存不足、任务失败。
解决方案汇总
| 方案 | 说明 | 适用场景 |
|---|---|---|
| 调整 JVM 内存 | 增加 reducer 的内存大小 | 内存不够但数据量可控 |
| 拆表关联 | 将一张大表拆成 N 张小表,逐个关联后合并 | 大表可按某个维度拆分 |
| 分桶优化 | 对大表按关联键预先分桶 | 两表按相同键分桶,可避免 shuffle |
| Map Join | 如果大表可以过滤成小表 | 关联前能大幅过滤数据 |
详细方案解释
方案一:调整 JVM 内存
bash
# 修改 hive-site.xml 或执行 set 命令 set mapreduce.map.memory.mb=4096; # map 内存 4GB set mapreduce.reduce.memory.mb=8192; # reduce 内存 8GB
方案二:拆表关联
sql
-- 将大表 A 按某个维度拆成 N 张小表 -- 例如按部门拆分成 A_dept10, A_dept20, A_dept30 -- 逐个关联 insert into tmp1 select /*+ mapjoin(a) */ * from A_dept10 a left join 大表B b on a.id = b.id; insert into tmp2 select /*+ mapjoin(a) */ * from A_dept20 a left join 大表B b on a.id = b.id; -- 最后合并所有结果 insert into result select * from tmp1 union all select * from tmp2 union all ...
方案三:分桶优化
sql
-- 创建分桶表时,让两张表按相同的键分桶,桶数相同
create table A_bucketed (
id int, name string
)
clustered by (id) into 128 buckets;
create table B_bucketed (
id int, name string
)
clustered by (id) into 128 buckets;
-- 分桶后 JOIN,相同 id 的数据已在同一 bucket,避免 shuffle
insert into result
select * from A_bucketed a
join B_bucketed b on a.id = b.id;
方案四:先过滤再关联
sql
-- 如果大表关联前能大幅过滤数据,先用子查询过滤 insert into result select * from (select * from 大表A where date = '2024-01-01') a left join (select * from 大表B where date = '2024-01-01') b on a.id = b.id;
八、面试常见问题:Hive 优化参数汇总
| 参数 | 作用 | 建议值 |
|---|---|---|
hive.auto.convert.join |
自动 Map Join | true |
hive.mapjoin.smalltable.filesize |
小表阈值 | 25000000(25MB) |
hive.exec.dynamic.partition.mode |
动态分区模式 | nonstrict(允许动态分区) |
hive.exec.dynamic.partition |
是否开启动态分区 | true |
hive.groupby.skewindata |
处理 group by 数据倾斜 | true |
mapreduce.map.memory.mb |
Map 内存 | 根据实际情况调整 |
mapreduce.reduce.memory.mb |
Reduce 内存 | 根据实际情况调整 |
九、命令速查表
| 操作 | 命令 |
|---|---|
| 直接执行 SQL | hive -e "SELECT * FROM table" |
| 执行 SQL 文件 | hive -f script.sql |
| 指定存储格式 | STORED AS orc |
| Map Join 提示 | SELECT /*+ MAPJOIN(small_table) */ ... |
| 设置自动 Map Join | set hive.auto.convert.join=true; |
| 设置动态分区 | set hive.exec.dynamic.partition.mode=nonstrict; |
| 设置 group by 倾斜处理 | set hive.groupby.skewindata=true; |
十、核心概念总结
| 概念 | 一句话总结 |
|---|---|
hive -e |
直接在 Linux 命令行执行 Hive SQL |
hive -f |
执行文件中的 Hive SQL |
| ORC 格式 | 列式存储,压缩比高,适合大数据量 |
| ORDER BY | 全局排序,只有一个 reducer |
| SORT BY | 每个 reducer 内部排序 |
| DISTRIBUTE BY | 按指定字段分发到不同 reducer |
| CLUSTER BY | 分发 + 排序(同一字段) |
| Map Join | 把小表放内存,避免 shuffle |
| 数据倾斜 | key 分布不均导致某些节点负载过高 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)