聊聊ClickHouse MergeTree引擎的固定/自適應索引粒度

前言

我們在剛開始學習ClickHouse的MergeTree引擎時,建表語句的末尾總會有SETTINGS index_granularity = 8192這句話(其實不寫也可以),表示索引粒度爲8192。在每個data part中,索引粒度參數的含義有二:

  • 每隔index_granularity行對主鍵組的數據進行採樣,形成稀疏索引,並存儲在primary.idx文件中;
  • 每隔index_granularity行對每一列的壓縮數據([column].bin)進行採樣,形成數據標記,並存儲在[column].mrk文件中。

index_granularity、primary.idx、[column].bin/mrk之間的關係可以用下面的簡圖來表示。

但是早在一年前的ClickHouse 19.11.8版本,社區就引入了自適應(adaptive)索引粒度的特性,並且在當前的版本中是默認開啓的。也就是說,主鍵索引和數據標記生成的間隔可以不再固定,更加靈活。下面通過實例來講解固定索引粒度和自適應索引粒度之間的不同之處。

固定索引粒度

利用Yandex.Metrica提供的hits_v1測試數據集,創建如下的表。

CREATE TABLE datasets.hits_v1_fixed
(
    `WatchID` UInt64,
    `JavaEnable` UInt8,
    `Title` String,
    -- A lot more columns...
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID))
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192, 
         index_granularity_bytes = 0;  -- Disable adaptive index granularity

注意使用SETTINGS index_granularity_bytes = 0取消自適應索引粒度。將測試數據導入之後,執行OPTIMIZE TABLE語句觸發merge,以方便觀察索引和標記數據。

來到merge完成後的數據part目錄中——筆者這裏是201403_1_32_3,並利用od(octal dump)命令觀察primary.idx中的內容。注意索引列一共有3列,Counter和intHash32(UserID)都是32位整形,EventDate是16位整形(Date類型存儲的是距離1970-01-01的天數)。

[root@ck-test001 201403_1_32_3]# od -An -i -j 0 -N 4 primary.idx 
          57  # Counter[0]
[root@ck-test001 201403_1_32_3]# od -An -d -j 4 -N 2 primary.idx 
 16146        # EventDate[0]
[root@ck-test001 201403_1_32_3]# od -An -i -j 6 -N 4 primary.idx 
    78076527  # intHash32(UserID)[0]
[root@ck-test001 201403_1_32_3]# od -An -i -j 10 -N 4 primary.idx 
        1635  # Counter[1]
[root@ck-test001 201403_1_32_3]# od -An -d -j 14 -N 2 primary.idx 
 16149        # EventDate[1]
[root@ck-test001 201403_1_32_3]# od -An -i -j 16 -N 4 primary.idx 
  1562260480  # intHash32(UserID)[1]
[root@ck-test001 201403_1_32_3]# od -An -i -j 20 -N 4 primary.idx 
        3266  # Counter[2]
[root@ck-test001 201403_1_32_3]# od -An -d -j 24 -N 2 primary.idx 
 16148        # EventDate[2]
[root@ck-test001 201403_1_32_3]# od -An -i -j 26 -N 4 primary.idx 
   490736209  # intHash32(UserID)[2]

能夠看出ORDER BY的第一關鍵字Counter確實是遞增的,但是不足以體現出index_granularity的影響。因此再觀察一下標記文件的內容,以8位整形的Age列爲例,比較簡單。

[root@ck-test001 201403_1_32_3]# od -An -l -j 0 -N 320 Age.mrk
                    0                    0
                    0                 8192
                    0                16384
                    0                24576
                    0                32768
                    0                40960
                    0                49152
                    0                57344
                19423                    0
                19423                 8192
                19423                16384
                19423                24576
                19423                32768
                19423                40960
                19423                49152
                19423                57344
                45658                    0
                45658                 8192
                45658                16384
                45658                24576

上面打印出了兩列數據,表示被選爲標記的行的兩個屬性:第一個屬性爲該行所處的壓縮數據塊在對應bin文件中的起始偏移量,第二個屬性爲該行在數據塊解壓後,在塊內部所處的偏移量,單位均爲字節。由於一條Age數據在解壓的情況下正好佔用1字節,所以能夠證明數據標記是按照固定index_granularity的規則生成的。

