ClickHouse如何更新數據(精)

ClickHouse系列文章:

  1. ClickHouse如何更新數據
  2. ClickHouse Join爲什麼被大家詬病?
  3. 有必要了解下ClickHouse的索引原理

問題背景

在 OLAP 數據庫中,可變數據通常不受歡迎。ClickHouse 也不歡迎可變數據。然而現實情況,更新情況不可避免。那麼ClickHouse如何進行更新數據了?以及如何進行準實時更新了?

更新方法

Partition Operations

這個方法是比較早就提出的解法,大致思路是就是操作分區,有更新就將刪掉原分區,然後用新的分區替代。

用法

下面是通過分區進行數據更新的步驟:

  1. Create modified partition with updated data on another table
  2. Copy data for this partition to detached directory
  3. DROP PARTITION in main table
  4. ATTACH PARTITION in main table

適用場景

分區交換對於低頻率的批量數據更新比較有用,但當需要實時的高頻率的更新數據時,它們就不那麼方便了。此外,開發人員操作分區還是不太方便的,因此這種方法一般用的比較少。

詳細內容請看:How to Update Data in ClickHouse

Incremental Log

Incremental log的思想是什麼了?比如對於用戶瀏覽統計表中的一條數據,如下所示:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 
│ 4324182021466249494 │         5 │      146 │    1 │ 
└─────────────────────┴───────────┴──────────┴──────┘

現在有更新了:用戶又瀏覽了一個頁面,所以我們應該改變pageview從5到6,以及持續時間從146到185。那麼按照Incremental log的思想,再插入兩行:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 
│ 4324182021466249494 │         5 │      146 │   -1 │ 
│ 4324182021466249494 │         6 │      185 │    1 │ 
└─────────────────────┴───────────┴──────────┴──────┘

第一個是刪除行。它和我們已經得到的行是一樣的只是Sign被設爲-1。第二個更新行,所有數據設置爲新值。之後我們有三行數據:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐ 
│ 4324182021466249494 │         5 │      146 │    1 │ 
│ 4324182021466249494 │         5 │      146 │   -1 │ 
│ 4324182021466249494 │         6 │      185 │    1 │ 
└─────────────────────┴───────────┴──────────┴──────┘

那麼對於count,sum,avg的計算方法如下:

-- number of sessions
count() -> sum(Sign)  
-- total number of pages all users checked 
sum(PageViews) -> sum(Sign * PageViews)  
-- average session duration, how long user usually spent on the website 
avg(Duration) -> sum(Sign * Duration) / sum(Sign)

這就是Incremental log方法,這種方法的不足之處在於:

  • 首先需要獲取到原數據,那麼就需要先查一遍CK,或者將數據保存到其他存儲中便於檢索查詢,然後我們纔可以針對原數據插入一條 ‘delete’ rows;
  • Sign operations在某些計算場景並不適合,比如min、max、quantile等其他場景;
  • 額外的寫入放大:當每個對象的平均更新次數爲個位數時,更適合使用。

針對Incremental log方式的寫入方案存儲開銷問題,clickhouse提供了CollapsingMergeTree,使用CollapsingMergeTree,“刪除”行和舊的“刪除”行將在合併過程中摺疊。但是,注意這個引擎,只是解決了寫放大問題,並不是說查詢模式就不是Incremental Log這種,我們還是需要通過對sign的特殊計算方式,達到效果。

實踐一下,建表如下:

CREATE TABLE UAct
(
    UserID UInt64,
    PageViews UInt8,
    Duration UInt8,
    Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID;

插入狀態行,注意sign一列的值爲1:

INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1);

插入一行取消行,用於抵消上述狀態行。注意sign一列的值爲-1,其餘值與狀態行一致;並且插入一行主鍵相同的新狀態行,用來將PageViews從5更新至6,將Duration從146更新爲185.

INSERT INTO UAct VALUES 
(4324182021466249494, 5, 146, -1), 
(4324182021466249494, 6, 185, 1);

查詢數據:可以看到未Compaction之前,狀態行與取消行共存:

爲了獲取正確的sum值,需要改寫SQL:

-- sum(PageViews) => sum(PageViews * Sign)、 
-- sum(Duration) => sum(Duration * Sign)
SELECT UserID,
       sum(PageViews * Sign) AS PageViews,     
       sum(Duration * Sign) AS Duration
FROM UAct 
GROUP BY UserID 
HAVING sum(Sign) > 0; 

最後,我們強制讓其摺疊一下:

optimize table UAct final;

再查詢數據,結果已經是摺疊後的的結果了

