clickHouse常用語法升階版(JOIN/WHERE/PREWHERE/GROUP BY等)


一、Join操作

在ClickHouse中,對連接操作定義了不同的精度,包含ALL、ANY和ASOF三種類型,默認為ALL。可以通過join_default_strictness配置修改默認精度(位於system.settings表中)。

下面分別說明這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')
('7', 'ClickHouse', '2019-05-01 14:00:00')
('8', '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. 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 │ └────┴───────────────┴──────┘

 

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 │
└────┴───────────────┴──────┘

 

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 │  1002019-05-01 12:00:002019-05-01 11:55:00 │
│ 2  │ Spark      │   902019-05-01 12:30:002019-05-01 12:01:00 │
└────┴────────────┴──────┴─────────────────────┴─────────────────────┘
SELECT expressions_list
FROM table_1
ASOF LEFT JOIN table_2
ON equi_cond AND closest_match_cond
 
         
--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 │  1002019-05-01 12:00:002019-05-01 11:55:00 │
│ 2  │ Spark      │   902019-05-01 12:30:002019-05-01 12:01:00 │
└────┴────────────┴──────┴─────────────────────┴─────────────────────┘

 

對 asof_colum 字段的使用有2點需要注意:

  ①asof_column 必須是整型、浮點型和日期型這類有序序列的數據類型

  ②asof_column不能是數據表內的唯一字段,也就是說連接鍵(JOIN KEY)和asof_column不能是同一字段

 

4. Join性能

  在執行JOIN時,ClickHouse對執行的順序沒有特別優化,JOIN操作會在WHERE以及聚合查詢前運行。

JOIN操作結果不會緩存,所以每次JOIN操作都會生成一個全新的執行計划。如果應用程序會大量使用JOIN,則需進一步考慮借助上層應用側的緩存服務或使用JOIN表引擎來改善性能(JOIN表引擎不支持ASOF精度)。JOIN表引擎會在內存中保存JOIN結果。

  在某些情況下,IN的效率比JOIN要高。

在使用JOIN連接維度表時,JOIN操作可能並不會特別高效,因為右則表對每個query來說,都需要加載一次。在這種情況下,外部字典(external dictionaries)的功能會比JOIN性能更好。

 

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:終止操作但並不拋出異常

 

二、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操作。

 

 注意:

1)一個查詢可以同時指定PREWHEREWHERE在這種情況下,PREWHERE先於WHERE.

2)如果將optimize_move_to_prewhere設置設置為 0,則會禁用自動將部分表達式從WHERE移動到prewhere的啟發式方法。

3)如果查詢具有FINAL修飾符,則PREWHERE優化並不總是正確的。僅當設置optimize_move_to_prewhereoptimize_move_to_prewhere_if_final都打開時才啟用。

【PREWHERE部分是在之前執行FINAL的,因此當使用不在表的部分中的字段時,FROM ... FINAL查詢的結果可能會出現偏差。】

三、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三種修飾符獲取額外的匯總信息。

 

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...-3d3d5a91fc24 │ │ 638 │ │ .inner_id.604be4d8-bb5c...a9d3d5a91fc24 │ 953e60a1e8747360786c2b70a223788d_2_4_1 │ 318 │ │ .inner_id.604be4d8-bb5c...b-d3d5a91fc24 │ 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 第一行)。

 

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:0070 │
│ bob   │ ACC    │ 2021-01-03 11:00:0090 │
│ alice │ BS     │ 2021-01-01 11:00:0040 │
…

┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
│      │        │ 2021-01-01 11:00:0040 │
│      │        │ 2021-01-03 11:00:0066.66666666666667 │
│      │        │ 2021-01-02 11:00:0060 │
│      │        │ 2021-01-04 11:00:00100 │
└──────┴────────┴─────────────────────┴───────────────────┘
┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
│      │        │ 1970-01-01 00:00:0065.71428571428571 │
└──────┴────────┴─────────────────────┴───────────────────┘

可以看到結果中會生成 8 個統計結果(部分結果已省略)。

 

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:0070 │
│ bob   │ ACC    │ 2021-01-03 11:00:0090 │
│ alice │ BS     │ 2021-01-01 11:00:0040 │
│ jane  │ CS     │ 2021-01-02 11:00:0050 │
│ jane  │ MATH   │ 2021-01-04 11:00:00100 │
│ tom   │ CS     │ 2021-01-03 11:00:0060 │
│ bob   │ BS     │ 2021-01-03 11:00:0050 │
└───────┴────────┴─────────────────────┴─────────────┘

Totals:
┌─name─┬─course─┬────────────────year─┬───────AVG(points)─┐
│      │        │ 1970-01-01 00:00:0065.71428571428571 │
└──────┴────────┴─────────────────────┴───────────────────┘

 

四、 查看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的執行日志。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM