目前在ClickHouse中,按照特色能夠將表引擎大體分紅6個系列,分別是合併樹、外部存儲、內存、文件、接口和其餘,每個系列的表引擎都有着獨自的特色與使用場景。在它們之中,最爲核心的當屬MergeTree系列,由於它們擁有最爲強大的性能和最普遍的使用場合。算法
你們應該已經知道了MergeTree有兩層含義:數據庫
其一,表示合併樹表引擎家族;多線程
其二,表示合併樹家族中最基礎的MergeTree表引擎。函數
而在整個家族中,除了基礎表引擎MergeTree以外,經常使用的表引擎還有ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree和VersionedCollapsingMergeTree。每一種合併樹的變種,在繼承了基礎MergeTree的能力以後,又增長了獨有的特性。其名稱中的「合併」二字奠基了全部類型MergeTree的基因,它們的全部特殊邏輯,都是在觸發合併的過程當中被激活的。在本章後續的內容中,會逐一介紹它們的特色以及使用方法。性能
MergeTree做爲家族系列最基礎的表引擎,提供了數據分區、一級索引和二級索引等功能。測試
TTL即Time To Live,顧名思義,它表示數據的存活時間。在MergeTree中,能夠爲某個列字段或整張表設置TTL。當時間到達時,若是是列字段級別的TTL,則會刪除這一列的數據;若是是表級別的TTL,則會刪除整張表的數據;若是同時設置了列級別和表級別的TTL,則會以先到期的那個爲主。不管是列級別仍是表級別的TTL,都須要依託某個DateTime或Date類型的字段,經過對這個時間字段的INTERVAL操做,來表述TTL的過時時間,例如:大數據
TTL time_col + INTERVAL 3 DAY
上述語句表示數據的存活時間是time_col時間的3天以後。又例如:spa
TTL time_col + INTERVAL 1 MONTH
上述語句表示數據的存活時間是time_col時間的1月以後。INTERVAL完整的操做包括SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER和YEAR。線程
若是想要設置列級別的TTL,則須要在定義表字段的時候,爲它們聲明TTL表達式,主鍵字段不能被聲明TTL。如下面的語句爲例:設計
CREATE TABLE ttl_table_v1 ( id String, create_time DateTime, code String TTL create_time + INTERVAL 10 SECOND, type UInt8 TTL create_time + INTERVAL 10 SECOND ) ENGINE = MergeTree PARTITION BY toYYYYMM(create_time) ORDER BY id ;
其中,create_time是日期類型,列字段code與type均被設置了TTL,它們的存活時間是在create_time的取值基礎之上向後延續10秒。如今寫入測試數據,其中第一行數據create_time取當前的系統時間,而第二行數據的時間比第一行增長10分鐘:
SELECT * FROM ttl_table_v1;
接着心中默數10秒,而後執行optimize命令強制觸發TTL清理:
OPTIMIZE TABLE ttl_table_v1 FINAL;
再次查詢ttl_table_v1則可以看到,因爲第一行數據知足TTL過時條件(當前系統時間 >= create_time + 10秒),它們的code和type列會被還原爲數據類型的默認值:
若是想要修改列字段的TTL,或是爲已有字段添加TTL,則可使用ALTER語句,示例以下:
ALTER TABLE ttl_table_v1 MODIFY column code String TTL create_time + INTERVAL 1 DAY
目前ClickHouse沒有提供取消列級別TTL的方法。
表級別TTL
若是想要爲整張數據表設置TTL,須要在MergeTree的表參數中增長TTL表達式,例以下面的語句:
CREATE TABLE tt1_table_v2( id String, create_time DateTime, code String TTL create_time + INTERVAL 1 MINUTE , type UInt8 ) ENGINE = MergeTree PARTITION BY toYYYYMM(create_time) ORDER BY create_time TTL create_time + INTERVAL 1 DAY ;
ttl_table_v2整張表被設置了TTL,當觸發TTL清理時,那些知足過時時間的數據行將會被整行刪除。一樣,表級別的TTL也支持修改,修改的方法以下:
ALTER TABLE tt1_table_v2 MODIFY TTL create_time + INTERVAL 3 DAY;
表級別TTL目前也沒有取消的方法。
TTL的運行機理
在知道了列級別與表級別TTL的使用方法以後,如今簡單聊一聊TTL的運行機理。若是一張MergeTree表被設置了TTL表達式,那麼在寫入數據時,會以數據分區爲單位,在每一個分區目錄內生成一個名爲ttl.txt的文件。以剛纔示例中的ttl_table_v2爲例,它被設置了列級別TTL:
code String TTL create_time + INTERVAL 1 MINUTE
同時被設置了表級別的TTL:
TTL create_time + INTERVAL 1 DAY
那麼,在寫入數據以後,它的每一個分區目錄內都會生成ttl.txt文件:
進一步查看ttl.txt的內容:
經過上述操做會發現,原來MergeTree是經過一串JSON配置保存了TTL的相關信息,其中:
❑ columns用於保存列級別TTL信息;
❑ table用於保存表級別TTL信息;
❑ min和max則保存了當前數據分區內,TTL指定日期字段的最小值、最大值分別與INTERVAL表達式計算後的時間戳。
若是將table屬性中的min和max時間戳格式化,並分別與create_time最小與最大取值對比:
則可以印證,ttl.txt中記錄的極值區間剛好等於當前數據分區內create_time最小與最大值增長1天(1天= 86400秒)所表示的區間,與TTL表達式create_time +INTERVAL 1 DAY的預期相符。
在知道了TTL信息的記錄方式以後,如今看看它的大體處理邏輯。
(1)MergeTree以分區目錄爲單位,經過ttl.txt文件記錄過時時間,並將其做爲後續的判斷依據。
(2)每當寫入一批數據時,都會基於INTERVAL表達式的計算結果爲這個分區生成ttl. txt文件。
(3)只有在MergeTree合併分區時,纔會觸發刪除TTL過時數據的邏輯。
(4)在選擇刪除的分區時,會使用貪婪算法,它的算法規則是儘量找到會最先過時的,同時年紀又是最老的分區(合併次數更多,MaxBlockNum更大的)。
(5)若是一個分區內某一列數據由於TTL到期所有被刪除了,那麼在合併以後生成的新分區目錄中,將不會包含這個列字段的數據文件(.bin和.mrk)。
這裏還有幾條TTL使用的小貼士。
(1)TTL默認的合併頻率由MergeTree的merge_with_ttl_timeout參數控制,默認86400秒,即1天。它維護的是一個專有的TTL任務隊列。有別於MergeTree的常規合併任務,若是這個值被設置的太小,可能會帶來性能損耗。
(2)除了被動觸發TTL合併外,也可使用optimize命令強制觸發合併。例如,觸發一個分區合併:
optimize TABLE table_name;
觸發全部分區合併:
optimize TABLE table_name FINAL;
(3)ClickHouse目前雖然沒有提供刪除TTL聲明的方法,可是提供了控制全局TTL合併任務的啓停方法:
SYSTEM STOP/START TTL MERGES;
雖然還不能作到按每張MergeTree數據表啓停,但聊勝於無吧。
雖然MergeTree擁有主鍵,可是它的主鍵卻沒有惟一鍵的約束。這意味着即使多行數據的主鍵相同,它們仍是可以被正常寫入。在某些使用場合,用戶並不但願數據表中含有重複的數據。ReplacingMergeTree就是在這種背景下爲了數據去重而設計的,它可以在合併分區時刪除重複的數據。它的出現,確實也在必定程度上解決了重複數據的問題。爲何說是「必定程度」?此處先按下不表。
建立一張ReplacingMergeTree表的方法與建立普通MergeTree表無異,只須要替換Engine:
ENGINE = ReplacingMergeTree(ver)
其中,ver是選填參數,會指定一個UInt*、Date或者DateTime類型的字段做爲版本號。這個參數決定了數據去重時所使用的算法。
接下來,用一個具體的示例說明它的用法。首先執行下面的語句建立數據表:
CREATE TABLE replace_table( id String, code String, create_time DateTime ) ENGINE = ReplacingMergeTree() partition by toYYYYMM(create_time) ORDER BY(id,code) PRIMARY KEY id ;
注意這裏的ORDER BY是去除重複數據的關鍵,排序鍵ORDER BY所聲明的表達式是後續做爲判斷數據是否重複的依據。在這個例子中,數據會基於id和code兩個字段去重。假設此時表內的測試數據以下:
那麼在執行optimize強制觸發合併後,會按照id和code分組,保留分組內的最後一條(觀察create_time日期字段):
optimize TABLE replace_table FINAL;
將其他重複的數據刪除:
從執行的結果來看,ReplacingMergeTree在去除重複數據時,確實是以ORDERBY排序鍵爲基準的,而不是PRIMARY KEY。由於在上面的例子中,ORDER BY是(id, code),而PRIMARY KEY是id,若是按照id值去除重複數據,則最終結果應該只剩下A00一、A002和A003三行數據。
到目前爲止,ReplacingMergeTree看起來完美地解決了重複數據的問題。事實果然如此嗎?如今嘗試寫入一批新數據:
insert into replace_table values ('A001','C1','2020-07-02 12:01:01');
寫入以後,執行optimize強制分區合併,並查詢數據:
再次觀察返回的數據,能夠看到A001:C1依然出現了重複。這是怎麼回事呢?這是由於ReplacingMergeTree是以分區爲單位刪除重複數據的。只有在相同的數據分區內重複的數據才能夠被刪除,而不一樣數據分區之間的重複數據依然不能被剔除。這就是上面說ReplacingMergeTree只是在必定程度上解決了重複數據問題的緣由。
如今接着說明ReplacingMergeTree版本號的用法。如下面的語句爲例:
CREATE TABLE replace_table_v ( id String, code String, create_time DateTime ) ENGINE = ReplacingMergeTree(create_time) PARTITION BY toYYYYMM(create_time) ORDER BY id ;
replace_table_v基於id字段去重,而且使用create_time字段做爲版本號,假設表內的數據以下所示:
那麼在刪除重複數據的時候,會保留同一組數據內create_time時間最長的那一行:
在知道了ReplacingMergeTree的使用方法後,如今簡單梳理一下它的處理邏輯。
(1)使用ORBER BY排序鍵做爲判斷重複數據的惟一鍵。
(2)只有在合併分區的時候纔會觸發刪除重複數據的邏輯。
(3)以數據分區爲單位刪除重複數據。當分區合併時,同一分區內的重複數據會被刪除;不一樣分區之間的重複數據不會被刪除。
(4)在進行數據去重時,由於分區內的數據已經基於ORBER BY進行了排序,因此可以找到那些相鄰的重複數據。
(5)數據去重策略有兩種:
❑ 若是沒有設置ver版本號,則保留同一組重複數據中的最後一行。
❑ 若是設置了ver版本號,則保留同一組重複數據中ver字段取值最大的那一行。
假設有這樣一種查詢需求:終端用戶只須要查詢數據的彙總結果,不關心明細數據,而且數據的彙總條件是預先明確的(GROUP BY條件明確,且不會隨意改變)。
對於這樣的查詢場景,在ClickHouse中如何解決呢?最直接的方案就是使用MergeTree存儲數據,而後經過GROUP BY聚合查詢,並利用SUM聚合函數彙總結果。這種方案存在兩個問題。
❑ 存在額外的存儲開銷:終端用戶不會查詢任何明細數據,只關心彙總結果,因此不該該一直保存全部的明細數據。
❑ 存在額外的查詢開銷:終端用戶只關心彙總結果,雖然MergeTree性能強大,可是每次查詢都進行實時聚合計算也是一種性能消耗。
SummingMergeTree就是爲了應對這類查詢場景而生的。顧名思義,它可以在合併分區的時候按照預先定義的條件聚合彙總數據,將同一分組下的多行數據彙總合併成一行,這樣既減小了數據行,又下降了後續彙總查詢的開銷。
在先前介紹MergeTree原理時曾說起,在MergeTree的每一個數據分區內,數據會按照ORDER BY表達式排序。主鍵索引也會按照PRIMARY KEY表達式取值並排序。而ORDER BY能夠指代主鍵,因此在通常情形下,只單獨聲明ORDER BY便可。此時,ORDER BY與PRIMARY KEY定義相同,數據排序與主鍵索引相同。
若是須要同時定義ORDER BY與PRIMARY KEY,一般只有一種可能,那即是明確但願ORDER BY與PRIMARY KEY不一樣。這種狀況一般只會在使用SummingMergeTree或AggregatingMergeTree時纔會出現。這是爲什麼呢?這是由於SummingMergeTree與AggregatingMergeTree的聚合都是根據ORDER BY進行的。由此能夠引出兩點緣由:主鍵與聚合的條件定義分離,爲修改聚合條件留下空間。
如今用一個示例說明。假設一張SummingMergeTree數據表有A、B、C、D、E、F六個字段,若是須要按照A、B、C、D彙總,則有:
ORDER BY (A,B,C,D)
可是如此一來,此表的主鍵也被定義成了A、B、C、D。而在業務層面,其實只須要對字段A進行查詢過濾,應該只使用A字段建立主鍵。因此,一種更加優雅的定義形式應該是:
ORDER BY (A,B,C,D) PRIMARY KEY A
若是同時聲明瞭ORDER BY與PRIMARY KEY, MergeTree會強制要求PRIMARYKEY列字段必須是ORDER BY的前綴。例以下面的定義是錯誤的:
ORDER BY(B,C) PRIMARY KEY A
PRIMARY KEY必須是ORDER BY的前綴:
ORDER BY (B,C) PRIMARY KEY B
這種強制約束保障了即使在二者定義不一樣的狀況下,主鍵仍然是排序鍵的前綴,不會出現索引與數據順序混亂的問題。
假設如今業務發生了細微的變化,須要減小字段,將先前的A、B、C、D改成按照A、B聚合彙總,則能夠按以下方式修改排序鍵:
ALTER TABLE table_name MODIFY ORDER BY (A,B)
在修改ORDER BY時會有一些限制,只能在現有的基礎上減小字段。若是是新增排序字段,則只能添加經過ALTER ADD COLUMN新增的字段。可是ALTER是一種元數據的操做,修改爲本很低,相比不能被修改的主鍵,這已經很是便利了。
如今開始正式介紹SummingMergeTree的使用方法。表引擎的聲明方式以下所示:
ENGINE = SummingMergeTree((col1,col2,...))
其中,col一、col2爲columns參數值,這是一個選填參數,用於設置除主鍵外的其餘數值類型字段,以指定被SUM彙總的列字段。如若不填寫此參數,則會將全部非主鍵的數值類型字段進行SUM彙總。接來下用一組示例說明它的使用方法:
CREATE TABLE summing_table( id String, city String, v1 UInt32, v2 Float64, create_time DateTime ) ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(create_time) ORDER BY (id,city) PRIMARY KEY id ;
注意,這裏的ORDER BY是一項關鍵配置,SummingMergeTree在進行數據彙總時,會根據ORDER BY表達式的取值進行聚合操做。假設此時表內的數據以下所示:
執行optimize強制進行觸發和合並操做:
optimize TABLE summing_table FINAL
再次查詢,表內數據會變成下面的樣子:
至此可以看到,在第一個分區內,同爲A001:wuhan的兩條數據彙總成了一行。其中,v1和v2被SUM彙總,不在彙總字段之列的create_time則選取了同組內第一行數據的取值。而不一樣分區之間,數據沒有被彙總合併。
SummingMergeTree也支持嵌套類型的字段,在使用嵌套類型字段時,須要被SUM彙總的字段名稱必須以Map後綴結尾,例如:
CREATE TABLE summing_table_nested( id1 String, nestMap Nested( id UInt32, key UInt32, val UInt64 ), create_time DateTime ) ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(create_time) ORDER BY id1 ;
在使用嵌套數據類型的時候,默認狀況下,會以嵌套類型中第一個字段做爲聚合條件Key。假設表內的數據以下所示:
上述示例中數據會按照第一個字段id聚合,彙總後的數據會變成下面的樣子:
數據彙總的邏輯示意以下所示:
在使用嵌套數據類型的時候,也支持使用複合Key做爲數據聚合的條件。爲了使用複合Key,在嵌套類型的字段中,除第一個字段之外,任何名稱是以Key、Id或Type爲後綴結尾的字段,都將和第一個字段一塊兒組成複合Key。例如將上面的例子中小寫key改成Key:
上述例子中數據會以id和Key做爲聚合條件。在知道了SummingMergeTree的使用方法後,如今簡單梳理一下它的處理邏輯。
(1)用ORBER BY排序鍵做爲聚合數據的條件Key。
(2)只有在合併分區的時候纔會觸發彙總的邏輯。
(3)以數據分區爲單位來聚合數據。當分區合併時,同一數據分區內聚合Key相同的數據會被合併彙總,而不一樣分區之間的數據則不會被彙總。(4)若是在定義引擎時指定了columns彙總列(非主鍵的數值類型字段),則SUM彙總這些列字段;若是未指定,則聚合全部非主鍵的數值類型字段。
(5)在進行數據彙總時,由於分區內的數據已經基於ORBER BY排序,因此可以找到相鄰且擁有相同聚合Key的數據。
(6)在彙總數據時,同一分區內,相同聚合Key的多行數據會合併成一行。其中,彙總字段會進行SUM計算;對於那些非彙總字段,則會使用第一行數據的取值。
(7)支持嵌套結構,但列字段名稱必須以Map後綴結尾。嵌套類型中,默認以第一個字段做爲聚合Key。除第一個字段之外,任何名稱以Key、Id或Type爲後綴結尾的字段,都將和第一個字段一塊兒組成複合Key。
有過數據倉庫建設經驗的讀者必定知道「數據立方體」的概念,這是一個在數據倉庫領域十分常見的模型。它經過以空間換時間的方法提高查詢性能,將須要聚合的數據預先計算出來,並將結果保存起來。在後續進行聚合查詢的時候,直接使用結果數據。
AggregatingMergeTree就有些許數據立方體的意思,它可以在合併分區的時候,按照預先定義的條件聚合數據。同時,根據預先定義的聚合函數計算數據並經過二進制的格式存入表內。將同一分組下的多行數據聚合成一行,既減小了數據行,又下降了後續聚合查詢的開銷。能夠說,AggregatingMergeTree是SummingMergeTree的升級版,它們的許多設計思路是一致的,例如同時定義ORDER BY與PRIMARY KEY的緣由和目的。可是在使用方法上,二者存在明顯差別,應該說AggregatingMergeTree的定義方式是MergeTree家族中最爲特殊的一個。
聲明使用AggregatingMergeTree的方式以下:
ENGINE = AggrefatingMergeTree()
AggregatingMergeTree沒有任何額外的設置參數,在分區合併時,在每一個數據分區內,會按照ORDER BY聚合。而使用何種聚合函數,以及針對哪些列字段計算,則是經過定義AggregateFunction數據類型實現的。如下面的語句爲例:
CREATE TABLE agg_table ( id String, city String, code AggregateFunction(uniq,String), value AggregateFunction(sum,UInt32), create_time DateTime ) ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMM(create_time) ORDER BY(id,city) PRIMARY KEY id ;
上例中列字段id和city是聚合條件,等同於下面的語義:
GROUP BY id,city
而code和value是聚合字段,其語義等同於:
UNIQ(code),SUM(value)
AggregateFunction是ClickHouse提供的一種特殊的數據類型,它可以以二進制的形式存儲中間狀態結果。其使用方法也十分特殊,對於AggregateFunction類型的列字段,數據的寫入和查詢都與尋常不一樣。在寫入數據時,須要調用State函數;而在查詢數據時,則須要調用相應的Merge函數。其中,*表示定義時使用的聚合函數。例如示例中定義的code和value,使用了uniq和sum函數:
code AggregateFunction(uniq,String), value AggregateFunction(sum,UInt32),
那麼,在寫入數據時須要調用與uniq、sum對應的uniqState和sumState函數,並使用INSERT SELECT語法:
INSERT INTO TABLE agg_table SELECT 'A000','wuhan', uniqState('code'), sumState(toUInt32(200)), now();
在查詢數據時,若是直接使用列名訪問code和value,將會是沒法顯示的二進制形式。此時,須要調用與uniq、sum對應的uniqMerge、sumMerge函數:
SELECT id,city,uniqMerge(code),sumMerge(value) FROM agg_table GROUP BY id,city;
講到這裏,你是否會認爲AggregatingMergeTree使用起來過於煩瑣了?連正常進行數據寫入都須要藉助INSERT…SELECT的句式並調用特殊函數。若是直接像剛纔示例中那樣使用AggregatingMergeTree,確實會很是麻煩。不過各位讀者並不須要憂慮,由於目前介紹的這種使用方法,並非它的主流用法。
AggregatingMergeTree更爲常見的應用方式是結合物化視圖使用,將它做爲物化視圖的表引擎。而這裏的物化視圖是做爲其餘數據表上層的一種查詢視圖
如今用一組示例說明。首先,創建明細數據表,也就是俗稱的底表:
CREATE TABLE agg_table_basic( id String, city String, code String, value UInt32 ) ENGINE = MergeTree() PARTITION BY city ORDER BY(id,city);
一般會使用MergeTree做爲底表,用於存儲全量的明細數據,並以此對外提供實時查詢。接着,新建一張物化視圖:
CREATE MATERIALIZED VIEW agg_view ENGINE = AggregatingMergeTree() PARTITION BY city ORDER BY (id,city) AS SELECT id,city, uniqState(code) AS code, sumState(value) as value FROM agg_table_basic group by id,city;
物化視圖使用AggregatingMergeTree表引擎,用於特定場景的數據查詢,相比MergeTree,它擁有更高的性能。
在新增數據時,面向的對象是底表MergeTree:
INSERT INTO TABLE agg_table_basic VALUES('A000','wuhan','code1',100), ('A000','wuhan','code2',200), ('A000','zhuhai','code1',200) ;
數據會自動同步到物化視圖,並按照AggregatingMergeTree引擎的規則處理。
在查詢數據時,面向的對象是物化視圖AggregatingMergeTree:
SELECT id,sumMerge(value),uniqMerge(code) FROM agg_view GROUP BY id,city ;
接下來,簡單梳理一下AggregatingMergeTree的處理邏輯。
(1)用ORBER BY排序鍵做爲聚合數據的條件Key。
(2)使用AggregateFunction字段類型定義聚合函數的類型以及聚合的字段。
(3)只有在合併分區的時候纔會觸發聚合計算的邏輯。
(4)以數據分區爲單位來聚合數據。當分區合併時,同一數據分區內聚合Key相同的數據會被合併計算,而不一樣分區之間的數據則不會被計算。(5)在進行數據計算時,由於分區內的數據已經基於ORBER BY排序,因此可以找到那些相鄰且擁有相同聚合Key的數據。
(6)在聚合數據時,同一分區內,相同聚合Key的多行數據會合併成一行。對於那些非主鍵、非AggregateFunction類型字段,則會使用第一行數據的取值。
(7)AggregateFunction類型的字段使用二進制存儲,在寫入數據時,須要調用State函數;而在查詢數據時,則須要調用相應的Merge函數。其中,*表示定義時使用的聚合函數。
(8)AggregatingMergeTree一般做爲物化視圖的表引擎,與普通MergeTree搭配使用。
假設如今須要設計一款數據庫,該數據庫支持對已經存在的數據實現行級粒度的修改或刪除,你會怎麼設計?一種最符合常理的思惟多是:首先找到保存數據的文件,接着修改這個文件,刪除或者修改那些須要變化的數據行。然而在大數據領域,對於ClickHouse這類高性能分析型數據庫而言,對數據源文件修改是一件很是奢侈且代價高昂的操做。相較於直接修改源文件,它們會將修改和刪除操做轉換成新增操做,即以增代刪。
CollapsingMergeTree就是一種經過以增代刪的思路,支持行級數據修改和刪除的表引擎。它經過定義一個sign標記位字段,記錄數據行的狀態。若是sign標記爲1,則表示這是一行有效的數據;若是sign標記爲-1,則表示這行數據須要被刪除。當CollapsingMergeTree分區合併時,同一數據分區內,sign標記爲1和-1的一組數據會被抵消刪除。這種1和-1相互抵消的操做,猶如將一張瓦楞紙摺疊了通常。這種直觀的比喻,想必也正是摺疊合併樹(CollapsingMergeTree)名稱的由來,其摺疊的過程如圖所示。
聲明CollapsingMergeTree的方式以下:
ENGINE = CollapsingMergeTree(sign)
其中,sign用於指定一個Int8類型的標誌位字段。一個完整的使用示例以下所示:
CREATE TABLE collpase_table( id String, code Int32, create_time DateTime, sign Int8 ) ENGINE = CollapsingMergeTree(sign) PARTITION BY toYYYYMM(create_time) ORDER BY id ;
與其餘的MergeTree變種引擎同樣,CollapsingMergeTree一樣是以ORDER BY排序鍵做爲後續判斷數據惟一性的依據。按照以前的介紹,對於上述collpase_table數據表而言,除了常規的新增數據操做以外,還可以支持兩種操做。
其一,修改一行數據:
--修改前的源數據,它須要被修改 INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1); --鏡像數據,ORDER BY字段與源數據相同(其餘字段能夠不一樣),sign取反爲-1,它會和源數據摺疊 INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',-1); --修改後的數據,sign爲1 INSERT INTO TABLE collpase_table VALUES('A000',120,'2019-02-20 00:00:00',1);
其二,刪除一行數據:
--修改前的源數據,它須要被修改 INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',1); --鏡像數據,ORDER BY字段與源數據相同(其餘字段能夠不一樣),sign取反爲-1,它會和源數據摺疊 INSERT INTO TABLE collpase_table VALUES('A000',100,'2019-02-20 00:00:00',-1);
CollapsingMergeTree在摺疊數據時,遵循如下規則。
❑ 若是sign=1比sign=-1的數據多一行,則保留最後一行sign=1的數據。
❑ 若是sign=-1比sign=1的數據多一行,則保留第一行sign=-1的數據。❑ 若是sign=1和sign=-1的數據行同樣多,而且最後一行是sign=1,則保留第一行sign=-1和最後一行sign=1的數據。
❑ 若是sign=1和sign=-1的數據行同樣多,而且最後一行是sign=-1,則什麼也不保留。
❑ 其他狀況,ClickHouse會打印警告日誌,但不會報錯,在這種情形下,查詢結果不可預知。在使用CollapsingMergeTree的時候,還有幾點須要注意。
在使用CollapsingMergeTree的時候,還有幾點須要注意。
(1)摺疊數據並非實時觸發的,和全部其餘的MergeTree變種表引擎同樣,這項特性也只有在分區合併的時候纔會體現。因此在分區合併以前,用戶仍是會看到舊的數據。解決這個問題的方式有兩種。
❑ 在查詢數據以前,使用optimize TABLE table_name FINAL命令強制分區合併,可是這種方法效率極低,在實際生產環境中慎用。
❑ 須要改變咱們的查詢方式。以collpase_table舉例,若是原始的SQL以下所示:
SELECT id,SUM(code),COUNT(code),AVG(code),uniq(code) FROM collpase_table GROUP BY id ;
則須要改寫成以下形式:
SELECT id,SUM(code * sign),COUNT(code * sign),AVG(code * sign),UNIQ(code * sign) FROM collpase_table GROUP BY id HAVING SUM(sign) > 0;
(2)只有相同分區內的數據纔有可能被摺疊。不過這項限制對於CollapsingMergeTree來講一般不是問題,由於修改或者刪除數據的時候,這些數據的分區規則一般都是一致的,並不會改變。
(3)最後這項限制多是CollapsingMergeTree最大的命門所在。CollapsingMergeTree對於寫入數據的順序有着嚴格要求。如今用一個示例說明。若是按照正常順序寫入,先寫入sign=1,再寫入sign=-1,則可以正常摺疊:
INSERT INTO TABLE collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1); INSERT INTO TABLE collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1);
SELECT * FROM collpase_table;
optimize table collpase_table FINAL; SELECT * FROM collpase_table;
如今將寫入的順序置換,先寫入sign=-1,再寫入sign=1,則不可以摺疊:
INSERT INTO TABLE collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1); INSERT INTO TABLE collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1);
這種現象是CollapsingMergeTree的處理機制引發的,由於它要求sign=1和sign=-1的數據相鄰。而分區內的數據基於ORBER BY排序,要實現sign=1和sign=-1的數據相鄰,則只能依靠嚴格按照順序寫入。
若是數據的寫入程序是單線程執行的,則可以較好地控制寫入順序;若是須要處理的數據量很大,數據的寫入程序一般是多線程執行的,那麼此時就不能保障數據的寫入順序了。在這種狀況下,CollapsingMergeTree的工做機制就會出現問題。爲了解決這個問題,ClickHouse另外提供了一個名爲VersionedCollapsingMergeTree的表引擎。
VersionedCollapsingMergeTree表引擎的做用與CollapsingMergeTree徹底相同,它們的不一樣之處在於,VersionedCollapsingMergeTree對數據的寫入順序沒有要求,在同一個分區內,任意順序的數據都可以完成摺疊操做。VersionedCollapsingMergeTree是如何作到這一點的呢?其實從它的命名各位就應該可以猜出來,是版本號。
在定義VersionedCollapsingMergeTree的時候,除了須要指定sign標記字段之外,還須要指定一個UInt8類型的ver版本號字段:
ENGINE = VersionedCollapsingMergeTree(sign,ver)
一個完整的例子以下:
CREATE TABLE ver_collpase_table( id String, code Int32, create_time DateTime, sign Int8, ver UInt8 ) ENGINE = VersionedCollapsingMergeTree(sign,ver) PARTITION BY toYYYYMM(create_time) ORDER BY id ;
VersionedCollapsingMergeTree是如何使用版本號字段的呢?其實很簡單,在定義ver字段以後,VersionedCollapsingMergeTree會自動將ver做爲排序條件並增長到ORDER BY的末端。以上面的ver_collpase_table表爲例,在每一個數據分區內,數據會按照ORDER BY id , ver DESC排序。因此不管寫入時數據的順序如何,在摺疊處理時,都能回到正確的順序。
能夠用一組示例證實,首先是刪除數據:
--刪除 INSERT INTO TABLE ver_collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1,1); INSERT INTO TABLE ver_collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1,1); select * from ver_collpase_table;
接着是修改數據:
optimize table ver_collpase_table FINAL; select * from ver_collpase_table;
INSERT INTO TABLE ver_collpase_table VALUES('A000',101,'2019-02-20 00:00:00',-1,1); INSERT INTO TABLE ver_collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1,1); INSERT INTO TABLE ver_collpase_table VALUES('A000',102,'2019-02-20 00:00:00',1,2); select * from ver_collpase_table;
optimize table ver_collpase_table FINAL; select * from ver_collpase_table;