其他疑問:字段是否可以用case when 對標誌位進行過濾?
答案:不行,因爲達不到效果。我們要達到的效果,首先是刪除原來的列,然後再新增一個有效列。沒法根據標誌位判斷哪一個是有效的,除非標誌位的值不斷增加,然後不斷增加,那就不屬於這種方式,下文會提到根據時間argMax的方法。

詳細內容請看:How to Update Data in ClickHouse

Alter/Update Table

ClickHouse團隊在2018年發佈了UPDATE和DELETE,但是它不是原生的UPDATE和DELETE語句,而是被實現爲ALTER TABLE UPDATE語句,如下所示:

ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr;

還是針對上面的表,修改瀏覽數,ALTER UPDATE語句如下:

ALTER TABLE UPDATE PageViews=7 WHERE UserID=4324182021466249700;

然後查看結果:

其實並沒有更新,這是爲什麼了?因爲更新是一個異步的操作。當用戶執行一個如上的Update操作獲得返回時,ClickHouse內核其實只做了兩件事情:
1.檢查Update操作是否合法;
2.保存Update命令到存儲文件中,喚醒一個異步處理merge和mutation的工作線程;

異步線程的工作流程極其複雜,總結其精髓描述如下:先查找到需要update的數據所在datapart,之後對整個datapart做掃描,更新需要變更的數據,然後再將數據重新落盤生成新的datapart,最後用新的datapart做替代並remove掉過期的datapart。

這就是ClickHouse對update指令的執行過程,可以看出,頻繁的update指令對於ClickHouse來說將是災難性的。(當然,我們可以通過設置,將這個異步的過程變成同步的過程,詳細請看:Synchronicity of ALTER Queries,然而同步阻塞就會比較嚴重)。

ClickHouse對Update語句支持的不好,但是對於Insert語句,尤其是批量插入支持的很好。所以更新操作用Insert替代會很快就返回。 但是用Insert,我們如何完成更新這個動作,以及如何保證查詢到最新數據了?

Insert+xxxMergeTree

用Insert加特定引擎,也可以實現更新效果。該方法適用於xxxMergeTree,如ReplacingMergeTree或AggregatingMergeTree。但是了,更新是異步的。因此剛插入的數據,並不能馬上看到最新的結果,因此並不是準實時的。

比如使用AggregatingMergeTree,用法如下:

CREATE TABLE IF NOT EXISTS whatever_table ON CLUSTER default (     
  user_id UInt64,
  gender SimpleAggregateFunction(anyLast, Nullable(Enum('女' = 0, '男' = 1))),
  ...
)
ENGINE = AggregatingMergeTree() partition by toYYYYMMDD(reg_date) ORDER BY user_id;

就以上建標語句展開分析,AggregatingMergeTree會將除主鍵(user)外的其餘列,配合anyLast函數,替換每行數據爲一種預聚合狀態。其中anyLast聚合函數聲明聚合策略爲保留最後一次的更新數據。

詳細內容,請看:ClickHouse:抓住你的每一個目標用戶,人羣圈選業務的大殺器

實時性: 非準實時。

優點在於:
ClickHouse提供的這些mergeTree引擎,可以幫助我們達到最終一致性。

缺點在於:
xxxMergeTree並不能保證任何時候的查詢都是聚合過後的結果,並且也沒有提供標誌位用於查詢數據的聚合狀態與進度。因此,爲了確保數據在查詢前處於已聚合的狀態,還需手動下發optimize指令強制聚合過程的執行。

Insert+xxxxMergeTree+Final

用xxxMergeTree是異步的,如何達到準實時的效果了?ClickHouse提供了FINAL關鍵字來解決這個問題。。當指定FINAL後,ClickHouse會在返回結果之前完全合併數據,從而執行給定表引擎合併期間發生的所有數據轉換。

用法

首先Insert數據:

INSERT INTO test_a (*) VALUES (1, 'a', 1) ;

查詢時,加入final關鍵字,如下所示:

SELECT COUNT()FROM test_a FINAL

優缺點

對上述語句,explain後,查詢執行計劃如下所示:

Expression ((Projection + Before ORDER BY))
  Aggregating
    Expression (Before GROUP BY)
      SettingQuotaAndLimits (Set limits and quota after reading from storage)
        Expression (Remove unused columns after reading from storage)
          MergingFinal (Merge rows for FINAL)
            Expression (Calculate sorting key expression)
              ReadFromStorage (MergeTree with final)

從執行計劃可以看出代價比較高:

  • 是一個串行過程;
  • 會進行分區合併;

因此,這個FINAL,也不宜頻繁的使用。

Insert + argMax

用法

argMax 函數的參數如下所示,它能夠按照 field2 的最大值取 field1 的值:

argMax(field1,field2)

當我們更新數據時,會寫入一行新的數據,通過查詢最大的 create_time 得到修改後的字段值,例如通過下面的語句可以得到最新的 score :

