ClickHouse表引擎決定了:
- 數(shù)據(jù)的存儲(chǔ)方式和位置,寫到哪里以及從哪里讀取數(shù)據(jù);
- 支持哪些查詢以及如何支持;
- 并發(fā)數(shù)據(jù)訪問;
- 索引的使用(如果存在);
- 是否可以執(zhí)行多線程請(qǐng)求;
- 數(shù)據(jù)復(fù)制參數(shù);
1 - MergeTree
在大多數(shù)場景中, 我們所使用的引擎主要是 MergeTree 家族。MergeTree適用于高負(fù)載任務(wù)的最通用和功能最強(qiáng)大的表引擎。這些引擎的共同特點(diǎn)是可以快速插入數(shù)據(jù)并進(jìn)行后續(xù)的后臺(tái)數(shù)據(jù)處理。MergeTree系列引擎支持?jǐn)?shù)據(jù)復(fù)制(使用Replicated的引擎版本)。
該類型的引擎有:
- MergeTree
- ReplacingMergeTree
- SummingMergeTree
- AggregatingMergeTree
- CollapsingMergeTree
- VersionedCollapsingMergeTree
- GraphiteMergeTree
1.1 - MergeTree
Clickhouse 中最強(qiáng)大的表引擎當(dāng)屬 MergeTree (合并樹)引擎及該系列(*MergeTree)中的其他引擎。
MergeTree 引擎系列的基本理念如下。當(dāng)有巨量數(shù)據(jù)要插入到表中,要高效地一批批寫入數(shù)據(jù)片段,并希望這些數(shù)據(jù)片段在后臺(tái)按照一定規(guī)則合并。相比在插入時(shí)不斷修改(重寫)數(shù)據(jù)進(jìn)存儲(chǔ),這種策略會(huì)高效很多。
主要特點(diǎn):
- 存儲(chǔ)的數(shù)據(jù)按主鍵排序
- 允許使用分區(qū)
- 支持?jǐn)?shù)據(jù)副本,
ReplicatedMergeTree
系列的表便是用于此 - 支持?jǐn)?shù)據(jù)采樣
注意:Merge引擎不屬于MergeTree系列
建表
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
-
ENGINE
- 引擎名和參數(shù)。ENGINE = MergeTree()
,MergeTree
引擎沒有參數(shù)。 -
PARTITION BY
- 分區(qū)鍵。要按月分區(qū),可以使用表達(dá)式toYYYYMM(date_column)
。 -
ORDER BY
— 表的排序鍵。可以是一組列的元組或任意的表達(dá)式。 例如:ORDER BY (CounterID, EventDate)
。 -
PRIMARY KEY
- 主鍵,默認(rèn)情況下主鍵跟排序鍵(由ORDER BY子句指定)相同。因此,大部分情況下不需要再專門指定一個(gè)PRIMARY KEY子句。 -
SAMPLE BY
— 用于抽樣的表達(dá)式,如果要用抽樣表達(dá)式,主鍵中必須包含這個(gè)表達(dá)式。例如:SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID))
-
SETTINGS
— 影響MergeTree
性能的額外參數(shù):-
index_granularity
— 索引粒度。即索引中相鄰『標(biāo)記』間的數(shù)據(jù)行數(shù)。默認(rèn)值,8192 。 -
index_granularity_bytes
— 索引粒度,以字節(jié)為單位,默認(rèn)值: 10Mb。如果僅按數(shù)據(jù)行數(shù)限制索引粒度, 請(qǐng)?jiān)O(shè)置為0(不建議)。 -
enable_mixed_granularity_parts
— 啟用或禁用通過index_granularity_bytes
控制索引粒度的大小。在19.11版本之前, 只有index_granularity
配置能夠用于限制索引粒度的大小。當(dāng)從大表(數(shù)十或數(shù)百兆)中查詢數(shù)據(jù)時(shí)候,index_granularity_bytes
配置能夠提升ClickHouse的性能。如果你的表內(nèi)數(shù)據(jù)量很大,可以開啟這項(xiàng)配置用以提升SELECT 查詢的性能。 -
use_minimalistic_part_header_in_zookeeper
— 數(shù)據(jù)片段頭在 ZooKeeper 中的存儲(chǔ)方式。如果設(shè)置了use_minimalistic_part_header_in_zookeeper=1
,ZooKeeper 會(huì)存儲(chǔ)更少的數(shù)據(jù)。 -
min_merge_bytes_to_use_direct_io
— 使用直接 I/O 來操作磁盤的合并操作時(shí)要求的最小數(shù)據(jù)量。合并數(shù)據(jù)片段時(shí),ClickHouse 會(huì)計(jì)算要被合并的所有數(shù)據(jù)的總存儲(chǔ)空間。如果大小超過了min_merge_bytes_to_use_direct_io
設(shè)置的字節(jié)數(shù),則 ClickHouse 將使用直接 I/O 接口(O_DIRECT 選項(xiàng))對(duì)磁盤讀寫。如果設(shè)置min_merge_bytes_to_use_direct_io = 0
,則會(huì)禁用直接 I/O。默認(rèn)值:10 * 1024
-
merge_with_ttl_timeout
— TTL合并頻率的最小間隔時(shí)間。默認(rèn)值: 86400 (1 天) -
write_final_mark
— 啟用或禁用在數(shù)據(jù)片段尾部寫入最終索引標(biāo)記。默認(rèn)值: 1(不建議更改)
-
示例配置:
ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID) SETTINGS index_granularity=8192
示例中,按月分區(qū)。同時(shí)設(shè)置了一個(gè)按用戶ID哈希的抽樣表達(dá)式。這讓你可以有該表中每個(gè) CounterID
和 EventDate
下面的數(shù)據(jù)的偽隨機(jī)分布。如果你在查詢時(shí)指定了 SAMPLE 子句。 ClickHouse會(huì)返回對(duì)于用戶子集的一個(gè)均勻的偽隨機(jī)數(shù)據(jù)采樣。
數(shù)據(jù)存儲(chǔ)
表由按主鍵排序的數(shù)據(jù)片段組成。
當(dāng)數(shù)據(jù)被插入到表中時(shí),會(huì)分成數(shù)據(jù)片段并按主鍵的字典序排序。例如,主鍵是(CounterID, Date)
時(shí),片段中數(shù)據(jù)按 CounterID
排序,具有相同 CounterID
的部分按 Date
排序。
不同分區(qū)的數(shù)據(jù)會(huì)被分成不同的片段,ClickHouse 在后臺(tái)合并數(shù)據(jù)片段以便更高效存儲(chǔ)。不會(huì)合并來自不同分區(qū)的數(shù)據(jù)片段。這個(gè)合并機(jī)制并不保證相同主鍵的所有行都會(huì)合并到同一個(gè)數(shù)據(jù)片段中。
ClickHouse 會(huì)為每個(gè)數(shù)據(jù)片段創(chuàng)建一個(gè)索引文件,索引文件包含每個(gè)索引行(”標(biāo)記“)的主鍵值。索引行號(hào)定義為 n * index_granularity
。最大的 n 等于總行數(shù)除以 index_granularity
的值的整數(shù)部分。對(duì)于每列,跟主鍵相同的索引行處也會(huì)寫入”標(biāo)記“。這些”標(biāo)記“可以直接找到數(shù)據(jù)所在的列。
可以只用一單一大表并不斷地一塊塊往里面加入數(shù)據(jù) – MergeTree 引擎的就是為了這樣的場景。
主鍵和索引在查詢中的表現(xiàn)
以 (CounterID, Date)
為主鍵。排序好的索引的圖示會(huì)是下面這樣:
全部數(shù)據(jù) : [-------------------------------------------------------------------------]
CounterID: [aaaaaaaaaaaaaaaaaabbbbcdeeeeeeeeeeeeefgggggggghhhhhhhhhiiiiiiiiikllllllll]
Date: [1111111222222233331233211111222222333211111112122222223111112223311122333]
標(biāo)記: | | | | | | | | | | |
a,1 a,2 a,3 b,3 e,2 e,3 g,1 h,2 i,1 i,3 l,3
標(biāo)記號(hào): 0 1 2 3 4 5 6 7 8 9 10
如果指定查詢?nèi)缦拢?/p>
-
CounterID in ('a', 'h')
,服務(wù)器會(huì)讀取標(biāo)記號(hào)在[0, 3)
和[6, 8)
區(qū)間中的數(shù)據(jù)。 -
CounterID IN ('a', 'h') AND Date = 3
,服務(wù)器會(huì)讀取標(biāo)記號(hào)在[1, 3)
和[7, 8)
區(qū)間中的數(shù)據(jù)。 -
Date = 3
,服務(wù)器會(huì)讀取標(biāo)記號(hào)在[1, 10]
區(qū)間中的數(shù)據(jù)。
上面例子可以看出使用索引通常會(huì)比全表描述要高效。
稀疏索引會(huì)引起額外的數(shù)據(jù)讀取。當(dāng)讀取主鍵單個(gè)區(qū)間范圍的數(shù)據(jù)時(shí),每個(gè)數(shù)據(jù)塊中最多會(huì)多讀 index_granularity * 2
行額外的數(shù)據(jù)。大部分情況下,當(dāng) index_granularity = 8192
時(shí),ClickHouse的性能并不會(huì)降級(jí)。
稀疏索引能操作有巨量行的表。因?yàn)檫@些索引是常駐內(nèi)存(RAM)的。
ClickHouse 不要求主鍵惟一。所以,你可以插入多條具有相同主鍵的行。
主鍵的選擇
主鍵中列的數(shù)量并沒有明確的限制。依據(jù)數(shù)據(jù)結(jié)構(gòu),應(yīng)該讓主鍵包含多些或少些列。這樣可以:
- 改善索引的性能。
如果當(dāng)前主鍵是 (a, b),然后加入另一個(gè) c列,滿足下面條件時(shí),則可以改善性能:
- 有帶有 c列條件的查詢。
- 很長的數(shù)據(jù)范圍( index_granularity的數(shù)倍)里 (a, b)都是相同的值,并且這種的情況很普遍。換言之,就是加入另一列后,可以讓你的查詢略過很長的數(shù)據(jù)范圍。
- 改善數(shù)據(jù)壓縮。ClickHouse 以主鍵排序片段數(shù)據(jù),所以,數(shù)據(jù)的一致性越高,壓縮越好。
長的主鍵會(huì)對(duì)插入性能和內(nèi)存消耗有負(fù)面影響,但主鍵中額外的列并不影響 SELECT 查詢的性能。
選擇跟排序鍵不一樣主鍵
指定一個(gè)跟排序鍵(用于排序數(shù)據(jù)片段中行的表達(dá)式)不一樣的主鍵(用于計(jì)算寫到索引文件的每個(gè)標(biāo)記值的表達(dá)式)是可以的。這種情況下,主鍵表達(dá)式元組必須是排序鍵表達(dá)式元組的一個(gè)前綴。
索引和分區(qū)在查詢中的應(yīng)用
對(duì)于 SELECT
查詢,ClickHouse 分析是否可以使用索引。如果 WHERE/PREWHERE
子句具有下面這些表達(dá)式(作為謂詞鏈接一子項(xiàng)或整個(gè))則可以使用索引:
- 基于主鍵或分區(qū)鍵的列或表達(dá)式的部分的等式或比較運(yùn)算表達(dá)式;
- 基于主鍵或分區(qū)鍵的列或表達(dá)式的固定前綴的
IN
或LIKE
表達(dá)式; - 基于主鍵或分區(qū)鍵的列的某些函數(shù);
- 基于主鍵或分區(qū)鍵的表達(dá)式的邏輯表達(dá)式。
因此,在索引鍵的一個(gè)或多個(gè)區(qū)間上快速地跑查詢都是可能的。下面例子中,指定標(biāo)簽;指定標(biāo)簽和日期范圍;指定標(biāo)簽和日期;指定多個(gè)標(biāo)簽和日期范圍等運(yùn)行查詢,都會(huì)非常快。
ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate) SETTINGS index_granularity=8192
當(dāng)這種情況下,這些查詢:
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 會(huì)依據(jù)主鍵索引剪掉不符合的數(shù)據(jù),依據(jù)按月分區(qū)的分區(qū)鍵剪掉那些不包含符合數(shù)據(jù)的分區(qū)。上文的查詢顯示,即使索引用于復(fù)雜表達(dá)式。因?yàn)樽x表操作是組織好的,所以,使用索引不會(huì)比完整掃描慢。
下面這個(gè)例子中,不會(huì)使用索引。
SELECT count() FROM table WHERE CounterID = 34 OR URL LIKE '%upyachka%'
要檢查 ClickHouse 執(zhí)行一個(gè)查詢時(shí)能否使用索引,可設(shè)置force_index_by_date
和force_primary_key
。按月分區(qū)的分區(qū)鍵是只能讀取包含適當(dāng)范圍日期的數(shù)據(jù)塊。這種情況下,數(shù)據(jù)塊會(huì)包含很多天(最多整月)的數(shù)據(jù)。在塊中,數(shù)據(jù)按主鍵排序,主鍵第一列可能不包含日期。因此,僅使用日期而沒有帶主鍵前綴條件的查詢將會(huì)導(dǎo)致讀取超過這個(gè)日期范圍。
跳數(shù)索引
需要設(shè)置 allow_experimental_data_skipping_indices
為 1 才能使用此索引。(執(zhí)行 SET allow_experimental_data_skipping_indices = 1
)。
此索引在 CREATE
語句的列部分里定義。
INDEX index_name expr TYPE type(...) GRANULARITY granularity_value
*MergeTree
系列的表都能指定跳數(shù)索引。
這些索引是由數(shù)據(jù)塊按粒度分割后的每部分在指定表達(dá)式上匯總信息 granularity_value
組成(粒度大小用表引擎里 index_granularity
的指定)。
這些匯總信息有助于用 where
語句跳過大片不滿足的數(shù)據(jù),從而減少 SELECT
查詢從磁盤讀取的數(shù)據(jù)量。
示例
CREATE TABLE table_name
(
u64 UInt64,
i32 Int32,
s String,
...
INDEX a (u64 * i32, s) TYPE minmax GRANULARITY 3,
INDEX b (u64 * length(s)) TYPE set(1000) GRANULARITY 4
) ENGINE = MergeTree()
...
上例中的索引能讓 ClickHouse 執(zhí)行下面這些查詢時(shí)減少讀取數(shù)據(jù)量。
SELECT count() FROM table WHERE s < 'z'
SELECT count() FROM table WHERE u64 * i32 == 10 AND u64 * length(s) >= 1234
索引的可用類型
-
minmax
:存儲(chǔ)指定表達(dá)式的極值(如果表達(dá)式是tuple
,則存儲(chǔ)tuple
中每個(gè)元素的極值),這些信息用于跳過數(shù)據(jù)塊,類似主鍵。 -
set(max_rows)
:存儲(chǔ)指定表達(dá)式的惟一值(不超過max_rows
個(gè),max_rows=0
則表示“無限制”)。這些信息可用于檢查WHERE
表達(dá)式是否滿足某個(gè)數(shù)據(jù)塊。 -
ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed)
存儲(chǔ)包含數(shù)據(jù)塊中所有 n 元短語的布隆過濾器
。只可用在字符串上。可用于優(yōu)化equals
,like
和in
表達(dá)式的性能。-
n
– 短語長度 -
size_of_bloom_filter_in_bytes
– 布隆過濾器大小,單位字節(jié)。(因?yàn)閴嚎s得好,可以指定比較大的值,如256或512)。 -
number_of_hash_functions
– 布隆過濾器中使用的 hash 函數(shù)的個(gè)數(shù)。 -
random_seed
– hash 函數(shù)的隨機(jī)種子。 -
tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed)
跟ngrambf_v1
類似,不同于ngrams
存儲(chǔ)字符串指定長度的所有片段。它只存儲(chǔ)被非字母數(shù)據(jù)字符分割的片段。
-
INDEX sample_index (u64 * length(s)) TYPE minmax GRANULARITY 4
INDEX sample_index2 (u64 * length(str), i32 + f64 * 100, date, str) TYPE set(100) GRANULARITY 4
INDEX sample_index3 (lower(str), str) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 4
并發(fā)數(shù)據(jù)訪問
應(yīng)對(duì)表的并發(fā)訪問,使用多版本機(jī)制。換言之,當(dāng)同時(shí)讀和更新表時(shí),數(shù)據(jù)從當(dāng)前查詢到的一組片段中讀取。沒有冗長的的鎖。插入不會(huì)阻礙讀取。對(duì)表的讀操作是自動(dòng)并行的。
列和表的TTL
TTL可以設(shè)置值的生命周期,它既可以為整張表設(shè)置,也可以為每個(gè)列字段單獨(dú)設(shè)置。如果TTL同時(shí)作用于表和字段,ClickHouse會(huì)使用先到期的那個(gè)。
被設(shè)置TTL的表,必須擁有日期或日期時(shí)間類型的字段。要定義數(shù)據(jù)的生命周期,需要在這個(gè)日期字段上使用操作符,例如:
TTL time_column
TTL time_column + interval
要定義interval
, 需要使用時(shí)間間隔操作符。
TTL date_time + INTERVAL 1 MONTH
TTL date_time + INTERVAL 15 HOUR
列字段 TTL
當(dāng)列字段中的值過期時(shí), ClickHouse會(huì)將它們替換成數(shù)據(jù)類型的默認(rèn)值。如果分區(qū)內(nèi),某一列的所有值均已過期,則ClickHouse會(huì)從文件系統(tǒng)中刪除這個(gè)分區(qū)目錄下的列文件。
TTL
子句不能被用于主鍵字段。
示例說明:創(chuàng)建一張包含 TTL
的表
CREATE TABLE example_table
(
d DateTime,
a Int TTL d + INTERVAL 1 MONTH,
b Int TTL d + INTERVAL 1 MONTH,
c String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(d)
ORDER BY d;
為表中已存在的列字段添加 TTL
ALTER TABLE example_table
MODIFY COLUMN
c String TTL d + INTERVAL 1 DAY;
修改列字段的 TTL
ALTER TABLE example_table
MODIFY COLUMN
c String TTL d + INTERVAL 1 MONTH;
表 TTL
當(dāng)表內(nèi)的數(shù)據(jù)過期時(shí), ClickHouse會(huì)刪除所有對(duì)應(yīng)的行。
示例說明:創(chuàng)建一張包含 TTL
的表
CREATE TABLE example_table
(
d DateTime,
a Int
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(d)
ORDER BY d
TTL d + INTERVAL 1 MONTH;
修改表的 TTL
ALTER TABLE example_table
MODIFY TTL d + INTERVAL 1 DAY;
刪除數(shù)據(jù)
當(dāng)ClickHouse合并數(shù)據(jù)分區(qū)時(shí), 會(huì)刪除TTL過期的數(shù)據(jù)。
當(dāng)ClickHouse發(fā)現(xiàn)數(shù)據(jù)過期時(shí), 它將會(huì)執(zhí)行一個(gè)計(jì)劃外的合并。要控制這類合并的頻率, 可以設(shè)置 merge_with_ttl_timeout
。如果該值被設(shè)置的太低, 它將導(dǎo)致執(zhí)行許多的計(jì)劃外合并,這可能會(huì)消耗大量資源。
如果在合并的時(shí)候執(zhí)行SELECT
查詢, 則可能會(huì)得到過期的數(shù)據(jù)。為了避免這種情況,可以在SELECT
之前使用 OPTIMIZE
查詢。