在Hive中有四种数据模型——内部表、外部表、分区表、桶,下面一一介绍下这四种不同的模型。

Hive内部表和外部表

内部表和外部表最直观的区分其实是通过是否使用external关键词进行修饰,被external修饰的为外部表。内部表和外部表具有以下三个主要的不同点:

1) 是否被关键词external修饰。

2) 删除外部表、元数据会被删除,但是实际的数据不会真正的删除,还是会存在指定的位置,而内部表的删除,元数据和数据都会被删除。

3)在导入数据到外部表时,数据并没有移动到数据仓库(/user/hive/warehouse)目录下,且数据是由HDFS管理。而内部表的数据都会存储在(/user/hive/warehouse)目录下,且数据是由Hive自身管理。

两者的使用场景也是有所区别的。

外部表使用场景——导入HDFS的数据,可以用户存储一些日志信息,但是数据不会被删除。

内部表使用场景——用于存放Hive处理的中间表、结果表,逻辑处理的中间过程生成的中间表,或者一些临时表,可以使用完后直接删除。

在实际应用过程中,通常是外部和内部表一起使用。例如将每日的日志数据传入HDFS中,一天一个目录,Hive基于流入的数据建立外部表,将每天HDFS上的原始日志映射到外部表的每天分区中;然后再外部表的基础上做统计分析,使用内部存储中间表、结果表,通过SELECT+外部表+INSERT 进入内部表。

在Hive中建表的方式有三种:直接建表、抽取建表、Like建表。分别用案列解释下这几种建表方式

  • 直接建表

直接建表是可以直接进行字段类型,字段备注,数据存储格式自定义等。

-- 创建内部表
create table article_internal(
    sentence string
)
row format delimited fields terminated by '\t'  -- 字符之间的分割符
lines terminated by '\n'; -- 每一行之间的分割符

-- 创建外部表
create external table article_external(
    sentence string
) 
row format delimited fields terminated by '\t' -- 字符之间的分割符
lines terminated by '\n'; -- 每一行之间的分割符

load data local inpath '/usr/local/src/code/hive_data/The_Man_of_Property.txt'
overwrite into table article_external -- 导入本地数据到指定的表

load data local inpath '/usr/local/src/code/hive_data/The_Man_of_Property.txt'
overwrite into table article_internal -- 导入本地数据到指定表

在上面的代码中展示了内部表和外部表的建表语句,我们也可以直接导入数据到指定的表中去,上面是指定的本地数据,如果是想将HDFS上的数据导入到表中,则只需要先将文件上传到HDFS,然后执行上面的load data语句只是去除local关键词,就会将HDFS上的数据导入到指定表中。

我们也通过这个案例区分一下内部表和外部表之间的不同之处。首先,查看表结构(show create table xxx)。可以看出其实最大的区别就是外部表多一个external关键词,其他的都是类似。

hive分区和分桶 hive分区分桶那个优先级高_分桶

其次。验证内部表和外部表的删除。在导入表的数据之后,会发现无论是内部表还是外部表,其实都会把数据上传到HDFS上。Hive表的删除分为删除数据(保留表)、删除表。删除数据使用truncate table 表名,但是这种只能删除内部表的数据,因为外部表的数据并不是存放在Hive元数据存储中;删除表用Drop table 表名。这里就实践一下drop删除表来查看内部表和外部表的不同。


hive分区和分桶 hive分区分桶那个优先级高_内部表/外部表_02

删除之前HDFS存储的数据

 

内部表drop之后,会看到表结构和HDFS上的数据都不存在,

hive分区和分桶 hive分区分桶那个优先级高_分桶_03

外部表删除后,元数据被删除,但是在HDFS上的数据依然存在。

hive分区和分桶 hive分区分桶那个优先级高_分区表_04

  • 抽取建表——as

抽取(as)建表的使用场景常见于中间逻辑处理的时候,进行建表,直接复制表的数据和结构时。

create table article_as as select * from artilce;
  • like建表

Like建表适用于只关注表结构,不需要数据。

create table article_like like article

分区

