ClickHouse高級

来源:https://www.cnblogs.com/xiaoQQya/archive/2022/05/26/16313680.html
-Advertisement-
Play Games

ClickHouse高級 1. 執行計劃 在 ClickHouse 20.6 版本之前要查看 SQL 語句的執行計劃需要設置日誌級別為 TRACE 才可以看到,並且只能真正執行 SQL,在執行日誌裡面查看。在 20.6 版本引入了原生的執行計劃語法,併在 20.6.3.28 版本成為正式功能。 1. ...


ClickHouse高級

目錄

1. 執行計劃

在 ClickHouse 20.6 版本之前要查看 SQL 語句的執行計劃需要設置日誌級別為 TRACE 才可以看到,並且只能真正執行 SQL,在執行日誌裡面查看。在 20.6 版本引入了原生的執行計劃語法,併在 20.6.3.28 版本成為正式功能。

1.1 基本語法

EXPLAIN [AST | SYNTAX | PLAN | PIPELINE] [setting = value, ...] SELECT ... [FORMAT ...]
  • PLAN:用於查看執行計劃,預設值
    • header:列印計劃中各個步驟的 head 說明,預設關閉,預設值 0
    • description:列印計劃中各個步驟的描述,預設開啟,預設值 1
    • actions:列印計劃中各個步驟的詳細信息,預設關閉,預設值 0
  • AST:用於查看語法樹
  • SYNTAX:用於優化語法
  • PIPELINE:用於查看 PIPELINE 計劃
    • header:列印計劃中各個步驟的 head 說明,預設關閉
    • graph:用 DOT 圖像語言描述管道圖,預設關閉,查看相關的圖形需要 graphviz 查看
    • actions:如果開啟了 graph,緊湊列印,預設開啟

註:PLAN 和 PIPELINE 可以進行額外的顯示設置,如上參數所示。

1.2 案例實操

1.2.1 新版本

  1. PLAN

    -- 簡單查詢
    :) EXPLAIN PLAN SELECT arrayJoin([1,2,3,NULL,NULL]);
    
    EXPLAIN
    SELECT arrayJoin([1, 2, 3, NULL, NULL])
    
    Query id: dbecbf4d-ccd9-4064-900f-8fc95e04f36b
    
    ┌─explain───────────────────────────────────────────────────────────────────┐
    │ Expression ((Projection + Before ORDER BY))                               │
    │   SettingQuotaAndLimits (Set limits and quota after reading from storage) │
    │     ReadFromStorage (SystemOne)                                           │
    └───────────────────────────────────────────────────────────────────────────┘
    
    3 rows in set. Elapsed: 0.001 sec.
     
    -- 複雜 SQL 的執行計劃
    :) EXPLAIN
    :-] SELECT database, table, count(1) cnt
    :-] FROM system.parts
    :-] WHERE database IN ('datasets', 'system')
    :-] GROUP BY database, table
    :-] ORDER BY database, cnt DESC
    :-] LIMIT 2 BY database;
    
    EXPLAIN
    SELECT
        database,
        table,
        count(1) AS cnt
    FROM system.parts
    WHERE database IN ('datasets', 'system')
    GROUP BY
        database,
        table
    ORDER BY
        database ASC,
        cnt DESC
    LIMIT 2 BY database
    
    Query id: 500e3a0e-d29e-4316-a863-01de50d811f6
    
    ┌─explain─────────────────────────────────────────────────────────────────────────────────────┐
    │ Expression (Projection)                                                                     │
    │   LimitBy                                                                                   │
    │     Expression (Before LIMIT BY)                                                            │
    │       MergingSorted (Merge sorted streams for ORDER BY)                                     │
    │         MergeSorting (Merge sorted blocks for ORDER BY)                                     │
    │           PartialSorting (Sort each block for ORDER BY)                                     │
    │             Expression (Before ORDER BY)                                                    │
    │               Aggregating                                                                   │
    │                 Expression (Before GROUP BY)                                                │
    │                   Filter (WHERE)                                                            │
    │                     SettingQuotaAndLimits (Set limits and quota after reading from storage) │
    │                       ReadFromStorage (SystemParts)                                         │
    └─────────────────────────────────────────────────────────────────────────────────────────────┘
    
    12 rows in set. Elapsed: 0.004 sec.
    
    -- 打開全部的參數的執行計劃
    :) EXPLAIN header = 1, actions = 1, description = 1
    :-] SELECT number
    :-] FROM system.numbers
    :-] LIMIT 10;
    
    EXPLAIN header = 1, actions = 1, description = 1
    SELECT number
    FROM system.numbers
    LIMIT 10
    
    Query id: 7495adae-1de1-4dd6-a206-3915b2174a9a
    
    ┌─explain───────────────────────────────────────────────────────────────────┐
    │ Expression ((Projection + Before ORDER BY))                               │
    │ Header: number UInt64                                                     │
    │ Actions: INPUT :: 0 -> number UInt64 : 0                                  │
    │ Positions: 0                                                              │
    │   SettingQuotaAndLimits (Set limits and quota after reading from storage) │
    │   Header: number UInt64                                                   │
    │     Limit (preliminary LIMIT (without OFFSET))                            │
    │     Header: number UInt64                                                 │
    │     Limit 10                                                              │
    │     Offset 0                                                              │
    │       ReadFromStorage (SystemNumbers)                                     │
    │       Header: number UInt64                                               │
    └───────────────────────────────────────────────────────────────────────────┘
    
    12 rows in set. Elapsed: 0.002 sec.
    
  2. AST 語法樹

    :) EXPLAIN AST
    :-] SELECT number
    :-] FROM system.numbers
    :-] LIMIT 10;
    
    EXPLAIN AST
    SELECT number
    FROM system.numbers
    LIMIT 10
    
    Query id: 5fa352a1-4e72-4e3d-953f-fd249eddf144
    
    ┌─explain─────────────────────────────────────┐
    │ SelectWithUnionQuery (children 1)           │
    │  ExpressionList (children 1)                │
    │   SelectQuery (children 3)                  │
    │    ExpressionList (children 1)              │
    │     Identifier number                       │
    │    TablesInSelectQuery (children 1)         │
    │     TablesInSelectQueryElement (children 1) │
    │      TableExpression (children 1)           │
    │       TableIdentifier system.numbers        │
    │    Literal UInt64_10                        │
    └─────────────────────────────────────────────┘
    
    10 rows in set. Elapsed: 0.002 sec.
    
  3. SYNTAX 語法優化

    -- 執行查詢
    :) SELECT number = 1 ? 'hello' : (number = 2 ? 'world' : 'atguigu')
    :-] FROM numbers(10);
    
    SELECT if(number = 1, 'hello', if(number = 2, 'world', 'atguigu'))
    FROM numbers(10)
    
    Query id: 16e12575-e91c-46ff-b82e-3d84df5e9fd9
    
    ┌─multiIf(equals(number, 1), 'hello', equals(number, 2), 'world', 'atguigu')─┐
    │ atguigu                                                                    │
    │ hello                                                                      │
    │ world                                                                      │
    │ atguigu                                                                    │
    │ atguigu                                                                    │
    │ atguigu                                                                    │
    │ atguigu                                                                    │
    │ atguigu                                                                    │
    │ atguigu                                                                    │
    │ atguigu                                                                    │
    └────────────────────────────────────────────────────────────────────────────┘
    
    10 rows in set. Elapsed: 0.003 sec.
    
    -- 查看語法優化
    :) EXPLAIN SYNTAX
    :-] SELECT number = 1 ? 'hello' : (number = 2 ? 'world' : 'atguigu')
    :-] FROM numbers(10);
    
    EXPLAIN SYNTAX
    SELECT if(number = 1, 'hello', if(number = 2, 'world', 'atguigu'))
    FROM numbers(10)
    
    Query id: 5f2be03d-d378-4490-b803-648e0a2a64ef
    
    ┌─explain────────────────────────────────────────────────────────────┐
    │ SELECT if(number = 1, 'hello', if(number = 2, 'world', 'atguigu')) │
    │ FROM numbers(10)                                                   │
    └────────────────────────────────────────────────────────────────────┘
    
    2 rows in set. Elapsed: 0.001 sec.
    
    -- 開啟三元運算符優化
    SET optimize_if_chain_to_multiif = 1;
    
    -- 再次查看語法優化
    :) EXPLAIN SYNTAX
    :-] SELECT number = 1 ? 'hello' : (number = 2 ? 'world' : 'atguigu')
    :-] FROM numbers(10);
    
    EXPLAIN SYNTAX
    SELECT if(number = 1, 'hello', if(number = 2, 'world', 'atguigu'))
    FROM numbers(10)
    
    Query id: a41d06bf-31a2-475a-bfcc-e96496fada63
    
    ┌─explain─────────────────────────────────────────────────────────────┐
    │ SELECT multiIf(number = 1, 'hello', number = 2, 'world', 'atguigu') │
    │ FROM numbers(10)                                                    │
    └─────────────────────────────────────────────────────────────────────┘
    
    2 rows in set. Elapsed: 0.002 sec.
    
  4. PIPELINE

    :) EXPLAIN PIPELINE
    :-] SELECT sum(number)
    :-] FROM numbers_mt(100000)
    :-] GROUP BY number % 20;
    
    EXPLAIN PIPELINE
    SELECT sum(number)
    FROM numbers_mt(100000)
    GROUP BY number % 20
    
    Query id: b7eb588e-4987-4489-86ab-ed0442061d66
    
    ┌─explain─────────────────────────┐
    │ (Expression)                    │
    │ ExpressionTransform             │
    │   (Aggregating)                 │
    │   Resize 4 → 1                  │
    │     AggregatingTransform × 4    │
    │       (Expression)              │
    │       ExpressionTransform × 4   │
    │         (SettingQuotaAndLimits) │
    │           (ReadFromStorage)     │
    │           NumbersMt × 4 0 → 1   │
    └─────────────────────────────────┘
    
    10 rows in set. Elapsed: 0.003 sec.
    
    -- 更多參數
    EXPLAIN PIPELINE header = 1,graph = 1
    SELECT sum(number)
    FROM numbers_mt(100000)
    GROUP BY number % 20;
    

