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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章