Clickhouse上用Order By保證絕對正確結果但代價是性能

一些聚合函數的結果跟流入數據的順序有關,CH文檔明確說明這樣的函數的結果是不確定的。這是為什麼呢?讓我們用explain pipeline來一探究竟。

以一個很簡單的查詢為例:

select any( step ) from events group by request_id;

events表的定義如下:

CREATE TABLE default.events
(
    `ID` UInt64,
    `request_id` String,
    `step_id` Int64,
    `step` String
)
ENGINE = MergeTree
ORDER BY ID

該查詢從events表裏面讀取數據步驟 step 和請求ID request_id ,按照request_id分組並取第一個step

我們看一下這個查詢的pipeline:

localhost :) explain pipeline select any( `step`) from events group by request_id

┌─explain────────────────────────────────┐
│ (Expression)                           │
│ ExpressionTransform                    │
│   (Aggregating)                        │
│   Resize 32 → 1                        │
│     AggregatingTransform × 32          │
│       StrictResize 32 → 32             │
│         (Expression)                   │
│         ExpressionTransform × 32       │
│           (SettingQuotaAndLimits)      │
│             (ReadFromMergeTree)        │
│             MergeTreeThread × 32 0 → 1 │
└────────────────────────────────────────┘

可以看出沒有sorting步驟。這個查詢在多核服務器中速度是相當快的,因為充分利用了多核,直到最後一步才歸併成一個數據流由一個線程來處理。

可是要注意 這個查詢的結果每次都不一樣,可以用加過濾條件的計數來測試,測試的SQL如下:

select countIf(A='step1') from (select any( `step`) as A from (select * from events) group by request_id)

結果是:2500579, 2500635,2500660。結果差距都不大,但都不是絕對正確的結果。這是因為多線程執行時並不能嚴格保證是按照engine=MergeTree 的表的存儲順序來處理數據的。如果能容忍誤差就沒問題,因為這個查詢的效率是非常高的。

但如果要追求絕對的正確結果。則需要顯示地指定順序,改造查詢如下:

select any( step ) from (select * from events order by ID) group by request_id;

查詢的pipeline變成這樣:

localhost :) explain pipeline select any( step ) from (select * from events order by ID) group by request_id;

┌─explain─────────────────────────────────┐
│ (Expression)                            │
│ ExpressionTransform                     │
│   (Aggregating)                         │
│   AggregatingTransform                  │
│     (Expression)                        │
│     ExpressionTransform                 │
│       (Sorting)                         │
│       MergingSortedTransform 36 → 1     │
│         (Expression)                    │
│         ExpressionTransform × 36        │
│           (SettingQuotaAndLimits)       │
│             (ReadFromMergeTree)         │
│             MergeTreeInOrder × 36 0 → 1 │
└─────────────────────────────────────────┘

注意到pipeline中增加了重要的一步MergingSortedTransform 36 → 1 ,這一步保證了查詢的正確性,但是將多個線程的數據流歸集到一起,排序後繼續由一個線程完成剩下的處理步驟,效率上受到很大的影響。測試結果表示:加了ORDER BY 子句的查詢能夠得到一致的正確結果,但效率差了至少10倍。越是核數多的服務器,其差距越大。