谈笑间学会大数据-Hive设计模式

谈笑间学会大数据-Hive设计模式

​ Hive看上去以及实际香味都像是一个关系型数据库。用户对如表和列这列术语比较熟悉,而且Hive提供的查询语言和用户之前使用过的SQL方言非常地相似。不过,Hive实现和使用的方法和传统的关系型数据库是非常不通的。通常,用户视图移植关系型数据库中的模式,而事实上Hive是反模式的。

按天划分的表

​ 按天划分表就是一种模式,其通常会在表名中加入一个时间戳,例如表名为test_2020_04_13、test_0413等等。这种每天一张表的方式在数据库领域就是反模式的一种方式,但是因为实际情况下数据集增长得很快,这种方式的应用还是比较广泛的。

create table test_04_13 (id int, part string, quantity int) ;
create table test_04_14 (id int, part string, quantity int) ;
create table test_04_15 (id int, part string, quantity int) ;

... load data ...
select part,quantity from test_04_13
union all
select part,quantity from test_04_14
where quantity < 4 
;

​ 对于Hive,这种情况下应该使用分区表。Hive通过where子句中的表达式来选择查询岁需要的指定的分区。这样的查询效率就比较高,并且也比较清晰明了:

create table test (id int, part string, quantity int)
partitioned by (int day)
;

-- 手动添加分区
alter table test add partition (day=20200413);
alter table test add partition (day=20200414);
alter table test add partition (day=20200415);

-- 加载离线数据的写法
... load data ...

-- 过滤查询
select 
	part, 
	quantity 
from test
where day>=20200413 and day<20200415 and quantity<4
;

关于分区

​ Hive中分区的功能是非常有用的。这是因为Hive通常要输入进行全盘扫描,来满足查询条件(这里我们先忽略掉Hive的索引功能)。通过创建很多的分区确实可以优化一些查询,但是同时可能也会对其他的一些重要的查询不利,比如如下:

create table weblog (url string, time long)
partitioned by (day int, state string, city string)
;

-- 只查询单个分区
select * from weblog where day=20200413;

​ HDFS用于设计存储数百万的大文件,而非数十亿的小文件。使用过多分区可能导致的一个问题就是会创建大量的非必须的Hadoop文件和文件夹。一个分区就对应着一个包含有很多个文件的文件夹。如果指定的表存在数百个分区,那么可能每天都会创建几万个文件。如果保持这样的表很多年,那么最终就会超出NameNode对云数据信息的处理能力。因为NameNode必须要将所有的系统文件的元数据信息保存在内存中。虽然每个文件只需要少量字节大小的元数据(大约是150字节/文件),但是这样也会限制一个HDFS实例所能管理的文件总数的上限。而其他的文件系统,比如MapR和Amazon S3 就没有这个限制。

​ MapReduce会将一个任务(job)转换成多个任务(task)。默认情况下,每个task都是一个JVM实例,都需要开启和销毁的开销。对于小文件,每个文件都会对应一个task。在一些情况下,JVM开启和销毁的时间中销毁可能会比实际处理数据的时间消耗更长!

​ 因此,一个理想的分区方案不应该导致产生太多的分区和文件目录,并且每个目录下的文件应该足够大,应该是文件系统中块大小的若干倍。

​ 按时间范围进行分区的一个好的策略就是按照不同的时间粒度来确定合适大小的数据积累量,而且安装这个时间粒度。随着时间的推移,分区数量的增长是“均匀的”,而且每个分区下包含的文件大小至少是文件系统中块的大小或者块大小的数倍。这个平衡可以保持使分区足够大,从而优化一般情况下查询的数据吞吐量。同时有必要考虑这种粒度级别在未来是否是适用的,特别是查询where子句选择比较小的粒度的范围情况:

create table weblogs (url string, time long, state string, city string)
partitioned by (day int)
;

select * from weblogs where day=20200413;

​ 另一个解决方案是使用两个级别的分区并且使用不同的维度。例如,第一个分区可能是按照天(day)进行计划分的,而二级分区可能通过如州名(state)这样的地理区域进行划分:

create table weblogs (url string, time long, city string)
partitioned by (day int, state string)
;

select * from weblogs where day=20200413;

