一、UPDATE+Optimize方式
--建表及插入數據 CREATE TABLE tb_test( ts DateTime, uid String, biz String ) ENGINE = MergeTree() ORDER BY (ts) SETTINGS index_granularity = 8192; INSERT INTO tb_test VALUES ('2019-06-07 20:01:01', 'a', 'a1'); INSERT INTO tb_test VALUES ('2019-06-07 20:01:01', 'b', 'b1'); INSERT INTO tb_test VALUES ('2019-06-07 20:01:01', 'c', 'c1'); --執行更新 alter table tb_test update biz = 'ccccc' where uid = 'c'; optimize table tb_test;
物理文件的前后變化
剛插入時,因為是執行了三次insert,所以生成了3個part
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:16 all_1_1_0 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:16 all_2_2_0 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:16 all_3_3_0 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:15 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:15 format_version.txt
執行alter和optimize后
update字段的值就對應着mutation操作,可以看到part(all_1_1_0、all_2_2_0、all_3_3_0)每個都執行了mutation,生成了新的part(all_1_1_0_4、all_2_2_0_4、all_3_3_0_4),后面又執行了merge合並成了all_1_3_1_4這個part。
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:16 all_1_1_0 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:17 all_1_1_0_4 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:16 all_2_2_0 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:17 all_2_2_0_4 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:16 all_3_3_0 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:17 all_3_3_0_4 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:15 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:15 format_version.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:17 mutation_4.txt
由於clickhouse是異步刪除的,所以過程中還會有part(all_1_1_0、all_2_2_0、all_3_3_0)短時間存在,但最終會變為如下文件
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:18 all_1_3_1_4 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:15 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:15 format_version.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:17 mutation_4.txt
如果在執行了alter和optimize后沒有上述變化,可以將optimize語句final
optimize table tb_test final;
由於本次數據壓力較小,所以mutation很快執行完了,但也不影響final的實驗效果,執行完上述sql后,clickhouse會強制part更新,並且是同步操作,直至成功或失敗。
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:18 all_1_3_1_4 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:19 all_1_3_2_4 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:15 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:15 format_version.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:17 mutation_4.txt
可以發現,新生成了all_1_3_2_4(all_{min}_{max}_{merge_verson}_{mutation_version}),雖然min和max沒變,但是merge_verson加一了,並真正生成了新的part。
all_1_3_1_4后續也會被刪掉,最終形態如下
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:19 all_1_3_2_4 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:15 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:15 format_version.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:17 mutation_4.txt
注意點:
利用該方法可以讀取到最新的數據,但是是建立在強制clickhouse去做新part的生成去代替老part,如果part非常多,optimize的耗時會非常長甚至失敗,可以根據實際情況和partiton的分布使數據的更新只涉及部分part,可以提高效率。
二、UPDATE+ SETTING mutations_sync方式
mutations_sync有3種配置,默認為0,即所有的mutation都為異步操作;為1,表示等待當前節點完成mutation操作;為2,表示等待所有節點都完成mutation操作。如果是MergeTree只會判斷0或非0,如果是ReplicatedMergeTree才會支持1和2的配置
同樣借助上面的表
alter table tb_test update biz = 'ddddd' where uid = 'c' settings mutations_sync = 1
物理文件變化
執行完上述sql就會生成新的part(all_1_3_2_5),並且mutation版本加一
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:19 all_1_3_2_4 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:29 all_1_3_2_5 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:15 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:15 format_version.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:17 mutation_4.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:29 mutation_5.txt
最終會刪掉all_1_3_2_4,變為如下狀態
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:29 all_1_3_2_5 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:15 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:15 format_version.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:17 mutation_4.txt -rw-r----- 1 clickhouse clickhouse 102 3月 7 14:29 mutation_5.txt
注意點:
該方法不需要執行optimize,但原理上還是通過新生成part去代替老part后才能提供新的准確的數據。如果part非常多同樣會遇到第一種方式的問題。clickhouse在執行同步操作時可能會因為各種原因失敗,但是該方法還是會在后台繼續進行更新,具體情況可以根據system.mutations中的記錄判斷。
三、INSERT+Final方式
這種方法需要ReplacingMergeTree表引擎配置使用
CREATE TABLE tb_test_replacing( ts DateTime, uid String, biz String ) ENGINE = ReplacingMergeTree(ts) ORDER BY (ts) SETTINGS index_granularity = 8192; INSERT INTO tb_test_replacing VALUES ('2019-06-07 20:01:01', 'c', 'c1');
這種方式就不用執行alter了,而是以insert的形式來代替alter操作,即每次select時都是取最新的一條數據,sql語句如下
INSERT INTO tb_test_replacing VALUES ('2019-06-07 20:01:01', 'c', 'c2');
此時查詢該表,發現還是兩條數據,並沒有實現更新
SELECT * FROM tb_test_replacing Query id: 02cfecf5-18f1-4f9f-ad55-e7b596935de8 ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:01 │ c │ c2 │ └─────────────────────┴─────┴─────┘ ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:01 │ c │ c1 │ └─────────────────────┴─────┴─────┘ 2 rows in set. Elapsed: 0.004 sec.
在sql后面加上final,發現只返回了最新的一條,是符合預期的
SELECT * FROM tb_test_replacing FINAL Query id: 614c7e93-48c2-4129-a734-c8a0dd722fcd ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:01 │ c │ c2 │ └─────────────────────┴─────┴─────┘ 1 rows in set. Elapsed: 0.007 sec.
ReplacingMergeTree在建表時可以看到ENGINE = ReplacingMergeTree(ts),其中的ts就是版本信息,clickhouse會每次插入記錄版本,就是依據這個字段,在查詢時會返回最后最新的版本數據。所以第二個insert的ts和第一個insert的ts字段的值一樣,所以會以第二條記錄為准,即實現了更新。具體用法見:ReplacingMergeTree
這時物理文件並沒有發生實質的合並
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:32 all_1_1_0 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:32 all_2_2_0 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:32 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:32 format_version.txt
這時如果再執行optimize,就會發生真正的merge,生成了all_1_2_1,后續all_1_1_0、all_2_2_0會被異步刪除
drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:32 all_1_1_0 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:35 all_1_2_1 drwxr-x--- 2 clickhouse clickhouse 152 3月 7 14:32 all_2_2_0 drwxr-x--- 2 clickhouse clickhouse 6 3月 7 14:32 detached -rw-r----- 1 clickhouse clickhouse 1 3月 7 14:32 format_version.txt
再查詢該表,不加final
SELECT * FROM tb_test_replacing Query id: 14ae82a1-7ce3-4b92-918b-b9f2496f3034 ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:01 │ c │ c2 │ └─────────────────────┴─────┴─────┘ 1 rows in set. Elapsed: 0.003 sec.
可知,ReplacingMergeTree在進行merge時是會進行去重的,最終只會保留最新版本數據。
注意點
這里要注意,ReplacingMergeTree是根據orderby做去重的,而不是根據primarykey。舉個例子:
CREATE TABLE tb_test_replacing2 ( `ts` DateTime, `uid` String, `biz` String ) ENGINE = ReplacingMergeTree(ts) PRIMARY KEY uid ORDER BY (uid, ts) SETTINGS index_granularity = 8192; INSERT INTO tb_test_replacing2 VALUES ('2019-06-07 20:01:01', 'a', 'a1'); INSERT INTO tb_test_replacing2 VALUES ('2019-06-07 20:01:02', 'a', 'b1');
進行final的查詢,會發現並沒有根據主鍵(primarykey)去重,而是兩條數據都查出來了,因為雖然兩條記錄的主鍵一樣,但是ts字段不一樣,一個是20:01:01,另一個是20:01:02。
SELECT * FROM tb_test_replacing2 FINAL Query id: e70e0d00-0ae0-4e42-bcbd-09b852467ce5 ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:02 │ a │ b1 │ └─────────────────────┴─────┴─────┘ ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:01 │ a │ a1 │ └─────────────────────┴─────┴─────┘ 2 rows in set. Elapsed: 0.007 sec.
既然是知道是這個規則,那再插入一條數據,保證uid和ts值都一樣,biz換為a2
INSERT INTO tb_test_replacing2 VALUES ('2019-06-07 20:01:01', 'a', 'a2'); --再次查詢,可以發現只顯示最新一條記錄了,符合預期
SELECT * FROM tb_test_replacing2 FINAL Query id: da9b8464-e7bc-4a4f-97db-9e4304513f63 ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:02 │ a │ b1 │ └─────────────────────┴─────┴─────┘ ┌──────────────────ts─┬─uid─┬─biz─┐ │ 2019-06-07 20:01:01 │ a │ a2 │ └─────────────────────┴─────┴─────┘ 2 rows in set. Elapsed: 0.007 sec.
在原理上,使用select final是將數據讀取后在內存排序才能根據orderby鍵找到最新的一條記錄,雖然物理文件不需要做merge但是在內存中也做了類似merge的方式,會有性能損耗。
總結
綜上所述,上述三種方法各有各的特點,使用時可以根據業務特點(實時數據、離線數據、T+1數據等)進行更適合的方式。