楔子
日常工作中,我們更多地還是對數據表中的數據進行操作,而對於 OLAP 類型的數據庫而言,這些操作還都是查詢操作。不過查詢涉及到的內容非常多,我們會單獨展開,這里先看看如何進行增刪改。
增
跟絕大部分關系型數據庫一樣,ClickHouse 使用 INSERT 語句進行數據的插入。並且 INSERT語句支持三種語法范式,三種范式各有不同,可以根據寫入的需求靈活運用。
其中,第一種是使用 VALUES 格式的常規語法:
-- 中括號表示里面的內容可以省略
INSERT INTO [db.]table_name [(col1, col2, col3...)] VALUES (val1, val2, val3, ...), (val1, val2, val3, ...), ...
這個和其它關系型數據庫沒什么兩樣,就不贅述了。
在使用 VALUES 格式的語法寫入數據時,還支持加入表達式或函數,例如:
INSERT INTO partizion_v2 VALUES('matsuri', toString(1+2), now())
第二種是使用指定格式的語法:
INSERT INTO [db.]table_name [(col1, col2, col3...)] FORMAT format_name data_set
ClickHouse 支持多種數據格式,以常用的 CSV 格式寫入為例:
INSERT INTO partition_v2 FORMAT CSV \
'mea', 'www.mea.com', '2019-01-01'
'nana', 'www.nana.com', '2019-02-01'
'matsuri', 'www.matsuri.com', '2019-03-01'
第三種是使用 SELECT 子句形式的語法:
INSERT INTO [db.]table_name [(col1, col2, col3...)] SELECT ...
通過 SELECT 子句可將查詢結果寫入數據表,假設需要將 partition_v1 的數據寫入 partition_v2,則可以 使用下面的語句:
INSERT INTO partition_v2 SELECT * FROM partition_v1
當然也可以這么做:
-- 加入表達式也是可以的,比如這里的 now()
INSERT INTO partition_v2 SELECT 'aqua', 'www.aqua.com', now()
雖然 VALUES 和 SELECT 子句的形式都支持聲明表達式或函數,但是表達式或函數會帶來額外的性能開銷,從而導致寫入性能下降。所以如果追求極致的寫入性能,應該盡量避免使用它們。
在前面曾介紹過,ClickHouse 內部所有的數據操作都是面向 Block 數據塊的,所以 INSERT 查詢最終會將數據轉換為 Block 數據塊。也正因為如此,INSERT 語句在單個數據塊的寫入過程中是具有原子性的。在默認情況下,每個數據塊最多可以寫入 1048576 條數據(由 max_insert_block_size 參數控制)。也就是說,如果一條 INSERT 語句寫入的數據行數少於 max_insert_block_size,那么這批數據的寫入是具有原子性的,要么全部成功,要么全部失敗。但是需要注意的是,只有在 ClickHouse 服務端處理數據的時候才具有這種原子寫入的特性,例如使用 HTTP 接口,因為 max_insert_block_size 參數在使用 CLI 命令行或者 INSERT SELECT 子句寫入時是不生效的。
刪除與修改
ClickHouse 提供了 DELETE 和 UPDATE 的能力,這類操作被稱為 Mutation 查詢,它可以看作 ALTER 語句的變種。雖然 Mutation 能最終實現修改和刪除,但不能完全以通常意義上的 UPDATE 和 DELETE 來理解,我們必須清醒地認識到它的不同。首先,Mutation 語句是一種 "很重" 的操作,更適用於批量數據的修改和刪除;其次,它不支持事務,一旦語句被提交執行,就會立刻對現有數據造成影響,無法回滾;最后,Mutation 語句的執行是一個異步的后台過程,語句被提交之后就會立即返回。所以這並不代表具體邏輯已經執行完畢,它的具體執行進度需要通過 system.mutations 系統表查詢。
DELETE 語句的完整語法如下所示:
ALTER TABLE [db_name.]table_name DELETE WHERE filter_expr
數據刪除的范圍由 WHERE 查詢子句決定。例如,執行下面語句可以刪除 partition_v2 表內所有 ID 等於 'xxx' 的數據:
ALTER TABLE partition_v2 DELETE WHERE ID ='xxx'
如果數據很少的話,那么 DELETE 操作給人的感覺和常用的 OLTP 數據庫無異,但我們心中應該要明白這是一個異步的后台執行動作。
下面我們來實際刪除數據,就以 partition_v1 為例吧,先來看看對應目錄(/var/lib/clickhouse/data/default/partition_v1)里面的內容:
執行該語句:ALTER TABLE partition_v1 DELETE WHERE ID ='xxx' 進行數據刪除,執行完之后再看一下目錄結構:
可以發現,在執行了 DELETE 操作后數據目錄發生了一些變化,每一個原有的數據目錄都額外增加了一個同名目錄,並且在末尾處增加了 _3 后綴。此外,目錄下還多了一個名為 mutation_3.txt 文件,里面的內容如下:
[root@satori partition_v1]# cat mutation_3.txt
format version: 1
create time: 2021-08-16 14:55:41
commands: DELETE WHERE ID = \'xxx\'
[root@satori partition_v1]#
原來 mutation3.txt 是一個日志文件,它完整地記錄了這次 DELETE 操作的執行語句和時間,而文件名的后綴 _3 與新增目錄的后綴對應。那么后綴的數字從何而來呢?繼續查詢 system.mutations 系統表,一探究竟:
SELECT database, table, mutation_id, block_numbers.number as num, is_done FROM system .mutations
至此,整個 Mutation 操作的邏輯就比較清晰了。每執行一條 ALTER DELETE 語句,都會在 mutations 系統表中生成一條對應的執行計划,當 is_done 等於 1 時表示執行完畢。與此同時,在數據表的根目錄下,會以 mutation_id 作為名字生成與之對應的日志文件用於記錄相關信息。而數據刪除的過程是以數據表的每個分區目錄為單位,將所有目錄重寫為新的目錄,新目錄的命名規則是在原有名稱上加上 system.mutations.block_numbers.number。數據在重寫的過程中會將需要刪除的數據去掉,舊的數據目錄並不會立即刪除,而是會被標記成非激活狀態(active 為 0)。等到 MergeTree 引擎的下一次合並動作觸發時,這些非激活目錄才會被真正從物理意義上刪除。
數據修改除了需要指定具體的要更新的列字段之外,整個邏輯與數據刪除別無二致,它的完整語法如下所示:
ALTER TABLE [db_name.]table_name UPDATE column1 = expr1 [, ...] WHERE filter_expr
UPDATE 支持在一條語句中同時定義多個修改字段,但是分區鍵和主鍵不能作為修改字段。