以(CounterID, Date)
主鍵為例。在這種情況下,排序和索引可以說明如下:
Whole data: [---------------------------------------------] CounterID: [aaaaaaaaaaaaaaaaaabbbbcdeeeeeeeeeeeeefgggggggghhhhhhhhhiiiiiiiiikllllllll] Date: [1111111222222233331233211111222222333211111112122222223111112223311122333] Marks: | | | | | | | | | | | a,1 a,2 a,3 b,3 e,2 e,3 g,1 h,2 i,1 i,3 l,3 Marks numbers: 0 1 2 3 4 5 6 7 8 9 10
如果數據查詢指定:
CounterID in ('a', 'h')
[0, 3)
,服務器讀取標記和范圍內的數據[6, 8)
。CounterID IN ('a', 'h') AND Date = 3
[1, 3)
,服務器讀取標記和范圍內的數據[7, 8)
。Date = 3
,服務器讀取標記范圍內的數據[1, 10]
。
1)使用索引總是比完全掃描更有效。
2)稀疏索引允許讀取額外的數據。讀取單個范圍的主鍵時,index_granularity * 2
每個數據塊中最多可以讀取額外的行。
3)稀疏索引允許處理大量表行,因為在大多數情況下,此類索引適合計算機的 RAM。
4)ClickHouse 不需要唯一的主鍵。可以使用相同的主鍵插入多行。
可以在and子句中使用Nullable
-typed 表達式,但強烈建議不要這樣做。要允許此功能,請打開allow_nullable_key設置。NULLS_LAST原則適用於子句中的值。PRIMARY KEY
ORDER BY
NULL
ORDER BY
可以理解為一對的(CounterID、Date)間隔地生成了一個Marks,例如(a,1),(a,2);根據Marks又生成了相應的Marks numbers。(a,1),(a,2)這2個索引之間,間隔了好幾個數據,即:
(1)index_granularity這個參數規定了數據按照索引規定排序以后,間隔多少行會建立一個索引的Marks,即索引值
(2)稀疏索引的意義即是Clickhouse不對所以的列都建立索引(相比較Mysql的B樹索引會為每行都建立),而是間隔index_granularity列才建立一個。
(3)Marks與Marks number均被保存在內存中,利於查詢的時候快速檢索。
1.主鍵的選擇
主鍵中的列數沒有明確限制。根據數據結構,可以在主鍵中包含更多或更少的列。這可能:
-
提高索引的性能。
如果主鍵是,那么如果滿足以下條件
(a, b)
,則添加另一列將提高性能:c
- 有關於 column 條件的查詢
c
。 index_granularity
具有相同值的長數據范圍(比 長幾倍)(a, b)
很常見。換句話說,當添加另一列時,可以跳過相當長的數據范圍。
- 有關於 column 條件的查詢
-
改進數據壓縮。
ClickHouse 按主鍵對數據進行排序,一致性越高,壓縮效果越好。
-
在CollapsingMergeTree和SummingMergeTree引擎中合並數據部分時提供額外的邏輯。
在這種情況下,指定不同於主鍵的排序鍵是有意義的。
1)長主鍵會對插入性能和內存消耗產生負面影響,但主鍵中的額外列不會影響SELECT
查詢期間的 ClickHouse 性能。
2)ORDER BY tuple()
可以使用語法創建沒有主鍵的表。在這種情況下,ClickHouse 按插入順序存儲數據。如果要在通過INSERT ... SELECT
查詢插入數據時保存數據順序,請設置max_insert_threads = 1。
3)要按初始順序選擇數據,請使用單線程 SELECT
查詢。
操作更改排序鍵(但不影響主鍵):
ALTER TABLE [db].name [ON CLUSTER cluster] MODIFY ORDER BY new_expression
該命令將表的排序鍵new_expression
更改為(表達式或表達式元組)。主鍵保持不變。
從某種意義上說,該命令是輕量級的,它只更改元數據。要保持數據部分行按排序鍵表達式排序的屬性,您不能將包含現有列的表達式添加到排序鍵(僅ADD COLUMN
在同一ALTER
查詢中由命令添加的列,沒有默認列值)。
僅適用於MergeTree
族中的表(包括復制表)。
針對主鍵的延伸介紹
clickhouse會在每個分區目錄下生成一個索引文件primary.idx
,記錄了主鍵排序后按照索引粒度采樣的值,以二進制的方式存儲,可以通過od命令進行查看:
[ad@data1 ~/clickhouse/data/isv_data_prod/dm_order_today/202203_210_210_0]$ od -l -j 0 -N 80 --width=8 primary.idx 0000000 3545230323819229728 0000010 4048794554679177271 0000020 3617006580806400307 0000030 4121693288097986611 0000040 3688503285954781237 0000050 3689911760500897585 0000060 3616481000926753077 0000070 3617858594440033330 0000100 3472893445236864053 0000110 3905520523080577331 0000120
因為是稀疏索引,所以顯然只靠一級索引文件是無法精確定位到數據的,這時候就需要標記文件登場了。在分區目錄下,你可以看到很多后綴為.bin和.mrk2的文件,其中.bin是真實的數據內容,.mrk2就是標記文件。因為clickhouse底層是按列進行存儲的,因此每一列會對應一個.bin文件和.mrk2文件。
[ad@data1 ~/clickhouse/data/isv_data_prod/dm_order_today/202203_210_210_0]$ od -l -j 0 -N 240 --width=24 ./TRADE_TYPE.mrk2 0000000 0 0 7024 0000030 0 22801 7024 0000060 0 46070 7024 0000110 15454 0 7024 0000140 15454 22406 7024 0000170 15454 44990 7024 0000220 29733 0 7024 0000250 29733 22901 7024 0000300 29733 47127 7024 0000330 47062 0 7024 0000360
一行標記數據使用一個元組表示,元組內包含數據壓縮塊位置(在.bin文件中數據是切分成若干個數據塊壓縮存儲的),數據塊內偏移和索引粒度的大小。
檢索方式如圖,首先索引文件和標記文件在行上是對齊的,從上面索引文件和標記文件的示例可以看出來,二者的行數是一樣的
查詢的時候,會先根據要索引的值或范圍,在primary.idx文件中確定一個行號范圍(遞歸交集的判斷),然后按照相同的行號范圍在每一列的.mrk中查詢,得到要查詢的值在數據文件.bin的哪一個壓縮塊,以及將該壓縮塊解壓之后在什么位置,然后將查詢到的數據結果返回。
通過partition + 一級索引 + 標記文件,層層縮小數據掃描范圍,clickhouse達到了其快速檢索的目的。
如果沒有查詢條件命中索引的話clickhouse是怎么處理的呢?
掃描每個partition,不過因為.bin文件分了若干個小的壓縮塊,clickhouse利用多線程讀取壓縮塊的方式在一定程度上也可以加速查找過程。
2.選擇與排序鍵不同的主鍵
可以指定與排序鍵(用於對數據部分中的行進行排序的表達式)不同的主鍵(具有寫入索引文件中每個標記的值的表達式)。
在這種情況下,主鍵表達式元組必須是排序鍵表達式元組的前綴。
此功能在使用SummingMergeTree和 AggregatingMergeTree表引擎時很有幫助。在使用這些引擎的常見情況下,表有兩種類型的列:維度和度量。GROUP BY
典型的查詢聚合具有任意和按維度過濾的度量列的值。因為 SummingMergeTree 和 AggregatingMergeTree 聚合了具有相同排序鍵值的行,所以很自然地將所有維度添加到其中。因此,鍵表達式由一長串列組成,並且必須經常使用新添加的維度更新此列表。
在這種情況下,在主鍵中只保留幾列是有意義的,這將提供有效的范圍掃描,並將剩余的維度列添加到排序鍵元組中。
排序鍵的ALTER是一種輕量級操作,因為當一個新列同時添加到表和排序鍵時,不需要更改現有數據部分。由於舊排序鍵是新排序鍵的前綴,並且新添加的列中沒有數據,因此在修改表的那一刻,數據同時按新舊排序鍵排序。
3.在查詢中使用索引和分區
對於SELECT
查詢,ClickHouse 會分析是否可以使用索引。WHERE/PREWHERE
如果子句具有表示相等或不等比較操作的表達式(作為連接元素之一或完全),或者如果它在主鍵中的列或表達式上具有固定前綴IN
或LIKE
帶有固定前綴,則可以使用索引或分區鍵,或這些列的某些部分重復的功能,或這些表達式的邏輯關系。
因此,可以在一個或多個主鍵范圍上快速運行查詢。在此示例中,針對特定跟蹤標簽、特定標簽和日期范圍、特定標簽和日期、具有日期范圍的多個標簽等運行查詢將很快。
看一下配置如下的引擎:
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。
按月分區的鍵允許只讀取那些包含適當范圍內日期的數據塊。在這種情況下,數據塊可能包含許多日期(最多一整個月)的數據。在一個塊中,數據按主鍵排序,主鍵可能不包含日期作為第一列。因此,使用僅包含未指定主鍵前綴的日期條件的查詢將導致讀取比單個日期更多的數據。
4.單調主鍵索引
例如,考慮一個月中的幾天。它們形成一個月的單調序列,但在更長時間內不是單調的。這是一個部分單調的序列。如果用戶使用部分單調的主鍵創建表,ClickHouse 會像往常一樣創建稀疏索引。當用戶從此類表中選擇數據時,ClickHouse 會分析查詢條件。如果用戶想要獲取索引的兩個標記之間的數據,並且這兩個標記都在一個月內,ClickHouse 可以在這種特殊情況下使用索引,因為它可以計算查詢參數和索引標記之間的距離。
如果查詢參數范圍內的主鍵值不代表單調序列,ClickHouse 不能使用索引。在這種情況下,ClickHouse 使用全掃描方法。
ClickHouse 不僅對月份序列中的天數使用此邏輯,而且對表示部分單調序列的任何主鍵都使用此邏輯。
5.跳數索引
索引聲明位於CREATE
查詢的列部分。
INDEX index_name expr TYPE type(...) GRANULARITY granularity_value
GRANULARITY:跳數數據根據指的表達式聚合數據塊上的信息,聚合信息的粒度是由創建索引的時候指定GRANULARITY的值決定的。
對於*MergeTree
族中的表,可以指定跳數索引。
這些索引聚合了有關塊上指定表達式的一些信息,這些信息由granularity_value
顆粒組成(顆粒的大小使用index_granularity
表引擎中的設置指定)。然后在查詢中使用這些聚合,通過跳過無法滿足查詢SELECT
的大數據塊來減少從磁盤讀取的數據量。where
例子
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 可以使用示例中的索引來減少在以下查詢中從磁盤讀取的數據量:
SELECT count() FROM table WHERE s < 'z' SELECT count() FROM table WHERE u64 * i32 == 10 AND u64 * length(s) >= 1234
1)可用的索引類型
-
minmax
存儲指定表達式的極值(如果表達式是
tuple
,那么它存儲 的每個元素的極值tuple
),使用存儲的信息來跳過像主鍵這樣的數據塊。 -
set(max_rows)
存儲指定表達式的唯一值(不超過
max_rows
行,max_rows=0
表示“無限制”)。使用這些值來檢查WHERE
表達式是否在數據塊上不可滿足。 -
ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed)
存儲一個包含數據塊中所有 ngram的Bloom 過濾器。僅適用於數據類型:String、FixedString和Map。可用於優化
EQUALS
,LIKE
和IN
表達式。n
— ngram 大小,size_of_bloom_filter_in_bytes
— 布隆過濾器大小(以字節為單位)(可以在此處使用較大的值,例如 256 或 512,因為它可以很好地壓縮)。number_of_hash_functions
— 布隆過濾器中使用的哈希函數的數量。random_seed
— 布隆過濾器哈希函數的種子。
-
tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed)
與 相同
ngrambf_v1
,但存儲標記而不是 ngram。標記是由非字母數字字符分隔的序列。 -
bloom_filter([false_positive])
— 存儲指定列的布隆過濾器。可選
false_positive
參數是從過濾器接收到誤報響應的概率。可能的值:(0, 1)。默認值:0.025。支持的數據類型:
Int*
,UInt*
,Float*
,Enum
,Date
,DateTime
,String
,FixedString
,Array
,LowCardinality
,Nullable
,UUID
,Map
.對於數據類型,客戶端可以使用mapKeys或mapValues函數
Map
指定是否應為鍵或值創建索引。以下函數可以使用過濾器:equals、notEquals、in、notIn、has、hasAny、hasAll。
Map
數據類型的索引以及其他索引創建示例
INDEX map_key_index mapKeys(map_column) TYPE bloom_filter GRANULARITY 1 INDEX map_key_index mapValues(map_column) TYPE bloom_filter GRANULARITY 1
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
跳數索引應用示例:
①minmax
報錯的最大最小值(極值)。如下例子是2個列相乘,記錄其minmax值的跳數索引。
INDEX a (u64 * i32, s) TYPE minmax GRANULARITY 3,
適合場景
SELECT count() FROM table WHERE u64 * i32 == 10
②set
一組數量有限的(max_rows)的唯一數結果集。如下是一個字段和另一個字段長度的乘積。 最多有1000個不重復值。
INDEX b (u64 * length(s)) TYPE set(1000) GRANULARITY 4
適合場景
SELECT count() FROM table WHERE u64 * length(s) >= 1234
根據ngram切分的布隆過濾器。可用於優化 equals
, like
和 in
表達式的性能。
③ngrambf_v1
樣例
INDEX index_name (ID, Code) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 5; -- 3: token 長度,把數據切割成長度為 3 的短語 -- 256: 布隆過濾器大小 -- 2: 哈希函數個數 -- 0: 哈希函數隨機種子
適合場景
where code like '%123%'
④tokenbf_v1
與ngrambf_v1一樣,不同於 ngrams 存儲字符串指定長度的所有片段。它只存儲被非字母數字字符分割的片段。
INDEX d ID TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5
⑤bloom_filter
INDEX t_idx ip TYPE bloom_flter(0.025) GRANULARITY 5
2)功能支持
子句中的條件WHERE
包含對列操作的函數的調用。如果列是索引的一部分,ClickHouse 會在執行函數時嘗試使用該索引。ClickHouse 支持使用索引的不同函數子集。
該set
索引可用於所有功能。其他索引的函數子集如下表所示。
Function (operator) / Index | primary key | minmax | ngrambf_v1 | tokenbf_v1 | bloom_filter |
---|---|---|---|---|---|
equals (=, ==) | ✔ | ✔ | ✔ | ✔ | ✔ |
notEquals(!=, <>) | ✔ | ✔ | ✔ | ✔ | ✔ |
like | ✔ | ✔ | ✔ | ✔ | ✗ |
notLike | ✔ | ✔ | ✔ | ✔ | ✗ |
startsWith | ✔ | ✔ | ✔ | ✔ | ✗ |
endsWith | ✗ | ✗ | ✔ | ✔ | ✗ |
multiSearchAny | ✗ | ✗ | ✔ | ✗ | ✗ |
in | ✔ | ✔ | ✔ | ✔ | ✔ |
notIn | ✔ | ✔ | ✔ | ✔ | ✔ |
less (<) | ✔ | ✔ | ✗ | ✗ | ✗ |
greater (>) | ✔ | ✔ | ✗ | ✗ | ✗ |
lessOrEquals (<=) | ✔ | ✔ | ✗ | ✗ | ✗ |
greaterOrEquals (>=) | ✔ | ✔ | ✗ | ✗ | ✗ |
empty | ✔ | ✔ | ✗ | ✗ | ✗ |
notEmpty | ✔ | ✔ | ✗ | ✗ | ✗ |
hasToken | ✗ | ✗ | ✗ | ✔ | ✗ |
具有小於 ngram 大小的常量參數的函數不能ngrambf_v1
用於查詢優化。
!!! "注意" 布隆過濾器可能有誤報匹配,因此ngrambf_v1
,tokenbf_v1
和bloom_filter
索引不能用於優化預期函數結果為假的查詢,例如:
- 可優化:
s LIKE '%test%'
NOT s NOT LIKE '%test%'
s = 1
NOT s != 1
startsWith(s, 'test')
- 無法優化:
NOT s LIKE '%test%'
s NOT LIKE '%test%'
NOT s = 1
s != 1
NOT startsWith(s, 'test')
3)對歷史數據重建索引
建表語句:
CREATE TABLE ip_test(found_time String,ip String, INDEX ip_idx1(ip) TYPE minmax GRANULARITY 1, INDEX ip_idx2 ip TYPE bloom flter(0.025)0 GRANULARITY 1, INDEX ip_idx3 ip TYPE ngrambf_v1(3, 512, 5, 0) GRANULARITY 1, INDEX ip_idx4 ip TYPE tokenbf_v1(512, 5, 0) GRANULARITY 1, INDEX ip_idx5 ip TYPE set(0) GRANULARITY4 )ENGINE= MergeTree() order by found_time partition by toYYYYMMDD(toDateTime(found_time)) SETTINGS index_granularity= 4;
刪除索引:
ALTER TABLE ip_test DROP INDEX ip_idx1;
重建索引:重建索引(對歷史數據add索引的時候,只是改變了表的schema,實際的索引文件並沒有生成,需要再使用重建索引的語句對歷史數據建立索引):
1)添加索引
ALTER TABLE ip_test add INDEX ip_jidx2 ip TYPE bloom_ filter(0.025) GRANULARITY 1;
2)重建索引
ALTER TABLE ip_test MATERIALIZEINDEX ip_jidx2 [ IN PARTITION partition name]
6.總結
(1)雖然是稀疏索引,但是如果索引中的列過多,則根據索引來划分數據會更稀疏,建立的索引也需要更多,影響寫入性能,也會增加內存的使用。
(2)相比普通的B樹索引,稀疏索引需要的內存更少,但是可能導致需要掃描的行數比實際的多。
(3)官網推薦是不需要去改"8192"這個值。除非要做為索引的這個列的值分布非常非常集中,可能幾w行數據才可能變化一個取值,否則無需去做調大去建立更稀疏的索引,不過如果這個列這個集中的分布,也不大適合作為索引;如果要調小這個值,是會帶來索引列增加,但是同樣也會帶來內存使用增加、寫入性能受影響。
(4)有多個列組合做組合索引,選稀疏的值放在第一位。只能選擇一個列做單索引,如果有2個備選的值,要選比較稀疏的。通常需要滿足高級列在前、查詢頻率大的在前原則。基數特別大的不適合做索引列(可以對比上圖索引創建規則),如用戶表的userid字段
(5)通常篩選后的數據滿足在百萬以內為最佳。
(6)建表優化,創建字段的時候盡量不要使用nullable,日期盡量都使用date類型。