ClickHouse介紹(四)ClickHouse使用操作


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 │  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

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 │  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點需要注意:

  1. asof_column 必須是整型、浮點型和日期型這類有序序列的數據類型
  2. 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操作消耗的內存:

  1. max_rows_in_join:限制hash table中的行數
  2. max_bytes_in_join:限制hash table的大小

在達到任何上述limit后,ClickHouse會以join_overflow_mode 的參數進行動作。此參數包含2個可選值:

  1. THROW:拋出異常並終止操作
  2. 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─┐
│ 18510739429445387666 │
└────────────┴──────────────┘

聚合查詢還能配合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_0358

可以看到第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: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.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 │
└──────┴────────┴─────────────────────┴───────────────────┘

 

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的執行日志。

 


免責聲明!

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



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