​ 然而,由于一些州可能会比其他州具有更多的数据,用户可能会发现map task处理数据时会出现不均,这是因为处理数据量多的州需要比处理数据量小的州需要消耗更多的时间。

​ 如果用户不能找到好的、大小相对合适的分区方式的话,那么可以考虑使用桶表进行“分桶存储”。

唯一键和标准化

​ 关系型数据库通常使用唯一键、索引和标准化来存储数据集,通常是全部或者大部分存储到内存的。然而,Hive没有主键或者基于序列密钥生产的自增键的概念。如果可以的话,应避免对非标准化数据进行连接(join)操作。复杂的数据类型,如array、map和struct,有助于实现在单行中存储一对多数据。这并不是说不应该进行标准化,但是星型架构设计并非最优的。

​ 避免标准化主要的原因是为了最小化磁盘寻道,比如那些通常需要外键关系的情况。非标准化数据允许被扫描或者写入较大的、连续的磁盘存储区域,从而优化磁盘驱动器的I/O性能。然而,非标准化数据可能导致数据重复,而且有更大的导致数据不一致的风险。

​ 实例如下:员工表

create table employees (
  name					string,
  salary				float,
  subordinates	array<string>,
  deductions		map<string, float>,
  address				struct<street:string, city:string, state:string, zip:int>
);

​ 这个例子中的数据模型从很多方面打破了传统的设计原则。

​ 首先,我们非正式地使用了name作为主键,而我们都知道名字往往不是唯一的!现在暂时先不考虑这个问题。一个关系模型中如果使用name作为键,那么从一个员工记录到经理记录都应该有唯一的一个外键关系。这里我们使用了另一种方式来表达这个关系,即在subordinates数组字段中保存了这个员工所有下属的名字。

​ 其次,对于每名员工来说,其各项税收除额都是不同的,但是map的键都是一样的,即使用户使用“标记”(例如,整数)来作为键实际对应的值。一个常规的关系模型通常使用一个单独的、具有2个列的表来激励税收扣除项的扣除名称(或标志)和具体的值,而员工表和这个税收扣除项之间是一对多的关系。

​ 最后,有些雇员还是有可能住在同一个地址的,但是我们为了每个雇员都记录了其对应的住址,而不是使用一个雇员住址表,然后和雇员表建立一对一的关系。

​ 下面轮到我们管理引用完整性(或处理结果),然后解决特定的发生了改变的数据中的重复数据。Hive本身没有提供方便的方式来进行对单行数据执行update操作。不过当用户的数据量达到数十TB到PB级别时,相对于这些局限性而言,优化执行速度显得更加重要。

同一份数据多种处理

​ Hive本身提供了一个独特的语法,它可以从一个数据源产生多个数据聚合,而无需每次聚合都要重新扫描一次。对于大的数据源输入集来说,这个优化可以节约非常可观的时间,

例如:下面这2个查询都会从源表history表读取数据,然后导入到2个不同的表中

insert overwrite table sales
select * from history where action='purchased';
insert overwrite table credits
select * from history where action='returned';

​ 上面的查询,语法是正确的,不过执行效率低下。而如下这个查询可以达到同样的目的,却只需要扫描history表一次就可以了:

from history
insert overwrite table sales
select * from where action='purchased';
insert overwrite table credits
select * from where action='returned';

嗯,相信你的眼睛,没有看错,from后面可以不跟表名的,表名在from…语句那里…

对于每个表的分区

​ 很多的ETL处理过程会涉及到多个处理的步骤,而每个步骤可能会产生一个或多个临时表,这些表仅供下一个job使用。可能有人会觉得将这些临时表进行分区不是那么有必要。不过,想象一下这样的场景:由于查询或者原始数据处理的某个步骤出现问题而导致需要对好几天的输入数据重跑ETL过程。这是用户可能就需要执行那些一条执行一次的处理过程,来保证所有的任务都完成之前不会有job将临时表覆盖重写。

​ 例如,下面这个例子设计了一个名为distinct_ip_in_logs的中间表,其会在后续处理步骤中使用到:

$ hive -hiveconf dt=2011-01-01
hive> insert overwrite table distinct_ip_in_logs 
		> select distinct(ip) as ip from weblogs
		> where hit_date='${hiveconf:dt}';
