ClickHouse MergeTree引擎

Clickhouse 中最強大的表引擎當屬 MergeTree (合併樹)引擎及該系列(*MergeTree)中的其他引擎。

MergeTree 系列的引擎被設計用於插入極大量的數據到一張表當中。數據可以以數據片段的形式一個接着一個的快速寫入,數據片段在後臺按照一定的規則進行合併。相比在插入時不斷修改(重寫)已存儲的數據,這種策略會高效很多。

主要特點:

  • 存儲的數據按主鍵排序。

這使得您能夠創建一個小型的稀疏索引來加快數據檢索。

  • 如果指定了 分區鍵 的話,可以使用分區。

在相同數據集和相同結果集的情況下 ClickHouse 中某些帶分區的操作會比普通操作更快。查詢中指定了分區鍵時 ClickHouse 會自動截取分區數據。這也有效增加了查詢性能。

  • 支持數據副本。

ReplicatedMergeTree 系列的表提供了數據副本功能。

  • 支持數據採樣。

需要的話,您可以給表設置一個採樣方法。

!!! note "注意" 合併 引擎並不屬於 *MergeTree 系列。

建表

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
    ...
    INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
    INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MergeTree()
ORDER BY expr
[PARTITION BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
[SETTINGS name=value, ...]

子句

  • ENGINE - 引擎名和參數。 ENGINE = MergeTree(). MergeTree 引擎沒有參數。

  • ORDER BY — 排序鍵。

可以是一組列的元組或任意的表達式。 例如: ORDER BY (CounterID, EventDate) 。

如果沒有使用 PRIMARY KEY 顯式指定的主鍵,ClickHouse 會使用排序鍵作爲主鍵。

如果不需要排序,可以使用 ORDER BY tuple(). 參考 選擇主鍵

  • PARTITION BY — 分區鍵 ,可選項。

大多數情況下,不需要分使用區鍵。即使需要使用,也不需要使用比月更細粒度的分區鍵。分區不會加快查詢(這與 ORDER BY 表達式不同)。永遠也別使用過細粒度的分區鍵。不要使用客戶端指定分區標識符或分區字段名稱來對數據進行分區(而是將分區字段標識或名稱作爲 ORDER BY 表達式的第一列來指定分區)。

要按月分區,可以使用表達式 toYYYYMM(date_column) ,這裏的 date_column 是一個 Date 類型的列。分區名的格式會是 "YYYYMM" 。

  • PRIMARY KEY - 如果要 選擇與排序鍵不同的主鍵,在這裏指定,可選項。

默認情況下主鍵跟排序鍵(由 ORDER BY 子句指定)相同。 因此,大部分情況下不需要再專門指定一個 PRIMARY KEY 子句。

  • SAMPLE BY - 用於抽樣的表達式,可選項。

如果要用抽樣表達式,主鍵中必須包含這個表達式。例如: SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID)) 。

  • TTL - 指定行存儲的持續時間並定義數據片段在硬盤和捲上的移動邏輯的規則列表,可選項。

表達式中必須存在至少一個 Date 或 DateTime 類型的列,比如:

TTL date + INTERVAl 1 DAY

