Hive除了有分区(Partition),还有分桶(Bucket),上一篇文章《Hive的分区》中介绍了分区,本文接着介绍分桶,以及和分桶经常一起使用的采样(Sampling)

其实不管是分区还是分桶都是为了更好的管理数据。分区将表的数据分到不同的目录存储,从而在查询的时候可以通过where条件过滤一部分数据,减小查询的数据量从而提高性能。但分区的这种机制往往在数据符合以下条件时才会表现的比较好:

  • 分区数目为有限个:一般也不能太大,不然太多的文件和目录对于HDFS的NameNode会造成比较大的内存压力。
  • 各个分区的数据量比较均衡:这个好理解,如果90%的数据跑到一个分区去,那分区的意义就不是很大了。

然而,并非所有场景都是符合上面两条的。比如比较常见的按照国家等地理位置去分区的时候,可能几个大国的数据就占了总数据量的百分之七八十,而剩下的所有国家只占了百分之二三十,也就是不符合上面的第二条。这个时候分区的好处就大打折扣了。为了克服这个问题,Hive增加了分桶的机制。我们先介绍一些分桶相关的基础概念,然后通过例子再做说明。

1. 分桶

1.1 基础理论

分桶的概念其实比较简单:指定桶的个数,选中表中的若干个列,通过对数据中这些列的值做哈希决定数据应该存储到哪个桶里面去,公式表示为:bucket num = hash_function(bucketing_column) mod num_buckets,不同的数据类型哈希函数不同。关于分桶还有以下一些关键点:

  • 根据分桶的原理,列(哈希列)值相同的的数据肯定存在同一个桶里面;
  • 在建表语句中使用CLUSTERED BY指定哈希列;
  • 物理上一个桶对应磁盘上一个文件,桶号从1开始计数;
  • 桶可以和分区一起使用,也可以单独使用;
  • 分桶机制可以保证基本上每部分的数据量基本是均衡的。

分桶也可以带来如下一些好处:

  • 分桶表比不分桶表的采样(samping)效率高。Hive提供了采样机制,用于我们从大量数据中提取一部分来做测试或调试,而分桶可以帮助我们更好的采样(见后面的例子)。
  • Map过程中的join会更快。主要有两方面原因:1. 分桶后,文件大小均衡。2. join的时候左表可以计算对应的右表符合条件的行在哪个桶里面,直接去获取数据即可。
  • 和分区一样,分桶后查询数据会更快
  • 我们在桶内做排序,这样可以让Map里面的join操作更快速。

下面我们看一个例子。

1.2 例子展示

场景需求还是使用上篇文章里面的用户数据UserRecords.txt,但这次只使用国家作为分区列,州做哈希列,也就是分桶列,并且桶内按照城市名排序。

和分区表一样,分桶表也不能使用LOAD DATA的方式导入数据,所以我们还是像前文一样,先把数据导到一张临时表temp_user里面,这个步骤省略。如果不清楚的,请看上一篇文章(Hive的分区)。然后我们创建分桶表:

