[CH] ClickHouse Getting Started
[TOC]
ClickHouse Client CLI
# 等價於文件中的 "./clickhouse client" 或 "clickhouse-client"
$ clickhouse client --help
$ clickhouse client -m # --multiline,在 CLI 中使用 ; 當作結尾
$ clickhouse client -q [query] # --query,在啟動 client 時,直接執行 query
$ clickhouse client -m --config-file="custom-config.xml"
在 ClickHouse Client 中下 Query 的方式:
- 如果希望換行輸入 Query,需要在換行前使用
\
- 如果有使用
multiline
模式,則可以隨意換行,直到使用;
後,程式才會執行 - 直接在啟動 CLI 時輸入 Query 可以使用
-q [query]
,例如clickhouse client -q "SHOW DATABASES"
。
# 進入 clickhouse client 後
USE "example-db"; # 切換到某一 database
SELECT * FROM db.table FORMAT VERTICAL;
ClickHouse Architecture
MergeTree Table Engine
在 ClickHouse 中,不同的 Table Engine 會影響到:
- 資料保存的方式與位置
- 能夠使用那些方式 queries
- 資料如何被複製(replicated)
- 能不能並行存取資料
- 是否資源 multithreaded requests
當你想要把資料放到 ClickHouse 中處理時,就會使用 MergeTree Table Engine,它適合用在 high-load tasks。MergeTree Table 系列的 Engine 設計來以快速插入資料並隨後在背景資料處理。它們專為高資料擷取率和巨量資料而設計。
TL;DR
granule
:和資料讀取有關,是 ClickHouse 在讀取資料時的最小單位,預設是 8,192 筆資料primary key
:資料表排序的方式,它應該要是搜尋資料時,WHERE
最常被用到的欄位primary index
:保存在記憶體中的索引,它會是每個 granule 中的第一筆資料part
:和資料寫入有關,ClickHouse 在每次 INSERT 資料時,就會建立一個新的資料夾,這個資料夾就稱作 part,裡面包含 column files 和 index file
Primary Key
keywords: granule
Primary Key 決定了:
- 資料如何被保存在硬碟上,會影響到資料儲存所佔用的容量
- Primary Index 中會有哪些值,會影響到能不能有效率的查詢資料
Best Practice
-
主鍵:
- 並不保證是唯一(unique)的
- 主鍵可以通過
PRIMARY KEY
參數來設置,它同時也會是 sorting keys。- 預設如果不設置 PRIMARY KEY,則
ORDER BY
中的欄位將自動成為 primary keys。 - 當同時設置
PRIMARY KEY
和ORDER BY
時,primary keys 必須是 sorting keys(ORDER BY
)的子集合,也就是 primary keys 一定要包含在 sorting keys 中
- 預設如果不設置 PRIMARY KEY,則
- 資料在保存時會根據 PRIMARY KEY 進行排序,因此 PRIMARY KEY 的設定會影響到資料能不能被有效的壓縮,進而影響到最終資料所使用到的硬碟容量
-
Primary Key 的選擇
- 在搜尋資料時,在
WHERE
中最常被使用到的欄位,就應該要把這個欄位設成 Primary Key。 - 只有這個欄位能幫助你跳過許多 granules 時,才把它加到 PK 中。添加過多無意義的 PK 只會增加記憶體的負擔,不會帶來實際的效益。
- 盡量選擇資料變異性小的資料作為 Primary Key 通常能提供越大的效果。把 String 的欄位放在 Primary Key 就會是一個很不好的使用。
- 在搜尋資料時,在
-
Primary Key 擺放的順序
- 除了把最常會用到的欄位放在第一個 PK 之外,一般可以把基數(資料變異 性)越低的欄位放在越前面越好,如此有機會在搜尋時,可以略過越多查詢不必要的資料。
-
當你要在一個 Table 建立多個 Primary Keys 時,請先考慮以下的做法:
- 建立兩個帶有相同資料的 Table,這兩個 Table 的差別只有在 Primary Key 不同
- 使用 Materialized View,透過
SELECT
把資料保存在另一個 Table 中,並在SELECT
中來排序該資料
- 使用 Materialized View,透過
- 使用 Projection
- ClickHouse 在背後建立一個隱藏的資料表,這個資料表會用不同的方式來排序
- 定義 Skipping Index
- 建立兩個帶有相同資料的 Table,這兩個 Table 的差別只有在 Primary Key 不同
Primary Keys 欄位的資料會被保存在記憶體中,一旦新增資料,這些保存在記憶體中索引就會需要重新建立。因此過的不必要的 Primary Keys 對於效能會有不好的影響。
CREATE TABLE helloworld.my_first_table
(
user_id UInt32,
message String,
timestamp DateTime,
metric Float32
)
ENGINE = MergeTree()
PRIMARY KEY (user_id, timestamp); # 設定的 Primary Keys 預設也會是 Sorting Keys
Granule
在 ClickHouse 中:
-
Primary Key 決定了資料會如何被保存和被搜尋
- ClickHouse 會在「每 8,192 個 rows」或「10 MB 的資料」在主鍵索 引檔案中建立一個條目(
entry
),這樣的顆粒度(granules)會是在以SELECT
進行查詢時的最小資料塊(column data)。 - 由於資料保存的時候會先以 Primary Key 排序,所以在進行查詢時,ClickHouse 不需要逐行掃描資料,而是先看這個「條目」,從這些條目中找出符合的資料塊,再從資料塊中找出符合條件的資料,從而提升查詢效率。
- 以下圖為例,在第 1、8,193、16,385、24,577 筆資料,都會被紀錄成條目。如果要找出
Column 1 = A
、Column 2 = 2
的資料,只需要先掃描這個條目,在開始掃描這兩個條目中的資料塊。
- ClickHouse 會在「每 8,192 個 rows」或「10 MB 的資料」在主鍵索 引檔案中建立一個條目(
不把每一個 row 都建立 index,而是每 8,192 筆資料才建立一個 entry 的方式稱作 Sparse Index,透過這種方式,即使有上百萬筆資料,也只需要建立幾百個索引即可。一個 granule(資料塊)中的第一筆資料就會時 Primary Index,它有稱作「Entry」或「Mark」。Entries 會被存在 Memory 中,所以可以和快速存取,同時它會保存在 part folder 中的 primary.cidx
中作為備份。
granule 時 ClickHouse 在讀取(搜尋)資料時的最小單位,也就是說,如果你只是要找一筆資料,ClickHouse 至少也會需要從 8,192 筆資料中去找到它。
granule 是資料搜尋時用的方式,和資料的保存沒有直接關係。
ClickHouse 在搜尋資料時,會用 Stripe of Granules 的概念:
- 每一個 Stripe of Granules(8,192 筆資料)都會被丟到一個 thread 去處理
- Stripe 彼此之間時 concurrently 的被處理(這就是為什麼 ClickHouse 可以這麼快!)
- Faster threads 可以從 slower threads 偷 tasks 來做
Partition
在 ClickHouse 中,Partition 的主要用途是管理資料(data management),並不會使用 Partition 來略過某些資料以優化查詢( 這是要用 Primary Key 來達到)。
使用 Partition 的唯一建議是:以「月」來分割資料,例如,PARTITION BY toYYYYMM(created_at)
。除非你真的每天都會有大量的資料進來,這時候再來考慮以「日」來分割資料。
- 大部分的情況下,你並不需要使用 Partition Key,而是應該聰明選擇你的 Primary Key。
亂用 Partition 會使得在 ClickHouse 中有過多的 Parts:
- 只有在同一個 Partition 中的 Parts 會被合併
- 不要使用
user_id
、地理位置、電子信箱等欄位作為 Partition Key
除非資料量真的非常大,否則使用 partition 只會讓 part 變成非常多。
邏輯上,設定 Partition 的確能達到優化資料查詢的結果,但這並不是使用 Partition 的理由。不要試著用 Partition 來嘗試略過 granules,而應該是要用 PRIMARY KEY。
CREATE TABLE partition_demo
(
`user_id` UInt32,
`message` String,
`timestamp` DateTime,
`metric` Decimal(30, 2)
)
ENGINE = MergeTree
# highlight-next-line
PARTITION BY toYYYYMM(timestamp)
PRIMARY KEY user_id;
Insert
keywords: part
Best Practice
# https://clickhouse.com/docs/en/guides/inserting-data
INSERT INTO helloworld.my_first_table (user_id, message, timestamp, metric) VALUES
(101, 'Hello, ClickHouse!', now(), -1.0 ),
(102, 'Insert a lot of rows per batch', yesterday(), 1.41421 ),
(102, 'Sort your data based on your commonly-used queries', today(), 2.718 ),
(101, 'Granules are the smallest chunks of data read', now() + 5, 3.14159 );
- 建議一次插入多筆資料,減少執行 INSERT 的次數
- 最好每次的 INSERT 都能至少有 1000 筆資料;最理想的話,是一次 10,000 到 100,000 筆資料
- 每次 bulk INSERT 時,這些資料都會被放在同一個資料夾中,這個資料夾有一個特殊的名稱,稱作 part;也就是說,每次 bulk INSERT 時,都會建立一個 part,這個 part 中可能包含數萬,甚至數百萬筆資料;每個 part 會被保存在它自己的資料夾中。
- 建議確保 batch insert 時資料的順序
- 當資料 INSERT 失敗時,只要當初插入的資料保有一樣的內容和順序,ClickHouse 會自動 deduplicate 這個插入(仍取決於所選的 ENGINE)。
- 因此,即使因為網路問題或節點問題而導致錯誤發生時,只要確保資料內容和順序相同,重複多次執行 INSERT 並不會有問題
- 如果一次能 INSERT 的資料筆數較少,建議使用 asynchronous inserts
- 如果 client 端沒辦法批次插入多筆資料,則應該考量使用 asynchronous insert,這可以將 batching insert 的責任委派給 ClickHouse。
- 透過 asynchronous inserts,資料會先被建立在記憶體的緩衝區,只有在緩衝區的資料被刷新後(flush),資料才會被保存進資料庫,否則,在刷新前,是沒有辦法在資料庫中查詢到這個資料的。
- 這個刷新頻率和 asynchronous inserts 是可以設定的
Part
在 ClickHouse 中,每次 INSERT 都會建立一個資料夾(part),也就是說,如果不使用 bulk INSERT 或 async INSERT,而是一筆一筆新增資料的話,就會建立出超多個資料夾(part)。
- 多個 Part 會在 ClickHouse 背景中被合併成一個新的 merged part,然後多個 merge 過的 part 又會在繼續合併成新的 merged part,就這樣持續合併,直到超過特定的檔案大小(
max_bytes_to_merge_at_max_space_in_pool
)後才停止。這也就是為什麼這個 Engine 被稱作 MergeTree。
SELECT Queries
ClickHouse 支援多種不多的輸入與輸出的資料型態。例如,在輸出時,可以使用 FORMAT
指令來以多種不同的格式來呈現資料:
SELECT *
FROM helloworld.my_first_table
ORDER BY timestamp
FORMAT VERTICAL;
Modeling Data
- Schema Design @ Data Modeling
在 ClickHouse 中,database 其實只是一個 namespace,一種分類 table 的方式。也就是說,如果 1 個 database 裡有 100 張表,和有 100 個 database,每個 database 中都只有一張表,這兩種方式消耗的資源基本上是一樣的。
CREATE DATABASE my_database;
CREATE TABLE my_table
(
user_id UInt32,
message String,
timestamp DateTime,
metric Decimal(9, 4)
)
ENGINE = MergeTree
PRIMARY KEY (user_id, timestamp);