前言
我們在剛開始學習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
民那晚安晚安。