1.2.2 老版本

$ clickhouse-client -h 主機名 --send_logs_level=trace <<< "sql" > /dev/null

其中,send_logs_level參數指定日誌等級為 trace,<<<將 SQL 語句重定向至 clickhouse-client 進行查詢,> /dev/null將查詢結果重定向到空設備吞掉,以便觀察日誌。

註意:通過將 ClickHouse 的服務日誌,設置到 DEBUG 或者 TRACE 級別,才可以變相實現 EXPLAIN 查詢的作用。

2. 建表優化

2.1 數據類型

2.1.1 時間欄位類型

建表時能用數值型或日期時間型表示的欄位就不要用字元串,全 String 類型在以 Hive 為中心的數倉建設中常見,但 ClickHouse 環境不應受此影響。

雖然 ClickHouse 底層將 DateTime 存儲為時間戳 Long 類型,但不建議存儲 Long 類型,因為 DateTime 不需要經過函數轉換處理,執行效率高、可讀性好。

-- create_time 不為 DateTime 類型時,需要經過 toDate 函數轉換
CREATE TABLE t_type2
(
    id           UInt32,
    sku_id       String,
    total_amount Decimal(16, 2),
    create_time  Int32
) ENGINE = ReplacingMergeTree(create_time)
      PARTITION BY toYYYYMMDD(toDate(create_time))
      PRIMARY KEY (id)
      ORDER BY (id, sku_id);

2.1.2 空值存儲類型

官方已經指出 Nullable 類型幾乎總還是會拖累性能,因為存儲 Nullable 列時需要創建一個額外的文件來存儲 NULL 標記,並且 Nullable 列無法被索引。因此除非極特殊情況,應直接使用欄位預設值表示空,或者自行指定一個在業務中無意義的值(例如用 -1 表示沒有商品 ID)。

-- 創建帶空值的表
CREATE TABLE t_null
(
    x Int8,
    y Nullable(Int8)
) ENGINE = TinyLog;

-- 插入數據
INSERT INTO t_null
VALUES (1, NULL),
       (2, 3);

-- 查詢
SELECT x + y
FROM t_null;

官方說明:Nullable | ClickHouse Documentation

2.2 分區和索引

分區粒度根據業務特點決定,不宜過粗或過細。一般選擇按天分區,也可以指定為 Tuple(),以單表一億數據為例,分區大小控制在 10-30 個最佳。

必須指定索引列,ClickHouse 中的索引列即排序列,通過 order by 指定,一般在查詢條件中經常被用來充當篩選條件的屬性被納入進來;可以是單一維度,也可以是組合維度的索引;通常需要滿足高級列在前、查詢頻率大的在前原則;還有基數特別大的不適合做索引列,如用戶表的 userid 欄位;通常篩選後的數據滿足在百萬以內為最佳

2.3 表參數

index_granularity 是用來控制索引粒度的,預設是 8192,如非必須不建議調整。

如果表中不是必須保留全量歷史數據,建議指定 TTL(生存時間值),可以免去手動清理過期歷史數據的麻煩,TTL 也可以通過alter table語句隨時修改。

2.4 寫入和刪除優化

儘量不要執行單條或小批量刪除和插入操作,這樣會產生小分區文件,給後臺 Merge 任務帶來巨大壓力。

不要一次寫入太多分區,或數據寫入太快,數據寫入太快會導致 Merge 速度跟不上而報錯,一般建議每秒鐘發起 2-3 次寫入操作,每次操作寫入 2w~5w 條數據(依伺服器性能而定)。

