系列文章目录
文章目录
- 系列文章目录
- Hive 面试知识点全面解析
-
- 一、函数相关
- 二、SQL 的书写和执行顺序
- 三、where 和 having 的区别
- 四、表连接的方式及区别
- 五、Hive 的排序方式及区别
- 六、Hive 的体系架构
- 七、Hive 的表分类
- 八、将数据导入Hive的方式
- 九、开窗函数
- 十、Hive SQL 执行的方式
- 十一、Hive 的自定义函数
- 十二、Hive 优化
- 十三、Hive 文件存储格式
- 十四、Hive 序列化
- 十五、Hive 中`maptask`和`reducetask`的数量
- 十六、Hive中的mapjoin、commonjoin、smbjoin的特点与区别
- 十七、Hive 中的压缩
- 十八、Hive 如何导出数据
- 十九、Hive SQL 的行转列和列转行(适用于 MySQL)
- 二十、如何实现 Hive 的动态分区
- 二十一、Hive 表的字段类型
- 二十二、Hive 的相关概念及特点总结
Hive 面试知识点全面解析
一、函数相关
(一)函数分类与特点
- 单行函数
- 特点:一进一出。
- 细分类型及示例:
- 字符串函数:
substring
:用于截取字符串的子串。例如,substring('abcdefg', 2, 4)
返回bcde
,表示从字符串'abcdefg'
的第2个位置开始截取,截取长度为4。concat
:连接多个字符串。例如,concat('hello', 'world')
返回helloworld
,将'hello'
和'world'
连接在一起。concat_ws
:与concat
类似,但可以指定连接字符串之间的分隔符。例如,concat_ws(', ', 'apple', 'banana', 'cherry')
返回apple, banana, cherry
,使用,
作为分隔符连接三个字符串。
- 日期函数:
datediff
:计算两个日期之间的天数差。例如,datediff('2023-10-10', '2023-10-01')
返回9,表示从2023-10-01
到2023-10-10
相差9天。date_add
:在给定日期上增加指定的天数。例如,date_add('2023-10-10', 5)
返回2023-10-15
,在2023-10-10
的基础上增加5天。
- 数值函数:
round
:对数值进行四舍五入。例如,round(3.14159, 2)
返回3.14,将3.14159
保留两位小数四舍五入。floor
:向下取整。例如,floor(3.9)
返回3,取不大于3.9
的最大整数。ceil
:向上取整。例如,ceil(3.1)
返回4,取不小于3.1
的最小整数。cast
:用于类型转换。例如,cast('123' as int)
将字符串'123'
转换为整数123。
- 流程控制函数:
case when then end
:用于条件判断。例如,select case when age > 18 then '成年' else '未成年' end from users
,根据age
字段的值判断是成年还是未成年。if
:简单的条件判断函数。例如,if(condition, value1, value2)
,如果condition
为真,则返回value1
,否则返回value2
。
- 集合函数:
nvl
:用于处理空值。例如,nvl(column_name, default_value)
,如果column_name
为NULL
,则返回default_value
,否则返回column_name
的值。
- 字符串函数:
- 聚合函数
- 特点:多进一出。
- 示例:
max
:求最大值。例如,select max(salary) from employees
,返回employees
表中salary
列的最大值。min
:求最小值。avg
:求平均值。count
:计数。sum
:求和。collect_list
:将分组后的某个列值存放在一个数组中,不去重。例如,select collect_list(id) from table
,会将id
列的值收集到一个数组中,每个分组对应一个数组。collect_set
:与collect_list
类似,但会对集合中元素去重。
- 炸裂函数
- 特点:一进多出。
- 示例:
explode
:将数组或映射中的元素拆分成多行。例如,对于一个包含数组字段的表,explode(array_column)
可以将数组中的每个元素作为一行输出。posexplode
:除了将元素拆分成多行,还会返回元素的位置索引。
(二)concat
和concat_ws
的区别
concat
函数用于简单地连接多个字符串,没有指定分隔符,直接将字符串拼接在一起。concat_ws
函数可以指定一个分隔符,用于在连接字符串时插入分隔符,使得连接后的字符串更具可读性和规范性。例如,在连接多个单词组成一个句子时,concat_ws(' ', 'hello', 'world')
可以清晰地用空格分隔每个单词,而concat('hello', 'world')
则是直接连接成helloworld
,没有分隔。
二、SQL 的书写和执行顺序
(一)书写顺序
select.. from.. join.. group by.. having.. order by.. limit
(二)执行顺序
from.. join.. group by.. having.. select.. order by.. limit
例如,在执行一个查询时,首先从指定的数据源(from
)读取数据,然后进行表连接(join
)操作,接着按照group by
指定的列进行分组,对分组后的数据应用having
筛选条件,之后进行select
列的选择和计算,最后按照order by
进行排序,并根据limit
限制返回的行数。
三、where 和 having 的区别
(一)筛选时机
where
在分组之前对数据进行筛选,它可以直接使用表中的任意字段进行条件判断,但不能使用聚合函数。having
在分组之后对分组结果进行筛选,只能使用分组字段和聚合函数作为筛选条件,不能使用分组字段以外其他原表字段。
(二)示例
假设我们有一个销售数据表,包含字段product_id
(产品ID)、quantity
(销售数量)、date
(销售日期)等。
- 如果我们要查询销售数量大于100的记录,可以使用
where
:select * from sales where quantity > 100
。 - 如果我们要查询每个产品的总销售数量,并找出总销售数量大于500的产品,需要先分组再筛选,此时使用
having
:select product_id, sum(quantity) as total_quantity from sales group by product_id having total_quantity > 500
。
四、表连接的方式及区别
(一)连接方式
- 左连接(Left Outer Join):以左边表为主表,如果主表中某条数据没有和右表数据连接上,则左表这条数据也会出现在查询结果中,右边表对应列输出
null
。例如,员工表有一个员工没有部门编号,如果使用左连接,以员工表为主表则该员工会被查询出来,对应的部门表的列为null
。 - 右连接(Right Outer Join):以右边表为主表,右表中所有的行以及与左表匹配的行都会出现在结果中,如果左表中没有匹配的行,将返回
NULL
值。 - 内连接(Inner Join):返回两个表中匹配的行,即只返回两个表中共有的行,没有连接上的行不会输出。
- 全连接(Full Outer Join):返回两个表中所有的行,并将不匹配的行填充为
NULL
值,两张表都是主表。
(二)区别示例
假设有两张表,table1
(包含id1
和value1
字段)和table2
(包含id2
和value2
字段),部分数据如下:
table1 | |
---|---|
id1 | value1 |
1 | a |
2 | b |
3 | c |
table2 | |
---|---|
id2 | value2 |
2 | x |
3 | y |
4 | z |
- 左连接
select * from table1 left join table2 on table1.id1 = table2.id2
结果:
id1 | value1 | id2 | value2 |
---|---|---|---|
1 | a | null | null |
2 | b | 2 | x |
3 | c | 3 | y |
- 右连接
select * from table1 right join table2 on table1.id1 = table2.id2
结果:
id1 | value1 | id2 | value2 |
---|---|---|---|
2 | b | 2 | x |
3 | c | 3 | y |
null | null | 4 | z |
- 内连接
select * from table1 inner join table2 on table1.id1 = table2.id2
结果:
id1 | value1 | id2 | value2 |
---|---|---|---|
2 | b | 2 | x |
3 | c | 3 | y |
- 全连接
select * from table1 full outer join table2 on table1.id1 = table2.id2
结果:
id1 | value1 | id2 | value2 |
---|---|---|---|
1 | a | null | null |
2 | b | 2 | x |
3 | c | 3 | y |
null | null | 4 | z |
五、Hive 的排序方式及区别
(一)排序方式
order by
(全局排序)- 特点:对应的 MapReduce 任务中只有一个
reduceTask
,即使设置了set mapreduce.job.reduces
大于1也不行。适用于分组聚合的结果数据量已经不大的情况下,可以保证整个数据集的有序性。 - 示例:
select * from table order by column_name
,会对table
表中的数据按照column_name
列进行全局排序。
- 特点:对应的 MapReduce 任务中只有一个
sort by
(局部排序)- 特点:对应的 MapReduce 任务可以有多个
reduceTask
,每个reduceTask
输出的数据是有序的。它只保证每个分区内的数据有序,不保证全局有序。 - 示例:
select * from table sort by column_name
,会在每个分区内对数据按照column_name
列进行排序。
- 特点:对应的 MapReduce 任务可以有多个
distribute by
(自定义分区)- 特点:负责指定分区规则,将某一类数据写入同一个
reduceTask
中。通常和sort by
配合使用,distribute by
指定如何分区,sort by
指定分区内数据的排序方式。 - 示例:
select * from table distribute by partition_column sort by sort_column
,首先根据partition_column
进行分区,然后在每个分区内按照sort_column
进行排序。
- 特点:负责指定分区规则,将某一类数据写入同一个
(二)区别
order by
对整个数据集进行排序,确保所有数据按照指定列有序,但在大数据集情况下可能导致性能问题,因为只有一个reduceTask
处理所有排序工作,数据量大时可能会内存不足等。sort by
在每个reduceTask
内进行局部排序,适合数据量较大且不需要全局严格有序的场景,能提高处理效率,多个reduceTask
可以并行处理排序任务。distribute by
主要用于将数据按照指定规则分区到不同的reduceTask
,结合sort by
可以实现更灵活的数据分布和局部有序处理。
六、Hive 的体系架构
(一)Hive 概述
Hive是一个构建在Hadoop上的数据仓库软件,它提供了类似SQL的查询语言,使得用户可以用SQL来查询存放在Hadoop上的数据。Hive是一种结构化数据的存储和查询机制,它可以将SQL语句转换为MapReduce任务在Hadoop上执行。
(二)主要组件
- 用户接口(User Interface)
- 支持多种方式,包括CLI(命令行界面),用户可以直接在命令行中输入Hive命令执行SQL操作。
- JDBC/ODBC:供其他程序通过JDBC或ODBC接口访问Hive,方便与其他应用程序集成,例如在Java程序中可以使用JDBC连接Hive进行数据查询和处理。
- WebUI(浏览器访问):提供了一个可视化的界面,用户可以通过浏览器进行一些基本的操作和查询监控等。
- 元数据存储(Metastore)
- 存储关于数据的信息,比如表的结构、分区、数据所在的HDFS路径等。通常使用关系型数据库(如MySQL)来存储元数据。它提供了一组API和服务,用于查询、更新和管理Hive表的元数据,以便不同的用户和进程可以共享和访问相同的元数据,从而协调和共享表的结构和属性。
- 驱动器(Driver)
- 包含解析器、编译器、优化器和执行器。
- 解析器:将SQL字符串转换成抽象语法树AST,并进行语法分析,检查SQL语句的语法是否正确。
- 编译器:将AST编译生成逻辑执行计划,确定如何执行查询,例如选择使用哪些表连接算法等。
- 优化器:对逻辑执行计划进行优化,例如选择最优的执行路径、调整执行顺序等,以提高查询性能。
- 执行器:把逻辑执行计划转换成可运行的物理计划,对于Hive来说就是MapReduce或Spark任务,负责实际执行查询操作,与Hadoop进行交互,调度和管理任务的执行。
- 与Hadoop的关系
- Hive底层依赖Hadoop的HDFS进行数据存储,数据以文件的形式存储在HDFS中,Hive表对应为HDFS上的指定目录。
- 使用MapReduce或其他计算框架(如Tez、Spark等)进行数据计算,根据执行计划将任务分解为MapReduce或其他任务在集群上执行。
七、Hive 的表分类
(一)管理表(内部表)
- 默认创建就是管理表,管理表被删除时会将HDFS存储的数据删除。例如,创建一个管理表
employees
:
CREATE TABLE employees (
id INT,
name STRING,
salary FLOAT
) STORED AS ORC;
当删除这个表时,HDFS中存储该表数据的目录也会被删除。
(二)外部表
- 创建表时使用
external
关键字,如create external table 表名
。外部表被删除时,只会删除MySQL中对应的元数据信息,并不会删除HDFS上表中的数据。外部表可以防止误删除实际数据,推荐使用。如果外部表被删除,重写创建表,通过location
指向原本的数据目录可以在创建完表的情况下,直接查询表到中的数据。例如:
CREATE EXTERNAL TABLE external_employees (
id INT,
name STRING,
salary FLOAT
) LOCATION '/user/data/external_employees' STORED AS ORC;
删除该外部表时,HDFS上/user/data/external_employees
目录中的数据不会被删除。
(三)分区表
- Hive中的表对应为HDFS上的指定目录,在查询数据时候,默认会对全表进行扫描,这样时间和性能的消耗都非常大。分区为HDFS上表目录的子目录,数据按照分区存储在子目录中。如果查询的
where
字句中包含分区条件,则直接从该分区去查找,而不是扫描整个表目录,合理的分区设计可以极大提高查询速度和性能。通常,在管理大规模数据集的时候都需要进行分区,比如将日志文件按天进行分区,从而保证数据细粒度的划分,使得查询性能得到提升。 - 创建分区表语法:可以使用
PARTITIONED BY
子句创建分区表。表可以包含一个或多个分区列,如partitioned by (分区列类型,分区列类型..)
。 - 添加分区:
alter table 表名 add partition (分区字段=值)
。例如,为一个日志表log_table
添加一个分区dt=2023-10-10
:alter table log_table add partition (dt='2023-10-10')
。 - 删除分区:
alter table 表名 drop partition(分区字段=值)
,如alter table log_table drop partition (dt='2023-10-10')
。
(四)桶表
- 桶表就是对指定列进行哈希(hash)计算,然后会根据hash值进行切分数据,将具有不同hash值的数据写到每个桶对应的文件中。创建桶表语法:
CLUSTERED BY(分区字段) INTO 桶的数量 BUCKETS
。例如:
CREATE TABLE bucket_table (
id INT,
data STRING
) CLUSTERED BY (id) INTO 5 BUCKETS STORED AS ORC;
这会将bucket_table
表按照id
列进行哈希分桶,共分为5个桶,数据会根据id
的哈希值分配到不同的桶文件中。桶表可以用于数据抽样、高效连接等操作,例如在连接操作中,如果两个表按照相同的列进行分桶,并且桶的数量成倍数关系,可以提高连接效率。
八、将数据导入Hive的方式
(一)将文件数据导入Hive表中
- 非分区表
load data local inpath '文件的路径' overwrite into table 表
:从本地文件系统加载数据到Hive表中,会覆盖表中原有数据。例如,load data local inpath '/home/user/data.txt' overwrite into table my_table;
这里假设data.txt
是本地文件,包含了适合my_table
表结构的数据。执行该命令后,data.txt
中的数据将被加载到my_table
表中,并且如果my_table
表中原来有数据,会被新数据覆盖。load data inpath '文件的路径' overwrite into table 表
:从HDFS文件系统加载数据到Hive表中,也会覆盖表中原有数据。例如,load data inpath '/user/hive/data/hive_data.txt' overwrite into table my_table;
这里假设hive_data.txt
是HDFS上的文件,执行该命令后,hive_data.txt
中的数据将被加载到my_table
表中,同样会覆盖原有的数据。如果文件在HDFS上的路径不存在,可能会导致加载失败,需要确保文件路径的正确性。
(二)直接将查询结果放入新创建的表中(执行查询的创建)
create table[view] 表 as select 语句 where.. group by;
例如:
create table new_table as select id, name, sum(salary) as total_salary from employees group by id, name;
这会创建一个名为new_table
的新表,并将employees
表中按照id
和name
分组后计算的每个组的总工资插入到新表中。如果使用create view
,则会创建一个视图,而不是实际的表,视图是一个虚拟的表,它基于查询结果定义,可以像表一样进行查询,但不存储实际数据。例如:
create view employee_salary_view as select id, name, salary from employees where department = 'sales';
这里创建了一个名为employee_salary_view
的视图,它显示了sales
部门员工的id
、name
和salary
信息。视图可以方便地对数据进行筛选和展示,而不需要实际创建一个新的表来存储数据,当原始表employees
的数据发生变化时,视图中的数据也会相应更新。
(三)将查询结果导入已经存在表
insert into table 表名 select语句...
:将查询结果插入到已存在的表中,如果表中已有数据,会在原有数据基础上追加新数据。- 例如:
这里假设insert into table existing_table select id, name from new_employees;
existing_table
是已经存在的表,new_employees
是另一个表或查询结果集。执行该语句后,new_employees
表中的id
和name
列的数据将被插入到existing_table
表中,如果existing_table
表原来有数据,新数据会追加到原有数据之后。insert overwrite table 表名 select语句...
:会覆盖表中原有数据,将查询结果插入到表中。- 例如:
这次执行后,insert overwrite table existing_table select id, name from new_employees;
existing_table
表中原来的数据将被删除,然后用new_employees
表中的id
和name
列的数据替换。insert overwrite table 表名 partition(分区字段=值) select语句...
:将查询结果插入到指定分区的表中,如果分区不存在会自动创建分区(如果表是分区表),并覆盖该分区原有数据。- 例如:
这里假设insert overwrite table partition_table partition(dt='2023-10-11') select id, name from new_employees where date='2023-10-11';
partition_table
是一个分区表,按照dt
字段进行分区。执行该语句后,如果dt='2023 - 10 - 11'
这个分区不存在,会自动创建该分区,然后将new_employees
表中date='2023 - 10 - 11'
的数据插入到该分区中,并且覆盖该分区原来可能存在的数据。如果分区已经存在,只是数据会被新数据覆盖。
(四)将HDFS中已经存在文件导入新建的Hive表中
create table Xxx(
...
)row format delimited
fields terminated by ','
location 'hdfs的表数据对应的目录';
例如,假设在HDFS的/user/data/hive_table_data
目录下有一些以逗号分隔的文本数据文件,要创建一个Hive表来读取这些数据:
create table hive_table(
id INT,
name STRING,
age INT
)row format delimited
fields terminated by ','
location '/user/data/hive_table_data';
这样创建的hive_table
表会从指定的HDFS目录中读取数据,并按照逗号分隔符解析每行数据,将其映射到表的相应列中。在创建表时,需要确保指定的列类型(如INT
、STRING
等)与数据文件中的实际数据格式相匹配,否则可能会导致数据导入错误或解析失败。同时,location
指定的HDFS目录必须存在且有足够的权限让Hive读取数据。如果数据文件的格式不是逗号分隔,或者有其他特殊的格式要求,可以根据实际情况调整fields terminated by
等参数,或者使用更复杂的序列化和反序列化方式来处理数据。
(五)insert into 表名 values
insert into 表名(列名1,列名2,列名3) values(值1,值2,值3);
这种方式用于向表中插入单行数据。例如,向一个名为students
的表(包含id
、name
、grade
列)插入一条数据:
insert into students(id, name, grade) values(1, 'Tom', 'A');
需要注意的是,每次执行该语句只能插入一行数据。如果要插入多行数据,需要多次执行该语句。而且要确保插入的值的数据类型与表中列的数据类型相匹配,否则会出现数据类型不兼容的错误。另外,这种方式适用于手动插入少量数据的情况,如果要插入大量数据,使用其他更高效的数据导入方式(如前面提到的load data
等)会更加合适。
九、开窗函数
(一)开窗函数语法及分类
- 语法
函数名() over(partition by.. order by... rows between x and y)
- 其中
partition by
用于指定分区字段,按照该字段对数据进行分组;order by
用于指定排序字段,在每个分区内对数据进行排序;rows between x and y
用于定义窗口范围,x
和y
可以是unbounded preceding
(窗口起点为分区的第一行)、current row
(当前行)、unbounded following
(窗口终点为分区的最后一行)等,也可以是具体的行数偏移量,用于控制函数在窗口内的计算范围。
- 分类
- 排序开窗函数
row_number()
:根据某个字段分区,在分区内进行排序,然后增加序号列,序号列从1开始然后递增。例如,row_number() over(partition by department order by salary desc)
会在每个部门内按照工资降序排列,并为每个员工分配一个从1开始的序号。rank()
:与row_number()
类似,但当排序字段值相同时,rank()
会给相同值的行分配相同的序号,而后续的序号会跳过相应的数量。例如,如果有两个员工工资相同,它们的序号都是2,那么下一个不同工资的员工序号将是4。dense_rank()
:也在分区内排序并为行分配序号,当排序字段值相同时,序号是连续的。例如,相同工资的员工序号依次递增,不会跳过序号。
- 聚合开窗函数
sum()
、avg()
、max()
、min()
、count()
等。例如,sum(salary) over(partition by department order by hire_date) as running_total
会在每个部门内,按照入职日期排序,计算截至到当前行的员工工资总和,并将其作为running_total
列显示在结果中。聚合开窗函数在分区内对数据进行逐行聚合,order by
指定了聚合的顺序,不写order by
时默认的定位框架是对分区内的第一行到最后一行进行整体聚合,写了order by
则是对分区内的数据从第一行到当前行数据进行聚合。
- 其他开窗函数
first_value()
:获取当前分区的第一行的某列的值。例如,first_value(name) over(partition by class order by score desc)
会在每个班级内按照成绩降序排列,返回每个分区的第一个学生的名字。last_value()
:获取当前分区的最后一行的某列的值。lag()
:获取上一行中某列的值,如果当前行没有上一行默认返回null
。也可以指定获取上n
行中某列的值,并设置默认值,如lag(列名, n, 默认值)
。lead()
:与lag()
相反,作用是获取下一行某列的值。
- 排序开窗函数
(二)应用场景
- 数据分析和报表生成
- 在销售数据分析中,可以使用
row_number()
为每个销售订单按照时间顺序分配一个序号,方便查看订单的处理顺序。例如,select order_id, order_date, customer_id, row_number() over(order by order_date) as order_sequence from sales_orders
,可以清晰地看到订单的先后顺序,有助于分析销售趋势和订单处理效率。 - 使用聚合开窗函数可以计算累计销售额、累计用户数等指标。比如,在每月销售报表中,通过
sum(sales_amount) over(order by month)
可以计算出每个月的累计销售额,帮助企业了解销售业绩的增长趋势。
- 在销售数据分析中,可以使用
- 数据比较和差异分析
- 利用
lag()
和lead()
函数可以比较当前行与上一行或下一行的数据。例如,在股票价格分析中,通过select date, price, lag(price, 1) over(order by date) as previous_price from stock_prices
可以计算出每天股票价格与前一天价格的差异,用于分析价格波动情况。 - 在用户行为分析中,可以使用
first_value()
和last_value()
来获取用户在某个时间段内的首次和最后操作记录,以便了解用户的行为模式和路径。
- 利用
十、Hive SQL 执行的方式
- hive
hive
交互式执行sql
,开发人员可以在交互式命令行编写sql
执行。在命令行中输入hive
进入交互式环境后,就可以输入SQL
语句并立即得到执行结果,适合进行快速的数据查询和探索性分析。例如,查询employees
表中的所有数据:
hive> select * from employees;
- hive -e “sql”
- 本质上还是
hive
命令,不过是直接传递sql
执行,没有交互。这种方式适用于在脚本中执行特定的Hive SQL
语句,例如在一个bash
脚本中执行Hive SQL
查询并将结果输出到文件中:
hive -e "select * from employees where department='sales'" >> output.txt
- 本质上还是
- hive -f sql文件
- 将
hive sql
写入一个文件中,直接执行该文件中的sql
语句。例如,有一个名为query.hive
的文件,内容如下:
可以通过SELECT count(*) FROM table_name WHERE condition;
hive -f query.hive
来执行文件中的SQL
语句。这种方式适合执行一系列相关的SQL
操作,便于管理和维护复杂的查询逻辑。 - 将
- hiveserver2 + beeline
- 需要先启动
hiveserver2
服务,然后通过beeline
客户端命令连接上hiveserver2
然后执行sql
。hiveserver2
是一个允许远程客户端连接到Hive
的服务,beeline
是一个用于连接hiveserver2
的客户端工具。例如,启动hiveserver2
后,在另一个终端中输入beeline -u jdbc:hive2://localhost:10000 -n username -p password
(其中localhost
是服务器地址,10000
是默认端口,username
和password
是登录凭据)连接到hiveserver2
,然后可以在beeline
命令行中执行Hive SQL
语句,这种方式适用于在分布式环境中多个客户端远程连接到Hive
进行查询和操作。
- 需要先启动
十一、Hive 的自定义函数
(一)函数类型及定义
- Hive可以自定义单行函数、聚合函数、炸裂函数。
- 定义单行函数,通过继承
UDF
,重写evaluate
方法。例如,创建一个自定义的字符串拼接函数:
import org.apache.hadoop.hive.ql.exec.UDF; import org.apache.hadoop.io.Text; public class CustomConcatUDF extends UDF { public Text evaluate(Text str1, Text str2) { if (str1 == null || str2 == null) { return null; } return new Text(str1.toString() + str2.toString()); } }
- 定义炸裂函数,继承
GenericUDTF
,重写process
方法。例如,创建一个将字符串拆分为单个字符的炸裂函数:
import org.apache.hadoop.hive.ql.exec.UDFArgumentException; import org.apache.hadoop.hive.ql.metadata.HiveException; import org.apache.hadoop.hive.ql.udf.generic.GenericUDTF; import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory; import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory; public class StringExplodeUDTF extends GenericUDTF { @Override public StructObjectInspector initialize(ObjectInspector[] args) throws UDFArgumentException { if (args.length!= 1 ||!(args[0].getCategory() == ObjectInspector.Category.PRIMITIVE && ((PrimitiveObjectInspector) args[0]).getPrimitiveCategory() == PrimitiveObjectInspector.PrimitiveCategory.STRING)) { throw new UDFArgumentException("StringExplodeUDTF expects a single string argument."); } StructObjectInspector soi = ObjectInspectorFactory.getStandardStructObjectInspector( new String[]{ "char"}, new ObjectInspector[]{ PrimitiveObjectInspectorFactory.writableStringObjectInspector} ); return soi; } @Override public void process(Object[] args) throws HiveException { String input = args[0].toString(); for (char c : input.toCharArray()) { forward(new Object[]{ String.valueOf(c)}); } } @Override public void close() throws HiveException { // No need to do anything here for this simple example } }
- 定义单行函数,通过继承
- 将项目打成
jar
包(mvn clean package
),上传到linux
服务器,如果是定义永久函数需要上传到HDFS
。 - 通过
create function 函数名
的方式创建函数即可。例如,创建上面的自定义字符串拼接函数:add jar /path/to/custom_function.jar; create function custom_concat as 'CustomConcatUDF';
(二)应用场景及临时函数与永久函数的区别
- 应用场景
- 单行函数生成随机数,函数的参数是生成随机数的最大值,例如:函数名(10),作用是生成1 - 10的随机数。可以用于数据模拟、测试等场景,比如在生成测试数据时,为每个记录生成一个随机的数值在指定范围内。
- 表中存储用户开会员的开始时间和结束时间,统计每个月的会员人数。通过自定义炸裂函数将一个用户开启会员的时间进行炸裂,得到用户,会员月份。例如 1001 2022 - 09 - 05 2023 - 09 - 05。生成炸裂结果就是1001 2022 - 09、1001 2022 - 10、… 。这样可以方便地对每个月的会员情况进行统计和分析,用于会员活跃度分析、业务决策等。
- 临时函数与永久函数的区别
- 临时函数:只在当前会话中有效,当会话结束时,函数定义失效。通常在交互式查询或临时的数据分析任务中使用,方便快速定义和使用一些特定的函数,而不需要在全局范围内进行管理。例如,在一个
hive
交互式会话中定义了一个临时函数来处理当前查询中的特定数据转换,会话结束后,该函数不能在其他会话中使用。 - 永久函数:定义后在整个
Hive
环境中都可用,所有用户都可以使用该函数,除非被删除。永久函数需要将函数的jar
包上传到HDFS
(如果是在分布式环境中),并且在Hive
中进行注册。适用于在多个不同的查询和任务中频繁使用的自定义函数,方便统一管理和维护函数定义,提高代码的可复用性。例如,一个企业内部定义了一个用于数据清洗的永久函数,在多个不同的数据分析项目和日常数据处理任务中都可以使用该函数。
- 临时函数:只在当前会话中有效,当会话结束时,函数定义失效。通常在交互式查询或临时的数据分析任务中使用,方便快速定义和使用一些特定的函数,而不需要在全局范围内进行管理。例如,在一个
十二、Hive 优化
(一)开启本地模式
默认本地模式是关闭的,开启之后默认情况下当输入的数据量小于128M且文件个数小于4时,Hive可以通过本地模式在单台机器上处理所有的任务。对于小数据集,执行时间可以明显被缩短。例如,对于一些简单的查询或测试任务,数据量较小,开启本地模式可以快速得到结果,而不需要启动分布式的MapReduce
任务,减少了任务启动和调度的开销。
(二)JVM 重用
默认一个JVM
上最多可以顺序执行的task
数目(属于同一个Job
)是1。也就是说一个task
启一个JVM
,这样JVM
的效率就比较低,需要JVM
重用。JVM
重用就是一个虚拟机开启以后,执行多个task
任务,再关闭。大大提高执行效率。例如,在一个包含多个MapReduce
任务的Hive
作业中,如果每个任务都单独启动一个JVM
,会有大量的JVM
启动和关闭开销,而通过JVM
重用,可以减少这种开销,提高整体作业的执行速度。
(三)设置并行执行
hive
会将一个查询转化成一个或者多个阶段(Stage
)。默认情况下,Hive
一次只会执行一个阶段。不过,某个特定的job
可能包含众多的阶段,而这些阶段可能并非完全互相依赖的,也就是说有些阶段是可以并行执行的,这样可能使得整个job
的执行时间缩短。例如,一个复杂的数据分析任务包含数据读取、数据转换和数据聚合等多个阶段,如果数据转换和数据聚合阶段之间没有依赖关系,可以设置它们并行执行,从而提高整体的执行效率。
(四)开启hive
严格模式
- 对分区表查询必须使用分区过滤,这可以避免全表扫描,提高查询效率。例如,如果有一个分区表按照日期分区,查询时如果不指定日期分区条件,在严格模式下会报错,强制用户使用分区过滤来减少数据读取量。
- 不允许出现笛卡尔积查询,因为笛卡尔积查询通常会产生大量的数据,可能导致性能问题和资源浪费,在严格模式下可以避免意外的笛卡尔积查询。
- 使用
order by
的时候,需要使用limit
,这是为了防止order by
对全量数据进行排序时可能导致的内存溢出等问题,通过limit
限制排序的数据量,提高查询的安全性和性能。
(五)通过mapjoin提高大表和小表的join执行速度、通过桶表的join解决大表和大表的join
- mapjoin原理及优势:
MapJoin
是小表缓存并发送到各个节点,直接在MapTask
阶段完成join
操作,没有Shuffle
的过程。小表缓存并发送到各个节点,以map
的形式存储,key
是连接条件,value
是其他值。MapTask
任务执行的时候只需要根据split
逻辑切片读取大表数据,读取大表的数据,按照列分隔符拆分,获取到连接条件,作为key
从map
集合中获取可以连接的value
,然后整合成一条结果输出。- 其优势主要体现在以下几点:
- 执行速度快:避免了
Shuffle
阶段带来的网络传输和数据排序等开销,大大提高了join
操作的速度。特别是对于大表和小表连接的场景,效果显著。例如,在处理大规模的用户行为数据和一个相对较小的用户属性表连接时,MapJoin
可以快速完成连接操作,减少执行时间。 - 内存利用高效:由于小表可以完全放入内存进行缓存,相比于在
Reduce
阶段进行连接操作,减少了对磁盘的频繁读写,提高了数据处理效率。但需要注意小表的大小不能超过内存的限制,否则可能会导致内存溢出问题。 - 简化数据处理流程:不需要复杂的
Shuffle
和Reduce
过程配置,降低了任务的复杂性和出错的可能性。对于开发人员来说,使用MapJoin
可以更方便地实现大表和小表的连接操作,提高开发效率。
- 执行速度快:避免了
- 通过桶表的join解决大表和大表的join(
Sort Merge Bucket Join
-SMBJoin
)- 原理:
- 首先了解一下分桶,分桶相对于分区来说是更加细粒度的数据组织方式。分桶会将一个分区下的数据或者未分区全表数据按照分桶字段求其
hash
值然后对分桶数量取余决定其所在的桶,那么分区下的数据文件数或者全表文件数就是分桶的个数。分桶还有一个重要的条件就是对分桶字段进行排序,每一个桶里面的数据按照分桶字段进行升序或者降序组织,那么其整体就是一个并归排序操作。 - 大表关联大表时,使用分而治之思想 +
map join
。对关联两表做成以join
条件为分桶字段的表,并按照同样的排序方式组织分桶数据,两表的分桶个数必须成整数倍数关系。在表数据关联的时候,将小表的数据加载到内存中,开启大表桶个数的map
任务,并且将小表桶数据加载到与大表对应桶位置对应(相同或者成倍数关系)的map
任务的内存中去,然后以map join
的方式执行。
- 首先了解一下分桶,分桶相对于分区来说是更加细粒度的数据组织方式。分桶会将一个分区下的数据或者未分区全表数据按照分桶字段求其
- 优势:
- 高效处理大表连接:适用于大表和大表连接的场景,尤其是在数据量巨大且可能存在数据倾斜的情况下。通过分桶和
map join
的结合,将大表连接转化为多个小的map join
操作,规避了Shuffle
操作带来的性能问题,减少了数据倾斜的影响,提高了连接效率。 - 优化数据读取和处理:由于数据按照桶进行组织和排序,在连接时可以更快速地定位和匹配相关数据,减少不必要的数据扫描和处理。例如,在进行两个大规模的交易表连接时,按照交易日期分桶后进行
SMBJoin
,可以快速找到对应日期桶中的数据进行连接,提高查询性能。 - 提高资源利用率:通过合理设置分桶个数和大小,可以更好地利用集群资源,避免某些节点负载过高或过低的情况。同时,将小表数据加载到内存中进行
map join
,可以充分利用内存资源,加快数据处理速度。
- 高效处理大表连接:适用于大表和大表连接的场景,尤其是在数据量巨大且可能存在数据倾斜的情况下。通过分桶和
- 原理:
(六)列裁剪与分区裁剪
- 列裁剪
sql
语句在进行查询的时候不要全部查询,使用哪个字段就查询哪个字段,不要动不动就*
。例如,只需要查询employees
表中的id
和name
字段时,使用select id, name from employees
而不是select * from employees
。这样做的好处是:- 减少数据读取量:对于大规模的表,只读取需要的列可以显著减少从磁盘或存储系统中读取的数据量。如果一个表有很多列,但查询只需要其中的几列,读取所有列会浪费大量的I/O资源和网络带宽,而列裁剪可以避免这种不必要的读取。
- 提高查询性能:减少了数据在网络和内存中的传输量,以及查询处理过程中的数据处理量。这可以加快查询的执行速度,特别是在处理海量数据时,效果更为明显。例如,在一个数据仓库中,对一个包含数百列的事实表进行查询,如果只选择需要的列进行查询,能够大大提高查询的响应时间,提升用户体验。
- 节省存储资源:在一些情况下,尤其是对于存储资源有限的环境,减少不必要列的读取和处理可以间接节省存储资源。因为不需要将所有列的数据加载到内存中进行处理,从而可以释放更多的内存空间用于其他操作。
- 分区裁剪
- 需要哪个分区就查询哪个分区,不要全查。比如一个按日期分区的日志表,只查询某一天的日志数据时,使用分区条件进行筛选,如
select * from log_table where partition_date='2023-10-10'
,而不是查询整个表的所有分区数据。这样做的优势在于:- 精准数据定位:能够快速准确地定位到需要查询的数据所在的分区,避免扫描不必要的分区数据。对于按日期、地区等维度进行分区的表,通过分区裁剪可以直接找到符合条件的分区,大大减少了数据扫描范围。例如,在一个按月份分区的销售数据表中,查询某个月的销售数据时,只需要扫描对应月份的分区,而不需要扫描其他月份的分区,提高了查询效率。
- 提高查询性能:减少了数据读取量和处理时间,因为只需要处理特定分区的数据。这对于大规模数据集尤其重要,能够显著缩短查询执行时间。同时,也减少了对存储系统的I/O压力,提高了整个系统的性能。
- 方便数据管理和维护:在数据管理方面,分区裁剪使得数据的管理更加灵活和高效。可以根据业务需求对不同分区的数据进行独立的管理、备份、删除等操作。例如,对于过期的分区数据,可以方便地进行清理,而不影响其他有用分区的数据。
- 需要哪个分区就查询哪个分区,不要全查。比如一个按日期分区的日志表,只查询某一天的日志数据时,使用分区条件进行筛选,如
(七)避免数据倾斜
- 阐述什么是数据倾斜
- 在分布式计算场景下,大量的数据集中在某一个节点而导致一个任务的执行时间变长。大数据组件处理海量数据不患多,而患不均。例如,在一个
group by
操作中,如果某个键的值在数据集中出现的频率非常高,那么在进行分组和聚合时,这个键对应的任务可能会处理大量的数据,而其他键对应的任务处理的数据量相对较少,导致整个作业的执行时间取决于这个处理大量数据的任务,从而出现数据倾斜现象。数据倾斜可能发生在MapReduce
的Map
阶段或Reduce
阶段,在Reduce
阶段更为常见。例如,在对用户行为数据按照用户ID进行分组统计时,如果某些用户的行为数据量远远大于其他用户,那么在Reduce
阶段处理这些大量数据的任务就会成为性能瓶颈,导致整个作业执行时间延长。
- 在分布式计算场景下,大量的数据集中在某一个节点而导致一个任务的执行时间变长。大数据组件处理海量数据不患多,而患不均。例如,在一个
- 怎么发现任务出现了数据倾斜现象
- 在
yarn
上可以查看task
的执行情况,如果一个阶段中有些task
很快执行完了,有些task
迟迟无法结束或者运行时间减少,则大概率出现了数据倾斜的现象。例如,在Hive
任务的yarn
监控界面中,可以看到每个MapReduce
任务的执行进度和时间,如果发现某些任务的进度明显落后于其他任务,并且持续很长时间没有进展,就可能是数据倾斜导致的。此外,还可以通过查看任务的日志信息,可能会有关于数据倾斜的提示或错误信息,例如某个任务处理的数据量过大、内存不足等。还可以通过一些性能监控工具或指标来判断,如任务的CPU使用率、内存使用率等,如果某个任务的资源使用率异常高,也可能是因为数据倾斜导致的。
- 在
- 描述1个数据倾斜的情景及解决方案
- 情景1:
select count(distinct user_id) from t_user;
- 解决方案:
- 设置提高
reduceTask
的个数,例如set mapreduce.job.reduces = 10
(可以根据数据量和集群资源适当调整),这样可以将数据分散到更多的reduce
任务中进行处理,减轻单个任务处理的数据量。增加reduceTask
的个数可以使数据在reduce
阶段更均匀地分布,避免某个任务处理过多的数据。但要注意不要设置过多的reduceTask
,以免导致任务启动和调度开销过大。 select count(*) from(select sex from t_person group by sex) t1;
可以先对数据进行一些预处理,比如按照某个字段(这里是sex
)进行分组后再进行计数,可能会减少数据倾斜的影响。通过这种预处理方式,可以将数据按照一定的规则进行分组,使得每个组的数据量相对较为均匀,然后再进行计数操作,减少数据倾斜对最终结果的影响。
- 设置提高
- 解决方案:
- 情景2:在
group by
分组的时候,某个key
过多。- 解决方案:将
key
打散,给key
增加随机前缀,分组之后,再将前缀去掉然后再分组。例如,对于一个用户行为表按照用户id
进行group by
时,如果某些用户的行为数据量非常大导致倾斜,可以给用户id
添加一个随机前缀,如random_prefix_user_id
,进行分组和聚合操作后,再去掉前缀得到最终结果。这样可以将数据随机分散到不同的reduce
任务中,避免某个reduce
任务处理过多的数据。同时,也可以结合使用其他优化策略,如增加reduceTask
的个数等,进一步提高处理效率。
- 解决方案:将
- 情况3:在
join
表连接的时候可能出现数据倾斜。- 解决方案:
mapjoin
:对于大表和小表连接,当小表足够小可以放入内存时,使用mapjoin
将小表缓存到内存中,在map
阶段直接与大表进行连接,避免reduce
阶段的数据倾斜。例如,select /*+ MAPJOIN(small_table) */ * from big_table join small_table on big_table.key = small_table.key;
在查询语句中通过/*+ MAPJOIN(small_table) */
提示Hive
使用mapjoin
。这种方式适用于大表和小表连接的场景,能够有效避免reduce
阶段因数据分布不均匀导致的倾斜问题,提高连接效率。- 大表打散、小表扩容:对大表的连接键进行哈希分区,将数据分散到多个
reduce
任务中,同时对小表进行复制或扩展,使其与大表的分区数相匹配,然后进行连接。例如,对大表按照连接键hash(key) % num_partitions
进行分区,小表复制num_partitions
份,分别与大表的各个分区进行连接。这样可以使数据在reduce
阶段更均匀地分布,减少倾斜。通过这种方式,可以将大表和小表的数据进行更合理的划分和匹配,提高连接的性能和稳定性。 smbjoin
(Sort Merge Bucket Join
):若关联量表的数据量都很大,使用分而治之的思想。首先对关联两表做成以join
条件为分桶字段的表,并按照同样的排序方式组织分桶数据,两表的分桶个数必须成整数倍数关系。在表数据关联的时候,将小表的数据加载到内存中,开启大表桶个数的map
任务,并且将小表桶数据加载到与大表对应桶位置对应(相同或者成倍数关系)的map
任务的内存中去,然后以map join
的方式执行。这种方式可以将大表和大表的连接转换为多个小的map join
操作,规避shuffle
操作,减少数据倾斜和提高连接效率。例如,对于两个大规模的交易表,按照交易ID进行分桶后使用SMBJoin
,可以有效地处理大表连接时的数据倾斜问题,提高查询性能。
- 解决方案:
- 情景1:
十三、Hive 文件存储格式
- 分类及特点
- Hive的存储格式分为两大类:一类纯文本文件,一类是二进制文件存储。
- Hive支持的存储数据的格式主要有:
TEXTFILE
、SEQUENCEFILE
、ORC
、PARQUET
。 TEXTFILE
和SEQUENCEFILE
的存储格式都是基于行存储的;ORC
和PARQUET
是基于列式存储的。
- 各格式特点及适用场景
TEXTFILE
(文本文件)- 特点:适用于存储结构简单的数据,易于读写和处理。文件未经过压缩,存储空间占用较大。数据以文本形式存储,每行记录是一个完整的字符串,字段之间通常使用分隔符(如逗号、制表符等)分隔。
- 适用场景:当数据需要被外部工具或人员直接查看和编辑时较为方便,适合小规模数据或对数据存储格式要求不高的场景,例如一些临时数据文件或需要快速验证数据内容的情况。
SEQUENCEFILE
(序列文件)- 特点:适用于大规模数据的读写操作,提供高压缩率和高性能。将数据以键值对的形式存储,可以分割和合并,方便并行处理。它支持压缩,如
Snappy
、Gzip
等压缩算法,可以有效减少存储空间和网络传输开销。 - 适用场景:在大规模数据处理中,需要高效存储和读取数据,并且对数据的分割和合并操作有需求的场景,如日志数据的存储和处理,以及需要对数据进行分布式并行处理的任务。
- 特点:适用于大规模数据的读写操作,提供高压缩率和高性能。将数据以键值对的形式存储,可以分割和合并,方便并行处理。它支持压缩,如
ORC
文件(Optimized Row Columnar
)- 特点:适用于大规模数据的读取和查询操作,提供更高的压缩率和查询性能。对数据进行了更高效的编码和压缩,支持复杂的数据类型和嵌套结构。它采用了列式存储和索引等技术,能够快速读取和处理大规模数据集中的特定列,并且在查询时可以只读取需要的列,减少不必要的数据读取。
- 适用场景:对于数据仓库中的大规模结构化数据,特别是需要频繁进行查询和分析的场景,能够显著提高查询性能和存储效率。例如,企业的财务数据、销售数据等分析场景。
PARQUET
文件- 特点:适用于大规模数据的读取和查询操作,提供更高的压缩率和列式存储优势。与
ORC
类似,按列存储数据,能够快速读取和处理大规模数据集中的特定列。它具有良好的兼容性和可扩展性,被广泛应用于各种大数据处理框架和工具中。 - 适用场景:在大数据分析和处理中,需要高效存储和查询大规模结构化数据的场景,尤其在与其他大数据工具(如
Hive
、Spark
等)集成的项目中,PARQUET
文件格式是一个常用的选择。例如,在数据湖项目中,用于存储和分析各种结构化数据。
- 特点:适用于大规模数据的读取和查询操作,提供更高的压缩率和列式存储优势。与
十四、Hive 序列化
- 概念解释
- Hive的序列化和反序列化指:
select
过程读取磁盘上的数据,创建row
对象,这种从磁盘读取文件并转换为对象的过程称之为反序列化,底层用到了InputFormat
。insert
过程将内存中的row
对象,存储为磁盘上的数据,这个过程是序列化的过程,底层用到了OutputFormat
。
- Hive的序列化和反序列化指:
- 具体的序列化方式
- 默认分隔符(\001)、自定义分隔符
row format delimited
:用于指定行格式的分隔方式。fields terminated by ','
:本质是LazySimpleSerde
,表示字段之间使用逗号分隔。例如,创建一个表时指定字段分隔符为逗号:create table my_table (id int, name string) row format delimited fields terminated by ',';
csv
分隔符(逗号)row format serde 'org.apache.hadoop.hive.serde2.OpenCSVSerde';
:使用OpenCSVSerde
来处理csv
格式的数据,逗号是默认的分隔符。这种方式适用于处理标准的csv
文件,它可以正确解析和处理csv
文件中的字段和记录。
json
分隔符row format serde 'org.apache.hadoop.hive.serde2.JsonSerDe';
:使用JsonSerDe
来处理json
格式的数据。对于包含json
数据的文件或字段,使用这个序列化器可以方便地将json
数据解析到Hive
表的列中,或者将Hive
表中的数据序列化为json
格式输出。
- 正则表达式分隔符(分隔符 || 、数据中包含分隔符 张三,20,我喜欢,篮球)
row format serde 'org.apache.hadoop.hive.serde2.RegexSerDe' with serdeproperties ('input.regex' = '^([^,]+),([^,]+),([^,]+,?[^,]+),(.+)$');
:使用RegexSerDe
并通过serdeproperties
指定正则表达式来解析数据。这里的正则表达式用于匹配和提取数据中的不同部分,将其映射到表的列中。例如,对于上面的数据“张三,20,我喜欢,篮球”,正则表达式可以将其解析为四个部分,分别对应表中的四个列。这种方式适用于数据格式不固定或需要复杂解析的情况,但需要编写合适的正则表达式来准确提取数据。
- 默认分隔符(\001)、自定义分隔符
十五、Hive 中maptask
和reducetask
的数量
maptask
个数的确定maptask
个数取决于split
逻辑切片的个数,在mapreduce
中,split
逻辑切片和表中文件大小和文件个数、blockSize
等参数有关。- Hive底层读取表默认使用的是
CombineHiveInputFormat
,会对小文件进行合并。 set mapreduce.input.fileinputformat.split.maxsize=100;
(这里的100
可以根据实际情况调整),CombineHiveInputFormat
底层逻辑切片计算的过程如下:- 如果文件大小大于
split.maxsize
的2倍,则形成一个独立的逻辑切片。例如,一个文件大小为200MB
,split.maxsize
为100MB
,那么这个文件会形成一个独立的逻辑切片。 - 如果文件大小大于
split.maxsize
,但是小于split.maxsize
的2倍,则拆分成两个平均大小的虚拟存储。比如一个文件大小为150MB
,会拆分成两个75MB
左右的虚拟存储。 - 如果文件大小小于
split.maxsize
,则形成一个虚拟存储。例如一个文件大小为50MB
,会形成一个虚拟存储。 - 判断虚拟存储的文件大小是否大于
split.MaxSize
,如果大于等于则形成一个逻辑切片,如果虚拟存储的文件大小小于split.maxsize
,则和下一个虚拟文件进行合并,直到大于split.maxsize
,共同形成一个切片。例如,有三个文件分别大小为30MB
、40MB
、50MB
,首先它们都会形成虚拟存储,然后第一个和第二个虚拟存储合并(因为30MB + 40MB < 100MB
),再和第三个虚拟存储合并(因为30MB + 40MB + 50MB > 100MB
),最终形成一个逻辑切片。
- 如果文件大小大于
reducetask
个数的确定set mapreduce.job.reduces=-1;
N = min(参数2,总输入数据量/参数1)
,其中参数1:set hive.exec.reducers.bytes.per.reducer=256000000
(表示每个reducer
处理的数据字节数),参数2:set hive.exec.reducers.max=1009
(表示reducer
的最大数量)。例如,如果总输入数据量为500MB
,hive.exec.reducers.bytes.per.reducer
为256MB
,那么理论上需要的reducer
个数为500MB / 256MB ≈ 2
(向上取整),但如果hive.exec.reducers.max
设置为10
,那么最终reducer
的个数为2(取min(2, 10)
)。
set mapreduce.job.reduces=3
:直接指定reducer
的个数为3,这种方式适用于对reducer
个数有明确要求的情况,比如已知数据分布和集群资源情况,希望固定reducer
的数量来控制任务的并行度和输出文件的数量等。
十六、Hive中的mapjoin、commonjoin、smbjoin的特点与区别
MapJoin原理及特点
- 原理:
- 小表缓存并发送到各个节点,直接在
MapTask
阶段完成join
操作,没有Shuffle
的过程。小表以map
的形式存储,key
是连接条件,value
是其他值。MapTask
任务执行时根据split
逻辑切片读取大表数据,读取大表的数据后,按照列分隔符拆分,获取到连接条件,作为key
从map
集合中获取可以连接的value
,然后整合成一条结果输出。
- 小表缓存并发送到各个节点,直接在
- 特点:
- 执行速度快:因为避免了
Shuffle
阶段,减少了网络传输和数据排序等开销。例如,在处理大量数据的连接操作时,MapJoin
可以显著缩短执行时间,特别是当小表与大表连接且小表数据量相对较小时,优势更为明显。 - 适用于小表和大表连接的场景,当小表足够小可以完全放入内存时效果最佳:但如果小表过大,可能导致内存不足问题。所以在使用
MapJoin
时,需要对小表的大小进行评估和控制。例如,对于一个包含少量产品信息的小表和一个大规模的销售记录大表进行连接,MapJoin
可以快速完成连接操作,提高查询效率。 - 对内存有一定要求:因为需要将小表缓存到内存中,如果小表过大可能导致内存不足问题。在实际应用中,需要根据集群的内存资源情况合理选择是否使用
MapJoin
以及合适的小表大小。可以通过调整内存参数等方式来优化MapJoin
的性能,但也要注意不要超出内存限制导致任务失败。
- 执行速度快:因为避免了
CommonJoin原理及特点
- 原理:
- 在
reduce
阶段的join
,底层会产生shuffle
。MapTask
根据split
逻辑切片读取A
表和B
表,因为MapTask
读取的都是局部数据,所以无法在MapTask
直接产生join
。MapTask
底层map
方法将连接条件作为key
,其他值作为value
,这样A
表和B
表需要连接在一起的数据,通过shuffle
会进入同一个reduceTask
中(此处需要知道就是key
相同,hash
值相同,和reduceTask
个数求余结果相同,这样相同key
值它们底层计算的分区号就相同,所以会进入同一个reduceTask
),因为在MapTask
阶段对A
表和B
表的数据进行了不同的标记,所以可以根据标记进行区分并且将A
表和B
表的数据join
在一起输出。
- 在
- 特点:
- 适用于各种大小表的连接场景,相对比较通用:它不需要像
MapJoin
那样对小表的大小有严格限制,可以处理不同规模的表连接。例如,在一些数据处理场景中,可能无法事先确定表的大小关系,或者需要连接的表大小较为接近,此时CommonJoin
可以作为一种较为稳妥的选择。 - 由于涉及Shuffle过程,在数据量较大且分布不均匀时可能会出现数据倾斜问题,导致某些
reduceTask
执行时间过长:当连接键的值在数据集中分布不均匀时,可能会导致某些reduceTask
处理的数据量远远大于其他reduceTask
,从而影响整体性能。例如,在对用户行为数据按照用户ID进行连接时,如果某些用户的行为数据量特别大,就可能出现数据倾斜。为了解决这个问题,可能需要采取一些数据倾斜处理措施,如调整reduceTask
的数量、对数据进行预处理等。 - 对于内存的要求相对MapJoin较低,因为不需要将整个小表缓存到内存中:它主要依赖
Shuffle
过程在reduce
阶段进行数据的连接操作,对内存的压力相对较小。但在处理大规模数据时,仍然需要考虑Shuffle
过程中的网络传输和reduce
阶段的资源消耗等问题,可能需要对相关参数进行优化配置,以提高性能。
- 适用于各种大小表的连接场景,相对比较通用:它不需要像
smbJoin原理及特点
- 原理:
Sort Merge Bucket Join
(分桶表Join
)。首先了解一下分桶,分桶相对于分区来说是更加细粒度的数据组织方式,分桶会将一个分区下的数据或者未分区全表数据按照分桶字段求其hash
值然后对分桶数量取余决定其所在的桶,那么分区下的数据文件数或者全表文件数就是分桶的个数。分桶还有一个重要的条件就是对分桶字段进行排序,每一个桶里面的数据按照分桶字段进行升序或者降序组织,那么其整体就是一个并归排序操作。大表关联大表时,使用分而治之思想 +map join
,对关联两表做成以join
条件为分桶字段的表,并且按照同样的排序方式组织分桶数据,两表的分桶个数必须成整数倍数关系。在表数据关联的时候,将小表的数据加载到内存中,开启大表桶个数的map
任务,并且将小表桶数据加载到与大表对应桶位置对应(相同或者成倍数关系)的map
任务的内存中去,然后以map join
的方式执行。
- 特点:
- 适用于大表和大表连接的场景,尤其是在数据量巨大且可能存在数据倾斜的情况下:通过分桶和
map join
的结合,能够有效地处理大表连接时的数据倾斜问题,提高连接效率。例如,在处理两个大规模的交易表连接时,smbJoin
可以根据交易ID等连接条件进行分桶,将数据合理划分到不同的桶中,然后进行连接操作,避免了数据倾斜对性能的影响。 - 通过分桶和map join的结合,将大表连接转化为多个小的
map join
操作,规避shuffle
操作:减少了数据在网络上的传输和排序开销,进一步提高了性能。每个小的map join
操作可以在本地节点上快速完成,然后将结果进行合并。这种方式充分利用了分桶和map join
的优势,适用于对性能要求较高的大表连接场景。 - 在数据处理过程中,需要对表进行合理的分桶设计和配置:包括确定合适的分桶字段、分桶数量以及保证两表的分桶个数成整数倍数关系等。这需要对数据的特点和分布有一定的了解,并且可能需要进行一些实验和调优来找到最佳的配置。同时,在将小表数据加载到内存时,也需要注意内存的使用情况,避免内存溢出等问题。
- 适用于大表和大表连接的场景,尤其是在数据量巨大且可能存在数据倾斜的情况下:通过分桶和
综上所述,mapjoin
适用于小表和大表连接且对内存有一定容忍度的场景,以换取快速的执行速度;commonjoin
较为通用,但在数据量大且分布不均时可能面临数据倾斜问题;smbjoin
则专门针对大表和大表连接且可能存在数据倾斜的情况进行了优化,通过分桶和map join
的巧妙结合,提高了连接性能和处理效率。在实际使用中,需要根据具体的数据情况和业务需求选择合适的连接方式。
十七、Hive 中的压缩
(一)Hive 在不同阶段的压缩
- 在 map 阶段压缩
map
阶段的设置,就是在MapReduce
的shuffle
阶段对mapper
产生的中间结果数据压缩。在这个阶段,优先选择一个低CPU
开销的算法。因为map
阶段要将数据传递给reduce
阶段,使用压缩可以提高传输效率。例如,可以使用Snappy
或LZO
等压缩算法,它们在提供较好压缩比的同时,对CPU
的性能影响相对较小。可以通过设置相关参数来启用map
阶段的压缩,如mapreduce.map.output.compress=true
(开启压缩)和mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec
(指定压缩算法为Snappy
)。
- 单条 SQL 语句的中间结果进行压缩
- 单条
SQL
语句的中间结果是指,两个MR
(一条SQL
语句可能需要通过多个MR
进行计算)之间的临时数据。对这些中间结果进行压缩可以减少磁盘读写和网络传输的开销,提高查询性能。可以在Hive
的配置文件中设置相关参数来启用中间结果的压缩,例如hive.exec.compress.intermediate=true
(开启中间结果压缩)和hive.intermediate.compression.codec=org.apache.hadoop.io.compress.GzipCodec
(指定压缩算法为Gzip
)。不同的压缩算法有不同的特点,Gzip
压缩比高但压缩和解压缩速度相对较慢,适合对中间结果进行长期存储或对网络带宽要求较高的场景;而Snappy
等算法压缩和解压缩速度快,适合对中间结果进行快速处理的场景。
- 单条
- 在 reduce 阶段的压缩
- 在
reduce
阶段对输出结果进行压缩可以减少存储占用空间和网络传输的数据量。可以使用与map
阶段类似的压缩算法和参数设置,如mapreduce.reduce.output.compress=true
(开启压缩)和mapreduce.reduce.output.compress.codec=org.apache.hadoop.io.compress.BZip2Codec
(指定压缩算法为BZip2
)。BZip2
算法在压缩比和压缩质量上有较好的表现,但压缩速度相对较慢,适合对存储占用空间要求较高且对压缩时间不太敏感的场景。在实际应用中,需要根据数据特点、集群资源和性能要求等因素选择合适的压缩算法和参数设置,以平衡压缩效果和计算资源消耗。
- 在
十八、Hive 如何导出数据
(一)使用 ETL 工具
- 可以使用
datax
、sqoop
等ETL
工具将hive
表数据导出到关系型数据库。例如,使用sqoop
可以将Hive
表中的数据导出到MySQL
数据库中,命令示例如下:
sqoop export --connect jdbc:mysql://localhost:3306/mydb --username myuser --password mypassword --table destination_table --export-dir /user/hive/warehouse/source_table
这里将Hive
中/user/hive/warehouse/source_table
表的数据导出到MySQL
数据库的destination_table
表中,需要指定MySQL
的连接信息、用户名和密码等。datax
也类似,通过配置相应的任务文件,可以实现从Hive
到各种数据源(包括关系型数据库、文件系统等)的数据导出。
(二)导出数据到本地目录
insert overwrite local directory '/root/out/00' select * from t_user;
:将Hive
表t_user
中的数据导出到本地目录/root/out/00
下,覆盖本地目录中已有的文件。如果本地目录不存在,会自动创建。hive -e "select * from test_hive.t_user" >> /root/out/a.txt
:通过hive -e
执行SQL
语句,将查询结果追加到本地文件/root/out/a.txt
中。这种方式适合在脚本中使用,可以将Hive
查询结果与其他本地文件处理操作结合起来。
(三)导出到 HDFS 的目录下
insert overwrite directory '/root/out/00' select * from t_user;
:将Hive
表t_user
中的数据导出到HDFS
目录/root/out/00
下,覆盖HDFS
目录中已有的文件。这个目录是在HDFS
文件系统中的路径,需要有相应的权限才能进行写入操作。- 注意事项:
- 在导出数据到
HDFS
目录时,要确保目标目录存在且有足够的权限。如果目录不存在,可以先使用hadoop fs -mkdir
命令创建目录。 - 对于大规模数据的导出,要考虑
HDFS
的存储容量和网络带宽等因素,避免因数据量过大导致导出过程出现问题。同时,可以根据实际需求选择合适的压缩格式来减少数据存储空间和传输时间,例如在导出时可以使用orc
或parquet
等压缩格式的表,并在导出语句中指定相应的压缩选项(如果支持的话)。
- 在导出数据到
十九、Hive SQL 的行转列和列转行(适用于 MySQL)
(一)示例表及数据
假设有以下t1
表和t2
表:
t1
表(原始数据,行式存储)- 表结构:
姓名 科目 成绩 - 数据:
| 张三 | 语文 | 98 |
| 张三 | 数学 | 92 |
| 张三 | 英语 | 96 |
| 李四 | 语文 | 88 |
| 李四 | 数学 | 78 |
| 李四 | 英语 | 89 |
- 表结构:
t2
表(目标格式,列式存储)- 表结构:
姓名 语文 数学 英语 - 数据:
| 张三 | 98 | 92 | 96 |
| 李四 | 88 | 78 | 89 |
- 表结构:
(二)行转列操作
- 思路:根据姓名分组,使用
case when
判断如果是语文返回成绩,否则返回null
,最终对该列进行sum
聚合,其他数学、英语列的输出道理一样。因为在分组后,对于每个姓名,通过case when
根据科目选择对应的成绩,然后sum
函数会将这些成绩进行汇总(由于同一姓名和科目只有一个成绩,所以这里的sum
实际上就是选择该成绩),对于其他科目对应的列,不符合条件的会返回null
,但在sum
计算时会忽略null
值,最终得到每个姓名对应的各科成绩列。 - SQL 语句示例(适用于 Hive 或 MySQL 等支持类似语法的数据库):
SELECT
name,
SUM(CASE WHEN subject = '语文' THEN score ELSE NULL END) AS 语文,
SUM(CASE WHEN subject = '数学' THEN score ELSE NULL END) AS 数学,
SUM(CASE WHEN subject = '英语' THEN score ELSE NULL END) AS 英语
FROM
t1
GROUP BY
name;
(三)列转行操作
- 思路:查询
t2
表姓名和语文、查询t2
表姓名和数学、查询t2
表姓名和英语,最终通过UNION ALL
联合输出。也就是将t2
表中的列式数据按照科目拆分成行,对于每个姓名和对应的科目及成绩,生成一条新的记录。 - SQL 语句示例(适用于 Hive 或 MySQL 等支持类似语法的数据库):
SELECT name, '语文' AS subject, 语文 AS score FROM t2
UNION ALL
SELECT name, '数学' AS subject, 数学 AS score FROM t2
UNION ALL
SELECT name, '英语' AS subject, 英语 AS score FROM t2;
二十、如何实现 Hive 的动态分区
(一)需求场景
假设要查询的数据有郑州、武汉、深圳等城市,需要将这批用户数据写入不同的城市分区下。例如,有一个用户表包含用户信息和所在城市字段,要根据城市将用户数据分别存储到对应的分区中,以便后续可以根据城市进行快速查询和分析。
(二)实现步骤
- 设置 Hive 的动态分区为非严格模式
- 在
Hive
中,默认的动态分区模式可能是严格模式,为了能够顺利进行动态分区插入,需要将其设置为非严格模式。可以通过在Hive
命令行或配置文件中设置以下参数来实现:
set hive.exec.dynamic.partition.mode=nonstrict;
- 在严格模式下,
Hive
对动态分区插入有一些限制,例如要求分区列必须是最后一列等,设置为非严格模式后可以更灵活地进行动态分区插入操作,但也需要注意数据的准确性和安全性,避免误操作导致数据混乱。
- 在
- 在
insert
语句中操作insert
后边partition
里面不指定分区列的值,在select
最后输出的字段中输出分区列的值。例如,假设有一个表user_data
,包含字段id
、name
、city
等,要将数据按照城市分区插入到分区表中,可以使用以下语句:
INSERT INTO TABLE partition_table PARTITION (city) SELECT id, name, city FROM user_data;
- 在这个语句中,
partition_table
是目标分区表,city
是分区列。在SELECT
语句中,选择了id
、name
和city
字段,Hive
会根据city
字段的值自动将数据插入到对应的城市分区中。如果user_data
表中有来自郑州、武汉、深圳等城市的数据,那么在执行这个INSERT
语句后,partition_table
表中会相应地创建郑州、武汉、深圳等分区,并将对应城市的数据存储到相应分区中。
二十一、Hive 表的字段类型
- 基本数据类型
string
:用于存储字符串值,例如姓名、地址等文本信息。double
:存储双精度浮点数,适用于需要高精度小数表示的数据,如科学计算中的数值、财务数据中的金额等。decimal
:用于精确的小数表示,相比于double
,它可以更精确地控制小数位数和精度,适合处理货币、百分比等需要精确数值的场景。date
:存储日期值,格式通常为YYYY-MM-DD
,用于表示日期信息,如订单日期、生日等。int
:存储整数,范围一般为-2147483648到2147483647,适用于整数类型的数据,如数量、年龄等。bigint
:存储大整数,范围更大,适用于需要存储较大整数值的数据,如身份证号码、订单编号等。
- 复杂数据类型
array
:数组类型,用于存储一组相同数据类型的元素。例如,可以用于存储一个用户的多个兴趣爱好,array<string>
表示存储字符串类型的兴趣爱好数组。map
:映射类型,存储键值对,其中键和值可以是不同的数据类型。比如,可以用于存储用户的属性信息,map<string, int>
表示键为字符串(如属性名称),值为整数(如属性值)的映射。struct
:结构体类型,用于存储包含多个不同类型字段的复合结构。例如,可以定义一个struct
来表示一个学生的信息,包括姓名(字符串)、年龄(整数)、成绩(数组或其他复杂类型)等字段。
这些字段类型在Hive
中用于定义表的结构,以适应不同的数据存储和查询需求。在设计表结构时,需要根据数据的特点和分析需求选择合适的字段类型,以提高数据存储效率和查询性能。例如,如果一个字段只需要存储简单的文本信息,string
类型可能是合适的选择;如果需要存储一组相关的数据,可以考虑使用array
或struct
类型来组织数据;对于需要进行快速键值查找的数据,可以使用map
类型。同时,不同的存储格式(如ORC
、Parquet
等)对这些字段类型的支持和处理方式也可能有所不同,在实际应用中需要综合考虑。
二十二、Hive 的相关概念及特点总结
(一)Hive 概述
- 定义
- Hive是一个构建在Hadoop之上的数据仓库基础架构,它提供了类似SQL的查询语言(HiveQL),用于查询和分析存储在Hadoop分布式文件系统(HDFS)中的大规模数据集。
- 主要功能
- 将结构化的数据文件映射为一张数据库表,提供了一种类似于传统数据库的表结构概念,使得用户可以方便地对大规模数据进行组织和管理。
- 提供高级查询和分析功能,让熟悉SQL的用户可以方便地处理大数据。用户可以使用HiveQL编写查询语句,进行数据筛选、聚合、连接等操作,而无需深入了解底层的MapReduce编程细节。例如,用户可以通过简单的SQL语句查询出满足特定条件的数据,或者对数据进行分组统计等操作。
(二)Hive 与传统关系型数据库的区别
- 数据存储方面
- Hive数据存储在HDFS中,可存储海量数据,能够处理PB级甚至更大规模的数据。这是因为HDFS是为大规模分布式数据存储设计的,具有良好的扩展性和容错性。而传统数据库将数据存储在本地磁盘,存储数据量相对较小,一般适用于处理GB或TB级别的数据。
- 执行引擎方面
- Hive的执行引擎是MapReduce、Tez、Spark等分布式计算框架。它将SQL查询转换为这些框架的任务来执行,通过分布式计算来处理大规模数据。传统数据库使用自己的查询处理引擎,通常是基于磁盘的索引和查询优化技术,更适合处理小规模数据的快速事务处理。
- 执行速度方面
- Hive处理速度相对较慢,尤其是对于实时性要求较高的查询。因为它涉及到将SQL转换为分布式任务的开销,以及在大规模数据上的分布式计算时间。传统数据库执行速度较快,更适用于实时事务处理,如在线交易系统等,能够快速响应单个事务的查询和更新操作。
- 数据更新方面
- Hive不支持频繁的数据更新操作,它主要侧重于数据的批量处理和分析。对数据的修改通常需要通过重新加载或覆盖整个数据集来实现。传统数据库支持频繁的数据插入、更新和删除操作,能够满足实时数据变更的需求,例如在业务系统中不断更新用户信息、订单状态等。
- 可扩展性方面
- Hive支持水平扩展,可以通过增加节点来扩展存储和计算能力,以应对不断增长的数据量和计算需求。传统数据库通常支持垂直扩展,即通过升级硬件设备(如增加内存、CPU核心等)来提高性能,但对水平扩展的支持相对较弱,在大规模数据处理场景下可能会遇到性能瓶颈。
(三)Hive 的架构及组件作用
- 用户接口(User Interface)
- CLI(命令行界面):提供了一种直接在终端输入Hive命令进行操作的方式,适合熟悉命令行操作的用户进行快速的数据查询和管理。例如,用户可以通过CLI执行
SELECT
、INSERT
等SQL语句,以及进行表的创建、删除等操作。 - JDBC/ODBC:使得其他程序可以通过标准的JDBC或ODBC接口连接到Hive,方便与各种应用程序集成。例如,在Java应用程序中,可以使用JDBC驱动连接到Hive,执行SQL查询并获取结果集,用于数据处理和分析。这为在企业级应用中集成Hive提供了便利,使得不同的系统可以共享和处理Hive中的数据。
- WebUI(浏览器访问):提供了一个可视化的界面,用户可以通过浏览器进行一些基本的操作和查询监控等。通过WebUI,用户可以更直观地查看Hive的相关信息,如表结构、查询执行进度等,对于不熟悉命令行操作的用户来说更加友好。同时,管理员也可以通过WebUI进行一些系统配置和管理操作。
- CLI(命令行界面):提供了一种直接在终端输入Hive命令进行操作的方式,适合熟悉命令行操作的用户进行快速的数据查询和管理。例如,用户可以通过CLI执行
- 元数据存储(Metastore)
- 负责管理和存储Hive表的元数据,包括表名、表所属的数据库、表的拥有者、列/分区字段、表的类型(是否是外部表)、表的数据所在目录等信息。它就像是Hive数据的“目录”,记录了数据的结构和位置等关键信息。
- 使用关系型数据库(如MySQL)来存储元数据,提供了一组API和服务,用于查询、更新和管理Hive表的元数据。不同的用户和进程可以通过这些API共享和访问相同的元数据,从而协调和共享表的结构和属性。例如,当一个用户创建了一个新的Hive表时,元数据存储会记录该表的相关信息,其他用户在查询该表时,可以通过元数据存储获取到表的结构和位置等信息,以便正确地进行数据操作。
- 驱动器(Driver)
- 解析器:将 SQL 字符串转换成抽象语法树 AST,并进行语法分析。它会检查 SQL 语句的语法是否正确,例如关键字的使用、表名和列名的合法性等。如果 SQL 语句语法错误,解析器会抛出相应的错误信息,以便用户进行修正。解析器的作用是将用户输入的 SQL 语句转换为 Hive 内部可以理解和处理的形式,为后续的查询优化和执行做好准备。
- 编译器:将抽象语法树 AST 编译成逻辑执行计划和物理执行计划。逻辑执行计划是一个高层次的描述,它表示了查询的逻辑步骤和操作顺序。物理执行计划则是具体的执行步骤,包括如何在分布式环境中执行查询,如使用哪些 MapReduce 任务、如何进行数据的分区和排序等。编译器的作用是将逻辑执行计划转换为可执行的物理执行计划,以便在 Hadoop 集群上实际执行查询。
- 执行器:负责执行物理执行计划,协调和管理 Hive 查询的执行过程。执行器会与 Hadoop 集群进行交互,启动相应的 MapReduce 任务或其他执行引擎,监控任务的执行进度,处理任务的失败和重试等。执行器的作用是确保查询能够正确地在分布式环境中执行,并返回结果给用户。
(四)Hive 的分区和桶
- 分区(Partition)概念、作用及管理
- 概念:是将表的数据按照某个列(通常是时间、地区等具有较高区分度的列)划分为多个子目录或文件。例如,一个销售数据表可以按照销售日期进行分区,每天的数据存储在一个单独的分区中。
- 作用:
- 提高查询性能和过滤效率:通过只扫描特定分区的数据,可以减少数据的读取量。例如,如果要查询某一天的销售数据,只需要扫描对应日期分区的数据,而不需要扫描整个表。
- 更灵活地管理和组织数据:可以根据业务需求按照不同的维度进行分区,方便数据的管理和维护。例如,按照地区分区可以方便地对不同地区的数据进行独立的分析和处理。
- 管理操作:
- 创建表时定义分区列:可以使用
PARTITIONED BY
子句在创建表时定义分区列。例如,CREATE TABLE sales_table (id INT, amount DECIMAL) PARTITIONED BY (date DATE);
创建了一个销售表,按照日期进行分区。 - 添加分区:使用
ALTER TABLE
命令来添加分区,如ALTER TABLE sales_table ADD PARTITION (date='2023-10-10');
向销售表中添加一个日期为2023-10-10
的分区。 - 删除分区:通过
ALTER TABLE
命令的DROP PARTITION
子句删除分区,例如ALTER TABLE sales_table DROP PARTITION (date='2023-10-10');
删除对应日期的分区。
- 创建表时定义分区列:可以使用
- 桶(Bucketing)概念、作用及创建
- 概念:是一种数据分桶技术,它将表的数据根据某个列的哈希值分成固定数量的桶。例如,对于一个用户表,可以按照用户ID的哈希值进行分桶,将数据分布到不同的桶中。
- 作用:
- 提高查询性能:通过将相关数据存储在同一个桶中,可以减少数据的扫描量。在进行查询和连接操作时,只需要扫描相关的桶,而不是整个表。例如,在进行连接操作时,如果两个表按照相同的列进行分桶,并且桶的数量成倍数关系,可以提高连接效率,因为可以快速定位到需要连接的桶。
- 方便进行数据的抽样和聚合操作:由于数据按照桶进行组织,可以更方便地对数据进行抽样,例如只抽取部分桶的数据进行分析。同时,在进行聚合操作时,也可以针对每个桶进行局部聚合,然后再进行全局聚合,提高聚合效率。
- 创建操作:可以使用
CLUSTERED BY
子句在创建表时定义分桶列,并使用SORTED BY
子句来指定排序列。例如,CREATE TABLE user_table (id INT, name STRING) CLUSTERED BY (id) INTO 4 BUCKETS SORTED BY (name);
创建了一个用户表,按照id
列进行分桶,共分为4个桶,并且在每个桶内按照name
列进行排序。
(五)Hive 支持的连接方式及特点
- 内连接(Inner Join)
- 特点:返回两个表中匹配的行,即只返回两个表中共有的行。它基于连接条件进行匹配,只有在两个表中都满足连接条件的行才会被包含在结果集中。适用于需要获取两个表中关联数据的场景,例如在一个订单表和客户表进行内连接时,可以获取到有订单的客户的详细信息以及对应的订单信息。
- 示例用法:
SELECT * FROM table1 INNER JOIN table2 ON table1.key = table2.key;
这里假设table1
和table2
有一个共同的列key
作为连接条件,查询会返回两个表中key
值相等的行的所有列数据。
- 左外连接(Left Outer Join)
- 特点:返回左表中所有的行以及与右表匹配的行。如果右表中没有匹配的行,将返回
NULL
值。以左表为基础,保证左表的所有行都在结果集中,即使在右表中没有对应的匹配行。适用于需要获取左表全部数据以及与右表相关联数据的场景,例如在一个员工表和部门表进行左外连接时,即使某个员工没有所属部门(在部门表中没有匹配记录),该员工的信息也会在结果集中显示,其部门相关的列值为NULL
。 - 示例用法:
SELECT * FROM table1 LEFT OUTER JOIN table2 ON table1.key = table2.key;
这里table1
是左表,查询会返回table1
的所有行以及与table2
中key
值匹配的行,如果table2
中没有匹配的行,对应的列将填充NULL
。
- 特点:返回左表中所有的行以及与右表匹配的行。如果右表中没有匹配的行,将返回
- 右外连接(Right Outer Join)
- 特点:返回右表中所有的行以及与左表匹配的行。如果左表中没有匹配的行,将返回
NULL
值。与左外连接相反,以右表为基础,确保右表的所有行都出现在结果集中。适用于需要获取右表全部数据以及与左表相关联数据的场景,例如在一个学生课程表和成绩表进行右外连接时,即使某些课程没有学生成绩记录(在成绩表中没有匹配记录),该课程的信息也会在结果集中显示,成绩相关的列值为NULL
。 - 示例用法:
SELECT * FROM table1 RIGHT OUTER JOIN table2 ON table1.key = table2.key;
这里table2
是右表,查询会返回table2
的所有行以及与table1
中key
值匹配的行,如果table1
中没有匹配的行,对应的列将填充NULL
。
- 特点:返回右表中所有的行以及与左表匹配的行。如果左表中没有匹配的行,将返回
- 全外连接(Full Outer Join)
- 特点:返回两个表中所有的行,并将不匹配的行填充为
NULL
值。它包含了左外连接和右外连接的结果,即无论左表还是右表中的行,只要在其中一个表中有记录,就会在结果集中出现。如果在另一个表中没有匹配的行,对应的列将填充NULL
。适用于需要获取两个表中所有数据,包括没有匹配关系的数据的场景,例如在一个用户表和用户权限表进行全外连接时,可以得到所有用户的信息以及他们对应的权限信息,如果某个用户没有权限记录或者某个权限没有对应的用户,相关列值将为NULL
。 - 示例用法:
SELECT * FROM table1 FULL OUTER JOIN table2 ON table1.key = table2.key;
查询会返回table1
和table2
中所有的行,对于没有匹配的行,相应的列用NULL
填充,以保证结果集中包含两个表的所有数据。
- 特点:返回两个表中所有的行,并将不匹配的行填充为
(六)Hive 中查询性能优化方法
- 数据存储格式优化
- 选择合适的存储格式,如
ORC
或Parquet
,它们具有较高的压缩率和查询性能。ORC
文件对数据进行了更高效的编码和压缩,支持复杂的数据类型和嵌套结构,能够快速读取和处理大规模数据集中的特定列。Parquet
文件也具有类似的列式存储优势,与ORC
相比,在一些场景下可能具有更好的兼容性和性能表现。例如,对于一个数据仓库中的大规模事实表,使用ORC
或Parquet
格式可以大大减少数据存储占用的空间,并且在查询时能够快速定位和读取需要的列数据,提高查询效率。相比之下,TEXTFILE
等文本格式存储效率较低,不适合大规模数据的高效存储和查询。
- 选择合适的存储格式,如
- 分区设计
- 根据查询的常用条件合理设计分区,减少数据扫描范围。例如,如果经常需要按照时间进行数据查询,可以将表按照时间字段进行分区,这样在查询特定时间范围内的数据时,只需要扫描相应的分区,而不是整个表。合理的分区设计可以极大地提高查询性能,特别是对于数据量巨大的表。同时,要注意分区的数量不宜过多,以免增加元数据管理的负担和影响查询性能。
- 分桶操作
- 对数据进行分桶可以提高连接操作的性能。通过对指定列进行哈希计算并分桶,可以将相关数据存储在同一个桶中,在进行连接等操作时可以快速定位到需要处理的数据桶,减少数据的扫描量。例如,在进行两个大表的连接操作时,如果两个表按照相同的列进行分桶,并且桶的数量成整数倍数关系,可以利用桶表的特性提高连接效率,类似于使用索引来加速数据的匹配和连接过程。
- 合理设置并行度
- 根据集群资源和数据量,调整
Map
和Reduce
的任务数量。如果任务数量设置不合理,可能导致资源浪费或任务执行效率低下。例如,对于数据量较大的任务,可以适当增加Map
和Reduce
的任务数量,以充分利用集群的计算资源,提高数据处理的并行度。但也要注意不要设置过多的任务,以免造成任务启动和调度的开销过大。可以通过设置相关的参数,如mapreduce.job.maps
和mapreduce.job.reduces
来调整任务数量,同时需要根据集群的硬件配置和实际数据情况进行优化。
- 根据集群资源和数据量,调整
- 利用缓存
- 对于频繁使用的中间结果或小表,可以使用缓存来提高查询速度。
Hive
提供了一些缓存机制,例如可以将一些常用的查询结果或小表缓存到内存中,下次查询时如果命中缓存,就可以直接从内存中获取数据,避免重复计算和读取磁盘数据,从而提高查询性能。可以通过设置相关的参数或使用Hive
的缓存功能接口来实现缓存的管理和使用。但要注意缓存的大小和有效期等设置,以避免内存占用过高或缓存数据过期导致的错误结果。
- 对于频繁使用的中间结果或小表,可以使用缓存来提高查询速度。
- 优化 SQL 语句
- 避免使用不必要的子查询、复杂的函数等,尽量简化
SQL
逻辑。复杂的SQL
语句可能会导致查询优化器难以生成高效的执行计划,从而影响查询性能。例如,尽量使用简单的JOIN
操作代替复杂的子查询连接,避免使用过于复杂的函数嵌套和计算,合理使用索引(如果有的话)等。同时,要注意SQL
语句的书写规范和可读性,以便于后续的维护和优化。对于一些大型的查询,可以逐步优化和测试,查看执行计划和性能指标,找出性能瓶颈并进行优化。
- 避免使用不必要的子查询、复杂的函数等,尽量简化
(七)Hive 中数据倾斜的相关问题
- 数据倾斜的定义和影响
- 定义:在分布式计算中,某些任务处理的数据量远远大于其他任务,导致这些任务的执行时间过长,影响整个作业的执行效率。例如,在一个
group by
操作中,如果某个键的值在数据集中出现的频率非常高,那么在进行分组和聚合时,处理这个键的任务可能会需要处理大量的数据,而其他键对应的任务处理的数据量相对较少,就会出现数据倾斜现象。 - 影响:数据倾斜会导致整个作业的执行时间延长,因为作业的完成时间取决于最慢的任务。它可能会使某些节点的资源利用率过高,而其他节点处于空闲或低负载状态,影响集群的整体性能和资源利用率。在严重的情况下,可能会导致任务超时失败,影响数据处理的及时性和准确性。
- 定义:在分布式计算中,某些任务处理的数据量远远大于其他任务,导致这些任务的执行时间过长,影响整个作业的执行效率。例如,在一个
- 发现数据倾斜的方法
- 在
yarn
上可以查看task
的执行情况,如果一个阶段中有些task
很快执行完了,有些task
迟迟无法结束或者运行时间明显较长,则大概率出现了数据倾斜的现象。例如,通过yarn
的监控界面或相关的任务管理工具,可以看到每个MapReduce
任务的执行进度和时间消耗。如果发现某些任务的进度条长时间停滞不前,而其他任务已经完成或接近完成,就需要考虑是否存在数据倾斜问题。此外,还可以查看任务的日志信息,可能会有关于数据倾斜的提示或错误信息,例如某个任务处理的数据量过大等。
- 在
- 解决数据倾斜的方法
- 单表
GROUP BY
操作导致的数据倾斜:- 使用加随机数的方式:可以在
group by
的字段上加上随机数,将数据随机分散到不同的reduce
任务中进行处理,然后再去除随机数进行最终的聚合。例如,对于select count(distinct user_id) from t_user;
可以修改为select count(distinct (user_id + rand())) from t_user;
(这里的rand()
函数可以根据实际情况选择合适的随机数生成方式),然后在后续处理中再去掉随机数部分进行计数。 - 双重聚合:先进行局部聚合,再进行全局聚合。例如,先在每个
map
任务中对数据进行初步的分组和聚合,然后将初步聚合的结果发送到reduce
任务中进行最终的聚合。这样可以减少每个reduce
任务处理的数据量,缓解数据倾斜。 - 配置参数
set hive.groupby.skewindata=true
:这会使Hive
在执行group by
操作时自动进行数据倾斜优化,它会将数据随机分发到多个reduce
任务中进行处理,然后再进行合并和聚合。 - 过滤出倾斜的
key
单独处理:可以先找出导致数据倾斜的关键key
,然后将包含这些key
的数据单独提取出来进行特殊处理,例如使用单独的reduce
任务或者采用其他优化策略进行处理,处理完后再与其他正常数据的处理结果进行合并。
- 使用加随机数的方式:可以在
JOIN
操作导致的数据倾斜:mapjoin
:对于大表和小表连接,当小表足够小可以放入内存时,使用mapjoin
避免reducer
。在map
阶段直接将小表缓存到内存中,并与大表进行连接操作,这样可以避免在reduce
阶段因数据分布不均匀导致的倾斜问题。例如,select /*+ MAPJOIN(small_table) */ * from big_table join small_table on big_table.key = small_table.key;
在查询语句中通过/*+ MAPJOIN(small_table) */
提示Hive
使用mapjoin
。- 大表打散、小表扩容:对大表的连接键进行哈希分区,将数据分散到多个
reduce
任务中,同时对小表进行复制或扩展,使其与大表的分区数相匹配,然后进行连接。例如,对大表按照连接键hash(key) % num_partitions
进行分区,小表复制num_partitions
份,分别与大表的各个分区进行连接。这样可以使数据在reduce
阶段更均匀地分布,减少倾斜。 - 增加
Reducer
的个数:可以通过调整相关参数(如mapreduce.job.reduces
)增加reducer
的数量,从而将数据更分散地分配到多个reducer
任务中进行处理,减轻单个reducer
处理大量数据的压力。但要注意不要设置过多的reducer
,以免导致任务启动和调度开销过大。 - 自定义分区器:根据数据的特点自定义分区器,使得数据在
reduce
阶段能够更均匀地分布到各个reduce
任务中。例如,根据业务逻辑将数据按照特定的规则进行分区,而不是简单地使用默认的哈希分区方式。这样可以更好地控制数据的分布,避免数据倾斜。
- 单表
以上内容对 Hive 的相关知识点进行了全面的梳理和详细的解释,涵盖了函数、SQL 执行、表连接、排序、体系架构、表分类、数据导入导出、开窗函数、自定义函数、优化、数据倾斜、存储格式、序列化、任务数量、连接方式特点、动态分区、字段类型等多个方面。