自適應索引粒度

創建同樣結構的表,寫入相同的測試數據,但是將index_granularity_bytes設爲1MB(爲了方便看出差異而已,默認值是10MB),以啓用自適應索引粒度。

CREATE TABLE datasets.hits_v1_adaptive
(
    `WatchID` UInt64,
    `JavaEnable` UInt8,
    `Title` String,
    -- A lot more columns...
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID))
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192, 
         index_granularity_bytes = 1048576;  -- Enable adaptive index granularity

index_granularity_bytes表示每隔表中數據的大小來生成索引和標記,且與index_granularity共同作用,只要滿足兩個條件之一即生成。

觸發merge之後,觀察primary.idx的數據。

[root@ck-test001 201403_1_32_3]# od -An -i -j 0 -N 4 primary.idx 
          57  # Counter[0]
[root@ck-test001 201403_1_32_3]# od -An -d -j 4 -N 2 primary.idx 
 16146        # EventDate[0]
[root@ck-test001 201403_1_32_3]# od -An -i -j 6 -N 4 primary.idx 
    78076527  # intHash32(UserID)[0]
[root@ck-test001 201403_1_32_3]# od -An -i -j 10 -N 4 primary.idx
          61  # Counter[1]
[root@ck-test001 201403_1_32_3]# od -An -d -j 14 -N 2 primary.idx
 16151        # EventDate[1]
[root@ck-test001 201403_1_32_3]# od -An -i -j 16 -N 4 primary.idx
  1579769176  # intHash32(UserID)[1]
[root@ck-test001 201403_1_32_3]# od -An -i -j 20 -N 4 primary.idx
          63  # Counter[2]
[root@ck-test001 201403_1_32_3]# od -An -d -j 24 -N 2 primary.idx
 16148        # EventDate[2]
[root@ck-test001 201403_1_32_3]# od -An -i -j 26 -N 4 primary.idx
  2037061113  # intHash32(UserID)[2]

通過Counter列的數據可見,主鍵索引明顯地變密集了,說明index_granularity_bytes的設定生效了。接下來仍然以Age列爲例觀察標記文件,注意文件擴展名變成了mrk2,說明啓用了自適應索引粒度。

[root@ck-test001 201403_1_32_3]# od -An -l -j 0 -N 2048 --width=24 Age.mrk2
                    0                    0                 1120
                    0                 1120                 1120
                    0                 2240                 1120
                    0                 3360                 1120
                    0                 4480                 1120
                    0                 5600                 1120
                    0                 6720                 1120
                    0                 7840                  352
                    0                 8192                 1111
                    0                 9303                 1111
                    0                10414                 1111
                    0                11525                 1111
                    0                12636                 1111
                    0                13747                 1111
                    0                14858                 1111
                    0                15969                  415
                    0                16384                 1096
# 略去一些
                17694                    0                 1102
                17694                 1102                 1102
                17694                 2204                 1102
                17694                 3306                 1102
                17694                 4408                 1102
                17694                 5510                 1102
                17694                 6612                  956
                17694                 7568                 1104
# ......

mrk2文件被格式化成了3列,前兩列的含義與mrk文件相同,而第三列的含義則是兩個標記之間相隔的行數。可以觀察到,每隔1100行左右就會生成一個標記(同時也說明該表內1MB的數據大約包含1100行)。同時,在偏移量計數達到8192、16384等8192的倍數時(即經過了index_granularity_bytes的倍數行),同樣也會生成標記,證明兩個參數是協同生效的。

最後一個問題:ClickHouse爲什麼要設計自適應索引粒度呢?當一行的數據量比較大時(比如達到了1kB甚至數kB),單純按照固定索引粒度會造成每個granule的數據量膨脹,拖累讀寫性能。有了自適應索引粒度之後,每個granule的數據量可以被控制在合理的範圍內,官方給定的默認值10MB在大多數情況下都不需要更改。

The End

民那晚安晚安。

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