常見錯誤:

  1. Too many parts:使用 WAL 預寫日誌(in_memory_parts_enable_wal預設為 true),提高寫入性能;
  2. Memory limit:在伺服器記憶體充裕的情況下增加記憶體配額,一般通過max_memory_usage參數來實現;在伺服器記憶體不充裕的情況下,建議將超出部分內容分配到系統硬碟上,但會降低執行速度,一般通過max_bytes_before_external_group_bymax_bytes_before_external_sort參數來實現。

2.5 常見配置

配置項主要在 config.xml 或 users.xml 中,基本上都在 users.xml 里。

config.xml 配置項:Server Settings | ClickHouse Documentation

users.xml 配置項:Settings | ClickHouse Documentation

2.5.1 CPU資源

配置 描述
background_pool_size 後臺線程池的大小,merge 線程就是在該線程池中執行,該線程池不僅僅是給 merge 線程用的,預設值 16,建議改成 CPU 個數的 2 倍(線程數)
background_schedule_pool_size 執行後臺任務的線程數,預設 128,建議改成CPU 個數的 2 倍(線程數)
background_distributed_schedule_pool_size 分散式發送執行後臺任務的線程數,預設 16,建議改成CPU 個數的 2 倍(線程數)
max_concurrent_queries 最大併發處理的請求數(包含 select、insert 等),預設值 100,建議150~300(不夠再加)
max_threads 單個查詢所能使用的最大 CPU 個數,預設是 CPU 核數

2.5.2 記憶體資源

配置 描述
max_memory_usage 此參數在 users.xml 中,表示單詞 Query 占用記憶體最大值,該值可以設置的比較大,這樣可以提升集群查詢的上限;保留部分給 OS,例如:128GB 記憶體的機器設置為 100GB
max_bytes_before_external_group_by 一般按照 max_memory_usage 的一半設置記憶體,當 group 使用記憶體超過閾值後會刷新到磁碟進行
max_bytes_before_external_sort 當 order by 已超過 max_bytes_before_external_sort 記憶體就進行溢寫磁碟(基於磁碟排序),如果不設置該值,那麼當記憶體不夠是直接拋錯,設置了該值 order by 可以正常完成,但是速度相對存記憶體來說較慢(實測非常慢)
max_table_size_to_drop 此參數在 config.xml 中,應用於需要刪除表或分區的情況,預設 50GB,意思是如果刪除 50GB 以上的分區表會失敗,建議改為 0(任何分區表都可刪除)

2.5.3 存儲

ClickHouse 不支持設置多數據目錄,為了提升數據 IO 性能,可以掛載虛擬捲組,一個捲組綁定多塊物理磁碟提升讀寫性能,多數據查詢場景 SSD 迴避普通機械硬碟快 2-3 倍。

3. 語法優化

# 1. 準備測試數據,下載官方數據集
$ curl -O https://datasets.clickhouse.com/hits/partitions/hits_v1.tar
$ curl -O https://datasets.clickhouse.com/visits/partitions/visits_v1.tar

# 2. 解壓到 ClickHouse 數據目錄
$ tar xvf hits_v1.tar -C /var/lib/clickhouse
$ tar xvf visits_v1.tar -C /var/lib/clickhouse

# 3. 修改數據目錄所屬用戶
$ sudo chown -R clickhouse:clickhouse /var/lib/clickhouse/data/datasets
$ sudo chown -R clickhouse:clickhouse /var/lib/clickhouse/metadata/datasets

# 4. 重啟 ClickHouse
$ sudo systemctl restart clickhouse-server

# 5. 執行查詢,hits_v1 表 130+ 欄位 880w+ 數據,visits_v1 表 180+ 欄位 160w+ 數據
$ clickhouse-client --query "SELECT count(*) FROM datasets.hits_v1"
$ clickhouse-client --query "SELECT count(*) FROM datasets.visits_v1"

ClickHouse 的 SQL 優化規則是基於 RBO(Rule Based Optimization),下麵為部分優化規則。

3.1 COUNT優化

在調用 count 函數時,如果沒有指定具體欄位且沒有 where 條件,則會直接使用 system.tables 的 total_rows,例如:

-- Optimized trivial count 是對 count 的優化
:) explain plan select count(*) from hits_v1;

EXPLAIN
SELECT count(*)
FROM hits_v1

Query id: 2c1bacf8-187c-430a-95b5-3a3ebdb747af

┌─explain──────────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))          │
│   MergingAggregated                                  │
│     ReadFromPreparedSource (Optimized trivial count) │
└──────────────────────────────────────────────────────┘

3 rows in set. Elapsed: 0.004 sec.

-- 如果 count 具體的列欄位,則不會優化
:) explain plan select count(UserID) from hits_v1;

EXPLAIN
SELECT count(UserID)
FROM hits_v1

Query id: f6f060aa-09de-4fa2-a0e9-2909f0800e42

┌─explain───────────────────────────────────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY))                                   │
│   Aggregating                                                                 │
│     Expression (Before GROUP BY)                                              │
│       SettingQuotaAndLimits (Set limits and quota after reading from storage) │
│         ReadFromMergeTree                                                     │
└───────────────────────────────────────────────────────────────────────────────┘

5 rows in set. Elapsed: 0.007 sec.

3.2 消除子查詢重覆欄位

下麵語句子查詢中有兩個重覆的 id 欄位,會被去重:

:) EXPLAIN SYNTAX
:-] SELECT a.UserID, b.VisitID, a.URL, b.UserID
:-] FROM datasets.hits_v1 AS a
:-]          LEFT JOIN (SELECT UserID, UserID, VisitID FROM datasets.visits_v1) AS b USING (UserID)
:-] LIMIT 3;

EXPLAIN SYNTAX
SELECT
    a.UserID,
    b.VisitID,
    a.URL,
    b.UserID
FROM datasets.hits_v1 AS a
LEFT JOIN
(
    SELECT
        UserID,
        UserID,
        VisitID
    FROM datasets.visits_v1
) AS b USING (UserID)
LIMIT 3

Query id: 119f7b30-39b0-4cc8-a6f4-81f302e35867

┌─explain─────────────────────┐
│ SELECT                      │
│     UserID,                 │
│     VisitID,                │
│     URL,                    │
│     b.UserID                │
│ FROM datasets.hits_v1 AS a  │
│ ALL LEFT JOIN               │
│ (                           │
│     SELECT                  │
│         UserID,             │
│         VisitID             │
│     FROM datasets.visits_v1 │
│ ) AS b USING (UserID)       │
│ LIMIT 3                     │
└─────────────────────────────┘

14 rows in set. Elapsed: 0.008 sec.

3.3 謂詞下推

