Hive 看上去以及实际行为都像一个关系型数据库,并且 Hive 提供的查询语言也确实和之前使用过的 SQL 语言非常地相似。不过,Hive 实现和使用的方式和传统的关系型数据库是非常不同的。通常用户视图移植关系型数据库中的模式,而事实上 Hive 是反模式的。
按天划分表就是一种模式,其通常会在表名中加入一个时间戳。每天一张表的方式在数据库领域是反模式的一种方式,但是因为实际情况下数据集增长的很快,这种方式应用还是比较广泛的。
对于 Hive,这种情况下应该使用分区表。Hive 通过 WHERE
子句中的表达式来选择查询所需要的指定的分区。这样的查询执行效率高,看起来也清晰明了。
CREATE TABLE supply (id int, part string, quantity int)
PARTITIONED BY (int day);
SELECT part, quantity FROM supply
WHERE day >= 20110102 AND day < 20110103 AND quantity < 4;
Hive 中分区的功能是非常有用的。这是因为 Hive 通常要对输入进行全盘扫描,来满足查询条件。通过创建很多的分区确实可以优化一些查询,但是同时可能会对其他一些重要的查询不利。
HDFS 用于设计存储数百万的大文件,而非数十亿的小文件。使用过多分区可能导致的一个问题就是会创建大量的非必须的 Hadoop 文件和文件夹。一个分区就对应着一个包含有多个文件的文件夹。如果指定的表存在数百个分区,那么可能每天都会创建好几万个文件。如果保持这样的表很多年,那么最终就会超出 NameNode
对系统云数据信息的处理能力。因为 NameNode
必须要将所有的系统文件的元数据信息保存在内存中。虽然每个文件只需要少量字节大小的元数据,但是这样也会限制一个 HDFS 实例所能管理的文件总数的上限。
因此,一个理想的分区方案不应该导致产生太多的分区和文件夹目录,并且每个目录下的文件应该足够得大,应该是文件系统中块大小的若干倍。
按时间范围进行分区是一个比较好的策略。我们可以按不同的时间粒度来确定合适大小的数据积累量。随着时间的推移,分区数量的增长是均匀的,而且每个分区下包含的文件大小至少是文件系统中块大小的数倍。这个平衡可以保持使分区足够大,从而优化一般情况下查询的数据吞吐量。
关系型数据库通常使用唯一键、索引和标准化来存储数据集,通常是全部或者大部分存储到内存的。然而,Hive 没有主键或基于序列密钥生成的自增键的概念。
如果可以的话,应避免对非标准化数据进行连接 JOIN
操作。复杂的数据类型,如 array
、map
和 struct
,有助于实现在单行中存储一对多数据。这并不是说不应该进行标准化,但是星型架构类型设计并非最优的。
避免标准化的主要原因是为了最小化磁盘寻道,比如那些通常需要外键关系的情况。非标准化数据允许被扫描或写入到大的、连续的磁盘存储区域,从而优化磁盘驱动器的 I / O
性能。然而,非标准化数据可能导致数据重复,而且有更大的导致数据不一致的风险。
Hive 本身提供了一个独特的语法,它可以从一个数据源产生多个数据聚合,而无需每次聚合都要重新扫描一次。对于大的数据输入集来说,这个优化可以节约非常可观的时间。
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 sales SELECT * WHERE action='purchased'
INSERT OVERWRITE credits SELECT * WHERE action='returned';
很多的 ETL 处理过程会涉及到多个处理步骤,而每个步骤可能会产生一个或多个临时表,这些表仅供下一个 job 使用。起先可能会觉得将这些临时表进行分区不是那么有必要的。
不过,想象一下这样的场景:由于查询或者原始数据处理的某个步骤出现问题而导致需要对好几天的输入数据重跑 ETL 过程。这时用户可能就需要执行那些一天执行一次的处理过程,来保证在所有的任务都完成之前,不会有 job 将临时表覆盖重写。
下面这个例子设计了一个名为 distinct_ip_in_logs
的中间表,其会在后续处理步骤中使用到。
$ hive -hiveconf dt=2011-01-01
INSERT OVERWRITE table distinct_ip_in_logs
SELECT distinct(ip) as ip from weblogs
WHERE hit_date='${hiveconf:dt}';
CREATE TABLE state_city_for_day (state string,city string);
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
INSERT OVERWRITE table distinct_ip_in_logs
PARTITION (hit_date=${dt})
SELECT distinct(ip) as ip from weblogs
WHERE hit_date='${hiveconf:dt}';
CREATE TABLE state_city_for_day (state string,city string)
PARTITIONED BY (hit_date string);
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 会限制动态分区可以创建的最大分区数,用来避免由于创建太多的分区导致超过了文件系统的处理能力以及其他一些问题。因此,如下命令可能会执行失败。
CREATE TABLE weblog (url STRING, source_ip STRING)
PARTITIONED BY (dt STRING, user_id INT);
FROM raw_weblog
INSERT OVERWRITE TABLE page_view PARTITION(dt='2012-06-08', user_id)
SELECT server_name, url, source_ip, dt, user_id;
不过如果我们对表 weblog
进行分桶,并使用 user_id
字段作为分桶字段,则字段值会根据用户指定的值进行哈希分发到桶中。同一个 user _id
下的记录通常会存储到同一个桶中。假设用户数要比通数多得多,那么每个桶内就将会包含多个用户的记录。
CREATE TABLE weblog (user_id INT, url STRING, source_ip STRING)
PARTITIONED BY (dt STRING)
CLUSTERED BY (user_id) INTO 96 BUCKETS;
分桶有几个优点。因为桶的数量是固定的,所以它没有数据波动。桶对于抽样再合适不过。如果两个表都是按照 user_id
进行分桶的话,那么 Hive 可以创建一个逻辑上正确的抽样。分桶同时有利于执行高效的 map-side join
。
Hive 允许在原始数据文件之上定义一个模式,而不像很多的数据库那样,要求必须以特定的格式转换和导入数据。这样的分离方式的好处是,当为数据文件增加新的字段时,可以容易地适应表定义的模式。
Hive 提供了 SerDe
抽象,其用于从输入中提取数据。SerDe
同样用于输出数据,尽管输出功能并非经常使用,因为 Hive 主要用于查询。一个 SerDe
通常是从左到右进行解析的,通过指定的分隔符将行分解成列。SerDe
通常是非常宽松的。例如如果某行的字段个数比预期的要少,那么缺少的字段将返回 null
。如果某行的字段个数比预期的要多,那么多出的字段将会被省略掉。增加新字段的命令只需要一条 ALTER TABLE ADD COLUMN
命令就可以完成。因为日志格式通常是对已有字段增加更多的信息。
随着时间的推移,可能会为底层数据增加一个新字段。如下这个例子就是展示为数据新增 user_id
字段的过程。需要注意的是,一些旧的原始数据文件中可能不包含这个字段。
Hive 通常使用行式存储,不过 Hive 也提供了一个列式 SerDe
来以混合列式格式存储信息。虽然这种格式是可以用于任意类型的数据的,不过对于某些数据集使用这种方式是最优的。
假设有足够多的行,像 state
字段和 age
字段这样的列将会有很多重复的数据。这种类型的数据如果使用列式存储将会非常的好。
如果表具有非常多的字段,用列式存储也会更好。
几乎在所有情况下,压缩都可以使磁盘上存储的数据量变小,这样可以通过降低 I / O
来提高查询执行速度。Hive 可以无缝地使用很多压缩类型。不使用压缩唯一令人信服的理由就是产生的数据用于外部系统,或者非压缩格式(例如文本格式)是最兼容的。
但是压缩和解压缩都会消耗 CPU 资源。MapReduce 任务往往是 I / O
密集型的,因此 CPU 开销通常不是问题。不过,对于工作流这样的 CPU 密集型的场景,例如一些机器学习算法,压缩实际上可能会从更多必要的操作中获取宝贵的 CPU 资源,从而降低性能。