hive> create table state_city_for_day (state string, city string);
hive> insert overwrite state_city_for_day
		> select distinct (state, city) from distinct_ip_in_logs
		> join geodata on (distinct_ip_in_logs.ip = geodata.ip);

​ 这种方式是有效的,不过当计算某一天的数据时会导致前一天的数据被insert overwrite语句覆盖掉。如果同时运行链各个这样的实例,用于处理不同日期的数据的话,那么他们就可能会相互影响到对方的结果数据。

​ 还有一个处理方法是在整个过程中使用分区。这样就不会存在同步问题。同时,这样还能带来一个好处,那就是可以允许用户对中间数据按日期进行比较:

$ hive -hiveconf dt=2011-01-01
hive> insert overwrite table distinct_ip_in_logs
		> partition (hit_date=${dt})
		> select distinct(ip) as ip from weblogs
		> where hit_date='${hiveconf:dt}';
		
hive> create table state_city_for_day (state string, city string)
		> partitioned by (hit_date string);
		
hive> insert overwrite table state_city_for_day partition(${hiveconf:df})
		> select distinct(state,city) from distinct_ip_in_logs
		> join geodata on (distinct_ip_in_logs.ip = geodata.ip)
		> where (hit_date = '${hiveconf:dt}');

​ 这种方法的一个缺点是,用户将需要管理中间表并删除旧分区,不过这些任务也很容易实现自动化处理。

分桶表数据存储

​ 分区提供一个隔离数据和优化查询的便利的方式。不过,并非所有的数据集都可以形成合理的分区,特别是之前所提到过的要确定合适的划分大小这个疑惑。

​ 分桶是将数据集分解成更容易管理的若干部分的另一个技术。

​ 例如:假设有一个表的一级分区是dt,代表日期,二级分区是user_id,那么这种划分方式可能会导致太多的小分区。如果用户使用动态分区来创建这些分区的话,那么默认情况下,Hive会限制动态分区可以创建的最大分区数,用来避免由于创建太多的分区导致超过了文件系统的处理能力以及其他一些问题。因此,如下命令可能执行失败:

hive> create table weblog (url string, source_ip string)
		> partitioned by (dt string, user_id int);
		
hive> from raw_weblog
		> insert overwrite table page_view partition(dt='2012-06-08', user_id)
		> select server_time, url, source_ip, dt, user_id;

​ 不过,如果我们对表weblog进行分桶,病史会用user_id 作为分桶字段,则字段值会根据用户指定的值进行哈希分发到桶中。同一个user_id下的记录通常会存储到同一个桶内。假设用户数要比桶数多得多那么每个桶内就将会包含多个用户的记录:

hive> create table weblog (user_id int, url string, source_ip string)
		> partitioned by (dt string)
		> clustered by (user_id) into 96 buckets
		;

​ 不过,将数据正确地插入到表的过程完全取决于用户自己!create table 语句中所规定的信息仅仅定义了元数据,而不影响实际填充表的命令。

​ 如何使用insert … table 语句来进行正确地填充表。首先,我们需要设置一个属性来强制Hive为目标表的分桶初始化过程设置一个正确的reducer个数。然后我们再执行一个查询来填充分区。例如:

hvie> set hive.enforce.bucketiong = true;
hive> from raw_logs
		> insert overwrite table weblog
		> partition(dt='2009-02-25')
		> select user_id,url,source_ip where dt = '2009-02-25'
		> ;

如果我们没有使用hive.enfore.bucketing 属性,那么我们就需要自己设置和分桶个数相匹配的reducer个数。例如:使用set mapred.reduce.tasks=96,然后再insert语句中,需要在select语句后增加cluster by 语句。

注意:对于所有表的元数据,指定分桶并不能保证表可以正确地填充。用户可以根据前面的示例来确保是否正确填充了表。

​ 分桶有几个优点,因为桶的数量是固定的,所以它没有数据波动。桶对于抽样再合适不过。如果两个表都是按照user_id进行分桶的话,那么Hive可以创建一个逻辑上正确的抽样。分桶同时有利于执行高效的map-side join。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MrZhangBaby

请博主喝杯奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值