當 group by 有 having 子句,但是沒有 with cube、with rollup 或者 with totals 修飾的時候,having 過濾會下推到 where 提前過濾。例如:

-- having 例子
:) EXPLAIN SYNTAX
:-] SELECT UserID
:-] FROM datasets.hits_v1
:-] GROUP BY UserID
:-] HAVING UserID = '8585742290196126178';

EXPLAIN SYNTAX
SELECT UserID
FROM datasets.hits_v1
GROUP BY UserID
HAVING UserID = '8585742290196126178'

Query id: 761000a4-0043-4f7b-8329-89b2bfbbe055

┌─explain──────────────────────────────┐
│ SELECT UserID                        │
│ FROM datasets.hits_v1                │
│ WHERE UserID = '8585742290196126178' │
│ GROUP BY UserID                      │
└──────────────────────────────────────┘

4 rows in set. Elapsed: 0.003 sec.

-- 子查詢也支持謂詞下推
:) EXPLAIN SYNTAX
:-] SELECT *
:-] FROM (SELECT UserID FROM datasets.visits_v1)
:-] WHERE UserID = '8585742290196126178';

EXPLAIN SYNTAX
SELECT *
FROM
(
    SELECT UserID
    FROM datasets.visits_v1
)
WHERE UserID = '8585742290196126178'

Query id: a70da9af-c8c1-4787-a536-e1794fcef33d

┌─explain──────────────────────────────────┐
│ SELECT UserID                            │
│ FROM                                     │
│ (                                        │
│     SELECT UserID                        │
│     FROM datasets.visits_v1              │
│     WHERE UserID = '8585742290196126178' │
│ )                                        │
│ WHERE UserID = '8585742290196126178'     │
└──────────────────────────────────────────┘

8 rows in set. Elapsed: 0.006 sec.

-- 複雜例子
:) EXPLAIN SYNTAX
:-] SELECT *
:-] FROM (SELECT *
:-]       FROM (SELECT UserID FROM datasets.visits_v1)
:-]       UNION ALL
:-]       SELECT *
:-]       FROM (SELECT UserID FROM datasets.visits_v1))
:-] WHERE UserID = '8585742290196126178';

EXPLAIN SYNTAX
SELECT *
FROM
(
    SELECT *
    FROM
    (
        SELECT UserID
        FROM datasets.visits_v1
    )
    UNION ALL
    SELECT *
    FROM
    (
        SELECT UserID
        FROM datasets.visits_v1
    )
)
WHERE UserID = '8585742290196126178'

Query id: 7b7ddf53-51b5-40d9-b079-ac65989479f2

┌─explain──────────────────────────────────────┐
│ SELECT UserID                                │
│ FROM                                         │
│ (                                            │
│     SELECT UserID                            │
│     FROM                                     │
│     (                                        │
│         SELECT UserID                        │
│         FROM datasets.visits_v1              │
│         WHERE UserID = '8585742290196126178' │
│     )                                        │
│     WHERE UserID = '8585742290196126178'     │
│     UNION ALL                                │
│     SELECT UserID                            │
│     FROM                                     │
│     (                                        │
│         SELECT UserID                        │
│         FROM datasets.visits_v1              │
│         WHERE UserID = '8585742290196126178' │
│     )                                        │
│     WHERE UserID = '8585742290196126178'     │
│ )                                            │
│ WHERE UserID = '8585742290196126178'         │
└──────────────────────────────────────────────┘

22 rows in set. Elapsed: 0.011 sec.

3.4 聚合計算外推

聚合函數內的計算會外推,例如:

:) EXPLAIN SYNTAX
:-] SELECT sum(UserID * 2)
:-] FROM datasets.visits_v1;

EXPLAIN SYNTAX
SELECT sum(UserID * 2)
FROM datasets.visits_v1

Query id: 205e2c27-8f38-41c3-8fe0-566ca9e0168d

┌─explain─────────────────┐
│ SELECT sum(UserID) * 2  │
│ FROM datasets.visits_v1 │
└─────────────────────────┘

2 rows in set. Elapsed: 0.004 sec.

3.5 聚合函數消除

如果對聚合鍵,即 group by key 使用 min、max、any 聚合函數,則將函數去除,例如:

:) EXPLAIN SYNTAX
:-] SELECT sum(UserID * 2), max(VisitID), max(UserID)
:-] FROM datasets.visits_v1
:-] GROUP BY UserID;

EXPLAIN SYNTAX
SELECT
    sum(UserID * 2),
    max(VisitID),
    max(UserID)
FROM datasets.visits_v1
GROUP BY UserID

Query id: f8931130-1bde-4086-a947-4921b76a6ec4

┌─explain─────────────────┐
│ SELECT                  │
│     sum(UserID) * 2,    │
│     max(VisitID),       │
│     UserID              │
│ FROM datasets.visits_v1 │
│ GROUP BY UserID         │
└─────────────────────────┘

6 rows in set. Elapsed: 0.003 sec.

3.6 刪除重覆的 order by key

重覆的排序鍵欄位會被去重,例如:

:) EXPLAIN SYNTAX
:-] SELECT UserID, VisitID
:-] FROM datasets.visits_v1
:-] ORDER BY UserID ASC, UserID ASC, VisitID ASC, VisitID ASC;

EXPLAIN SYNTAX
SELECT
    UserID,
    VisitID
FROM datasets.visits_v1
ORDER BY
    UserID ASC,
    UserID ASC,
    VisitID ASC,
    VisitID ASC

Query id: 82b72116-610f-410e-a64a-3607ff784d0b

┌─explain─────────────────┐
│ SELECT                  │
│     UserID,             │
│     VisitID             │
│ FROM datasets.visits_v1 │
│ ORDER BY                │
│     UserID ASC,         │
│     VisitID ASC         │
└─────────────────────────┘

7 rows in set. Elapsed: 0.003 sec.

註:重覆的 group by key 不會被刪除。

3.7 刪除重覆的 limit by key

重覆聲明的 limit by key 會被去重,例如:

:) EXPLAIN SYNTAX
:-] SELECT UserID, VisitID
:-] FROM datasets.visits_v1
:-] LIMIT 3 BY VisitID, VisitID
:-] LIMIT 10;

EXPLAIN SYNTAX
SELECT
    UserID,
    VisitID
FROM datasets.visits_v1
LIMIT 3 BY
    VisitID,
    VisitID
LIMIT 10

Query id: 23883608-f407-4d24-bff1-b1009567d262

┌─explain─────────────────┐
│ SELECT                  │
│     UserID,             │
│     VisitID             │
│ FROM datasets.visits_v1 │
│ LIMIT 3 BY VisitID      │
│ LIMIT 10                │
└─────────────────────────┘

6 rows in set. Elapsed: 0.003 sec.

3.8 刪除重覆的 using key

重覆的關聯鍵欄位會被去重,例如:

:) EXPLAIN SYNTAX
:-] SELECT a.UserID, a.UserID, b.VisitID, a.URL, b.UserID
:-] FROM datasets.hits_v1 AS a
:-]          LEFT JOIN datasets.visits_v1 AS b USING (UserID, UserID);

EXPLAIN SYNTAX
SELECT
    a.UserID,
    a.UserID,
    b.VisitID,
    a.URL,
    b.UserID
FROM datasets.hits_v1 AS a
LEFT JOIN datasets.visits_v1 AS b USING (UserID, UserID)

Query id: 1f1519f0-fa32-4520-ad6b-c762b310879e

┌─explain──────────────────────────────────────────────┐
│ SELECT                                               │
│     UserID,                                          │
│     UserID,                                          │
│     VisitID,                                         │
│     URL,                                             │
│     b.UserID                                         │
│ FROM datasets.hits_v1 AS a                           │
│ ALL LEFT JOIN datasets.visits_v1 AS b USING (UserID) │
└──────────────────────────────────────────────────────┘

8 rows in set. Elapsed: 0.008 sec.

3.9 標量替換

如果子查詢只返回一行數據,在被引用的時候用標量替換,例如下麵語句中的 total_disk_usage:

:) EXPLAIN SYNTAX
:-] WITH (SELECT sum(bytes) FROM system.parts WHERE active) AS total_disk_usage
:-] SELECT (sum(bytes) / total_disk_usage) * 100 AS table_disk_usage, table
:-] FROM system.parts
:-] GROUP BY table
:-] ORDER BY table_disk_usage DESC
:-] LIMIT 10;

EXPLAIN SYNTAX
WITH (
        SELECT sum(bytes)
        FROM system.parts
        WHERE active
    ) AS total_disk_usage
SELECT
    (sum(bytes) / total_disk_usage) * 100 AS table_disk_usage,
    table
FROM system.parts
GROUP BY table
ORDER BY table_disk_usage DESC
LIMIT 10

Query id: b1f006ec-8aaf-4c68-8843-41cbe7e9cc5e

┌─explain─────────────────────────────────────────────────────────────────────────┐
│ WITH identity(_CAST(0, 'Nullable(UInt64)')) AS total_disk_usage                 │
│ SELECT                                                                          │
│     (sum(bytes_on_disk AS bytes) / total_disk_usage) * 100 AS table_disk_usage, │
│     table                                                                       │
│ FROM system.parts                                                               │
│ GROUP BY table                                                                  │
│ ORDER BY table_disk_usage DESC                                                  │
│ LIMIT 10                                                                        │
└─────────────────────────────────────────────────────────────────────────────────┘

8 rows in set. Elapsed: 0.004 sec.

3.10 三元運算優化

如果開啟了optimize_if_chain_to_multiif參數,三元運算符會被替換成 multiIf 函數,例如:

:) EXPLAIN SYNTAX
:-] SELECT if(number = 1, 'hello', if(number = 2, 'world', 'atguigu'))
:-] FROM numbers(10) SETTINGS optimize_if_chain_to_multiif = 1;

EXPLAIN SYNTAX
SELECT if(number = 1, 'hello', if(number = 2, 'world', 'atguigu'))
FROM numbers(10)
SETTINGS optimize_if_chain_to_multiif = 1

Query id: 068adf60-9b81-4af8-9683-a880769ac49d

┌─explain─────────────────────────────────────────────────────────────┐
│ SELECT multiIf(number = 1, 'hello', number = 2, 'world', 'atguigu') │
│ FROM numbers(10)                                                    │
│ SETTINGS optimize_if_chain_to_multiif = 1                           │
└─────────────────────────────────────────────────────────────────────┘

3 rows in set. Elapsed: 0.002 sec.

4. 查詢優化

4.1 單表查詢

4.1.1 PREWHERE 替代 WHERE

PREWHERE 和 WHERE 語句的作用相同,用來過濾數據,不同之處在於 PREWHERE 只支持 *MergeTree 族系列引擎的表,首先會讀取指定的列數據,來判斷數據過濾,等待數據過濾之後再讀取 SELECT 聲明的列欄位來補全其餘屬性。

當查詢列明顯多於篩選列時使用 PREWHERE 可十倍提升查詢性能,PREWHERE 會自動優化執行過濾階段的數據讀取方式,降低 IO 操作。

在某些場合下,PREWHERE 語句比 WHERE 語句處理的數據量更少且性能更高。

-- 使用 where,預設自動優化 prewhere,需要關閉自動轉 prewhere
SELECT WatchID,
       JavaEnable,
       Title,
       GoodEvent,
       EventTime,
       EventDate,
       CounterID,
       ClientIP,
       ClientIP6,
       RegionID,
       UserID,
       CounterClass,
       OS,
       UserAgent,
       URL,
       Referer,
       URLDomain,
       RefererDomain,
       Refresh,
       IsRobot,
       RefererCategories,
       URLCategories,
       URLRegions,
       RefererRegions,
       ResolutionWidth,
       ResolutionHeight,
       ResolutionDepth,
       FlashMajor,
       FlashMinor,
       FlashMinor2
FROM datasets.hits_v1
WHERE UserID = '3198390223272470366' SETTINGS optimize_move_to_prewhere = 0;

-- 使用 prewhere
SELECT WatchID,
       JavaEnable,
       Title,
       GoodEvent,
       EventTime,
       EventDate,
       CounterID,
       ClientIP,
       ClientIP6,
       RegionID,
       UserID,
       CounterClass,
       OS,
       UserAgent,
       URL,
       Referer,
       URLDomain,
       RefererDomain,
       Refresh,
       IsRobot,
       RefererCategories,
       URLCategories,
       URLRegions,
       RefererRegions,
       ResolutionWidth,
       ResolutionHeight,
       ResolutionDepth,
       FlashMajor,
       FlashMinor,
       FlashMinor2
FROM datasets.hits_v1
PREWHERE UserID = '3198390223272470366';

預設情況下,一般不會關閉 WHERE 自動優化 PREWHERE,但是某些場景即使開啟自動優化也不會自動自動轉換成 PREWHERE,需要手動指定 PREWHERE:

  • 使用常量表達式;
  • 使用預設值為 alias 類型的欄位;
  • 包含了 arrayJOIN、globalIn、globalNotIn 或者 indexHInt 的查詢;
  • select 查詢的列欄位和 where 的謂詞相同;
  • 使用了主鍵欄位;

4.1.2 數據採樣

通過採樣運算可極大提升數據分析的性能。

-- SAMPLE 採樣,先採樣再統計
SELECT Title, count(*) AS PageViews
FROM datasets.hits_v1 SAMPLE 0.1
WHERE CounterID = 57
GROUP BY Title
ORDER BY PageViews DESC
LIMIT 1000;

