【Hive】09-设计模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shenchaohao12321/article/details/82947295

1、按天划分的表

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

hive>CREATE TABLE supply_2011_01_02(id int,part string,quantity int);
hive>CREATE TABLE supply_2011_01_03(id int,part string,quantity int);
hive>CREATE TABLE supply_2011_01_04(id int,part string,quantity int);
hive>load data ...
hive>SELECT part,quantity from supply_2011_01_02
       >UNIONALL
       >SELECT part,quantity from supply_2011_01_03
       >WHERE quantity<4;

对于Hive,这种情况下应该使用分区表。Hive通过WHERE子句中的表达式来选择查询所需要的指定的分区。这样的查询执行效率高,而且看起来清晰明了:

hive>CREATE TABLE supply (id int,part string,quantity int) PARTITIONED BY(int day);
hive>ALTER TABLE supply add PARTITION(day=20110102);
hive>ALTER TABLE supply add PARTITION(day=20110103);
hive>ALTER TABLE supply add PARTITION(day=20110104);
hive>load data ...
hive>SELECT part,quantity FROM supply
    >WHERE day>=20110102 AND day<20110103 AND quantity<4;

2、关于分区

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

hive>CREATE TABLE weblogs (url string ,time long,state string,city string)
    >PARTITIONED BY(day int);
hive>SELECT * FROM weblogs WHERE day=20110102;

HDFS用于设计存储数百万的大文件,而非数十亿的小文件。使用过多分区可能导致的一个问题就是会创建大量的非必须的Hadoop文件和文件夹。一个分区就对应着一个包含有多个文件的文件夹。如果指定的表存在数百个分区,那么可能每天都会创建好几万个文件。如果保持这样的表很多年,那么最终就会超出NameNode对系统云数据信息的处理能力。因为NameNode必须要将所有的系统文件的元数据信息保存在内存中。虽然每个文件只需要少量字节大小的元数据(大约是150字节/文件),但是这样也会限制一个HDFS实例所能管理的文件总数的上限。
MapReduce会将一个任务(job)转换成多个任务(task)o默认情况下,每个task都是一个新的JVM实例,都需要开启和销毁的开销。对于小文件,每个文件都会对应一个Task。在一些情况下,JVM开启和销毁的时间中销毁可能会比实际处理数据的时间消耗要长!
因此,一个理想的分区方案不应该导致产生太多的分区和文件夹目录,并且每个目录下的文件应该足够得大,应该是文件系统中块大小的若干倍。
按时间范围进行分区的一个好的策略就是按照不同的时间粒度来确定合适大小的积累量,而且安装这个时间粒度。随着时间的推移,分区数量的增长是“均匀的每个分区下包含的文件大小至少是文件系统中块的大小或块大小的数倍。这个平衡可以保持使分区足够大,从而优化一般情况下查询的数据吞吐量 。

同时有必要考虑这种粒度级别在未来是否是适用的,特别是查询中 WHERE 子句选择较小粒度的范围的情况:

hive>CREATE TABLE weblogs (url string,time long,state string,city string) PARTITIONED BY(day int); 
hive>SELECT * FROM weblogs WHERE day=20110102;

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

hive >CREATE TABLE weblogs (url string,time long,state string,city string) PARTITIONED BY(day int,state string); 
hive>SELECT * FROM weblogs WHERE day=20110102;

然而,由于一些州可能会比其他州具有更多的数据,用户可能会发现 map task 处理数据时会出现不均,这是因为处理数据量多的州需要比处理数据量 小的州要消耗更多的时间。如果用户不能够找到好的、大小相对合适的分区方式的话,那么可以考虑使用“分桶表数据存储”中介绍的分桶存储。

3、唯一键和标准化

关系型数据库通常使用唯一键、索引和标准化来存储数据集,通常是全部或者大部分存储到内存的。然而,Hive没有主键或基于序列密钥生成的自增键的概念。如果可以的话,应避免对非标准化数据进行连接(JOIN)操作。复杂的数据类型,如array、map
和struct有助于实现在单行中存储一对多数据。这并不是说不应该进行标准化,但是星形架构类型设计并非最优的。
避免标准化的主要的原因是为了最小化磁盘寻道,比如那些通常需要外键关系的情况。
非标准化数据允许被扫描或写人到大的、连续的磁盘存储区域,从而优化磁盘驱动器的I/0性能。然而,非标准化数据可能导致数据重复,而且有更大的导致数据不一致的风险。
例如,思考下我们的运行示例一一员工(employees)表·为了清晰地表述,这里再次做了一些修改:
 

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操作。
不过,当用户的数据量达到数十到PB级别时,相对于这些局限性而言,优化执行速度显得更加重要。