argMax(score, create_time) AS score

具體用法如下所示:

select ru_id,row_update_time,
       argMax(is_effective,row_update_time) is_effective
from t_ru_packaging_build
group by ru_id,row_update_time;

如果我們聚合統計指標,那麼SQL語句如下:

select ru_id,sum(case when is_effective =1 then 1 else 0 end) from (
    select ru_id,
           row_update_time,
           argMax(is_effective,row_update_time) is_effective
    from t_ru_packaging_build
    group by ru_id,row_update_time
) group by ru_id;

爲了簡化SQL,我們可以建立一個視圖,注意不是物化視圖,如下所示:

CREATE VIEW view_ru_packaging_build as
SELECT ru_id,
       row_update_time,
       argMax(is_effective,row_update_time) is_effective
FROM t_ru_packaging_build
GROUP BY ru_id,row_update_time;

此時我們只需查詢視圖即可,查詢語句如下所示:

SELECT ru_id,
       sum(case when is_effective =1 then 1 else 0 end) 
FROM   view_ru_packaging_build
GROUP BY ru_id;

實時性: 查詢每次都是實時的。

優點如下:

  • 爲更新數據提供了一個新的解法思路;

缺點如下:

  • 查詢語句比較複雜;
  • 如果還要做一些聚合統計邏輯,那麼就需要子查詢;
  • 內存開銷會大一些。

詳細內容,請看:ClickHouse準實時數據更新的新思路 - 騰訊雲

OPTIMIZE FINAL

因此在業務需要數據更新的場景下(如Mysql同步到Clickhouse),通常會使用ReplacingMergeTree或CollapsingMergeTree的數據合併邏輯繞行實現異步更新,這樣一方面可以保證數據的最終一致性,另一方面Clickhouse性能開銷也會比alter table小。但這種方式有一個缺點是MergeTree引擎的數據合併過程(merge)是Clickhouse基於策略控制的,執行時間比較隨機,因此數據一致性缺少時間保證,極端情況下數據過了一天也沒有完全合併。
而Optimize Table這個命令可以強制觸發MergeTree引擎的數據合併,可以用來解決數據合併時間不確定的問題。

OPTIMIZE FINAL 可以進行強制刷新,使用方式如下:

OPTIMIZE TABLE {tableName} FINAL

注意OPTIMIZE操作速度慢,代價高,因此不能頻繁的執行。

詳細介紹,請看:Clickhouse Optimize Table全面解析- 雲+社區 - 騰訊雲

方法總結

上面提供了七種更新方法,這裏對其進行一個對比總結:

方法名 實時性 優勢 不足 適合場景
Partition Operations 非準實時 1、不適用於實時場景;
2、操作不便
大批量修改,非準實時場景
Incremental Log 準實時 某些場景可以用這個辦法取巧解決 1、要先查原數據;
2、某些計算場景並不適合,比如min、max等其他場景;
對於sum、count、avg等飛unique場景
Alter/Update Table 非準實時 1、異步的過程
2、不能頻繁操作
Insert+ xxxMergeTree 非準實時 最終一致性,非準實時; 對實時性要求不高的場景
Insert+ xxxMergeTree+Final 準實時 可以實現準實時 準實時;
Final不能頻繁使用
如果是查的不頻繁,可以用這個來實現準實時
Insert+argMax 準實時 實時性好,開銷相對能接受 1、查詢語句複雜;
2、內存開銷稍微較大;
對比其他方法,這是對於修改較頻繁的場景適用的方法
OPTIMIZE FINAL 準實時 操作後,一定是最新的 代價很大,耗時很長,不可頻繁使用 某些驗證場景,或者臨時操作

總結一下,其實clickhouse在追求極致的速度面前,對更新其實還是支持的不是很好的,因此如果業務場景本身是更新很頻繁,同時又對更新實時性有很高的要求,那麼其實可能並不應該選擇使用ClickHouse,而是應該使用其他OLAP引擎。

如果更新不是很頻繁,且已經選定了要使用clickhouse,那就根據業務場景,以及對實時性的要求,選用上面6中更新方式中的一種吧。

參考文檔

ClickHouse準實時數據更新的新思路 - 騰訊雲
在ClickHouse 中處理實時更新- 掘金
ClickHouse - How to Update Data in ClickHouse
Clickhouse Optimize Table全面解析- 雲+社區 - 騰訊雲
https://altinity.com/blog/2018/10/16/updates-in-clickhouse
ClickHouse:抓住你的每一個目標用戶,人羣圈選業務的大殺器
Clickhouse UPDATE 和DELETE操作_vkingnew的博客
ClickHouse多種實時更新方法總結 - 墨天輪

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章