註:採樣修飾符只有在 MergeTree engine 表中才有效,且在創建表時需要指定採樣策略。

4.1.3 列裁剪與分區裁剪

數據量太大時避免使用 select * 操作,查詢的性能會與查詢的欄位大小和數量成反比,查詢的欄位越少,消耗的 IO 資源越少,性能就會越高。

-- 反例
SELECT * FROM datasets.hits_v1;

-- 正例
SELECT WatchID,
       JavaEnable,
       Title,
       GoodEvent,
       EventTime,
       EventDate,
       CounterID,
       ClientIP,
       ClientIP6,
       RegionID,
       UserID,
       CounterClass,
       OS,
       UserAgent,
       URL,
       Referer,
       URLDomain,
       RefererDomain,
       Refresh,
       IsRobot,
       RefererCategories,
       URLCategories,
       URLRegions,
       RefererRegions,
       ResolutionWidth,
       ResolutionHeight,
       ResolutionDepth,
       FlashMajor,
       FlashMinor,
       FlashMinor2
FROM datasets.hits_v1;

分區裁剪就是只讀取需要的分區,在過濾條件中指定。

SELECT WatchID,
       JavaEnable,
       Title,
       GoodEvent,
       EventTime,
       EventDate,
       CounterID,
       ClientIP,
       ClientIP6,
       RegionID,
       UserID
FROM datasets.hits_v1
WHERE EventDate = '2014-03-23';

4.1.4 order by 結合 where、limit

千萬以上數據集進行 order by 查詢時需要搭配 where 條件和 limit 語句一起使用。

-- 正例
SELECT UserID, Age
FROM datasets.hits_v1
WHERE CounterID = 57
ORDER BY Age DESC
LIMIT 1000;

-- 反例
SELECT UserID, Age
FROM datasets.hits_v1
ORDER BY Age DESC;

4.1.5 避免構建虛擬列

如非必須,不要在結果集上構建虛擬列,虛擬列非常消耗資源浪費性能,可以考慮客戶端進行處理,或者在表中構造實際欄位進行額外存儲。

-- 反例
SELECT Income, Age, Income / Age AS IncRate
FROM datasets.hits_v1;

-- 正例
SELECT Income, Age
FROM datasets.hits_v1;

4.1.6 uniqCombined 替代 distinct

性能可提升 10 倍以上,uniqCombined 底層採用類似 HyperLogLog 演算法實現,為近似去重,可直接使用這種去重方式提升查詢性能。Count(distinct) 會使用 uniqExact 精確去重。

不建議在千萬級不同數據上執行 distinct 去重查詢,改為近似去重 uniqCombined。

-- 反例
SELECT count(DISTINCT rand())
FROM datasets.hits_v1;

-- 正例
SELECT uniqCombined(rand())
FROM datasets.hits_v1;

4.1.7 使用物化視圖

參考第 6 節。

4.1.8 其它註意事項

  1. 查詢熔斷

    為了避免因個別慢查詢引起的服務雪崩問題,除了可以為單個查詢設置超時以外,還可以配置周期熔斷,在一個查詢周期內,如果用戶頻繁進行慢查詢操作超出規定閾值後將無法繼續進行查詢操作。

  2. 關閉虛擬記憶體

    物理記憶體和虛擬記憶體的數據交換,會導致查詢變慢,資源允許的情況下關閉虛擬記憶體。

  3. 配置 join_use_nulls

    為每一個賬戶添加 join_use_nulls 配置,左表中的一條記錄在右表中不存在,右表的相應欄位會返回該欄位相應數據類型的預設值,而不是標準 SQL 中的 NULL 值。

  4. 批量寫入先排序

    批量寫入數據時,必須控制每個批次的數據中涉及到的分區的數量,在寫入之前最好對需要導入的數據進行排序。無序的數據或者涉及的分區太多,會導致 ClickHouse 無法及時對新導入的數據進行合併,從而影響查詢性能。

  5. 關註 CPU

    CPU 一般在 50% 左右會出現查詢波動,達到 70% 會出現大範圍的查詢超時,CPU 是最關鍵的指標,要非常關註。

4.2 多表關聯

4.2.1 測試數據

-- 創建小表
CREATE TABLE IF NOT EXISTS datasets.visits_v2
    ENGINE = CollapsingMergeTree(Sign) PARTITION BY toYYYYMM(StartDate) ORDER BY (CounterID, StartDate, intHash32(UserID), VisitID) SAMPLE BY intHash32(UserID) SETTINGS index_granularity = 8192 AS
SELECT *
FROM datasets.visits_v1
LIMIT 10000;

-- 創建空表用來保存 join 結果,避免控制台列印過慢
CREATE TABLE IF NOT EXISTS datasets.hits_v2
    ENGINE = MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID) SETTINGS index_granularity = 8192 AS
SELECT *
FROM datasets.hits_v1
WHERE 1 = 0;

4.2.2 用 IN 代替 JOIN

當多表聯查時,查詢的數據僅從其中一張表出時,可考慮用 IN 操作而不是 JOIN。

-- 正例
INSERT INTO datasets.hits_v2
SELECT a.*
FROM datasets.hits_v1 a
WHERE a.CounterID IN (SELECT CounterID FROM datasets.visits_v1);

-- 反例
INSERT INTO datasets.hits_v2
SELECT a.*
FROM datasets.hits_v1 a
         LEFT JOIN datasets.visits_v1 b ON a.CounterID = b.CounterID;

4.2.3 大小表 JOIN

多表 JOIN 時要滿足小表在右的原則,右表關聯時被載入到記憶體中與左表進行比較,ClickHouse 中無論是 LEFT JOIN、RIGHT JOIN 還是 INNER JOIN 永遠都是拿著右表中的每一條記錄到左表中查找該記錄是否存在,所以右表必須是小表。

  1. 小表在右

    INSERT INTO datasets.hits_v2
    SELECT a.*
    FROM datasets.hits_v1 a
             LEFT JOIN datasets.visits_v2 b ON a.CounterID = b.CounterID;
    
  2. 大表在右

    INSERT INTO datasets.hits_v2
    SELECT a.*
    FROM datasets.visits_v2 b
             LEFT JOIN datasets.hits_v1 a ON a.CounterID = b.CounterID;
    

4.2.4 註意謂詞下推(版本差異)

ClickHouse 在 JOIN 查詢時不會主動發起謂詞下推的操作,需要每個子查詢提前完成過濾操作,需要註意的是,是否執行謂詞下推,對性能影響差別很大(新版本已經不存在此問題,但是需要註意謂詞位置的不同依然有性能的差異)。

-- having 左表
:) EXPLAIN SYNTAX
:-] SELECT a.UserID, b.VisitID
:-] FROM datasets.hits_v1 a
:-]          LEFT JOIN datasets.visits_v2 b ON a.CounterID = b.CounterID
:-] HAVING a.EventDate = '2014-03-27';

