該如何選擇ClickHouse的表引擎 本文將介紹ClickHouse中一個非常重要的概念—表引擎(table engine)。如果對MySQL熟悉的話,或許你應該聽說過InnoDB和MyISAM存儲引擎。不同的存儲引擎提供不同的存儲機制、索引方式、鎖定水平等功能,也可以稱之為表類型。ClickHo ...
該如何選擇ClickHouse的表引擎
本文將介紹ClickHouse中一個非常重要的概念—表引擎(table engine)。如果對MySQL熟悉的話,或許你應該聽說過InnoDB和MyISAM存儲引擎。不同的存儲引擎提供不同的存儲機制、索引方式、鎖定水平等功能,也可以稱之為表類型。ClickHouse提供了豐富的表引擎,這些不同的表引擎也代表著不同的表類型。比如數據表擁有何種特性、數據以何種形式被存儲以及如何被載入。本文會對ClickHouse中常見的表引擎進行介紹,主要包括以下內容:
表引擎的作用是什麼
MergeTree系列引擎
Log家族系列引擎
外部集成表引擎
其他特殊的表引擎
溫馨提示:本文內容較長,建議收藏
表引擎的作用是什麼
決定表存儲在哪裡以及以何種方式存儲
支持哪些查詢以及如何支持
併發數據訪問
索引的使用
是否可以執行多線程請求
數據複製參數
表引擎分類
引擎分類 引擎名稱
MergeTree系列 MergeTree 、ReplacingMergeTree 、SummingMergeTree 、 AggregatingMergeTree CollapsingMergeTree 、 VersionedCollapsingMergeTree 、GraphiteMergeTree
Log系列 TinyLog 、StripeLog 、Log
Integration Engines Kafka 、MySQL、ODBC 、JDBC、HDFS
Special Engines Distributed 、MaterializedView、 Dictionary 、Merge 、File、Null 、Set 、Join 、 URL View、Memory 、 Buffer
Log系列表引擎
應用場景
Log系列表引擎功能相對簡單,主要用於快速寫入小表(1百萬行左右的表),然後全部讀出的場景。即一次寫入多次查詢。
Log系列表引擎的特點
共性特點
數據存儲在磁碟上
當寫數據時,將數據追加到文件的末尾
不支持併發讀寫,當向表中寫入數據時,針對這張表的查詢會被阻塞,直至寫入動作結束
不支持索引
不支持原子寫:如果某些操作(異常的伺服器關閉)中斷了寫操作,則可能會獲得帶有損壞數據的表
不支持ALTER操作(這些操作會修改表設置或數據,比如delete、update等等)
區別
TinyLog
TinyLog是Log系列引擎中功能簡單、性能較低的引擎。它的存儲結構由數據文件和元數據兩部分組成。其中,數據文件是按列獨立存儲的,也就是說每一個列欄位都對應一個文件。除此之外,TinyLog不支持併發數據讀取。
StripLog支持併發讀取數據文件,當讀取數據時,ClickHouse會使用多線程進行讀取,每個線程處理一個單獨的數據塊。另外,StripLog將所有列數據存儲在同一個文件中,減少了文件的使用數量。
Log支持併發讀取數據文件,當讀取數據時,ClickHouse會使用多線程進行讀取,每個線程處理一個單獨的數據塊。Log引擎會將每個列數據單獨存儲在一個獨立文件中。
TinyLog表引擎使用
該引擎適用於一次寫入,多次讀取的場景。對於處理小批數據的中間表可以使用該引擎。值得註意的是,使用大量的小表存儲數據,性能會很低。
CREATE TABLE emp_tinylog ( emp_id UInt16 COMMENT '員工id', name String COMMENT '員工姓名', work_place String COMMENT '工作地點', age UInt8 COMMENT '員工年齡', depart String COMMENT '部門', salary Decimal32(2) COMMENT '工資' )ENGINE=TinyLog(); INSERT INTO emp_tinylog VALUES (1,'tom','上海',25,'技術部',20000),(2,'jack','上海',26,'人事部',10000); INSERT INTO emp_tinylog VALUES (3,'bob','北京',33,'財務部',50000),(4,'tony','杭州',28,'銷售事部',50000); 進入預設數據存儲目錄,查看底層數據存儲形式,可以看出:TinyLog引擎表每一列都對應的文件 [root@cdh04 emp_tinylog]# pwd /var/lib/clickhouse/data/default/emp_tinylog [root@cdh04 emp_tinylog]# ll 總用量 28 -rw-r----- 1 clickhouse clickhouse 56 9月 17 14:33 age.bin -rw-r----- 1 clickhouse clickhouse 97 9月 17 14:33 depart.bin -rw-r----- 1 clickhouse clickhouse 60 9月 17 14:33 emp_id.bin -rw-r----- 1 clickhouse clickhouse 70 9月 17 14:33 name.bin -rw-r----- 1 clickhouse clickhouse 68 9月 17 14:33 salary.bin -rw-r----- 1 clickhouse clickhouse 185 9月 17 14:33 sizes.json -rw-r----- 1 clickhouse clickhouse 80 9月 17 14:33 work_place.bin ## 查看sizes.json數據 ## 在sizes.json文件內使用JSON格式記錄了每個.bin文件內對應的數據大小的信息 { "yandex":{ "age%2Ebin":{ "size":"56" }, "depart%2Ebin":{ "size":"97" }, "emp_id%2Ebin":{ "size":"60" }, "name%2Ebin":{ "size":"70" }, "salary%2Ebin":{ "size":"68" }, "work_place%2Ebin":{ "size":"80" } } }
當我們執行ALTER操作時會報錯,說明該表引擎不支持ALTER操作
-- 以下操作會報錯:
-- DB::Exception: Mutations are not supported by storage TinyLog.
ALTER TABLE emp_tinylog DELETE WHERE emp_id = 5;
ALTER TABLE emp_tinylog UPDATE age = 30 WHERE emp_id = 4;
StripLog表引擎使用
相比TinyLog而言,StripeLog擁有更高的查詢性能(擁有.mrk標記文件,支持並行查詢),同時其使用了更少的文件描述符(所有數據使用同一個文件保存)。
CREATE TABLE emp_stripelog ( emp_id UInt16 COMMENT '員工id', name String COMMENT '員工姓名', work_place String COMMENT '工作地點', age UInt8 COMMENT '員工年齡', depart String COMMENT '部門', salary Decimal32(2) COMMENT '工資' )ENGINE=StripeLog; -- 插入數據 INSERT INTO emp_stripelog VALUES (1,'tom','上海',25,'技術部',20000),(2,'jack','上海',26,'人事部',10000); INSERT INTO emp_stripelog VALUES (3,'bob','北京',33,'財務部',50000),(4,'tony','杭州',28,'銷售事部',50000); -- 查詢數據 -- 由於是分兩次插入數據,所以查詢時會有兩個數據塊 cdh04 :) select * from emp_stripelog; SELECT * FROM emp_stripelog ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐ │ 1 │ tom │ 上海 │ 25 │ 技術部 │ 20000.00 │ │ 2 │ jack │ 上海 │ 26 │ 人事部 │ 10000.00 │ └────────┴──────┴────────────┴─────┴────────┴──────────┘ ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐ │ 3 │ bob │ 北京 │ 33 │ 財務部 │ 50000.00 │ │ 4 │ tony │ 杭州 │ 28 │ 銷售事部 │ 50000.00 │ └────────┴──────┴────────────┴─────┴──────────┴──────────┘ 進入預設數據存儲目錄,查看底層數據存儲形式 [root@cdh04 emp_stripelog]# pwd /var/lib/clickhouse/data/default/emp_stripelog [root@cdh04 emp_stripelog]# ll 總用量 12 -rw-r----- 1 clickhouse clickhouse 673 9月 17 15:11 data.bin -rw-r----- 1 clickhouse clickhouse 281 9月 17 15:11 index.mrk -rw-r----- 1 clickhouse clickhouse 69 9月 17 15:11 sizes.json
可以看出StripeLog表引擎對應的存儲結構包括三個文件:
data.bin:數據文件,所有的列欄位使用同一個文件保存,它們的數據都會被寫入data.bin。
index.mrk:數據標記,保存了數據在data.bin文件中的位置信息(每個插入數據塊對應列的offset),利用數據標記能夠使用多個線程,以並行的方式讀取data.bin內的壓縮數據塊,從而提升數據查詢的性能。
sizes.json:元數據文件,記錄了data.bin和index.mrk大小的信息
提示:
StripeLog引擎將所有數據都存儲在了一個文件中,對於每次的INSERT操作,ClickHouse會將數據塊追加到表文件的末尾
StripeLog引擎同樣不支持ALTER UPDATE 和ALTER DELETE 操作
Log表引擎使用
Log引擎表適用於臨時數據,一次性寫入、測試場景。Log引擎結合了TinyLog表引擎和StripeLog表引擎的長處,是Log系列引擎中性能最高的表引擎。
CREATE TABLE emp_log ( emp_id UInt16 COMMENT '員工id', name String COMMENT '員工姓名', work_place String COMMENT '工作地點', age UInt8 COMMENT '員工年齡', depart String COMMENT '部門', salary Decimal32(2) COMMENT '工資' )ENGINE=Log; INSERT INTO emp_log VALUES (1,'tom','上海',25,'技術部',20000),(2,'jack','上海',26,'人事部',10000); INSERT INTO emp_log VALUES (3,'bob','北京',33,'財務部',50000),(4,'tony','杭州',28,'銷售事部',50000); -- 查詢數據, -- 由於是分兩次插入數據,所以查詢時會有兩個數據塊 cdh04 :) select * from emp_log; SELECT * FROM emp_log ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐ │ 1 │ tom │ 上海 │ 25 │ 技術部 │ 20000.00 │ │ 2 │ jack │ 上海 │ 26 │ 人事部 │ 10000.00 │ └────────┴──────┴────────────┴─────┴────────┴──────────┘ ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐ │ 3 │ bob │ 北京 │ 33 │ 財務部 │ 50000.00 │ │ 4 │ tony │ 杭州 │ 28 │ 銷售事部 │ 50000.00 │ └────────┴──────┴────────────┴─────┴──────────┴──────────┘ 進入預設數據存儲目錄,查看底層數據存儲形式 [root@cdh04 emp_log]# pwd /var/lib/clickhouse/data/default/emp_log [root@cdh04 emp_log]# ll 總用量 32 -rw-r----- 1 clickhouse clickhouse 56 9月 17 15:55 age.bin -rw-r----- 1 clickhouse clickhouse 97 9月 17 15:55 depart.bin -rw-r----- 1 clickhouse clickhouse 60 9月 17 15:55 emp_id.bin -rw-r----- 1 clickhouse clickhouse 192 9月 17 15:55 __marks.mrk -rw-r----- 1 clickhouse clickhouse 70 9月 17 15:55 name.bin -rw-r----- 1 clickhouse clickhouse 68 9月 17 15:55 salary.bin -rw-r----- 1 clickhouse clickhouse 216 9月 17 15:55 sizes.json -rw-r----- 1 clickhouse clickhouse 80 9月 17 15:55 work_place.bin
Log引擎的存儲結構包含三部分:
列.bin:數據文件,數據文件按列單獨存儲
__marks.mrk:數據標記,統一保存了數據在各個.bin文件中的位置信息。利用數據標記能夠使用多個線程,以並行的方式讀取。.bin內的壓縮數據塊,從而提升數據查詢的性能。
sizes.json:記錄了.bin和__marks.mrk大小的信息
提示:
Log表引擎會將每一列都存在一個文件中,對於每一次的INSERT操作,都會對應一個數據塊
MergeTree系列引擎
在所有的表引擎中,最為核心的當屬MergeTree系列表引擎,這些表引擎擁有最為強大的性能和最廣泛的使用場合。對於非MergeTree系列的其他引擎而言,主要用於特殊用途,場景相對有限。而MergeTree系列表引擎是官方主推的存儲引擎,支持幾乎所有ClickHouse核心功能。
MergeTree表引擎
MergeTree在寫入一批數據時,數據總會以數據片段的形式寫入磁碟,且數據片段不可修改。為了避免片段過多,ClickHouse會通過後臺線程,定期合併這些數據片段,屬於相同分區的數據片段會被合成一個新的片段。這種數據片段往複合併的特點,也正是合併樹名稱的由來。
MergeTree作為家族系列最基礎的表引擎,主要有以下特點:
存儲的數據按照主鍵排序:允許創建稀疏索引,從而加快數據查詢速度
支持分區,可以通過PRIMARY KEY語句指定分區欄位。
支持數據副本
支持數據採樣
建表語法
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster] ( name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1], name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2], ... INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1, INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2 ) ENGINE = MergeTree() ORDER BY expr [PARTITION BY expr] [PRIMARY KEY expr] [SAMPLE BY expr] [TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...] [SETTINGS name=value, ...] ENGINE:ENGINE = MergeTree(),MergeTree引擎沒有參數 ORDER BY:排序欄位。比如ORDER BY (Col1, Col2),值得註意的是,如果沒有指定主鍵,預設情況下 sorting key(排序欄位)即為主鍵。如果不需要排序,則可以使用**ORDER BY tuple()**語法,這樣的話,創建的表也就不包含主鍵。這種情況下,ClickHouse會按照插入的順序存儲數據。必選。 PARTITION BY:分區欄位,可選。 PRIMARY KEY:指定主鍵,如果排序欄位與主鍵不一致,可以單獨指定主鍵欄位。否則預設主鍵是排序欄位。可選。 SAMPLE BY:採樣欄位,如果指定了該欄位,那麼主鍵中也必須包含該欄位。比如SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID))。可選。 TTL:數據的存活時間。在MergeTree中,可以為某個列欄位或整張表設置TTL。當時間到達時,如果是列欄位級別的TTL,則會刪除這一列的數據;如果是表級別的TTL,則會刪除整張表的數據。可選。 SETTINGS:額外的參數配置。可選。 建表示例 CREATE TABLE emp_mergetree ( emp_id UInt16 COMMENT '員工id', name String COMMENT '員工姓名', work_place String COMMENT '工作地點', age UInt8 COMMENT '員工年齡', depart String COMMENT '部門', salary Decimal32(2) COMMENT '工資' )ENGINE=MergeTree() ORDER BY emp_id PARTITION BY work_place ; -- 插入數據 INSERT INTO emp_mergetree VALUES (1,'tom','上海',25,'技術部',20000),(2,'jack','上海',26,'人事部',10000); INSERT INTO emp_mergetree VALUES (3,'bob','北京',33,'財務部',50000),(4,'tony','杭州',28,'銷售事部',50000); -- 查詢數據 -- 按work_place進行分區 cdh04 :) select * from emp_mergetree; SELECT * FROM emp_mergetree ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐ │ 3 │ bob │ 北京 │ 33 │ 財務部 │ 50000.00 │ └────────┴──────┴────────────┴─────┴────────┴──────────┘ ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐ │ 1 │ tom │ 上海 │ 25 │ 技術部 │ 20000.00 │ │ 2 │ jack │ 上海 │ 26 │ 人事部 │ 10000.00 │ └────────┴──────┴────────────┴─────┴────────┴──────────┘ ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐ │ 4 │ tony │ 杭州 │ 28 │ 銷售事部 │ 50000.00 │ └────────┴──────┴────────────┴─────┴──────────┴──────────┘ 查看一下數據存儲格式,可以看出,存在三個分區文件夾,每一個分區文件夾記憶體儲了對應分區的數據。 [root@cdh04 emp_mergetree]# pwd /var/lib/clickhouse/data/default/emp_mergetree [root@cdh04 emp_mergetree]# ll 總用量 16 drwxr-x--- 2 clickhouse clickhouse 4096 9月 17 17:45 1c89a3ba9fe5fd53379716a776c5ac34_3_3_0 drwxr-x--- 2 clickhouse clickhouse 4096 9月 17 17:44 40d45822dbd7fa81583d715338929da9_1_1_0 drwxr-x--- 2 clickhouse clickhouse 4096 9月 17 17:45 a6155dcc1997eda1a348cd98b17a93e9_2_2_0 drwxr-x--- 2 clickhouse clickhouse 6 9月 17 17:43 detached -rw-r----- 1 clickhouse clickhouse 1 9月 17 17:43 format_version.txt 進入一個分區目錄查看
checksums.txt:校驗文件,使用二進位格式存儲。它保存了餘下各類文件(primary. idx、count.txt等)的size大小及size的哈希值,用於快速校驗文件的完整性和正確性。
columns.txt:列信息文件,使用明文格式存儲。用於保存此數據分區下的列欄位信息,例如
[root@cdh04 1c89a3ba9fe5fd53379716a776c5ac34_3_3_0]# cat columns.txt
columns format version: 1
6 columns:
`emp_id` UInt16
`name` String
`work_place` String
`age` UInt8
`depart` String
`salary` Decimal(9, 2)
count.txt:計數文件,使用明文格式存儲。用於記錄當前數據分區目錄下數據的總行數
primary.idx:一級索引文件,使用二進位格式存儲。用於存放稀疏索引,一張MergeTree表只能聲明一次一級索引,即通過ORDER BY或者PRIMARY KEY指定欄位。藉助稀疏索引,在數據查詢的時能夠排除主鍵條件範圍之外的數據文件,從而有效減少數據掃描範圍,加速查詢速度。
列.bin:數據文件,使用壓縮格式存儲,預設為LZ4壓縮格式,用於存儲某一列的數據。由於MergeTree採用列式存儲,所以每一個列欄位都擁有獨立的.bin數據文件,並以列欄位名稱命名。
列.mrk2:列欄位標記文件,使用二進位格式存儲。標記文件中保存了.bin文件中數據的偏移量信息
partition.dat與minmax_[Column].idx:如果指定了分區鍵,則會額外生成partition.dat與minmax索引文件,它們均使用二進位格式存儲。partition.dat用於保存當前分區下分區表達式最終生成的值,即分區欄位值;而minmax索引用於記錄當前分區下分區欄位對應原始數據的最小和最大值。比如當使用EventTime欄位對應的原始數據為2020-09-17、2020-09-30,分區表達式為PARTITION BY toYYYYMM(EventTime),即按月分區。partition.dat中保存的值將會是2019-09,而minmax索引中保存的值將會是2020-09-17 2020-09-30。
註意點
多次插入數據,會生成多個分區文件
-- 新插入兩條數據 cdh04 :) INSERT INTO emp_mergetree VALUES (5,'robin','北京',35,'財務部',50000),(6,'lilei','北京',38,'銷售事部',50000); -- 查詢結果 cdh04 :) select * from emp_mergetree; ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐ │ 3 │ bob │ 北京 │ 33 │ 財務部 │ 50000.00 │ └────────┴──────┴────────────┴─────┴────────┴──────────┘ ┌─emp_id─┬─name──┬─work_place─┬─age─┬─depart───┬───salary─┐ │ 5 │ robin │ 北京 │ 35 │ 財務部 │ 50000.00 │ │ 6 │ lilei │ 北京 │ 38 │ 銷售事部 │ 50000.00 │ └────────┴───────┴────────────┴─────┴──────────┴──────────┘ ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐ │ 1 │ tom │ 上海 │ 25 │ 技術部 │ 20000.00 │ │ 2 │ jack │ 上海 │ 26 │ 人事部 │ 10000.00 │ └────────┴──────┴────────────┴─────┴────────┴──────────┘ ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐ │ 4 │ tony │ 杭州 │ 28 │ 銷售事部 │ 50000.00 │ └────────┴──────┴────────────┴─────┴──────────┴──────────┘ 可以看出,新插入的數據新生成了一個數據塊,並沒有與原來的分區數據在一起,我們可以執行optimize命令,執行合併操作 -- 執行合併操作 cdh04 :) OPTIMIZE TABLE emp_mergetree PARTITION '北京'; -- 再次執行查詢 cdh04 :) select * from emp_mergetree; SELECT * FROM emp_mergetree ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐ │ 1 │ tom │ 上海 │ 25 │ 技術部 │ 20000.00 │ │ 2 │ jack │ 上海 │ 26 │ 人事部 │ 10000.00 │ └────────┴──────┴────────────┴─────┴────────┴──────────┘ ┌─emp_id─┬─name──┬─work_place─┬─age─┬─depart───┬───salary─┐ │ 3 │ bob │ 北京 │ 33 │ 財務部 │ 50000.00 │ │ 5 │ robin │ 北京 │ 35 │ 財務部 │ 50000.00 │ │ 6 │ lilei │ 北京 │ 38 │ 銷售事部 │ 50000.00 │ └────────┴───────┴────────────┴─────┴──────────┴──────────┘ ┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐ │ 4 │ tony │ 杭州 │ 28 │ 銷售事部 │ 50000.00 │ └────────┴──────┴────────────┴─────┴──────────┴──────────┘
執行上面的合併操作之後,會新生成一個該分區的文件夾,原理的分區文件夾不變。
在MergeTree中主鍵並不用於去重,而是用於索引,加快查詢速度
-- 插入一條相同主鍵的數據
INSERT INTO emp_mergetree
VALUES (1,'sam','杭州',35,'財務部',50000);
-- 會發現該條數據可以插入,由此可知,並不會對主鍵進行去重
ReplacingMergeTree表引擎
上文提到MergeTree表引擎無法對相同主鍵的數據進行去重,ClickHouse提供了ReplacingMergeTree引擎,可以針對相同主鍵的數據進行去重,它能夠在合併分區時刪除重覆的數據。值得註意的是,ReplacingMergeTree只是在一定程度上解決了數據重覆問題,但是並不能完全保障數據不重覆。
建表語法
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster] ( name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1], name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2], ... ) ENGINE = ReplacingMergeTree([ver]) [PARTITION BY expr] [ORDER BY expr] [PRIMARY KEY expr] [