規則的類型 DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'指定了當滿足條件(到達指定時間)時所要執行的動作:移除過期的行,還是將數據片段(如果數據片段中的所有行都滿足表達式的話)移動到指定的磁盤(TO DISK 'xxx') 或 卷(TO VOLUME 'xxx')。默認的規則是移除(DELETE)。可以在列表中指定多個規則,但最多隻能有一個DELETE的規則。

  • SETTINGS — 控制 MergeTree 行爲的額外參數,可選項:

    index_granularity — 索引粒度。索引中相鄰的『標記』間的數據行數。默認值8192 。參考數據存儲。
    index_granularity_bytes — 索引粒度,以字節爲單位,默認值: 10Mb。如果想要僅按數據行數限制索引粒度, 請設置爲0(不建議)。
    min_index_granularity_bytes - 允許的最小數據粒度,默認值:1024b。該選項用於防止誤操作,添加了一個非常低索引粒度的表。參考數據存儲
    enable_mixed_granularity_parts — 是否啓用通過 index_granularity_bytes 控制索引粒度的大小。在19.11版本之前, 只有 index_granularity 配置能夠用於限制索引粒度的大小。當從具有很大的行(幾十上百兆字節)的表中查詢數據時候,index_granularity_bytes 配置能夠提升ClickHouse的性能。如果您的表裏有很大的行,可以開啓這項配置來提升SELECT 查詢的性能。
    use_minimalistic_part_header_in_zookeeper — ZooKeeper中數據片段存儲方式 。如果use_minimalistic_part_header_in_zookeeper=1 ,ZooKeeper 會存儲更少的數據。更多信息參考[服務配置參數](Server Settings | ClickHouse Documentation)這章中的 設置描述 。
    min_merge_bytes_to_use_direct_io — 使用直接 I/O 來操作磁盤的合併操作時要求的最小數據量。合併數據片段時,ClickHouse 會計算要被合併的所有數據的總存儲空間。如果大小超過了 min_merge_bytes_to_use_direct_io 設置的字節數,則 ClickHouse 將使用直接 I/O 接口(O_DIRECT 選項)對磁盤讀寫。如果設置 min_merge_bytes_to_use_direct_io = 0 ,則會禁用直接 I/O。默認值:10 * 1024 * 1024 * 1024 字節。

    merge_with_ttl_timeout — TTL合併頻率的最小間隔時間,單位:秒。默認值: 86400 (1 天)。
    write_final_mark — 是否啓用在數據片段尾部寫入最終索引標記。默認值: 1(不要關閉)。
    merge_max_block_size — 在塊中進行合併操作時的最大行數限制。默認值:8192
    storage_policy — 存儲策略。 參見 使用具有多個塊的設備進行數據存儲.
    min_bytes_for_wide_part,min_rows_for_wide_part 在數據片段中可以使用Wide格式進行存儲的最小字節數/行數。您可以不設置、只設置一個,或全都設置。參考:數據存儲
    max_parts_in_total - 所有分區中最大塊的數量(意義不明)
    max_compress_block_size - 在數據壓縮寫入表前,未壓縮數據塊的最大大小。您可以在全局設置中設置該值(參見max_compress_block_size)。建表時指定該值會覆蓋全局設置。
    min_compress_block_size - 在數據壓縮寫入表前,未壓縮數據塊的最小大小。您可以在全局設置中設置該值(參見min_compress_block_size)。建表時指定該值會覆蓋全局設置。
    max_partitions_to_read - 一次查詢中可訪問的分區最大數。您可以在全局設置中設置該值(參見max_partitions_to_read)。

示例配置

ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID) SETTINGS index_granularity=8192

在這個例子中,我們設置了按月進行分區。

同時我們設置了一個按用戶 ID 哈希的抽樣表達式。這使得您可以對該表中每個 CounterIDEventDate 的數據僞隨機分佈。如果您在查詢時指定了 SAMPLE 子句。 ClickHouse會返回對於用戶子集的一個均勻的僞隨機數據採樣。

數據存儲

表由按主鍵排序的數據片段(DATA PART)組成。

當數據被插入到表中時,會創建多個數據片段並按主鍵的字典序排序。例如,主鍵是 (CounterID, Date) 時,片段中數據首先按 CounterID 排序,具有相同 CounterID 的部分按 Date 排序。

不同分區的數據會被分成不同的片段,ClickHouse 在後臺合併數據片段以便更高效存儲。不同分區的數據片段不會進行合併。合併機制並不保證具有相同主鍵的行全都合併到同一個數據片段中。

數據片段可以以 Wide 或 Compact 格式存儲。在 Wide 格式下,每一列都會在文件系統中存儲爲單獨的文件,在 Compact 格式下所有列都存儲在一個文件中。Compact 格式可以提高插入量少插入頻率頻繁時的性能。

數據存儲格式由 min_bytes_for_wide_part 和 min_rows_for_wide_part 表引擎參數控制。如果數據片段中的字節數或行數少於相應的設置值,數據片段會以 Compact 格式存儲,否則會以 Wide 格式存儲。

每個數據片段被邏輯的分割成顆粒(granules)。顆粒是 ClickHouse 中進行數據查詢時的最小不可分割數據集。ClickHouse 不會對行或值進行拆分,所以每個顆粒總是包含整數個行。每個顆粒的第一行通過該行的主鍵值進行標記, ClickHouse 會爲每個數據片段創建一個索引文件來存儲這些標記。對於每列,無論它是否包含在主鍵當中,ClickHouse 都會存儲類似標記。這些標記讓您可以在列文件中直接找到數據。

