Hive 分区和桶

摘录自《Hadoop 权威指南》

Hive 把表组织成分区(partition)。这是一个根据分区列(partition column,如日期)的值对表进行粗略划分的机制。使用分区可以加快数据分片(slice)的查询速度。

表或分区可以进一步分为(bucket)。它会为数据提供额外的结构以获得更高效的查询处理。例如,通过根据用户 ID 来划分桶,可以在所有用户集合的随机样本上快速计算基于用户的查询。

1. 分区

以分区的常用情况为例。考虑日志文件,其中每条记录包含一个时间戳。如果根据日期来对它进行分区,那么同一天的记录就会被存放在同一个分区中。这样做的优点是:对于限制到某个或某些特定日期的查询,它们的处理可以变得非常高效。因为它们只需要扫描查询范围内分区中的文件。注意,使用分区并不会影响大范围查询的执行,可以查询跨分区的整个数据集。

一个表可以以多个维度来进行分区。例如在根据日期对日志进行分区以外,可能还要进一步根据国家对每个分区进行子分区(subparttion),以加速根据地理位置进行的查询。

分区是在创建表的时候用 PARTITIONED BY 子句定义的(在创建表后可以使用 ALTER TABLE 语句来增加或移除分区)。该子句需要定义列的列表。例如,对前面提到的假想的日志文件。可能需要要把表记录定义为由时间戳和日志行构成:

CREATE TABLE logs (ts BIGINT, line STRING)
PARTITIONED BY (dt STRING, country STRING);

在我们把数据加载到分区表的时候,要显示指定分区值:

LOAD DATA LOACL INPATH 'input/hive/partitions/file'
INTO TABLE logs
PARTITION (dt = '2001-01-01', country = 'GB');

在文件系统级别,分区只是表目录下嵌套的子目录。把更多文件加载到 logs 表以后,目录结构可能像下面这样:

/user/hive/warehouse/logs
 |——dt=2001-01-01/
 |        |——country=GB/
 |        |        |——file1
 |        |        |——file2
 |        |——country=US/
 |        |        |——file3
 |——dt=2001-01-02/
 |        |——country=GB/
 |        |        |——file4
 |        |——country=US/
 |        |        |——file5
 |        |        |——file6

可以用 SHOW PARTITIONS 命令显示表中有哪些分区:

hive> SHOW PARTITIONS logs;
dt=2001-01-01/country=GB
dt=2001-01-01/country=US
dt=2001-01-02/country=GB
dt=2001-01-02/country=US

记住,PARTITIONED BY 子句中的列定义是表中正式的列,称为分区列(partition column),但是,数据文件并不包含这些列的值,因为它们源于目录名。

可以在 SELECT 语句中以通常的方式使用分区列。Hive 会对输入进行修剪,从而只扫描相关的分区。例如:

SELECT ts, dt, line
FROM logs
WHERE country = 'GB';

2. 桶

把表(或分区)组织成桶(bucket)有两个理由:

  • 第一个理由是获得更高的查询处理效率。桶为表加上了额外的结构,Hive 在处理有些查询时能够利用这个结构。具体而言,连接两个在(包含连接列的)相同列上划分了桶的表,可以使用 map 连接(map-side join)高效地实现;
  • 第二个理由是使“取样”或者说“采样”(sampling)更高效。在处理大规模数据集时,在开发和修改查询的阶段,如果能在数据集的一小部分数据上运行查询,会带来很多方便

首先,来看如何告诉 Hive 一个表应该被划分成桶。使用 CLUSTERED BY 子句来指定划分桶所用的列和要划分的桶的个数:

CREATE TABLE bucketed_users(id INT, name STRING)
CLUSTERED BY (id) INTO 4 BUCKETS;

在这里,使用用户 ID 来确定如何划分分桶(Hive 对值进行哈希并将结果除以桶的个数取余数)。这样,任何一桶里都会有一个随机的用户集合。