EXPLAIN SYNTAX
SELECT
    a.UserID,
    b.VisitID
FROM datasets.hits_v1 AS a
LEFT JOIN datasets.visits_v2 AS b ON a.CounterID = b.CounterID
HAVING a.EventDate = '2014-03-27'

Query id: decc4c92-43cd-40fa-b741-6430e0f8a566

┌─explain──────────────────────────────────────────────────────────┐
│ SELECT                                                           │
│     UserID,                                                      │
│     VisitID                                                      │
│ FROM datasets.hits_v1 AS a                                       │
│ ALL LEFT JOIN datasets.visits_v2 AS b ON CounterID = b.CounterID │
│ PREWHERE EventDate = '2014-03-27'                                │
└──────────────────────────────────────────────────────────────────┘

6 rows in set. Elapsed: 0.015 sec.

-- having 右表
:) EXPLAIN SYNTAX
:-] SELECT a.UserID, b.VisitID
:-] FROM datasets.hits_v1 a
:-]          LEFT JOIN datasets.visits_v2 b ON a.CounterID = b.CounterID
:-] HAVING b.StartDate = '2014-03-27';

EXPLAIN SYNTAX
SELECT
    a.UserID,
    b.VisitID
FROM datasets.hits_v1 AS a
LEFT JOIN datasets.visits_v2 AS b ON a.CounterID = b.CounterID
HAVING b.StartDate = '2014-03-27'

Query id: 830fbbe2-b6ef-4036-bc49-6bc13af5b32d

┌─explain──────────────────────────────────────────────────────────┐
│ SELECT                                                           │
│     UserID,                                                      │
│     VisitID                                                      │
│ FROM datasets.hits_v1 AS a                                       │
│ ALL LEFT JOIN datasets.visits_v2 AS b ON CounterID = b.CounterID │
│ WHERE StartDate = '2014-03-27'                                   │
└──────────────────────────────────────────────────────────────────┘

6 rows in set. Elapsed: 0.005 sec.
-- 子查詢 JOIN WHERE 不會自動優化
-- 正例
:) EXPLAIN SYNTAX
:-] SELECT a.UserID, b.VisitID
:-] FROM (SELECT UserID, CounterID FROM datasets.hits_v1 WHERE EventDate = '2014-03-27') AS a
:-]          LEFT JOIN datasets.visits_v2 AS b ON a.CounterID = b.CounterID;

EXPLAIN SYNTAX
SELECT
    a.UserID,
    b.VisitID
FROM
(
    SELECT
        UserID,
        CounterID
    FROM datasets.hits_v1
    WHERE EventDate = '2014-03-27'
) AS a
LEFT JOIN datasets.visits_v2 AS b ON a.CounterID = b.CounterID

Query id: a8642ba3-a680-42aa-832b-8f8c30d4b808

┌─explain──────────────────────────────────────────────────────────┐
│ SELECT                                                           │
│     UserID,                                                      │
│     VisitID                                                      │
│ FROM                                                             │
│ (                                                                │
│     SELECT                                                       │
│         UserID,                                                  │
│         CounterID                                                │
│     FROM datasets.hits_v1                                        │
│     PREWHERE EventDate = '2014-03-27'                            │
│ ) AS a                                                           │
│ ALL LEFT JOIN datasets.visits_v2 AS b ON CounterID = b.CounterID │
└──────────────────────────────────────────────────────────────────┘

12 rows in set. Elapsed: 0.013 sec.

-- 反例
:) EXPLAIN SYNTAX
:-] SELECT a.UserID, b.VisitID
:-] FROM datasets.hits_v1 AS a
:-]          LEFT JOIN datasets.visits_v2 AS b ON a.CounterID = b.CounterID
:-] WHERE a.EventDate = '2014-03-27';

EXPLAIN SYNTAX
SELECT
    a.UserID,
    b.VisitID
FROM datasets.hits_v1 AS a
LEFT JOIN datasets.visits_v2 AS b ON a.CounterID = b.CounterID
WHERE a.EventDate = '2014-03-27'

Query id: 17a68a1e-19eb-443d-bbf4-66f9442e00d5

┌─explain──────────────────────────────────────────────────────────┐
│ SELECT                                                           │
│     UserID,                                                      │
│     VisitID                                                      │
│ FROM datasets.hits_v1 AS a                                       │
│ ALL LEFT JOIN datasets.visits_v2 AS b ON CounterID = b.CounterID │
│ PREWHERE EventDate = '2014-03-27'                                │
└──────────────────────────────────────────────────────────────────┘

6 rows in set. Elapsed: 0.015 sec.

4.2.5 分散式表使用 GLOBAL

兩張分散式表上的 IN 和 JOIN 之前必須加上GLOBAL關鍵字,右表只會在接收查詢請求的那個節點查詢一次,並將其分發到其它節點上。如果不加 GLOBAL 關鍵字的話,每個節點都會單獨發起一次對右表的查詢,而右表又是分散式表,就導致右表一共會被查詢 N2 次(N 是該分散式表的分片數量),這就是查詢放大,會帶來很大的開銷。

4.2.6 使用字典表

將一些需要關聯分析的業務創建成字典表進行 JOIN 操作,前提是字典表不宜太大,因為字典表會常駐記憶體

官方文檔:Dictionaries | ClickHouse Documentation

4.2.7 提前過濾

通過增加邏輯過濾可以減少數據掃描,達到提高執行速度及降低記憶體消耗的目的。

5. 數據一致性(重點)

通過查詢 ClickHouse 手冊發現,即使對數據一致性支持最好的 MergeTree,也只是保證最終一致性

ReplacingMergeTree 該引擎和 MergeTree 的不同之處在於它會刪除排序鍵值相同的重覆項。

數據的去重只會在數據合併期間進行,合併會在後臺不定期執行,因此無法預先做出計劃,有一些數據可能仍未被處理。儘管可以用OPTIMIZE語句發起計劃外的合併,但不要依靠它,因為OPTIMIZE語句會引起對數據的大量讀寫。

因此,ReplacingMergeTree適用於在後臺清除重覆的數據以節省空間,但是它不保證沒有重覆的數據出現。

在使用ReplacingMergeTreeSummingMergeTree這類表引擎的時候,會出現短暫數據不一致的情況。

在某些對一致性非常敏感的場景,通常有以下幾種方案。

-- 準備測試數據,創建表
CREATE TABLE IF NOT EXISTS datasets.test_a
(
    user_id     UInt64 COMMENT '數據去重更新標識',
    score       String,
    deleted     UInt8    DEFAULT 0 COMMENT '自定義標記位',
    create_time DateTime DEFAULT toDateTime(0) COMMENT '版本號欄位'
) ENGINE = ReplacingMergeTree(create_time)
      ORDER BY user_id;
      