4、同一份数据多种处理

Hive本身提供了一个独特的语法,它可以从一个数据源产生多个数据聚合,而无需每次聚合都要重新扫描一次。对于大的数据输人集来说,这个优化可以节约非常可观的时间。我们会在第5章详细讨论这个问题。
例如,下面这2个查询都会从源表history表读取数据,然后导人到2个不同的表中:

hive>INSERT OVERWRITE TABLE sales
    >SELECT * FROM history WHERE acton='purchased';
hive>INSERT OVERWRITE TABLE credits
    >SELECT FROM history WHERE acton='returned';

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

hive>FROM history
    >INSERT OVERWRITE sales SELECT * WHERE action='purchased'
    >INSERT OVERWRITE credits SELECT * WHERE acton='returned';

5、对于每个表的分区

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

这种方式是有效的,不过当计算某一天的数据时会导致前一天的数据被INSERT OVERWRITE语句覆盖掉。如果同时运行两个这样的实例,用于处理不同日期的数据的话,那么它们就可能会相互影响到对方的结果数据。
一个更具鲁棒性的处理方法是在整个过程中使用分区。这样就不会存在同步问题。同时,这样还能带来一个好处,那就是可以允许用户对中间数据按日期进行比较:

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

6、分桶表数据存储

分区提供一个隔离数据和优化查询的便利的方式。不过,并非所有的数据集都可形成合理的分区,特别是之前所提到过的要确定合适的划分大小这个疑虑。
分桶是将数据集分解成更容易管理的若干部分的另一个技术。
例如,假设有个表的一级分区是dt,代表日期,二级分区是user_id,那么这种划分方式可能会导致太多的小分区。回想下,如果用户是使用动态分区来创建这些分区的话,那么默认情况下,Hive会限制动态分区可以创建的最大分区数,用来避免由于创建太多的分区导致超过了文件系统的处理能力以及其他一些问题。因此,如下命令可能会执行失败:

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

不过,将数据正确地插人到表的过程完全取决于用户自己!CREATE TABLE语句中所规定的信息仅仅定义了元数据,而不影响实际填充表的命令。
下面介绍如何在使用INSERT ... TABLE语句时,正确地填充表。首先,我们需要设置一个属性来强制Hive为目标表的分桶初始化过程设置一个正确的reducer个数。然后我们再执行一个查询来填充分区。例如:

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

对于所有表的元数据,指定分桶并不能保证表可以正确地填充·用户可以根据前面的示例来确保是否正确地填充了表。
分桶有几个优点。因为桶的数量是固定的,所以它没有数据波动。桶对于抽样再合适不过。如果两个表都是按照user_id进行分桶的话,那么Hive可以创建一个逻辑上正确的抽样。分桶同时有利于执行高效的map-sideJOIN。

7、为表增加列

Hive允许在原始数据文件之上定义一个模式,而不像很多的数据库那样,要求必须以特定的格式转换和导人数据。这样的分离方式的好处是,当为数据文件增加新的字段时,可以容易地适应表定义的模式。
Hive提供了SerDe抽象,其用于从输人中提取数据。S同样用于输出数据,尽管输出功能并非经常使用,因为Hive主要用于查询。一个SerDe通常是从左到右进行解析的。通过指定的分隔符将行分解成列。通常是非常宽松的。例如,如果某行的字段个数比预期的要少,那么缺少的字段将返回null。如果某行的字段个数比预期的要多,那么多出的字段将会被省略掉。增加新字段的命令只需要一条ALTER TABLE ADD COLUMN命令就可完成。因为日志格式通常是对已有字段增加更多信息,所以这样是非常有用的:

ALTER TABLE weblogs ADD COLUMNS (userid string);

猜你喜欢

转载自blog.csdn.net/shenchaohao12321/article/details/82947295