MergeTree系列表引擎
ClickHouse中最核心的引擎當屬MergeTree系列引擎,其中基礎表引擎為MergeTree,常用的表引擎還有ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree和VersionedCollapsingMergeTree。每一種MergeTree的變種,在繼承了基礎MergeTree的能力后,還增加了它獨有的特性。其名稱中的“合並”二字奠定了所有類型MergeTree的基因,它們的所有特殊邏輯,都是在觸發合並的過程中被激活的。接下來主要介紹它們各自的特點與使用方法。
1. ReplacingMergeTree
在MergeTree中,雖然有主鍵,但是它沒有唯一鍵的約束。也就是說,寫入數據的主鍵是可以重復的。但在某些場合下,用戶不希望表中有重復數據,ReplacingMergeTree就是為此場景而設計,它可以在合並分區時,刪除重復的數據條目。不過此方法僅是在“一定程度”上解決了重復數據的問題,稍后會對此作出解釋。
創建測試表:
CREATE TABLE replace_table ( `id` String, `code` String, `create_time` DateTime ) ENGINE = ReplacingMergeTree PARTITION BY toYYYYMM(create_time) PRIMARY KEY id ORDER BY (id, code)
這里ORDER BY 是去除重復數據的關鍵,排序鍵ORDER BY所聲明的表達式是后續判斷是否有重復數據的依據。在這個例子中,數據會基於id 和 code 兩個字段做去重。
分2個批次插入數據:
INSERT INTO replace_table VALUES ('A001', 'C1', '2019-05-10 17:00:00'), ('A001', 'C2', '2019-05-14 17:00:00'), ('A001', 'C3', '2019-05-15 17:00:00') INSERT INTO replace_table VALUES ('A001', 'C1', '2019-05-11 17:00:00'), ('A001', 'C100', '2019-05-12 17:00:00'), ('A001', 'C200', '2019-05-13 17:00:00') # 查詢數據: SELECT * FROM replace_table
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-11 17:00:00 │
│ A001 │ C100 │ 2019-05-12 17:00:00 │
│ A001 │ C200 │ 2019-05-13 17:00:00 │
└──────┴──────┴─────────────────────┘
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-10 17:00:00 │
│ A001 │ C2 │ 2019-05-14 17:00:00 │
│ A001 │ C3 │ 2019-05-15 17:00:00 │
└──────┴──────┴─────────────────────┘
可以看到在表中是存在重復的id與<id, code>,使用 OPTIMIZE 命令強制觸發合並后再次查看數據條目:
SELECT * FROM replace_table
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-11 17:00:00 │
│ A001 │ C100 │ 2019-05-12 17:00:00 │
│ A001 │ C2 │ 2019-05-14 17:00:00 │
│ A001 │ C200 │ 2019-05-13 17:00:00 │
│ A001 │ C3 │ 2019-05-15 17:00:00 │
└──────┴──────┴─────────────────────┘
可以看到<id, code> 重復的條目已經被刪除了。在optimize強制觸發合並后,會按照<id, code> 進行分組,並保留分組內最后一條條目(觀察create_time日期字段)。
從測試結果可以知道,ReplacingMergeTree在去除重復數據時,是以ORDER BY排序鍵作為基准,而不是PRIMARY KEY。
前面提到ReplacingMergeTree僅在“一定程度上”解決了數據重復的問題,現在我們解釋這點。首先,再插入一條數據:
INSERT INTO replace_table VALUES ('A001', 'C1', '2019-08-10 17:00:00')
執行 OPTIMIZE TABLE 后再查看數據:
OPTIMIZE TABLE replace_table FINAL SELECT * FROM replace_table
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-08-10 17:00:00 │
└──────┴──────┴─────────────────────┘
┌─id───┬─code─┬─────────create_time─┐
│ A001 │ C1 │ 2019-05-11 17:00:00 │
│ A001 │ C100 │ 2019-05-12 17:00:00 │
│ A001 │ C2 │ 2019-05-14 17:00:00 │
│ A001 │ C200 │ 2019-05-13 17:00:00 │
│ A001 │ C3 │ 2019-05-15 17:00:00 │
└──────┴──────┴─────────────────────┘
可以看到表中存在相同的<id, code> 行,但是並沒有執行去重。這是因為ReplacingMergeTree是以分區為單位進行合並,並在此時刪除重復數據。不同分區之間的重復數據無法進行刪除。所以ReplacingMergeTree僅是在“一定程度上“解決了數據重復問題。
1.2. 版本號
ReplacingMergeTree中可以根據版本號來決定:在對重復數據進行去重時,保留哪條數據。
例如以下建表語句:
CREATE TABLE replace_table ( `id` String, `code` String, `create_time` DateTime ) ENGINE = ReplacingMergeTree(create_time) PARTITION BY toYYYYMM(create_time) ORDER BY id
可以看到在指定ReplacingMergeTree引擎時,傳入了一個列字段create_time。這個表基於id字段進行去重,並且使用create_time作為版本號。
在使用create_time作為版本號后,在對數據進行去重時,會保留id重復的條目中,create_time時間最長的一行。
1.3. ReplacingMergeTree總結
ReplacingMergeTree的處理邏輯為:
- 使用ORDER BY 排序鍵作為判斷數據是否重復的唯一鍵
- 只有在合並分區時才會觸發刪除重復數據的邏輯
- 僅有相同分區的數據會進行去重,無法跨分區進行去重
- 在進行去重操作時,由於分區內的數據已經基於ORDER BY進行了排序,所以能夠找到那些相鄰的重復數據
- 數據去重策略有2種:
- 如果沒有設置ver版本號,則保留同一組重復數據的最后一行
- 如果設置了ver版本號,則保留同一組重復數據中ver字段取值最大的那一行
2. SummingMergeTree
SummingMergeTree用於僅關系聚合結果,而不關心具體明細的場景。並且數據匯總條件是預先明確的(group by 的條件明確,不會隨意更改)。
SummingMergeTree可以在合並分區時,按照預先定義的條件,聚合匯總數據,將同一分區下的多行數據聚合成一行。這樣既減少了數據行,又降低了后續聚合查詢的開銷。
2.1. ORDER BY 與 PRIMARY KEY
在MergeTree中,數據會按照ORDER BY 的字段在分區內進行排序。主鍵索引也會按照PRIMARY KEY 表達式取值並排序。而ORDER BY可以指代主鍵,所以在一般情況下,只單獨聲明ORDER BY即可,此時數據排序與主鍵索引均相同。
如果需要同時定義ORDER BY與PRIMARY KEY,便是明確希望他倆不同。這種情況通常會在使用SummingMergeTree或AggregatingMergeTree時會出現,因為它們的聚合都是根據ORDER BY 進行的。由此可以引出2點原因:主鍵與聚合的條件定義分離,為修改聚合條件留下空間。
舉個例子,假設一張表使用的引擎為SummingMergeTree,包含6個字段,分別為A、B、C、D、E、F。如果需要按照A、B、C、D進行匯總,則有:
ORDER BY (A, B, C, D)
但是這樣的寫法也表示:表的PRIMARY KEY 被定義成A, B, C, D。若是在業務層面,僅需要對字段A進行查詢過濾,則應只使用A字段創建主鍵。所以更好的一種寫法是:
ORDER BY (A, B, C, D)
PRIMARY KEY A
如果同時聲明了ORDER BY 與 PRIMARY KEY,則MergeTree會強制要求PRIMARY KEY列字段必須是ORDER BY的前綴。
例如下面的定義是錯的:
ORDER BY (B, C)
PRIMARY KEY A
下面的是正確的:
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 是一種元數據的操作,修改成本很低,相比不能被修改的主鍵,這已經非常便利了。
2.2. SummingMergeTree的使用
SummingMergeTree引擎的聲明方式為:
ENGINE = SummingMergeTree((col1, col2, …))
這里col1,col2是選填參數,用於設置除主鍵外的其他數值類型字段,以指定被SUM聚合的列字段。若不指定此字段,則所有非主鍵的數值型字段均會進行SUM聚合。
下面舉例說明:
創建表:
CREATE TABLE summing_table ( `id` String, `city` String, `v1` UInt32, `v2` Float64, `create_time` DateTime ) ENGINE = SummingMergeTree PARTITION BY toYYYYMM(create_time) PRIMARY KEY id ORDER BY (id, city)
這里 order by 是關鍵配置,數據會以 order by 指定的列為維度進行聚合。
分批次插入數據:
INSERT INTO summing_table VALUES ('A001', 'wuhan', '10', '20', '2019-08-10 17:00:00') ('A001', 'jinzhou', '20', '30', '2019-08-10 17:00:00') INSERT INTO summing_table VALUES ('A001', 'wuhan', '10', '20', '2019-02-10 17:00:00') ('A001', 'wuhan', '20', '30', '2019-08-20 17:00:00') INSERT INTO summing_table VALUES ('A002', 'wuhan', '60', '50', '2019-10-10 17:00:00')
當前數據結果:
SELECT * FROM summing_table
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ wuhan │ 20 │ 30 │ 2019-08-20 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ wuhan │ 10 │ 20 │ 2019-02-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
┌─id───┬─city────┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ jinzhou │ 20 │ 30 │ 2019-08-10 17:00:00 │
│ A001 │ wuhan │ 10 │ 20 │ 2019-08-10 17:00:00 │
└──────┴─────────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A002 │ wuhan │ 60 │ 50 │ 2019-10-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
執行 OPTIMIZE TABLE 后查看結果:
OPTIMIZE TABLE summing_table FINAL SELECT * FROM summing_table
┌─id───┬─city────┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ jinzhou │ 20 │ 30 │ 2019-08-10 17:00:00 │
│ A001 │ wuhan │ 30 │ 50 │ 2019-08-10 17:00:00 │
└──────┴─────────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A001 │ wuhan │ 10 │ 20 │ 2019-02-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
┌─id───┬─city──┬─v1─┬─v2─┬─────────create_time─┐
│ A002 │ wuhan │ 60 │ 50 │ 2019-10-10 17:00:00 │
└──────┴───────┴────┴────┴─────────────────────┘
可以看到,在不同分區內,數據以ORDER BY 的字段<id, city> 進行了聚合,聚合的字段為v1 與 v2。而非可聚合字段create_time 取的是同組內第一行數據的取值。
SummingMergeTree也支持嵌套類型的字段,在使用嵌套類型字段時,需要被SUM聚合的字段名稱必須以Map后綴結尾,例如:
CREATE TABLE summing_table_nested ( `id` String, `nestMap` Nested(id UInt32, key UInt32, val UInt64), `create_time` DateTime ) ENGINE = SummingMergeTree PARTITION BY toYYYYMM(create_time) ORDER BY id
插入一條數據並查看結果:
INSERT INTO summing_table_nested VALUES ('A001', [1,1,2], [10,20,30], [40,50,60], '2019-08-10 17:00:00') SELECT * FROM summing_table_nested
┌─id───┬─nestMap.id─┬─nestMap.key─┬─nestMap.val─┬─────────create_time─┐
│ A001 │ [1,2] │ [30,30] │ [90,60] │ 2019-08-10 17:00:00 │
└──────┴────────────┴─────────────┴─────────────┴─────────────────────┘
可以看到此結果是按照 id 進行了聚合,相同 nestMap.id 的條目在nestMap.key 與 nestMap.val 上進行了sum 聚合。
在使用嵌套數據類型的時候,也支持使用復合Key作為數據聚合的條件。為了使用復合Key,在嵌套類型的字段中,除第一個字段外,任何名稱是以Key、Id或Type為后綴結尾的字段,都將和第一個字段一起組合成為復合Key。例如將上面的例子中小寫key改為大寫Key:
(
`id` String,
`nestMap` Nested(id UInt32, Key UInt32, val UInt64),
`create_time` DateTime
)
這樣數據就會以<id, Key> 作為條件進行聚合。
2.3. SummingMergeTree總結
SummingMergeTree的處理邏輯為:
- 用ORDER BY排序鍵作為聚合數據的條件Key
- 只有在合並分區的時候才會觸發聚合的邏輯
- 以分區為單位進行聚合,不同分區的數據之間不會聚合
- 如果在定義引擎時指定了column聚合列(非主鍵的數值類型字段),則sum聚合這些字段;如未指定,則聚合所有非主鍵的數值類型字段
- 在進行聚合時,由於分區內的數據已經基於ORDER BY排序,所以能夠找到相鄰且擁有相同聚合Key的數據
- 在聚合數據時,同一分區內,相同聚合Key的多行數據會合並為一行;聚合字段進行SUM運算;對於非聚合字段,使用第一行數據的值
- 支持嵌套結構,但列字段名必須以Map后綴結尾。嵌套類型中,默認以第1個字段作為聚合Key。除第1個字段外,任何名稱以Key、Id或Type為后綴結尾的字段,都將和第1個字段一起組成復合Key
3. AggregatingMergeTree
相信大家應該有了解過“數據立方體“(Cube)的概念,它在數倉領域非常常見。它的主要理念是:通過空間換取時間,對需要聚合的數據進行預計算,並將結果保存。在后續進行聚合查詢的時候,可以直接使用結果數據,提升查詢性能。
AggregatingMergeTree有些許“數據立方體“的意思,它能在合並分區的時候,按照預先定義的條件聚合數據。同時,根據預先定義的聚合函數,計算數據並以二進制的格式存入表內。
將同一分組下的多行數據聚合成一行,既減少了數據行,又降低了后續聚合查詢的開銷。可以說AggregatingMergeTree是SummingMergeTree的升級版,它們的許多設計思路是一致的,例如同時定義ORDER BY與PRIMARY KEY的原因和目的。但在使用方法上,兩者存在明顯差異。
AggregatingMergeTree沒有任何額外參數,在分區合並時,在每個數據分區內,會按照ORDER BY聚合。而使用何種聚合函數,以及針對哪些列字段計算,則是通過定義AggregateFunction數據類型實現的。
3.1. AggregatingMergeTree的使用
如下建表語句:
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) PRIMARY KEY id ORDER BY (id, city)
上例中列字段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('code1'), sumState(toUInt32(100)), '2019-09-10 17:00:00'
在查詢數據時,如果直接使用列名去訪問這里的code 與 value列時,結果為亂碼。這是因為它們的格式為二進制格式,在查詢時需要調用與uniq、sum對應的uniqMerge和sumMerge函數:
SELECT id, city, uniqMerge(code), sumMerge(value) FROM agg_table GROUP BY id, city
┌─id───┬─city──┬─uniqMerge(code)─┬─sumMerge(value)─┐
│ A000 │ wuhan │ 1 │ 100 │
└──────┴───────┴─────────────────┴─────────────────┘
以上 insert into 的方式並非是AggregatingMergeTree的常見用法,它更為常見的用法是結合物化視圖使用,將它作為物化視圖表的引擎。
3.2. 結合物化視圖使用
舉例說明,首先建立明細數據表(也就是俗稱的底表):
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) SELECT * FROM agg_table_basic
┌─id───┬─city───┬─code──┬─value─┐
│ A000 │ zhuhai │ code1 │ 200 │
└──────┴────────┴───────┴───────┘
┌─id───┬─city──┬─code──┬─value─┐
│ A000 │ wuhan │ code1 │ 100 │
│ A000 │ wuhan │ code2 │ 200 │
└──────┴───────┴───────┴───────┘
數據會自動同步到物化視圖,並按照AggregatingMergeTree引擎的規則進行處理:
SELECT id, city, sumMerge(value), uniqMerge(code) FROM agg_view GROUP BY id, city
┌─id───┬─city───┬─sumMerge(value)─┬─uniqMerge(code)─┐
│ A000 │ zhuhai │ 400 │ 1 │
│ A000 │ wuhan │ 600 │ 2 │
└──────┴────────┴─────────────────┴─────────────────┘
整個邏輯如下圖所示:
3.3. AggregatingMergeTree總結
AggregatingMergeTree的處理邏輯為:
- 使用ORDER BY 排序鍵作為聚合數據的條件key
- 使用AggregateFunction字段類型定義聚合函數的類型以及聚合的字段
- 只有在合並分區的時候才會觸發聚合計算的邏輯
- 以數據分區為單位聚合數據,僅在同一分區內能夠進行聚合,無法跨分區進行聚合
- 在進行數據計算時,由於分區內的數據已經基於ORDER BY進行排序,所以能夠找到那些相鄰且擁有相同聚合Key的數據
- 在聚合數據時,同一分區內,相同聚合Key的多行數據會合並成一行。對於那些非主鍵、非AggregateFunction類型字段,則會使用第一行數據的值
- AggregateFunction類型的字段使用二進制,在寫入數據時需要調用 *State函數;而在查詢數據時,需要調用相應的 *Merge 函數。其中 * 表示定義時使用的聚合函數
- AggregatingMergeTree通常作為物化視圖的表引擎,與普通MergeTree搭配使用
4. CollapsingMergeTree
CollapsingMergeTree是支持行級數據修改和刪除的引擎。在大數據領域,修改和刪除是代價非常高的操作,所以在實現時不會去修改源文件,而是將修改和刪除操作轉換為新增操作。
CollapsingMergeTree定義了一個sign標記位字段,用於記錄數據行的狀態:
- sign為1:表示這是一行有效數據
- sign為 -1:表示這行數據需要被刪除
在合並分區時,同一數據分區內,sign標記為1和-1的一組數據會被抵消刪除。這種相互抵消的操作,猶如將紙折疊一樣,所以這可能也是折疊合並樹(CollapsingMergeTree)名稱的由來。折疊過程如下圖所示:
4.1. CollapsingMergeTree的使用
建表:
CREATE TABLE collapse_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 排序字段判斷數據唯一性;除此之外,CollapsingMergeTree還支持其他2種特殊操作:修改與刪除。
下面舉例:
--插入原始數據,后續會被修改 INSERT INTO TABLE collapse_table VALUES ('A000', 100, '2019-02-20 00:00:00', 1) --原始數據的鏡像數據,ORDER BY 字段與原條目必須相同(其他字段可以不同),sign取 -1, --它會和原始條目折疊 INSERT INTO TABLE collapse_table VALUES ('A000', 100, '2019-02-20 10:00:00', -1) --強制OPTIMIZE 合並后查看結果,可以看到結果為空 SELECT * FROM collapse_table 0 rows in set. Elapsed: 0.001 sec. --修改后的數據,sign為1 INSERT INTO TABLE collapse_table VALUES ('A000', 120, '2019-02-20 00:00:00', 1) --檢查結果 SELECT * FROM collapse_table
┌─id───┬─code─┬─────────create_time─┬─sign─┐
│ A000 │ 120 │ 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會打印警告日志,但不會報錯,此時查詢結果未知
4.2. CollapsingMergeTree注意事項
1. 折疊數據並非實時觸發,而是在分區合並時在進行。所以在分區合並前,舊數據仍對用戶可見。解決此問題的方式有2種:
1.1. 在查詢數據前,執行 OPTIMIZE TABLE table_name FINAL 強制觸發分區合並,但此方法效率很低,在生產環境慎用。
1.2. 改變查詢方式。以collapse_table為例,假設原始SQL為:
SELECT id, SUM(code), COUNT(code), AVG(code), uniq(code)
FROM collapse_table
GROUP BY id
則可以改寫為:
SELECT id, SUM(code * sign), COUNT(code * sign), AVG(code * sign), uniq(code * sign)
FROM collapse_table
GROUP BY id
HAVING SUM(sign) > 0
2. 只有相同分區內的數據才可能被折疊,不過刪除或修改數據時,一般分區規則都是一致的。
3. CollapsingMergeTree最大的限制在於:對於寫入數據的順序有非常嚴格的要求。前面例子我們可以看到,若是先寫sign=1再寫sign=-1,則沒有任何問題。但若是先寫sign=-1再寫sign=1 的話呢:
INSERT INTO TABLE collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', -1) INSERT INTO TABLE collapse_table VALUES('A000', 102, '2019-02-20 00:00:00', 1) OPTIMIZE TABLE collapse_table FINAL select * from collapse_table;
┌─id───┬─code─┬─────────create_time─┬─sign─┐
│ A000 │ 101 │ 2019-02-20 00:00:00 │ -1 │
│ A000 │ 102 │ 2019-02-20 00:00:00 │ 1 │
└──────┴──────┴─────────────────────┴──────┘
可以看到在順序置換后,並沒有進行折疊。若是在單線程寫入時,順序是可以保證的,但是在多線程並行寫入就不一定了。為了解決這個問題,ClickHouse另外提供了一個名為VersionedCollapsingMergeTree的表引擎。
5. VersionedCollapsingMergeTree
VersionedCollapsingMergeTree表引擎的功能與CollapsingMergeTree完全相同,不同之處在於:它對數據的寫入順序沒有要求,同一分區內,任意順序的數據都能完成折疊操作。通過版本號實現。
在定義VersionedCollapsingMergeTree時,除了需要指定sign標記字段外,還需要指定一個UInt8類型的ver版本號字段。例如:
CREATE TABLE ver_collapse_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字段后,它會自動將此字段作為排序條件增加到ORDER BY 后面。例如上面定義的表,在每個數據分區內,數據均會以 ORDER BY id, ver DESC 進行排序。所以無論寫入時數據的順序如何,在折疊處理時,都能回到正確的順序。
例如:
刪除一行數據:
INSERT INTO TABLE ver_collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', -1, 1) INSERT INTO TABLE ver_collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', 1, 1) OPTIMIZE TABLE ver_collapse_table FINAL SELECT * FROM ver_collapse_table 0 rows in set. Elapsed: 0.001 sec.
修改數據:
INSERT INTO TABLE ver_collapse_table VALUES('A000', 101, '2019-02-20 00:00:00', -1, 1) INSERT INTO TABLE ver_collapse_table VALUES('A000', 102, '2019-02-20 00:00:00', 1, 1) INSERT INTO TABLE ver_collapse_table VALUES('A000', 103, '2019-02-20 00:00:00', 1, 2) OPTIMIZE TABLE ver_collapse_table; select * from ver_collapse_table;
┌─id───┬─code─┬─────────create_time─┬─sign─┬─ver─┐
│ A000 │ 103 │ 2019-02-20 00:00:00 │ 1 │ 2 │
└──────┴──────┴─────────────────────┴──────┴─────┘
6. MergeTree家族關系總結
MergeTree表引擎向下派生出6個變種表引擎:
7. 組合關系
上面是MergeTree關系,下面介紹ReplicatedMergeTree系列。
ReplicatedMergeTree與普通的MergeTree的區別:
虛線部分是MergeTree的能力邊界,而ReplicatedMergeTree是在MergeTree能力的基礎之上增加了分布式協同的能力。它借助了ZooKeeper的消息日志廣播功能,實現了副本實例之間的數據同步功能。
ReplicatedMergeTree系列通過組合關系來理解,如下圖所示:
在為MergeTree加上Replicated前綴后,又可以組合出7種新的表引擎,這些ReplicatedMergeTree擁有副本協同的能力。之后會詳細說明。