本文探討clickhouse的1個經典問題:
如何模擬實現記錄更新和刪除效果?(因為clickhouse自帶的update/delete實現極為低效)
跟着我的例子走吧。
創建數據庫db2
CREATE DATABASE IF NOT EXISTS db2 ON CLUSTER mycluster
上述語句創建db2數據庫,ON CLUSTER mycluster指定將該DDL操作廣播到整個集群的所有節點上。
創建商品表product
1
2
3
4
5
6
7
8
|
CREATE TABLE db2.product ON CLUSTER mycluster
(
id Int64,
name String,
sign Int8,
version UInt64
) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/db2/product', '{replica}', version)
ORDER BY (id);
|
ON CLUSTER mycluster是說把這張表廣播到所有節點上建立出來。
再說一下列:
- id:商品ID
- name:商品名
上述是業務字段,商品id是業務側的主鍵。
sign和version是我們設計出來的控制字段,用來模擬update和delete操作,方案如下:
- sign:1表示upsert,也就是插入或者更新;-1表示delete,表示刪除。
- version:版本號,要保證靠后發生的操作比先前發生的操作version更大。
ReplicatedReplacingMergeTree(‘/clickhouse/tables/{shard}/db2/product’, ‘{replica}’, version)最后的version是什么意思呢?
這里ReplacingMergeTree是一種compaction階段能夠對相同主鍵進行去重的引擎,當一個主鍵有多條記錄時,version大的被留下,其他被compaction丟掉。
我們就是想要這樣的效果,我們只關心同1個id最新version的數據內容~~~
光說還是不懂,下面我們就會進入演練,在此之前我們按常規流程創建出分布式表,后續只讀寫分布式表即可:
1
2
|
CREATE TABLE db2.dis_product ON CLUSTER mycluster AS db2.product
ENGINE = Distributed(mycluster, db2, product, rand());
|
ON CLUSTER mycluster在所有node上創建了dis_product分布式表,對它的讀取和寫入都將是對集群中所有product本地表的分布式處理。
模擬UPDATE/DELETE的思路分析
假定我們是同步mysql的binlog,然后寫入到clickhouse的dis_product表。
解析來的binlog主要包含3個信息:
1,操作類型(INSERT/UPDATE/DELETE)
2,本次事務ID,永遠遞增。
3,變化后的整行數據。
對於操作類型來說,INSERT/UPDATE我們都用sign=1統一為upsert操作,DELETE則用sign=-1表示刪除。
事務ID恰好就可以用來作為version,表示數據變更的發生先后關系,對於同一個商品id我們只關心最新version的數據長什么樣。
總結一下,
在clickhouse中模擬UPDATE和DELETE的核心思路就是:將UPDATE和DELETE操作都轉化為clickhouse表的插入操作,無非是sign和version在變化,最后查詢的時候對同一個商品id保留最新的version行即可。
為什么要用replcaingMergeTree呢?因為要讓存儲引擎自動淘汰掉舊版本的數據,免得存儲空間無限上漲。
實踐INSERT/UPDATE
我們實踐模擬出整個INSERT/UPDATE過程,我們假定數據源是來自mysql的binlog同步產生,mysql每行記錄變更都在獨立的事務中完成,所以version總是遞增(你可以利用canal+kafka自動向clickhouse生成這樣的數據,下面均手動模擬):
首先INSERT兩行記錄:
INSERT INTO db2.dis_product values(1,’尿不濕’,1,1);
INSERT INTO db2.dis_product values(2,’紙巾’,1,2);
它們的sign=1表示INSERT,然后各自的version是1和2。
然后我們模擬UPDATE了id=1的記錄:
INSERT INTO db2.dis_product values(1,’尿不濕2.0′,1,3);
這次sign=1表示update,版本號來到了3,再看一下數據:
現在出現問題了,id=1主鍵同時存在新舊2條記錄,我們期望只看到version=3的這個新版本數據,因此如果我們希望准確獲得表的實際情況,查詢時應該這樣做:
1
2
3
4
5
6
7
|
SELECT
id,
argMax(name,version) name,
argMax(sign,version) sign,
max(version) max_version
FROM db2.dis_product
GROUP BY id;
|
按主鍵ID分組,在組內利用argMax方法選出version最大的那行數據的各個列值。
argMax(name,version)的意思是在Group組內version最大的那行的name列。
說白了,每個id保留最新version的那行數據,結果也顯而易見:
對於id=1來說,version=3的尿不濕2.0被留下了,它的sign=1表示version=3這次變更是一個INSERT/UPDATE操作,數據是有效的。
(注,replacingMergTree雖然compaction時會自動刪除同主鍵舊version數據,但是compaction何時發生是不可知的,所以我們總是應該用SQL來自行去重)
模擬DELETE操作
delete操作我們應該插入一個sign=-1的行,version繼續跟隨事務ID遞增即可。
INSERT INTO db2.dis_product values(2,’紙巾’,-1,4);
我們插入上述語句實現對id=2記錄的刪除,version是4,sign=-1表示刪除。
當我們重新執行上面的查詢語句時:
你會發現id=2記錄的version=4記錄被保留了下來,但實際上因為version=4是sign=-1的刪除操作,我們其實不應該看得到這行被刪掉的記錄,所以我們得完善一下查詢SQL讓它能夠適應這種刪除記錄的操作:
1
2
3
4
5
6
7
8
|
SELECT
id,
argMax(name,version) name,
argMax(sign,version) sign,
max(version) max_version
FROM db2.dis_product
GROUP BY id
HAVING sign > 0;
|
只需要將sign=-1的那些分組刪除掉即可,比如id=2的分組最新版本的sign就是-1,最終被過濾掉:
用視圖簡化
后續我們做數據分析的話,肯定不希望寫每個SQL時都考慮上述sign和version的問題,所以把上述SQL作為一個視圖,后續數據分析SQL直接基於視圖即可,不必再重復處理sign和version問題。
1
2
3
4
5
6
7
8
9
10
|
CREATE VIEW db2.dis_product_view ON CLUSTER mycluster
AS
SELECT
id,
argMax(name,version) name,
argMax(sign,version) sign,
max(version) max_version
FROM db2.dis_product
GROUP BY id
HAVING sign > 0;
|
ON CLUSTER mycluster是在所有node上創建這個view,所以后續客戶端無論訪問任何節點都可以訪問到view。
視圖就是一個子查詢,當我們select * from db2.dis_product_view的時候相當於
select * from (SELECT id, argMax(name,version) name, argMax(sign,version) sign, max(version) max_version FROM db2.dis_product GROUP BY id HAVING sign > 0) as tmp
這就達到了簡化后續數據分析SQL復雜度的目的,現在我們直接select這個視圖看一下效果:
可見,我們沒有再關注過sign和version,但數據已經是規整的了,底層偽UPDATE/DELETE的實現細節已經被屏蔽了。
總結
本篇博客教給大家如何在clickhouse中模擬出update和delete操作,這也是mysql實時同步clickhouse的基本原理。
我們用到了關鍵的replacingMergeTree引擎,它可以在compaction時保留相同主鍵最新的數據,確保數據庫不會無限膨脹。
同時,我們定義了sign和version控制字段實現了數據行的多版本設計,通過SQL為每個主鍵保留最新一份數據並過濾掉被刪除的記錄,通過視圖屏蔽SQL負責性,為后續使用提供了便捷性。
你也許也看過clickhouse的折疊表等概念,但目前從官方和網上的做法來看replacingMergeTree+sign+version的方案是最為普遍、簡單、可靠的,沒有明顯缺點。