ClickHouse使用操作
這章主要介紹在ClickHouse使用的各個操作的注意點。常規的統一語法不做詳細介紹。
1. Join操作
在ClickHouse中,對連接操作定義了不同的精度,包含ALL、ANY和ASOF三種類型,默認為ALL。可以通過join_default_strictness配置修改默認精度(位於system.setting表中)。下面分別說明這3種精度。
首先建表並插入測試數據:
--表join_tb1 CREATE TABLE join_tb1 ( `id` String, `name` String, `time` DateTime ) ENGINE = MergeTree PARTITION BY toYYYYMM(time) ORDER BY id --表 join_tb2 CREATE TABLE join_tb2 ( `id` String, `rate` UInt8, `time` DateTime ) ENGINE = MergeTree PARTITION BY toYYYYMM(time) ORDER BY id --表 join_tb3 CREATE TABLE join_tb3 ( `id` String, `star` UInt8 ) ENGINE = MergeTree ORDER BY id --插入數據 INSERT INTO join_tb1 VALUES ('1', 'ClickHouse', '2019-05-01 12:00:00') ('2', 'Spark', '2019-05-01 12:30:00') ('3', 'ElasticSearch', '2019-05-01 13:00:00') ('4', 'HBase', '2019-05-01 13:30:00') (NULL, 'ClickHouse', '2019-05-01 14:00:00') (NULL, 'Spark', '2019-05-01 14:30:00') INSERT INTO join_tb2 VALUES ('1', 100, '2019-05-01 11:55:00') ('1', 105, '2019-05-01 11:50:00') ('2', 90, '2019-05-01 12:01:00') ('3', 80, '2019-05-01 13:10:00') ('5', 70, '2019-05-01 14:00:00') ('6', 60, '2019-05-01 13:55:00') INSERT INTO join_tb3 VALUES ('1', 1000) ('2', 900)
1.1. ALL
如果左表內的一行數據,在右表中有多行數據與之連接匹配,則返回右表種全部連接的數據。連接依據為:left.key=right.key。
SELECT a.id, a.name, b.rate FROM join_tb1 AS a ALL INNER JOIN join_tb2 AS b ON a.id=b.id SELECT a.id, a.name, b.rate FROM join_tb1 AS a ALL INNER JOIN join_tb2 AS b ON a.id = b.id ┌─id─┬─name──────────┬─rate─┐ │ 1 │ ClickHouse │ 100 │ │ 1 │ ClickHouse │ 105 │ │ 2 │ Spark │ 90 │ │ 3 │ ElasticSearch │ 80 │ └────┴───────────────┴──────┘
1.2. ANY
如果左表內的一行數據,在右表中有多行數據與之連接匹配,則僅返回右表中第一行連接的數據。連接依據同樣為:left.key=right.key
SELECT a.id, a.name, b.rate FROM join_tb1 AS a ANY INNER JOIN join_tb2 AS b ON a.id = b.id ┌─id─┬─name──────────┬─rate─┐ │ 1 │ ClickHouse │ 100 │ │ 2 │ Spark │ 90 │ │ 3 │ ElasticSearch │ 80 │ └────┴───────────────┴──────┘
1.3. ASOF
ASOF 是一種模糊連接,允許在連接鍵之后追加定義一個模糊連接的匹配條件asof_column,例如:
SELECT a.id, a.name, b.rate, a.time, b.time FROM join_tb1 AS a ASOF INNER JOIN join_tb2 AS b ON (a.id = b.id) AND (a.time >= b.time) ┌─id─┬─name───────┬─rate─┬────────────────time─┬──────────────b.time─┐ │ 1 │ ClickHouse │ 100 │ 2019-05-01 12:00:00 │ 2019-05-01 11:55:00 │ │ 2 │ Spark │ 90 │ 2019-05-01 12:30:00 │ 2019-05-01 12:01:00 │ └────┴────────────┴──────┴─────────────────────┴─────────────────────┘
根據官網介紹的語法:
SELECT expressions_list FROM table_1 ASOF LEFT JOIN table_2 ON equi_cond AND closest_match_cond
https://clickhouse.tech/docs/en/sql-reference/statements/select/join/
ASOF會先以 left.key = right.key 進行連接匹配,然后根據AND 后面的 closest_match_cond(也就是這里的a.time >= b.time)過濾出最符合此條件的第一行連接匹配的數據。
另一種寫法是使用USING,語法為:
SELECT expressions_list FROM table_1 ASOF JOIN table_2 USING (equi_column1, ... equi_columnN, asof_column)
舉例:
SELECT a.id, a.name, b.rate, a.time, b.time FROM join_tb1 AS a ASOF INNER JOIN join_tb2 AS b USING (id, time) Query id: 075f7e4a-7355-4e11-ae3b-0e3275912a3e ┌─id─┬─name───────┬─rate─┬────────────────time─┬──────────────b.time─┐ │ 1 │ ClickHouse │ 100 │ 2019-05-01 12:00:00 │ 2019-05-01 11:55:00 │ │ 2 │ Spark │ 90 │ 2019-05-01 12:30:00 │ 2019-05-01 12:01:00 │ └────┴────────────┴──────┴─────────────────────┴─────────────────────┘
對 asof_colum 字段的使用有2點需要注意:
- asof_column 必須是整型、浮點型和日期型這類有序序列的數據類型
- asof_column不能是數據表內的唯一字段,也就是說連接鍵(JOIN KEY)和asof_column不能是同一字段
1.4. Join性能
在執行JOIN時,ClickHouse對執行的順序沒有特別優化,JOIN操作會在WHERE以及聚合查詢前運行。
JOIN操作結果不會緩存,所以每次JOIN操作都會生成一個全新的執行計划。如果應用程序會大量使用JOIN,則需進一步考慮借助上層應用側的緩存服務或使用JOIN表引擎來改善性能(JOIN表引擎不支持ASOF精度)。JOIN表引擎會在內存中保存JOIN結果。
在某些情況下,IN的效率比JOIN要高。
在使用JOIN連接維度表時,JOIN操作可能並不會特別高效,因為右則表對每個query來說,都需要加載一次。在這種情況下,外部字典(external dictionaries)的功能會比JOIN性能更好。
1.5. JOIN的內存限制
默認情況下,ClickHouse使用Hash Join 算法。它會將右側表(right_table)加載到內存,並為它創建一個hash table。在達到了內存使用的一個閾值后,ClickHouse會轉而使用Merge Join 算法。
可以通過以下參數限制JOIN操作消耗的內存:
- max_rows_in_join:限制hash table中的行數
- max_bytes_in_join:限制hash table的大小
在達到任何上述limit后,ClickHouse會以join_overflow_mode 的參數進行動作。此參數包含2個可選值:
- THROW:拋出異常並終止操作
- BREAK:終止操作但並不拋出異常
2. WHERE與PREWHERE子句
WHERE可以通過表達式來過濾數據,如果過濾條件恰好為主鍵字段,則可以進一步借助索引加速查詢,所以WHERE子句是決定查詢語句是否能使用索引的判斷依據(前提是表引擎支持索引)。
除此之外,ClickHouse還提供了PREWHERE子句用於條件過濾,它可以更有效地進行過濾優化,僅用於MergeTree表系列引擎。
PREWHERE與WHERE不同之處在於:使用PREWHERE時,首先只會去PREWHERE指定的列字段數據,用於數據過濾的條件判斷。在數據過濾之后再讀取SELECT聲明的列字段以補全其余屬性。所以在一些場合下,PREWHERE相比WHERE而言,處理的數據更少,性能更高。
默認情況下,即使在PREWHERE子句沒有顯示指定的情況下,它也會自動移動到WHERE條件到PREWHERE階段。
下面做個對比:
# 默認自動開啟了PREWHERE,查詢速度為: select WatchID, Title, GoodEvent from hits_v1 where JavaEnable=1; … 6535088 rows in set. Elapsed: 1.428 sec. Processed 8.87 million rows, 863.90 MB (6.21 million rows/s., 604.82 MB/s.) # 關閉PREWHERE set optimize_move_to_prewhere=0 # 關閉自動PREWHERE,查詢速度為 6535088 rows in set. Elapsed: 1.742 sec. Processed 8.87 million rows, 864.55 MB (5.09 million rows/s., 496.20 MB/s.)
可以看到2條語句處理的數據總量沒有變化,但是其數據處理量稍有降低(PREWHERE為863.90MB),且每秒吞吐量上升(PREWHER為604.82MB/s,WHERE為496.20MB/s)。
對比2條語句的執行計划:
# PREWHERE explain select WatchID, Title, GoodEvent from hits_v1 prewhere JavaEnable=1; EXPLAIN SELECT WatchID, Title, GoodEvent FROM hits_v1 PREWHERE JavaEnable = 1 Query id: 103fd24a-e718-4304-9f75-4900528c1d1a ┌─explain───────────────────────────────────────────────────────────────────┐ │ Expression ((Projection + Before ORDER BY)) │ │ SettingQuotaAndLimits (Set limits and quota after reading from storage) │ │ ReadFromStorage (MergeTree) │ └───────────────────────────────────────────────────────────────────────────┘ # WHERE explain select WatchID, Title, GoodEvent from hits_v1 where JavaEnable=1; EXPLAIN SELECT WatchID, Title, GoodEvent FROM hits_v1 WHERE JavaEnable = 1 Query id: 9b470524-1320-4e9f-bade-cf8c2c9944c8 ┌─explain─────────────────────────────────────────────────────────────────────┐ │ Expression ((Projection + Before ORDER BY)) │ │ Filter (WHERE) │ │ SettingQuotaAndLimits (Set limits and quota after reading from storage) │ │ ReadFromStorage (MergeTree) │ └─────────────────────────────────────────────────────────────────────────────┘
可以看到相比WHERE語句,PREWHERE語句的執行計划省去了一次Filter操作。
3. Group By
Group By的用法非常常見,ClickHouse中執行聚合查詢時,若是SELECT后面只聲明了聚合函數,則GROUP BY 關鍵字可以省略:
SELECT SUM(data_compressed_bytes) AS compressed, SUM(data_uncompressed_bytes) AS uncompressed FROM system.parts Query id: e38e3ec1-968d-4442-ba7d-b8555f27e0d0 ┌─compressed─┬─uncompressed─┐ │ 1851073942 │ 9445387666 │ └────────────┴──────────────┘
聚合查詢還能配合WITH ROLLUP、WITH CUBE和WITH TOTALS三種修飾符獲取額外的匯總信息。
3.1. WITH ROLLUP
ROLLUP便是上卷數據,按聚合鍵從右到左,基於聚合函數依次生成分組小計和總計。如果設聚合鍵的個數為n,則最終會生成小計的個數為n+1。例如:
SELECT table, name, SUM(bytes_on_disk) FROM system.parts GROUP BY table, name WITH ROLLUP ORDER BY table ASC ┌─table──────────────────────────────────────────┬─name───────────────────────────────────┬─SUM(bytes_on_disk)─┐ │ │ │ 1857739143 │ │ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ │ 638 │ │ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ 953e60a1e8747360786c2b70a223788d_2_4_1 │ 318 │ │ .inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 │ acb795a12c7ba41b0ed4c3d94a008ecd_1_3_1 │ 320 │ │ agg_table │ │ 358 │ │ agg_table │ 201909_2_2_0 │ 358 │
可以看到第1行是一個匯總,統計的SUM(bytes_on_disk)的總行數。而每個table字段都有一個匯總(例如.inner_id.604be4d8-bb5c-437b-ada9-3d3d5a91fc24 表第一行以及agg_table 第一行)。
3.2. WITH CUBE
CUBE也是數倉里重要的概念,基於聚合鍵之間所有的組合生成統計信息。如果聚合鍵的個數為n,則最終聚合數據的個數為2的n次方。例如:
--建表 CREATE TABLE person ( `id` int, `name` String, `course` String, `year` DateTime, `points` int ) ENGINE = MergeTree ORDER BY id --插入數據 INSERT INTO person VALUES (1, 'jane', 'CS', '2021-01-02 11:00:00', 50), (2, 'tom', 'CS', '2021-01-03 11:00:00', 60), (3, 'bob', 'BS', '2021-01-03 11:00:00', 50), (4, 'alice', 'BS', '2021-01-01 11:00:00', 40), (5, 'jane', 'ACC', '2021-01-02 11:00:00', 70), (6, 'bob', 'ACC', '2021-01-03 11:00:00', 90), (7, 'jane', 'MATH', '2021-01-04 11:00:00', 100) --Cube計算 SELECT name, course, year, AVG(points) FROM person GROUP BY name, course, year WITH CUBE ┌─name──┬─course─┬────────────────year─┬─AVG(points)─┐ │ jane │ ACC │ 2021-01-02 11:00:00 │ 70 │ │ bob │ ACC │ 2021-01-03 11:00:00 │ 90 │ │ alice │ BS │ 2021-01-01 11:00:00 │ 40 │ … ┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐ │ │ │ 2021-01-01 11:00:00 │ 40 │ │ │ │ 2021-01-03 11:00:00 │ 66.66666666666667 │ │ │ │ 2021-01-02 11:00:00 │ 60 │ │ │ │ 2021-01-04 11:00:00 │ 100 │ └──────┴────────┴─────────────────────┴───────────────────┘ ┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐ │ │ │ 1970-01-01 00:00:00 │ 65.71428571428571 │ └──────┴────────┴─────────────────────┴───────────────────┘
可以看到結果中會生成 8 個統計結果(部分結果已省略)。
3.3. WITH TOTALS
WITH TOTALS會基於聚合函數對所有數據進行統計(比原結果多一行總的統計結果),例如:
SELECT name, course, year, AVG(points) FROM person GROUP BY name, course, year WITH TOTALS ┌─name──┬─course─┬────────────────year─┬─AVG(points)─┐ │ jane │ ACC │ 2021-01-02 11:00:00 │ 70 │ │ bob │ ACC │ 2021-01-03 11:00:00 │ 90 │ │ alice │ BS │ 2021-01-01 11:00:00 │ 40 │ │ jane │ CS │ 2021-01-02 11:00:00 │ 50 │ │ jane │ MATH │ 2021-01-04 11:00:00 │ 100 │ │ tom │ CS │ 2021-01-03 11:00:00 │ 60 │ │ bob │ BS │ 2021-01-03 11:00:00 │ 50 │ └───────┴────────┴─────────────────────┴─────────────┘ Totals: ┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐ │ │ │ 1970-01-01 00:00:00 │ 65.71428571428571 │ └──────┴────────┴─────────────────────┴───────────────────┘
4. 查看SQL執行計划
ClickHouse目前並沒有直接提供EXPLAIN的詳細查詢計划,當前EXPLAIN僅是輸出一個簡單的計划。不過我們仍可以借助后台服務日志來實現此功能,例如執行以下語句即可看到詳細的執行計划:
clickhouse-client --password xxx --send_logs_level=trace <<<'select * from tutorial.hits_v1' > /dev/null
打印信息如下(僅截取關鍵信息):
tutorial.hits_v1 (SelectExecutor): Key condition: unknown => 查詢未使用主鍵索引 tutorial.hits_v1 (SelectExecutor): MinMax index condition: unknown => 未使用分區索引 tutorial.hits_v1 (SelectExecutor): Not using primary index on part 201403_1_29_2 => 未在分區 201403_1_29_2 下使用primary index tutorial.hits_v1 (SelectExecutor): Selected 1 parts by partition key, 1 parts by primary key, 1094 marks by primary key, 1094 marks to read from 1 ranges => 選擇了1個分區,共計1094個marks executeQuery: Read 8873898 rows, 7.88 GiB in 21.9554721 sec., 404177 rows/sec., 367.50 MiB/sec. => 讀取 8873898條數據,7.88G 數據,耗時21.955秒… MemoryTracker: Peak memory usage (for query): 361.67 MiB. => 消耗內存量
下面優化一下查詢:
clickhouse-client --password xxx --send_logs_level=trace <<<"select WatchID from tutorial.hits_v1 where EventDate='2014-03-17'" > /dev/null
打印結果為:
InterpreterSelectQuery: MergeTreeWhereOptimizer: condition "EventDate = '2014-03-17'" moved to PREWHERE => 自動調用了PREWHERE tutorial.hits_v1 (SelectExecutor): Key condition: (column 1 in [16146, 16146]) => 使用了主鍵索引 tutorial.hits_v1 (SelectExecutor): MinMax index condition: (column 0 in [16146, 16146]) => 使用了分區索引 tutorial.hits_v1 (SelectExecutor): Selected 1 parts by partition key, 1 parts by primary key, 755 marks by primary key, 755 marks to read from 64 ranges => 根據分區鍵選擇了一個分區 executeQuery: Read 6102294 rows, 58.19 MiB in 0.032661599 sec., 186833902 rows/sec., 1.74 GiB/sec. => 讀到的數據,以及速度 MemoryTracker: Peak memory usage (for query): 11.94 MiB. => 消耗內存量
總的來說,ClickHouse未直接通過EXPLAIN語句提供查看語句執行的詳細過程,但是可以變相的將日志設置到DEBUG或是TRACE級別,實現此功能,並分析SQL的執行日志。