为什么要引入分区的概念?这是因为在单个表数据量越来越大时,Hive Select查询一般会扫描整个表的内容,会消耗很多的时间做没必要的工作,有时候只需要扫描表中最关心的一部分数据,因此在建表的时候引入partition的概念,用于减少查询的数据量,提供查询的执行效率。你不禁会问为什么不使用索引?索引难道不能实现吗?其实,在Hive中也是支持索引的,但是和分区还是有很大的区别,两者的区别点在于——索引不分割数据库,分区会分割数据库,索引其实就是拿额外的存储空间换查询时间,但是分区已经将整个大数据按照分区列拆分成了多个小的数据库。

那改如何分区?一般根据业务需求来进行分区,完全看业务场景,常用的是用年、月、日、男女性别、年龄段或者是能够平均将数据分到不同文件中的属性。选择合适的分区是十分重要的,因为分区不好将会直接导致查询结果延迟。在工作中常用日的维度来进行分区,用d或者dt字段表示,一般将今天当做t,分区的数据一般是t-1。

对于分区有以下五点小的细节之处:

1)一个表可以拥有一个或者多个分区,每个分区以文件夹的形式单独存在表文件夹的目录下。

2)表和列名不区分大小写。

3) 分区是以字段的形式在表结构中存在,通过desc table命令可以查看到字段存在(算是伪列),该字段实际上并不存放在实际数据内容中,仅仅是分区表示。

4) 分区有一级、二级设置,一般设置为一级分区。

5)分区也分为动态分区和静态分区。

上面的五点其实前四点很好理解,对于第五点其实比较疑惑,什么是动态?什么是静态?其实用下面的两个insert 语句就可以很好的区分两者的区别。

-- 静态分区插入数据
insert overwrite table udata_paratition partition(dt='2020-12-19')  select user_id,item_id,rating from udata  where user_id='305'

-- 动态分区插入数据
insert overwrite table udata_paratition partition(dt) select user_id,item_id,rating,to_date(from_unixtime(cast('timestamp' as bigint),'yyyy-MM-dd HH:mm:ss')) res  from udata  where user_id='244'

很明显,Hive的静态分区,就是要手动指定分区的值为静态值,这种对于小批量的分区插入比较友好,但是大量的导入就显得力不从心了。所以动态分区就是分区值设置为动态的值,动态分区你不指定查询结果字段值时,会默认最后一列的值作为分区值,或者手动设定查询结果的哪一列。

insert overwrite table udata_partition(dt=res) select  to_date(from_unixtime(cast('timestamp' as bigint),'yyyy-MM-dd HH:mm:ss')) res from udata

分区的数量需要根据业务需求严格规划,因为分区会占用IO资源,数量越多,IO资源消耗越大,查询时间和性能都是有损耗的。另外动态分区是默认是关闭的,因此如果使用动态分区之前需要手动打开动态分区。在实际工作中趋向于使用动态分区,而且对于前端日志埋点的数据,一般都会采取day+hour的形式作为分区(dt='2020-12-19'/hour='01')。

-- 打开动态分区模式
set hive.exec.dynamic.partition = true;
-- 设置分区模式为非严格模式
set hive.exec.dynamic.partition.mode=nonstrict;
-- 设置一条带有动态分区SQL语句所能创建的最大动态分区总数,超过则报错
set hive.exec.dynamic.partitions=10;

下面是分区的一些基本操作:


hive分区和分桶 hive分区分桶那个优先级高_分区表_05

创建分区

hive分区和分桶 hive分区分桶那个优先级高_分桶_06

查看分区表结构和分区数据

 对于分区的以下操作命令一般是比较常用的:

--查看表结构

show create table udata_partition;

desc udata_partition【可以看到分区信息】

-- 查看分区表分区

show partitions udata_paratition;

另外当表中有数据之后,在HDFS上会对应有生成分区目录。如下图所示:

hive分区和分桶 hive分区分桶那个优先级高_hive分区和分桶_07

 

分桶

为了将Hive中的数据进行不同程度上的划分,从而让查询在较小的范围数据上提高查询效率,Hive中提出了分区以及分桶的两种策略,分区是粗粒度的划分,分桶是可以更进一步的细粒度的划分。Hive才用对列值哈希,然后除以桶的个数求余的方式决定该条记录放在哪个桶中,可以理解成MapReduce中的HashPartitioner,两者都是基于Hash对数据进行分桶。

以下是分桶的一个计算过程。