顆粒的大小通過表引擎參數 index_granularity 和 index_granularity_bytes 控制。顆粒的行數的在 [1, index_granularity] 範圍中,這取決於行的大小。如果單行的大小超過了 index_granularity_bytes 設置的值,那麼一個顆粒的大小會超過 index_granularity_bytes。在這種情況下,顆粒的大小等於該行的大小。

主鍵的選擇

主鍵中列的數量並沒有明確的限制。依據數據結構,您可以在主鍵包含多些或少些列。這樣可以:

  • 改善索引的性能。

  • 如果當前主鍵是 (a, b) ,在下列情況下添加另一個 c 列會提升性能:

  • 查詢會使用 c 列作爲條件

  • 很長的數據範圍( index_granularity 的數倍)裏 (a, b) 都是相同的值,並且這樣的情況很普遍。換言之,就是加入另一列後,可以讓您的查詢略過很長的數據範圍。

  • 改善數據壓縮。

    ClickHouse 以主鍵排序片段數據,所以,數據的一致性越高,壓縮越好。

  • 在CollapsingMergeTree 和 SummingMergeTree 引擎裏進行數據合併時會提供額外的處理邏輯。

    在這種情況下,指定與主鍵不同的 排序鍵 也是有意義的。

長的主鍵會對插入性能和內存消耗有負面影響,但主鍵中額外的列並不影響 SELECT 查詢的性能。

可以使用 ORDER BY tuple() 語法創建沒有主鍵的表。在這種情況下 ClickHouse 根據數據插入的順序存儲。如果在使用 INSERT ... SELECT 時希望保持數據的排序,請設置 max_insert_threads = 1。

想要根據初始順序進行數據查詢,使用 單線程查詢

選擇與排序鍵不同的主鍵

Clickhouse可以做到指定一個跟排序鍵不一樣的主鍵,此時排序鍵用於在數據片段中進行排序,主鍵用於在索引文件中進行標記的寫入。這種情況下,主鍵表達式元組必須是排序鍵表達式元組的前綴(即主鍵爲(a,b),排序列必須爲(a,b,**))。

當使用 SummingMergeTree 和 AggregatingMergeTree 引擎時,這個特性非常有用。通常在使用這類引擎時,表裏的列分兩種:維度 和 度量 。典型的查詢會通過任意的 GROUP BY 對度量列進行聚合並通過維度列進行過濾。由於 SummingMergeTree 和 AggregatingMergeTree 會對排序鍵相同的行進行聚合,所以把所有的維度放進排序鍵是很自然的做法。但這將導致排序鍵中包含大量的列,並且排序鍵會伴隨着新添加的維度不斷的更新。

在這種情況下合理的做法是,只保留少量的列在主鍵當中用於提升掃描效率,將維度列添加到排序鍵中。

對排序鍵進行 ALTER 是輕量級的操作,因爲當一個新列同時被加入到表裏和排序鍵裏時,已存在的數據片段並不需要修改。由於舊的排序鍵是新排序鍵的前綴,並且新添加的列中沒有數據,因此在表修改時的數據對於新舊的排序鍵來說都是有序的。

索引和分區在查詢中的應用

對於 SELECT 查詢,ClickHouse 分析是否可以使用索引。如果 WHERE/PREWHERE 子句具有下面這些表達式(作爲完整WHERE條件的一部分或全部)則可以使用索引:進行相等/不相等的比較;對主鍵列或分區列進行IN運算、有固定前綴的LIKE運算(如name like 'test%')、函數運算(部分函數適用),還有對上述表達式進行邏輯運算。

因此,在索引鍵的一個或多個區間上快速地執行查詢是可能的。下面例子中,指定標籤;指定標籤和日期範圍;指定標籤和日期;指定多個標籤和日期範圍等執行查詢,都會非常快。

當引擎配置如下時:

ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate) SETTINGS index_granularity=8192

這種情況下,這些查詢:

SELECT count() FROM table WHERE EventDate = toDate(now()) AND CounterID = 34
SELECT count() FROM table WHERE EventDate = toDate(now()) AND (CounterID = 34 OR CounterID = 42)
SELECT count() FROM table WHERE ((EventDate >= toDate('2014-01-01') AND EventDate <= toDate('2014-01-31')) OR EventDate = toDate('2014-05-01')) AND CounterID IN (101500, 731962, 160656) AND (CounterID = 101500 OR EventDate != toDate('2014-05-01'))

