本文系统介绍了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_0000001_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 = True
set 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 代码实现自定义函数。

开发步骤

  1. 编写 Java 类,继承 UDF 类

  2. 实现 evaluate 方法

  3. 打包成 JAR 包

  4. 在 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;
    
  • 场景:需要先分区、再分区内排序,且分区和排序字段相同。

一句话终极总结(最好记)

  1. ORDER BY:全局有序,单 Reducer,慢
  2. SORT BY:分区内有序,多 Reducer,快
  3. DISTRIBUTE BY:只分区不排序,控制数据流向
  4. CLUSTER BY:分区 + 排序(同字段)= DISTRIBUTE + SORT

总结

  1. ORDER BY 全局唯一有序(单 Reducer,慢)
  2. SORT BY 分区内有序(多 Reducer,快)
  3. DISTRIBUTE BY 按字段分区(不排序)
  4. 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_idaccount_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 分布不均导致某些节点负载过高
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