ClickHouse(13)ClickHouse合併樹MergeTree家族表引擎之CollapsingMergeTree詳細解析

張飛的豬發表於2023-02-28

該引擎繼承於MergeTree,並在資料塊合併演算法中新增了摺疊行的邏輯。CollapsingMergeTree會非同步的刪除(摺疊)這些除了特定列Sign有1和-1的值以外,其餘所有欄位的值都相等的成對的行。沒有成對的行會被保留。因此,該引擎可以顯著的降低儲存量並提高SELECT查詢效率。
簡單來說就是,clickhouse會自動的合併有效和無效的資料,減少資料儲存,並減少update所產生的效能消耗。具體的邏輯,下面介紹。

建表

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = CollapsingMergeTree(sign)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

sign — 型別列的名稱:1是«狀態»行,也就是最後的有效行,-1是«取消»行,也就是無效行。列資料型別 — Int8。

建立CollapsingMergeTree表時,需要與建立 MergeTree 表時相同的子句。

摺疊

資料

考慮你需要為某個物件儲存不斷變化的資料的情景。似乎為一個物件儲存一行記錄並在其發生任何變化時更新記錄是合乎邏輯的,但是更新操作對DBMS來說是昂貴且緩慢的,因為它需要重寫儲存中的資料。如果你需要快速的寫入資料,則更新操作是不可接受的,但是你可以按下面的描述順序地更新一個物件的變化。

在寫入行的時候使用特定的列Sign。如果Sign=1則表示這一行是物件的狀態,我們稱之為«狀態»行。如果Sign=-1則表示是對具有相同屬性的狀態行的取消,我們稱之為«取消»行。

例如,我們想要計算使用者在某個站點訪問的頁面頁面數以及他們在那裡停留的時間。在某個時候,我們將使用者的活動狀態寫入下面這樣的行。

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

一段時間後,我們寫入下面的兩行來記錄使用者活動的變化。

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

第一行取消了這個物件(使用者)的狀態。它需要複製被取消的狀態行的所有除了Sign的屬性。

第二行包含了當前的狀態。因為我們只需要使用者活動的最後狀態,這些行可以在摺疊物件的失效(老的)狀態的時候被刪除。CollapsingMergeTree會在合併資料片段的時候做這件事。

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

這種方法的特殊屬性

  1. 寫入的程式應該記住物件的狀態從而可以取消它。«取消»字串應該是«狀態»字串的複製,除了相反的Sign。它增加了儲存的初始資料的大小,但使得寫入資料更快速。
  2. 由於寫入的負載,列中長的增長陣列會降低引擎的效率。資料越簡單,效率越高。
  3. SELECT的結果很大程度取決於物件變更歷史的一致性。在準備插入資料時要準確。在不一致的資料中會得到不可預料的結果,例如,像會話深度這種非負指標的負值。

演算法

當ClickHouse合併資料片段時,每組具有相同主鍵的連續行被減少到不超過兩行,一行Sign=1(«狀態»行),另一行Sign=-1(«取消»行),換句話說,資料項被摺疊了。

對每個結果的資料部分ClickHouse儲存的演算法

  1. 如果«取消»和«狀態»行數量相同,並且最後一行«狀態»行,保留第一個«取消»和最後一個«狀態»行。
  2. 如果«狀態»行比«取消»行多一個或一個以上,保留最後一個«狀態»行。
  3. 如果«取消»行比«狀態»行多一個或一個以上,保留第一個«取消»行。
  4. 沒有行,在其他所有情況下。合併會繼續,但ClickHouse會把此情況視為邏輯錯誤並將其記錄在服務日誌中。這個錯誤會在相同的資料被插入超過一次時出現。

因此,摺疊不應該改變統計資料的結果。變化逐漸地被摺疊,因此最終幾乎每個物件都只剩下了最後的狀態。

Sign是必須的因為合併演算法不保證所有有相同主鍵的行都會在同一個結果資料片段中,甚至是在同一臺物理伺服器上。ClickHouse用多執行緒來處理SELECT請求,所以它不能預測結果中行的順序。如果要從CollapsingMergeTree表中獲取完全«摺疊»後的資料,則需要聚合。

要完成摺疊,請使用GROUP BY子句和用於處理符號的聚合函式編寫請求。例如,要計算數量,使用sum(Sign)而不是 count()。要計算某物的總和,使用sum(Sign * x)而不是sum(x),並新增HAVING sum(Sign) > 0子句。

聚合體count,sum和avg可以用這種方式計算。如果一個物件至少有一個未被摺疊的狀態,則可以計算uniq聚合。min和 max聚合無法計算,因為CollaspingMergeTree不會儲存摺疊狀態的值的歷史記錄。

如果你需要在不進行聚合的情況下獲取資料(例如,要檢查是否存在最新值與特定條件匹配的行),你可以在 FROM 從句中使用 FINAL 修飾符。這種方法顯然是更低效的。

# 示例:

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

# 建表:

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

# 插入資料:

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

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

#我們使用兩次INSERT請求來建立兩個不同的資料片段。如果我們使用一個請求插入資料,ClickHouse只會建立一個資料片段且不會執行任何合併操作。

#獲取資料:

SELECT * FROM UAct

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



#我們看到了什麼,哪裡有摺疊?

#透過兩個 INSERT 請求,我們建立了兩個資料片段。
#SELECT請求在兩個執行緒中被執行,我們得到了隨機順序的行。
#沒有發生摺疊是因為還沒有合併資料片段。
#ClickHouse 在一個我們無法預料的未知時刻合併資料片段。

#因此我們需要聚合:

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

┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │         6 │      185 │
└─────────────────────┴───────────┴──────────┘

# 如果我們不需要聚合並想要強制進行摺疊,我們可以在 FROM 從句中使用 FINAL 修飾語。

SELECT * FROM UAct FINAL

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

# 這種查詢資料的方法是非常低效的。不要在大表中使用它。

資料分享

ClickHouse經典中文文件分享

參考文章

相關文章