ClickHouse 會依據主鍵索引剪掉不符合的數據,依據按月分區的分區鍵剪掉那些不包含符合數據的分區。

上文的查詢顯示,即使索引用於複雜表達式,因爲讀表操作經過優化,所以使用索引不會比完整掃描慢。

下面這個例子中,不會使用索引。

SELECT count() FROM table WHERE CounterID = 34 OR URL LIKE '%upyachka%'

要檢查 ClickHouse 執行一個查詢時能否使用索引,可設置 force_index_by_date 和 force_primary_key 。

使用按月分區的分區列允許只讀取包含適當日期區間的數據塊,這種情況下,數據塊會包含很多天(最多整月)的數據。在塊中,數據按主鍵排序,主鍵第一列可能不包含日期。因此,僅使用日期而沒有用主鍵字段作爲條件的查詢將會導致需要讀取超過這個指定日期以外的數據。

函數支持

WHERE 子句中的條件可以包含對某列數據進行運算的函數表達式,如果列是索引的一部分,ClickHouse會在執行函數時嘗試使用索引。不同的函數對索引的支持是不同的。

set 索引會對所有函數生效,其他索引對函數的生效情況見下表

image-20221221165930093

併發數據訪問

對於表的併發訪問,我們使用多版本機制。換言之,當一張表同時被讀和更新時,數據從當前查詢到的一組片段中讀取。沒有冗長的的鎖。插入不會阻礙讀取。

對錶的讀操作是自動並行的。

表 TTL

表可以設置一個用於移除過期行的表達式,以及多個用於在磁盤或捲上自動轉移數據片段的表達式。當表中的行過期時,ClickHouse 會刪除所有對應的行。對於數據片段的轉移特性,必須所有的行都滿足轉移條件。

TTL expr
    [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'][, DELETE|TO DISK 'aaa'|TO VOLUME 'bbb'] ...
    [WHERE conditions]
    [GROUP BY key_expr [SET v1 = aggr_func(v1) [, v2 = aggr_func(v2) ...]] ]

TTL 規則的類型緊跟在每個 TTL 表達式後面,它會影響滿足表達式時(到達指定時間時)應當執行的操作:

  • DELETE - 刪除過期的行(默認操作);
  • TO DISK 'aaa' - 將數據片段移動到磁盤 aaa;
  • TO VOLUME 'bbb' - 將數據片段移動到卷 bbb.
  • GROUP BY - 聚合過期的行

使用WHERE從句,您可以指定哪些過期的行會被刪除或聚合(不適用於移動)。GROUP BY表達式必須是表主鍵的前綴。如果某列不是GROUP BY表達式的一部分,也沒有在SET從句顯示引用,結果行中相應列的值是隨機的(就好像使用了any函數)。

示例:

創建時指定 TTL

CREATE TABLE example_table
(
    d DateTime,
    a Int
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(d)
ORDER BY d
TTL d + INTERVAL 1 MONTH [DELETE],
    d + INTERVAL 1 WEEK TO VOLUME 'aaa',
    d + INTERVAL 2 WEEK TO DISK 'bbb';

修改表的 TTL

ALTER TABLE example_table
    MODIFY TTL d + INTERVAL 1 DAY;

創建一張表,設置一個月後數據過期,這些過期的行中日期爲星期一的刪除:

CREATE TABLE table_with_where
(
    d DateTime,
    a Int
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(d)
ORDER BY d
TTL d + INTERVAL 1 MONTH DELETE WHERE toDayOfWeek(d) = 1;

創建一張表,設置過期的列會被聚合。列x包含每組行中的最大值,y爲最小值,d爲可能任意值。

CREATE TABLE table_for_aggregation
(
    d DateTime,
    k1 Int,
    k2 Int,
    x Int,
    y Int
)
ENGINE = MergeTree
ORDER BY (k1, k2)
TTL d + INTERVAL 1 MONTH GROUP BY k1, k2 SET x = max(x), y = min(y);

刪除數據

ClickHouse 在數據片段合併時會刪除掉過期的數據。

當ClickHouse發現數據過期時, 它將會執行一個計劃外的合併。要控制這類合併的頻率, 您可以設置 merge_with_ttl_timeout。如果該值被設置的太低, 它將引發大量計劃外的合併,這可能會消耗大量資源。

如果在兩次合併的時間間隔中執行 SELECT 查詢, 則可能會得到過期的數據。爲了避免這種情況,可以在 SELECT 之前使用 OPTIMIZE 。

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