分桶计算:hive计算桶列的hash值再除以桶的个数取模,得到某条记录到底属于哪个桶。

定义桶数:3个 0 1 2

输入以下数据:

    user_id    order_id    gender

    196         12010200       1

    186         19201000        0

    22           12891999        1

    244          19192908        0

计算:

    196%3=1 ,放入1号桶

    186%3=0,放入0号桶

    22%3=1,放入1号桶

    244%3=1,放入1号桶

分桶常用的应用场景包括:数据抽样、Map-Side Join。

Map-Side Join:可以获得更高的查询处理效率。桶为了表加上额外的结构,(利用原有字段进行分桶),Hive在处理有些查询时能利用这个结构。具体而言,连接两个在(包含连接列)相同列上划分了同的表,可以使用Map端连接(Map-side join)高效的实现。比如JOIN操作,对于JOIN操作两个表有一个相同的列,如果对这两个表都进行桶操作。那么将保存相同列值的桶进行JOIN操作就可以,可以大大减少JOIN的数据量。

数据抽样:在处理大规模数据集时,尤其数据挖掘阶段,可以用一份数据验证一下,代码是否可以运行成功,进行局部测试,也可以抽样进行一些代表性统计分析。具体如何进行数据抽样,见下面的SQL。

select * from student tablesample(bucket 1 out of 32 on id)

tablesample是抽样语句,语法为:Tablesample(Bucket x OUT OF y);

-x表示从第几个分桶进行抽样,y表示每隔几个分桶取一个分桶,y必须是table总bucket数的倍数或者因子。hive根据y的大小,决定抽样比列。例如,table总共分为64份,当y=32时,抽取(64/32)2个bucket的数据,当y=128时,抽取(64/128)0.5个bucket数据。x表示从哪个bucket抽取。当table总Bucket数为32,tablesmaple(bucket 1 out of 16),表示总共抽取2个bucke数据,分别为第1个和第(1+16)17个bucket的数据。

Bucket和动态分区一样,在最开始是默认关闭的,因此需要我们手动去设置为启用——set hive.enforce.bucketing = true。

下面是关于分桶的基本操作:

-- 创建分桶
create table bucket_user(
    id int
)clustered by(id) into 4 buckets;

-- 导入数据
insert overwrite table bucket_user select cast(user_id as int) from udata;

hive分区和分桶 hive分区分桶那个优先级高_内部表/外部表_08

有了分区和分桶的概念,那什么时候使用分区?什么时候使用分桶呢?数据量比较大的时候,为了快速查询使用分区;更加细粒度的查询、数据抽样或者数据倾斜使用分桶。

Hive分桶的概念就是MapReduce的分区概念,两者完全相同。物理上每个桶就是目录里的一个文件,一个作业产生的桶(输出文件)数量和Reduce任务个数是相同的。而分区表的概念,则是新的概念。分区代表了数据的仓库,也就是文件夹目录。每个文件夹下面可以放不同的数据文件。通过文件夹可以查询里面存放的文件。但文件夹本身和数据内容毫无关系。桶则是按照数据内容的某个值进行分桶,把一个大文件散列成为一个个小文件。这些小文件可以单独排序。如果另外一个表也按照同样的规则分成了一个个小文件。两个表Join的时候,就不必要扫描整个表,只需要匹配相同分桶的数据即可,大大的提升了效率。同样对数据抽样的时候,也不需要扫描整个文件,只需要对每个分区按照相同规则抽取一部分数据即可。

Hive表的执行顺序

在Hive中执行语句的SQL一般书写格式为:select .... from .... where ... group by ... having...order by...

它的执行顺序为:from...where...select...group by...having...order by..

执行顺序其实可以分为两个部分,刚好对应MapReduce的两个阶段,Map阶段:from...where...select,剩余则是在Redcue端进行执行。

  • Map阶段:

① 执行from加载,进行表的查找与加载

② 执行where过滤,进行条件过滤与筛选

③ 执行select查询,进行输出项的筛选

④ map端文件合并:map端本地溢写文件的合并操作,每个map最终形成一个临时文件。然后按列映射到对应的reduce。

  • Reduce阶段:

① group by :对map端发送过来的数据进行分组并进行计算。

② having:最后过滤列用于输出结果

③ order by :排序后进行结果输出到HDFS文件。