对于 map 端连接的情况,首先两个表以相同的方式划分桶,处理左边表内某个桶的 mapper 知道右边表内相匹配的行在对应的桶内,这样,mapper 只需要获取那个桶(这只是右边表内存储数据的一小部分)即可进行连接。这一优化方法并不一定要求两个表必须具有相同的桶的个数,两个表的桶个数是倍数关系也可以。

桶中的数据可以根据一个或多个列另外进行排序。由于这样对每个桶的连接变成了高效的归并排序(merge-sort),因此可以进一步提升 map 连接的效率。以下语法声明一个表使其使用排序桶:

CREATE TABLE bucketed_users(id INT, name STRING)
CLUSTERED BY (id) SORTED BY (id ASC) INTO 4 BUCKETS;

如何保证表中的数据都划分成桶了呢?把在Hive外生成的数据加载到划分成桶的表中,当然是可以的。其实让 Hive 来划分桶更容易。这一操作是针对已有的表。

Hive 并不检查数据文件中的桶是否和表定义中的桶一致(无论是对于桶的数量或用于划分桶的列)。如果两者不匹配,在查询时可能会碰到错误或未定义的结果。因此,建议让 Hive 来进行划分桶的操作。

有一个没有划分桶的用户表:

hive> select * from users;
0    Nat
2    Joe
3    Kay
4    Ann

要向分桶后的表填充成员,需要将 hive.enforce.bucketing 属性设置为 true。这样,Hive 就知道用表定义中声明的数量来创建桶,然后使用 INSERT 命令即可:

SET hive.enforce.bucketing = true;

INSERT OVERWRITE TABLE bucketed_users
SELECT * FROM users;

物理上,每个桶就是表(或分区)目录里的一个文件。它的文件名并不重要,但是桶 n 是按照字典排序的第 n 个文件。事实上,桶对应 MapReduce 的输出文件分区:一个作业产生的桶(输出文件)和 reduce 任务个数相同。可以通过查看刚才创建的 bucketed_users 表的布局来了解这一情况。运行如下命令:

hive> dfs -ls /user/hive/warehouse/bucketed_users

# 将显示有 4 个新建的问价。文件名如下(文件名由 Hive 产生)
000000_0
000001_0
000002_0
000003_0

第一个桶里包括用户 ID 0 和 4,因为一个 INT 的哈希值就是这个整数本身,在这里除以桶数(4)以后的余数:

hive> dfs -cat /user/hive/warehouse/bucketed_users/000000_0
0Nat
4Ann

用 TABLESAMPLE 子句对表进行抽样,可以获得相同的结果。这个子句会将查询限定在表的一部分桶内,而不是使用整个表:

hive> SELECT * FROM  bucketed_users
    > TABLESAMPLE(BUCKET 1 OUT OF 4 ON id);
4 Ann
0 Nat

桶的个数从 1 开始计数。因此,前面的查询从 4 个桶的第一个中获取所有的用户,对于一个大规模、均匀分布的数据集,这会返回表中约 1/4 的数据行。也可以用其它比例对若干个桶进行取样(因为取样并不是一个精确的操作,因此这个比例不一定是桶数的整数倍)。例如,下面的查询返回一半的桶:

hive> SELECT * FROM  bucketed_users
    > TABLESAMPLE(BUCKET 1 OUT OF 2 ON id);
4 Ann
0 Nat
2 Joe

BUCKET 从第几个桶开始
OUT OF 从当前桶开始的第几个桶

因为查询只需要读取和 TABLESAMPLE 子句匹配的桶,所以取样分桶表是非常高效的操作。如果使用 rand() 函数对没有划分成桶的表进行操作,即使只需要读取很小一部分样本,也要扫描整个输入数据集(以下结果是随机的):

hive> select * from users
    > TABLESAMPLE(BUCKET 1 OUT OF 4 ON rand());
2 Joe
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章