-- 寫入 1000w 測試數據
INSERT INTO datasets.test_a (user_id, score)
WITH (SELECT ['A', 'B', 'C', 'D', 'E', 'F', 'G']) AS dict
SELECT number AS user_id, dict[number % 7 + 1] AS score
FROM numbers(10000000);

-- 修改前 50w 數據,修改 score 欄位與 create_time 欄位
INSERT INTO datasets.test_a (user_id, score, create_time)
WITH (SELECT ['AA', 'BB', 'CC', 'DD', 'EE', 'FF', 'GG']) AS dict
SELECT number AS user_id, dict[number % 7 + 1] AS score, now() AS create_time
FROM numbers(500000);

-- 查詢總數(1050w)
SELECT count()
FROM datasets.test_a;

5.1 手動 OPTIMIZE

在寫入數據後,立刻執行OPTIMIZE強制觸發新寫入分區的合併動作。

OPTIMIZE TABLE datasets.test_a FINAL;

-- 語法
OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition | PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]]

5.2 通過 GROUP BY 去重

  1. 執行去重的查詢

    SELECT user_id,
           argMax(score, create_time)   AS score,
           argMax(deleted, create_time) AS deleted,
           max(create_time)             AS ctime
    FROM datasets.test_a
    GROUP BY user_id
    HAVING deleted = 0;
    

    函數說明:

    • argMax(field1, field2):按照 field2 的最大值取 field1 的值。

    當更新數據時,會寫入一行新的數據,例如上面的語句中,通過查詢最大的 create_time 得到修改後的 score 欄位值。

  2. 創建視圖

    CREATE VIEW view_test_a AS
    SELECT user_id,
           argMax(score, create_time)   AS score,
           argMax(deleted, create_time) AS deleted,
           max(create_time)             AS ctime
    FROM datasets.test_a
    GROUP BY user_id
    HAVING deleted = 0;
    
  3. 插入重覆數據

    -- 插入 user_id 重覆數據
    INSERT INTO datasets.test_a (user_id, score, create_time)
    VALUES (0, 'AAAA', now());
    
    -- 通過視圖查詢
    SELECT *
    FROM view_test_a
    WHERE user_id = 0;
    
  4. 刪除數據

    -- 刪除數據
    INSERT INTO datasets.test_a (user_id, score, deleted, create_time)
    VALUES (0, 'AAAA', 1, now());
    
    -- 通過視圖查詢
    SELECT *
    FROM datasets.view_test_a
    WHERE user_id = 0;
    

    這行數據並沒有被真正的刪除,而是被過濾掉了。在一些合適的場景下,可以結合表級別的 TTL 最終將物理數據刪除。

5.3 通過 FINAL 查詢

在查詢語句後增加 FINAL 修飾符,這樣在查詢的過程中將會執行 Merge 的特殊邏輯(例如數據去重,預聚合等)。

但是該方法在早期版本基本沒有人使用,因為在增加 FINAL 之後,我們的查詢將會變成一個單線程的執行過程,查詢速度非常慢。

在 20.5.2.7 版本中,FINAL 查詢支持多線程執行,並且可以通過max_final_threads 參數控制單個查詢的線程數。但是目前讀取部分的動作依然是串列的。

FINAL 查詢最終的性能和很多因素相關,列欄位的大小、分區的數量等等都會影響到最終的查詢時間,所以還要結合實際場景取捨。

參考鏈接:Parallel final by KochetovNicolai · Pull Request #10463 · ClickHouse/ClickHouse (github.com)

-- 普通語句執行計劃
:) EXPLAIN PIPELINE
:-] SELECT *
:-] FROM datasets.visits_v1
:-] WHERE StartDate = '2014-03-17'
:-] LIMIT 100;

EXPLAIN PIPELINE
SELECT *
FROM datasets.visits_v1
WHERE StartDate = '2014-03-17'
LIMIT 100

Query id: 730be1e8-f678-4e30-9de2-3dc3d5c7f228

Connecting to database datasets at localhost:9000 as user default.
Connected to ClickHouse server version 21.9.2 revision 54449.

┌─explain──────────

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Linux查看系統硬體信息(2021.06.22) 1. CPU # 查看 cpu 的統計信息 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 64 On-li ...
  • 鏡像下載、功能變數名稱解析、時間同步請點擊 阿裡雲開源鏡像站 在VMware上搭建docker的時候報了Failed to start docker.service: Unit not found。查看了好多 博主的分享,但是因為圖片有限,不能確定是否問題一樣,查到這位博主的時候眼前一亮,一毛一樣啊!並且博 ...
  • 本篇關鍵詞:內核重定位、MMU、SVC棧、熱啟動、內核映射表 內核彙編相關篇為: v74.01 鴻蒙內核源碼分析(編碼方式) | 機器指令是如何編碼的 v75.03 鴻蒙內核源碼分析(彙編基礎) | CPU上班也要打卡 v76.04 鴻蒙內核源碼分析(彙編傳參) | 如何傳遞複雜的參數 v77.01 ...
  • 鏡像下載、功能變數名稱解析、時間同步請點擊 阿裡雲開源鏡像站 部署rocketmq和可視化客戶端 一、 伺服器資源 服務名稱:Linux伺服器 IP:[請查看資源分配文檔] 操作系統:CentOS 7.8 x64 二、rocketmq安裝 2.1下載 下載地址:rocketmq.apache.org/dow ...
  • tree Linux tree命令用於以樹狀圖列出目錄的內容。 執行tree指令,它會列出指定目錄下的所有文件,包括子目錄里的文件。 語法 tree [-aACdDfFgilnNpqstux][-I <範本樣式>][-P <範本樣式>][目錄...] 參數說明: - -a 顯示所有文件和目錄。 - ...
  • 一、ZooKeeper概述 Apache ZooKeeper 是一個集中式服務,用於維護配置信息、命名、提供分散式同步和提供組服務,ZooKeeper 致力於開發和維護一個開源伺服器,以實現高度可靠的分散式協調,其實也可以認為就是一個分散式資料庫,只是結構比較特殊,是樹狀結構。官網文檔:https: ...
  • Explain簡介 MySQL優化器在基於成本的計算和基於規則的SQL優化會生成一個所謂的執行計劃,我們就可以使用執行計劃查看MySQL對該語句具體的執行方式。 介紹這個好啰嗦就是了,我們可以通過這個優化器展示的執行計劃,查看優化器對我們的SQL進行優化的步驟,連接轉換成單表訪問時的優化。以及對於之 ...
  • 我們在上一篇博客中學習瞭如何用Hadoop-MapReduce實現單詞計數,現在我們來看如何用Spark來實現同樣的功能。Spark框架也是MapReduce-like模型,採用“分治-聚合”策略來對數據分佈進行分佈並行處理。不過該框架相比Hadoop-MapReduce,具有以下兩個特點:對大數據... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...