分区表与分桶表
我们创建一个hive
表时,此时在hdfs
上就在默认路径下创建了一个以表的名字命名的文件夹。
Hive
表中的数据在hdfs
上则是对应文件夹下的所有文件。在查询表中数据时,其实就是将文件夹下的所有文件进行读取,在海量数据的场景下,这无疑是非常耗时的,并且在实际生产环境中,往往会进行查询过滤。
所以,如何在海量数据的场景下进行高效的查询过滤呢?
分区表
- 分区表实际上就是对应一个
HDFS
文件系统上的独立的文件夹。 - 该文件夹下是该分区所有的数据文件。
Hive
中的分区就是分目录,把一个大的数据集根据业务需要分割成小的数据集。- 在查询时通过
WHERE
子句中的表达式选择查询所需要的指定的分区,这样的查询效率会提高很多。
分区表基本操作
- 创建分区表
例如,我们对前面的部分信息表进行模拟,将部门信息表按照时间进行分区存储。
1 | create table if not exists dept_partition( |
注意:分区字段不能在表中出现,可以将分区字段看作表的伪列。
- 数据准备
为每个分区准备数据,我们根据日期对日志进行管理,通过部门信息模拟,首先在目录/opt/module/hive-3.1.2/datas/
创建以下三个文件。
dept_20200401.log
1 | 10 行政部 1700 |
dept_20200402.log
1 | 30 教学部 1900 |
dept_20200403.log
1 | 50 运营部 2000 |
接下来向dept_partition
表中的分区加载数据
1 | load data local inpath '/opt/module/hive-3.1.2/datas/dept_20200401.log' |
数据加载完后,在HDFS
中即可看到对应的分区目录。
- 查询分区表的分区详情
1 | show partitions dept_partition; |
- 使用分区表进行数据查询
1 | select * from dept_partition; |
直接将分区表数据全部打印出来,可以看到其实分区字段在使用的时候可以当成普通字段使用。
单分区查询:
1 | select * from dept_partition where day="20200402"; |
多分区联合查询:
1 | select * from dept_partition where day in ("20200401", "20200403"); |
- 增加分区
增加单个分区:
1 | alter table dept_partition add partition (day="20200404"); |
增加多个分区
1 | alter table dept_partition add partition (day="20200405") partition (day="20200406"); |
添加多个分区时使用的是空格去连接不同的分区名。
- 删除分区
删除单个分区
1 | alter table dept_partition drop partition (day="20200406"); |
删除多个分区
1 | alter table dept_partition drop partition (day="20200405"), partition (day="20200404"); |
二级分区
思考: 在根据日期分区后,如果一天的日志数据量也很大,如何再将数据拆分?
这个时候考虑创建二级分区,在天目录下再细分为小时。
创建二级分区表
1 | create table if not exists dept_partition2( |
加载数据
1 | -- 加载数据 |
这个时候进入到HDFS
中就可以看到,进入到以及分区目录下后,可以看到二级分区的目录。
在二级分区内部就有我们上传的具体数据文件。
直接查询整个表的数据。
1 | select * from dept_partition2; |
查询二级分区表的分区结果
1 | show partitions dept_partition2; |
思考一个问题,我们在
HDFS
相应的目录下可以直接看到对应的分区目录,那么如果我们直接把对应的数据文件放到HDFS
上,并且按照相应的规则手动创建好分区目录,在Hive
中能否查询出来呢?例如:我们手动在
HDFS
上创建目录/user/hive/warehouse/dept_partition2/day=20200404/hour=10
,然后自己手动将数据上传到该目录下此时能够通过
Hive
查询到该数据呢?
1 select * from dept_partition2 where day="20200404" and hour="10";此时明显是无法查询出来的。
原因其实也很简单,我们在这里只是机械的将文件放入到了
Hive
中,而Hive
中的数据是由元数据+实际数据
构成,我们在这里实际上是只上传了实际数据,但是Hive
并不知道。要想要
Hive
知道该数据也很简单,一般有两种方式:
第一种方式是自己手动创建对应的分区,相当于自己手动告诉
Hive
元数据。
1 alter table dept_partition2 add partition (day="20200404", hour="10");此时再次查找对应的数据就可以找到了
1 select * from dept_partition2 where day="20200404" and hour="10";第二种方式是让
Hive
自己去修复这个表的元数据和实际数据
的对应规则和关系执行以下命令进行修复
1 msck repair table dept_partition2;此时
Hive
会执行MR
去修复元数据与真实数据
的对应关系,如果元数据缺失分区会自动创建。为了演示效果,首先,执行以下命令将数据复制到
day=20200405/hour=10
1 hdfs dfs -cp /user/hive/warehouse/dept_partition2/day=20200404 /user/hive/warehouse/dept_partition2/day=20200405接下来执行修复命令
1 msck repair table dept_partition2;此时就会自动在
Hive
中添加分区的元数据信息。
动态分区
关系型数据库中,对分区表Insert
数据时候,数据库自动会根据分区字段的值,将数据插入到相应的分区中,Hive
中也提供了类似的机制,即动态分区(Dynamic Partition)
,只不过,使用Hive
的动态分区,需要进行相应的配置。
动态分区参数设置
开启动态分区功能(默认
true
,开启)1
set hive.exec.dynamic.partition=true;
设置为非严格模式(动态分区的模式,默认
strict
,表示必须指定至少一个分区为静态分区,nonstrict
模式表示允许所有的分区字段都可以使用动态分区。)1
set hive.exec.dynamic.partition.mode=nonstrict;
在所有执行
MR
的节点上,最大一共可以创建多少个动态分区。默认1000
1
set hive.exec.max.dynamic.partitions=1000;
每个执行
MR
的节点上,最大可以创建多少个动态分区。该参数需要根据实际的数据来设定。比如:源数据中包含了一年的数据,即
day
字段有365
个值,那么该参数就需要设置成大于365
,如果使用默认值100
,则会报错。1
set hive.exec.max.dynamic.partitions.pernode=100;
整个
MR Job
中,最大可以创建多少个HDFS
文件。默认100000
1
set hive.exec.max.created.files=100000;
当有空分区生成时,是否抛出异常。一般不需要设置。默认
false
1
set hive.error.on.empty.partition=false;
案例实操:
将dept
表中的数据按照地区(loc
字段),插入到目标表dept_partition_dynamic
的相应分区中。
创建分区表:
1 | create table if not exists dept_partition_dynamic( |
注意这里要使用
loc
字段进行自动分区,那么在创建表的时候就不应该包含该字段。
以动态分区的方式向dept_partition_dynamic
表中插入数据
1 | -- 设置为非严格模式 |
要使用动态分区,在查询的时候必须将分区字段放到最后,例如这里的
loc
字段。
此时在HDFS
下的目录/user/hive/warehouse/dept_partition_dynamic
中就可以看到以下分区目录。
查询dept_partition_dynamic
分区结果
1 | show partitions dept_partition_dynamic; |
自动分区完成。
分桶表
分区提供一个隔离数据和优化查询的便利方式。但是,并非所有的数据集都可形成合理的分区。
对于一张表或者分区,Hive
可以进一步组织成桶,也就是更为细粒度的数据范围划分。
分桶是将数据集分解成更容易管理的若干部分的另一个技术。
分区针对的是数据的存储路径(细分文件夹);分桶针对的是数据文件(按规则多文件放一起)。
创建分桶表
1 | create table emp_bucket( |
注意,在这里一定要保证设置的
reduce
数量大于等于分桶的数量,否则会导致无法分桶报错。也即参数
mapreduce.job.reduces
的值。
加载数据
1 | load data local inpath "/opt/module/hive-3.1.2/datas/emp.txt" into table emp_bucket; |
此时会提交2
个MR
任务将数据分为4个桶进行存储。
注意:此时如果采用本地文件可能会导致报错,因为我们最终是把这个任务封装成了一个
MR
任务提交到集群,那么该集群在执行MR
任务的时候回去找相关的本地文化,如果该MR
任务提交到了其他服务器中,其他服务器内是没有这个数据的,可能会导致报错,报错几率约等于2/3
。所以建议这里不要采用本地文件数据
local
参数。
函数
系统内置函数
- 查看系统所有自带函数
1 | show functions; |
可以看到系统中一共有289
个内置函数。
- 显示自带的函数用法
1 | desc function year; |
- 显示详细自带函数的用法
1 | desc function extended year; |
常用内置函数
空字段赋值(NVL)
函数说明:
1 | desc function extended nvl; |
函数功能:给值为NULL
的数据赋值,它的格式是NVL(value, default_value)
。如果value
为NULL
,则NVL
函数返回default_value
的值,否则返回value
的值
如果两个参数都为NULL
,则返回NULL
。
案例实操:采用数据表emp
的数据。
如果
job
的值为NULL
,那么将其赋值为总经理
。1
select ename, nvl(job, '总经理') from emp;
如果
job
值为NULL
,那么使用其deptno
代替。1
select ename, nvl(job, deptno), deptno from emp;
判断语句(CASE WHEN THEN ELSE END, IF)
函数说明:
1 | desc function extended case; |
函数功能:
1 | CASE a WHEN b THEN c [WHEN d THEN e]* [ELSE f] END |
如果a=b
,则返回c
;如果a=d
,则返回e
;其他情况返回f
。
案例实操:
首先,准备数据,在/opt/module/hive-3.1.2/datas/
下创建emp_sex.txt
文件,内容如下:
1 | 悟空,A,男 |
1 | -- 数据准备 |
- 求出不同部门男女各多少人
1 | -- 求出不同部门男女各多少人。 |
IF
语句比较简单
IF(条件表达式,A,B)
:当条件表达式成立时返回A
,条件表达式不成立时返回B
。
1 | select if(1>2, '111', '000'); |
行转列
相关函数说明:
CONCAT(string A/col, string B/col ...)
:返回输入字符串连接后的结果,支持任意个输入字符串;1
select concat('1', '2', '3', 'a', 'b', 'c');
CONCAT_WS(separator, str1, str2,...)
:也是将字符串进行连接,不过在这里可以使用分隔符进行连接,第一个参数就是指定的分隔符。1
select concat_ws(',', '2', '3', 'a', 'b', 'c')
COLLECT_SET(col)
:将某个字段的值放入一个数组中,并对结果进行去重。1
select collect_set(deptno) from emp;
COLLECT_LIST(col)
:将某个字段的值放入一个数组中,这里不对结果进行去重,这个是与COLLECT_SET
的主要区别。1
select collect_list(deptno) from emp;
案例实操:
name | constellation | blood_type |
---|---|---|
孙悟空 | 白羊座 | A |
大海 | 射手座 | A |
宋宋 | 白羊座 | B |
猪八戒 | 白羊座 | A |
凤姐 | 射手座 | A |
苍老师 | 白羊座 | B |
案例需求:把星座和血型一样的人归类到一起。结果如下:
首先进行数据准备,依然是在datas
目录下创建文件constellation.txt
1 | 孙悟空,白羊座,A |
数据导入:
1 | create table if not exists constellation( |
案例实现sql
:
1 | select t1.newcol, concat_ws('|', collect_list(name)) |
列转行
相关函数说明:
首先查询,前面我们创建的persioninfo
表,内容如下所示。
EXPLODE(col):将
hive
表的一列中复杂的array
或者map
结构拆分成多行。1
select explode(friends) from personinfo;
如果是
map
那么拆分出来为key
和value
两列。1
select explode(children) from personinfo;
SPLIT(string str, string regex): 按照
regex
字符串分割str
,会返回分割后的字符串数组。1
select split('1,2,3,4,5,6,a,b,c', ',');
LATERAL VIEW
LATERAL VIEW udtf(expression) tableAlias AS columnAlias
用于和
split
,explode
等UDTF
一起使用,它能够将一列数据拆成多行数据,在此基础上可以对拆分后的数据进行聚合。lateral view
首先为原始表的每行调用UDTF
,UDTF
会报一行拆分成一行或者多行,lateral view
再把结果组合,产生一个支持别名表的虚拟表。
案例演练:
movie | category |
---|---|
《疑犯追踪》 | 悬疑,动作,科幻,剧情 |
《Lie to me》 | 悬疑,警匪,动作,心理,剧情 |
《战狼2》 | 战争,动作,灾难 |
将上述数据转化为以下格式:
1 | 《疑犯追踪》 悬疑 |
数据准备,在datas
下创建文件movie_info.txt
内容如下:
1 | 《疑犯追踪》 悬疑,动作,科幻,剧情 |
然后创建数据表,并导入数据:
1 | create table if not exists movie_info( |
案例实现sql
:
1 | select movie_name, category_ |
窗口函数(开窗函数)
1 | Function(arg1 ……) over([partition by arg1 ……] [order by arg1 ……] [<window_expression>]) |
如果不指定
partition by
,则不对数据进行分区,换句话说,所有数据看作同一个分区。如果不指定
order by
, 则不对各分区进行排序,通常用于那些顺序无关的窗口函数,如sum()
如果不指定窗口子句:
不指定
order by
,默认使用分区内所有行,等同于1
Function() over(rows between unbounded precedeing and unbounded following)
如果指定
order by
,默认使用分区内第起点到当前行,等同于1
Function() over(rows between unbounded preceding and current row)
案例演练,接下来使用多个案例,具体演示相关开窗函数的用法:
首先,进行数据准备。在datas
下新建文件business.txt
,内容如下所示:
1 | jack,2017-01-01,10 |
导入数据:
1 | -- 开窗函数 |
查询在
2017
年4
月份购买过的顾客及总人数1
2
3
4
5
6select
name,
count(1) over()
from business
where month(ordertime) == '4'
group by name;查询顾客的购买明细及月购买总额
1
2
3
4
5
6select
name,
ordertime,
cost,
sum(cost) over(partition by name, month(ordertime))
from business;加了
partition by
分区后,默认的窗口大小是整个分区。上述的场景, 将每个顾客的
cost
按照日期进行累加1
2
3
4
5
6select
name,
ordertime,
cost,
sum(cost) over(partition by name, month(ordertime) order by ordertime)
from business;加了
order by
后,每行的分区大小是从分区的起始位置到当前行。rows between unbounded perceding and current now
所有行进行累加
1
2
3
4
5
6select
name,
ordertime,
cost,
sum(cost) over()
from business;按照name 分组,组内数据累加
1
2
3
4
5
6select
name,
ordertime,
cost,
sum(cost) over(partition by name)
from business;按照name分区,组内数据按照日期有序累加
1
2
3
4
5
6select
name,
ordertime,
cost,
sum(cost) over(partition by name order by ordertime)
from business;按照name分区,组内数据按照日期排序,由起点到当前行进行累加
1
2
3
4
5
6
7
8
9
10
11-- 案例7:按照name分区,组内数据按照日期排序,由起点到当前行进行累加
select
name,
ordertime,
cost,
sum(cost)
over(
partition by name
order by ordertime
rows between unbounded preceding and current row )
from business;按照name分区,组内数据按照日期排序,由前一行和当前行进行做聚合
1
2
3
4
5
6
7
8
9
10
11--- 案例8:按照name分区,组内数据按照日期排序,由前一行和当前行进行做聚合
select
name,
ordertime,
cost,
sum(cost)
over(
partition by name
order by ordertime
rows between 1 preceding and current row )
from business;按照name分区,组内数据按照日期排序,由当前行和前一行和后一行做聚合
1
2
3
4
5
6
7
8
9
10
11-- 案例9:按照name分区,组内数据按照日期排序,由当前行和前一行和后一行做聚合
select
name,
ordertime,
cost,
sum(cost)
over(
partition by name
order by ordertime
rows between 1 preceding and 1 following )
from business;按照name分区,组内数据按照日期排序,由当前行和后面所有行作聚合
1
2
3
4
5
6
7
8
9
10
11-- 案例10:按照name分区,组内数据按照日期排序,由当前行和后面所有行作聚合
select
name,
ordertime,
cost,
sum(cost)
over(
partition by name
order by ordertime
rows between current row and unbounded following )
from business;查询每个顾客上次的购买时间
1
2
3
4
5
6
7-- 案例11:查询每个顾客上次的购买时间
select
name,
ordertime,
lag(ordertime, 1) over (partition by name order by ordertime) before_ordertime,
cost
from business;查询每个顾客下次的购买时间
1
2
3
4
5
6
7-- 案例12:查询每个顾客下次的购买时间
select
name,
ordertime,
lead(ordertime, 1) over (partition by name order by ordertime) next_ordertime,
cost
from business;查询前20%时间的订单信息
1
2
3
4
5
6
7
8
9select
t1.name, ordertime, cost, part_num
from (select name,
ordertime,
cost,
ntile(5) over (order by ordertime) part_num
from business
) t1
where t1.part_num = 1;ntile
函数的原理是将所有数据按照分区将数据分为若干组,每条数据都会给上一个组号,这个时候我们再按照组号进行筛选就可以筛选出对应的时间。1
2
3
4
5select name,
ordertime,
cost,
ntile(5) over (order by ordertime) part_num
from business
排序函数(RANK, DENSE_RANK, ROW_NUMBER)
RANK()
:排序相同时会重复,总数不会变 重复的名次一样但是下一名名次会以前面人数+1
来定。DENSE_RANK()
:排序相同时会重复,总数会减少 就是若有重复则最后一名的名次不会和总数相等即并列。ROW_NUMBER()
:会根据顺序计算,字段相同就按排头字段继续排。
案例演练:
首先进行数据准备,在datas
目录下创建文件score.txt
,文件内容如下所示:
1 | 孙悟空 语文 87 |
创建表并导入数据
1 | create table if not exists score( |
接下来分别使用三个排序函数,计算每门学科成绩排名
1 | select |
其他常用函数
常用日期函数
unix_timestamp
:返回当前或指定时间的时间戳1
2select unix_timestamp();
select unix_timestamp("2020-10-28",'yyyy-MM-dd');from_unixtime
:将时间戳转为日期格式1
select from_unixtime(1603843200);
current_date
:当前日期1
select current_date;
current_timestamp
:当前的日期加时间1
select current_timestamp;
to_date
:抽取日期部分1
select to_date('2022-10-28 12:12:12');
year
:获取年1
select year('2022-10-28 12:12:12');
month
:获取月1
select month('2022-10-28 12:12:12');
day
:获取日1
select day('2022-10-28 12:12:12');
hour
:获取时1
select hour('2022-10-28 12:13:14');
minute
:获取分1
select minute('2020-10-28 12:13:14');
second
:获取秒1
select second('2020-10-28 12:13:14');
weekofyear
:当前时间是一年中的第几周1
select weekofyear('2020-10-28 12:12:12');
dayofmonth
:当前时间是一个月中的第几天1
select dayofmonth('2020-10-28 12:12:12');
months_between
: 两个日期间的月份1
select months_between('2020-04-01','2020-10-28');
add_months
:日期加减月1
select add_months('2020-10-28',-3);
datediff
:两个日期相差的天数1
select datediff('2020-11-04','2020-10-28');
date_add
:日期加天数1
select date_add('2020-10-28',4);
date_sub
:日期减天数1
select date_sub('2020-10-28',-4);
last_day
:日期的当月的最后一天1
select last_day('2020-02-30');
date_format()
: 格式化日期1
select date_format('2020-10-28 12:12:12','yyyy/MM/dd HH:mm:ss');
常用取整函数
round
: 四舍五入1
2select round(3.14);
select round(3.54);ceil
: 向上取整1
2select ceil(3.14);
select ceil(3.54);floor
: 向下取整1
2select floor(3.14);
select floor(3.54);
常用字符串操作函数
upper
: 转大写1
select upper('low');
lower
: 转小写1
select lower('low');
length
: 长度1
select length("atguigu");
trim
: 前后去空格1
select trim(" atguigu ");
lpad
: 向左补齐,到指定长度1
select lpad('atguigu',9,'g');
rpad
: 向右补齐,到指定长度1
select rpad('atguigu',9,'g');
regexp_replace
:使用正则表达式匹配目标字符串,匹配成功后替换!1
SELECT regexp_replace('2020/10/25', '/', '-');
集合操作
size
: 集合中元素的个数1
select size(friends) from test3;
map_keys
: 返回map
中的key
1
select map_keys(children) from test3;
map_values
: 返回map
中的value
1
select map_values(children) from test3;
array_contains
: 判断array
中是否包含某个元素1
select array_contains(friends,'bingbing') from test3;
sort_array
: 将array
中的元素排序1
select sort_array(friends) from test3;
多维分析
grouping sets
:多维分析
自定义函数
自定义函数:当Hive
提供的内置函数无法满足你的业务处理需要时
此时就可以考虑使用用户自定义函数(UDF:user-defined function
)。
根据用户自定义函数类别分为以下三种:
- UDF(
User-Defined-Function
)—> 一进一出,例如:length
- UDAF(
User-Defined Aggregation Function
) —> 聚合函数,多进一出,例如:count/max/min
- UDTF(
User-Defined Table-Generating Functions
)—> 炸裂函数,一进多出,例如:explode()
自定义UDF函数
需求:自定义一个UDF实现计算给定字符串的长度,例如:
1 | hive(default)> select my_len("abcd"); |
案例实操:
创建Maven
工程,然后创建包com.tipdm.MyHiveFunction
接下来在包中创建类MyStringLength
,输入以下内容:
1 | package com.tipdm.MyHiveFunction; |
接下来打包Maven
工程,然后上传到datas
目录。
创建临时函数
进入Hive
客户端,然后输入以下内容,导入jar
包,并创建临时函数。
1 | -- 添加jar包 |
调用自定义临时函数:
1 | select my_len("123123123"); |
临时函数只跟会话有关系,跟库没有关系,只要创建临时函数的会话不断,再当前会话下,任意一个库都可以使用,其他会话全部不能使用。
创建永久函数
因为add jar
的方式本身也是临时生效,所以在创建永久函数的时候,需要执行路径(应且因为元数据的原因,这个路径还得是HDFS
上的路径)
首先将jar
包导入到HDFS
.
1 | hdfs dfs -put /opt/module/hive-3.1.2/datas/MRDemo-1.0-SNAPSHOT.jar /input/ |
然后在Hive
客户端下使用以下命令创建永久函数。
1 | create function my_len2 as "com.tipdm.MyHiveFunction.MyStringLength" using jar "hdfs://hadoop102:8020/input/MRDemo-1.0-SNAPSHOT.jar"; |
此时重新创建一个新的Hive
客户端。
1 | select ename, my_len2(ename) from emp; |
在使用时就会自动的去导入对应的jar
包,并添加函数。