# 语法
CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name      
[(col_name data_type [COMMENT col_comment], ... [constraint_specification])]
  [COMMENT table_comment]
  [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]
  [CLUSTERED BY (col_name, col_name, ...) [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS]

# 创建bucketed_user表
CREATE TABLE bucketed_user (
    firstname  VARCHAR(64),
    lastname    VARCHAR(64),
    address     STRING,
    city        VARCHAR(64),
    state           VARCHAR(64),
    post        STRING,
    phone1      VARCHAR(64),
    phone2      STRING,
    email       STRING,
    web         STRING
)
    COMMENT 'A bucketed sorted user table'
    PARTITIONED BY(country VARCHAR(64))
    CLUSTERED BY(state) SORTED BY(city) INTO 32 BUCKETS;

我们使用可以看到,不像分区列是一个虚拟列,分桶列是表中的字段。在插入字段之前,我们先介绍两个和分桶相关的配置项:

  • hive.enforce.bucketing:如果建表时设置了分桶,插入数据的时候是否强制分桶。Hive 2.x版本之前默认为false,之后去掉了该配置,并将默认情况改为true。见HIVE-12331.
  • hive.enforce.sorting:如果建表时设置了分桶,插入数据的时候是排序,Hive 2.x版本之前默认为false,之后去掉了该配置,并将默认情况改为true。见HIVE-12331.

在Hive 2.x之前,在定义表结构时通过CLUSTERED BY指定分桶信息,然后在插入数据的时候如果设置hive.enforce.bucketing为true,则会按照表定义里面的桶个数进行自动分桶(dynamic bucket);如果不设置为true,用户也可以指定如何分桶(主要是指定reduce的个数set mapred.reduce.tasks=xx)。这样导致的问题就是表结构里面虽然定义了分桶信息,但实际插入数据的时候可能并没有分桶或者分桶方式与表结构里面定义的不一致。这样后续操作的时候有了很多不确定性,容易产生各种问题,所以在2.x版本之后,去掉了该项配置,直接将默认情况设置为了true。如果你用的Hive是2.x之前的版本,使用分桶的时候记得将这两个选项置为true。另外需要说明的是桶的个数决定了reduce的个数。

下面我们来插入数据:

# 因为我们使用了动态分区,所以和前文一样,设置动态分区相关的参数,参数含义见《Hive的分区》一文
set hive.exec.dynamic.partition=true;
set hive.exec.dynamic.partition.mode=nonstrict;
set hive.exec.max.dynamic.partitions.pernode=1000;

# 覆盖式插入数据
INSERT OVERWRITE TABLE bucketed_user
    PARTITION(country)
    SELECT firstname, lastname, address, city, state, post, phone1, phone2, email, web,
    country
    FROM temp_user;

执行插入语句的时候,大家可以看打印的日志,一共有32个reducers,和桶的个数一致。插入完成之后我们来看HDFS上面的数据:

➜  ~ hadoop fs -ls -R /user/hive/warehouse/bucketed_user
drwxrwxr-x   - allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000000_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000001_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000002_0
-rwxrwxr-x   1 allan supergroup        806 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000003_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000004_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000005_0
-rwxrwxr-x   1 allan supergroup      17140 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000006_0
-rwxrwxr-x   1 allan supergroup        950 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000007_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000008_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000009_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000010_0
-rwxrwxr-x   1 allan supergroup      11314 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000011_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000012_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000013_0
-rwxrwxr-x   1 allan supergroup       4427 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000014_0
-rwxrwxr-x   1 allan supergroup       6132 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000015_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000016_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000017_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000018_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000019_0
-rwxrwxr-x   1 allan supergroup      12405 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000020_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000021_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000022_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000023_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000024_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000025_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000026_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000027_0
-rwxrwxr-x   1 allan supergroup      15262 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000028_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000029_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000030_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000031_0
drwxrwxr-x   - allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=CA
-rwxrwxr-x   1 allan supergroup
……省略后续输出……

可以看到每个分区内的数据被分成了32份,因为数据量太小,所以很多桶内是空的。如果数据量大的话,各个桶的数据量会比较均衡。

2. 采样

从大量数据中采样得到少量数据进行测试或者调试是数据分析中非常常见的操作,抛开业务在数据库我们可以使用LIMIT语句实现该功能,但Hive提供了一个TABLESAMPLE语法用来更好的实现采样,TABLESAMPLE语句可以放到任何FROM语句中。目前支持两种采样方式:基于表的采样Sampling Bucketized Table和基于块的采样Block Sampling

2.1 Sampling Bucketized Table

基于分桶表的采样的语法格式如下:

table_sample: TABLESAMPLE (BUCKET x OUT OF y [ON colname] [table_alias])
  • colname表示基于表中哪个字段进行采样,这个字段可以是分桶列中的某个字段,也可以不是;还可以使用rand()表示基于整行而不是单独某个字段进行采样。
  • x out of y:表示要取哪些桶的数据。假设表有32个桶:

    • 3 out of 32表示总共取32/32=1个桶的数据,取的是第3个桶的数据;
    • 3 out of 16表示总共取32/16=2个桶的数据,分别取第3个、第19(3+16)个桶的数据;
    • 3 out of 64表示总共取32/64=0.5个桶的数据,取第3个桶一半的数据。
  • table_alias给表起的别名,类似MySQL的AS。

一些例子:

# 基于state字段采样
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(BUCKET 3 OUT OF 32 ON state);
+-------------+----------+--------------+---------------------------------+
|  firstname  | country  |    state     |              city               |
+-------------+----------+--------------+---------------------------------+
| Marleen     | CA       | BC           | Abbotsford                      |
| Carole      | CA       | BC           | Abbotsford                      |
| Lasandra    | CA       | BC           | Abbotsford                      |
| Yvette      | CA       | AB           | Big Valley                      |
| Annamae     | CA       | BC           | Burnaby                         |
| Adela       | CA       | BC           | Burnaby                         |
……省略……
| Shelia      | UK       | Cumbria      | Silloth-on-Solway               |
| Mauricio    | UK       | Cumbria      | Walney North Ward               |
| Quentin     | US       | MN           | Burnsville                      |
| Cyndy       | US       | MN           | Burnsville                      |
| Novella     | US       | HI           | Hilo                            |
| Brandon     | US       | HI           | Honolulu                        |
| Angella     | US       | HI           | Honolulu                        |
| Fatima      | US       | MN           | Hopkins                         |
| Skye        | US       | MN           | Minneapolis                     |
| Rodolfo     | US       | MN           | Northfield                      |
| Rolande     | US       | HI           | Pearl City                      |
| Chantell    | US       | MN           | Saint Paul                      |
| Matthew     | US       | MN           | Shakopee                        |
+-------------+----------+--------------+---------------------------------+
131 rows selected (1.06 seconds)

# 使用rand()采样
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(BUCKET 3 OUT OF 32 ON rand());
+-------------+----------+---------------------+---------------------------------+
|  firstname  | country  |        state        |              city               |
+-------------+----------+---------------------+---------------------------------+
| Nenita      | AU       | NS                  | Botany                          |
| Aide        | AU       | NS                  | Rhodes                          |
| Hester      | AU       | NS                  | The Risk                        |
| Annita      | AU       | NT                  | Karama                          |
| Mariko      | AU       | WA                  | Hamel                           |
| Emelda      | AU       | WA                  | Nedlands                        |
| Leatha      | AU       | WA                  | Two Rocks                       |
| Kenny       | AU       | TA                  | Nicholls Rivulet                |
| Eveline     | AU       | VI                  | Camberwell West                 |
……省略……
| Martina     | US       | FL                  | Orlando                         |
| Billye      | US       | MS                  | Pearl                           |
| Timothy     | US       | NY                  | Staten Island                   |
| Pamella     | US       | CO                  | Denver                          |
| Minna       | US       | PA                  | Kulpsville                      |
| Fabiola     | US       | PA                  | York                            |
| Junita      | US       | NJ                  | Cedar Grove                     |
| Helaine     | US       | NJ                  | Jersey City                     |
| Heike       | US       | NJ                  | Little Falls                    |
| Eladia      | US       | NJ                  | Ramsey                          |
| Felicidad   | US       | NJ                  | Riverton                        |
+-------------+----------+---------------------+---------------------------------+
58 rows selected (0.595 seconds)

需要注意的是这种采样方式并不要求表一定要是分桶的,如果没有分桶或者分桶了但采样的字段不在分桶字段里面,那也是可以正常采样的,只不过采样时会扫描全表数据,不是很高效而已。所以大多数情况这种采样方式都是和分桶一起使用的,采样的字段就是分桶的字段,这样采样时只扫描对应的桶就行,可以大大提高效率。

2.2 Block Sampling

基于块的采样是后来新增的一项功能(从Hive 0.8版本开始,见HIVE-2121),这里的块指的是HDFS的Block。目前有三种方式,基本语法为:

# 基于百分比
block_sample: TABLESAMPLE (n PERCENT)

# 基于大小
block_sample: TABLESAMPLE (ByteLengthLiteral)

ByteLengthLiteral : (Digit)+ ('b' | 'B' | 'k' | 'K' | 'm' | 'M' | 'g' | 'G')

# 基于行数
block_sample: TABLESAMPLE (n ROWS)

基于百分比采样和基于大小的采样实质是一样的(见HIVE-3401),这两种方式目前不支持一些压缩的格式。如果采样失败了,就会返回整个表或者分区的数据。需要注意的是,因为是基于块采样的,所以最小的采样单位就是HDFS的一个block,也就是说返回的数据可能会比实际的数据多。比如%1的数据是100MB,但HDFS的一个block是256MB,那采样得到的数据将是256MB。一个示例:

# 采样0.1%的数据
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(0.1 PERCENT) sampled_bucketed_user;
+------------+----------+-------------+---------+
| firstname  | country  |    state    |  city   |
+------------+----------+-------------+---------+
| Soledad    | AU       | AC          | Barton  |
| Darell     | CA       | ON          | Ajax    |
| Allene     | UK       | Derbyshire  | Barlow  |
| Devorah    | US       | NM          | Clovis  |
+------------+----------+-------------+---------+
4 rows selected (0.467 seconds)

# 采样100MB数据
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(100B) sampled_bucketed_user;
+------------+----------+------------------+--------------------+
| firstname  | country  |      state       |        city        |
+------------+----------+------------------+--------------------+
| Santos     | AU       | NS               | Allworth           |
| Avery      | CA       | NS               | Amherst            |
| Lewis      | UK       | South Yorkshire  | Central Ward       |
| Weldon     | US       | IL               | Arlington Heights  |
+------------+----------+------------------+--------------------+
4 rows selected (0.187 seconds)

如果我们想保证每次采样的数据一样,可以设置种子:

set hive.sample.seednumber=<INTEGER>;

默认值是0,比如我们改为100,再采一次样:

0: jdbc:hive2://localhost:10000> set hive.sample.seednumber=100;
No rows affected (0.006 seconds)
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(0.1 PERCENT) sampled_bucketed_user;
+------------+----------+------------------+--------------------+
| firstname  | country  |      state       |        city        |
+------------+----------+------------------+--------------------+
| Santos     | AU       | NS               | Allworth           |
| Avery      | CA       | NS               | Amherst            |
| Lewis      | UK       | South Yorkshire  | Central Ward       |
| Weldon     | US       | IL               | Arlington Heights  |
+------------+----------+------------------+--------------------+
4 rows selected (0.26 seconds)

基于行数的采样和前面两种方式不太一样:

  • 没有数据格式的限制;
  • 采集的条数n会在每个分片(split)都运行一次,所以采集到的总条数和输入的分片数也有关系。
# 从输入的每个分片从采5条数据(这里只有一个分片)
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(5 ROWS) sampled_bucketed_user;
+------------+----------+--------+-----------------+
| firstname  | country  | state  |      city       |
+------------+----------+--------+-----------------+
| Soledad    | AU       | AC     | Barton          |
| Annamae    | AU       | AC     | Civic Square    |
| Katheryn   | AU       | AC     | Fyshwick        |
| Roy        | AU       | AC     | Red Hill        |
| Jamie      | AU       | AC     | Tuggeranong Dc  |
+------------+----------+--------+-----------------+
5 rows selected (0.411 seconds)

本文介绍了Hive的分桶和采样,分桶很好的弥补了分区的一些不足。同时分桶之后,可以帮助我们更好的实现采样。需要注意的是不论是分区还是分桶,都是存在计算的,所以分区或者分桶之后,数据导入会比不分区不分桶慢,但换来的是后面查询会更快速。

References