你需要懂一點ClickHouse的基礎知識
- 2019 年 11 月 14 日
- 筆記
應用場景:
1.
絕大多數請求都是用於讀訪問的 2.
數據需要以大批次(大於1000行)進行更新,而不是單行更新;或者根本沒有更新操作
3.
數據只是添加到資料庫,沒有必要修改 4.
讀取數據時,會從資料庫中提取出大量的行,但只用到一小部分列
5.
表很「寬」,即表中包含大量的列 6.
查詢頻率相對較低(通常每台伺服器每秒查詢數百次或更少)
7.
對於簡單查詢,允許大約50毫秒的延遲 8.
列的值是比較小的數值和短字元串(例如,每個URL只有60個位元組) 9.
在處理單個查詢時需要高吞吐量(每台伺服器每秒高達數十億行) 10.
不需要事務
11.
數據一致性要求較低 12.
每次查詢中只會查詢一個大表。除了一個大表,其餘都是小表 13.
查詢結果顯著小於數據源。即數據有過濾或聚合。返回結果不超過單個伺服器記憶體大小
相應地,使用ClickHouse也有其本身的限制:
1.
不支援真正的刪除/更新支援 不支援事務(期待後續版本支援) 2.
不支援二級索引 3.
有限的SQL支援,join實現與眾不同 4.
不支援窗口功能 5.
元數據管理需要人工干預維護
常用SQL語法
-- 列出資料庫列表 show databases; -- 列出資料庫中表列表 show tables; -- 創建資料庫 create database test; -- 刪除一個表 drop table if exists test.t1; -- 創建第一個表 create /*temporary*/ table /*if not exists*/ test.m1 ( id UInt16 ,name String ) ENGINE = Memory ; -- 插入測試數據 insert into test.m1 (id, name) values (1, 'abc'), (2, 'bbbb'); -- 查詢 select * from test.m1;
默認值
默認值 的處理方面, ClickHouse 中,默認值總是有的,如果沒有顯示式指定的話,會按欄位類型處理:
數字類型, 0 字元串,空字元串 數組,空數組 日期, 0000-00-00 時間, 0000-00-00 00:00:00 註:NULLs 是不支援的
數據類型
1.
整型:UInt8,UInt16,UInt32,UInt64,Int8,Int16,Int32,Int64 範圍U開頭-2N/2~2N-1;非U開頭0~2^N-12.
枚舉類型:Enum8,Enum16 Enum('hello'=1,'test'=-1),Enum是有符號的整型映射的,因此負數也是可以的3.
字元串型:FixedString(N),String N是最大位元組數,不是字元長度,如果是UTF8字元串,那麼就會佔3個位元組,GBK會佔2位元組;String可以用來替換VARCHAR,BLOB,CLOB等數據類型4.
時間類型:Date5.
數組類型:Array(T) T是一個基本類型,包括arry在內,官方不建議使用多維數組6.
元組:Tuple7.
結構:Nested(name1 Type1,name2 Type2,…) 類似一種map的結
物化列
指定 MATERIALIZED 表達式,即將一個列作為物化列
處理了,這意味著這個列的值不能從insert
語句獲取,只能是自己計算出來的。同時, 物化列也不會出現在 select *
的結果中:
drop table if exists test.m2; create table test.m2 ( a MATERIALIZED (b+1) ,b UInt16 ) ENGINE = Memory; insert into test.m2 (b) values (1); select * from test.m2; select a, b from test.m2;
表達式列
ALIAS 表達式列某方面跟物化列相同,就是它的值不能從 insert 語句獲取。不同的是, 物化列 是會真正保存數據(這樣查詢時不需要再計算), 而表達式列不會保存數據(這樣查詢時總是需要計算),只是在查詢時返回表達式的結果。
create table test.m3 (a ALIAS (b+1), b UInt16) ENGINE = Memory; insert into test.m3(b) values (1); select * from test.m3; select a, b from test.m3;
引擎/engine
引擎是clickhouse設計的精華部分
TinyLog
最簡單的一種引擎,每一列保存為一個文件,裡面的內容是壓縮過的,不支援索引 這種引擎沒有並發控制,所以,當你需要在讀,又在寫時,讀會出錯。並發寫,內容都會壞掉。
應用場景:
a. 基本上就是那種只寫一次 b. 然後就是只讀的場景。 c. 不適用於處理量大的數據,官方推薦,使用這種引擎的表最多 100 萬行的數據
drop table if exists test.tinylog; create table test.tinylog (a UInt16, b UInt16) ENGINE = TinyLog; insert into test.tinylog(a,b) values (7,13);
此時/var/lib/clickhouse/data/test/tinylog
保存數據的目錄結構:
├── a.bin ├── b.bin └── sizes.json
a.bin 和 b.bin 是壓縮過的對應的列的數據, sizes.json 中記錄了每個 *.bin 文件的大小
Log
這種引擎跟 TinyLog 基本一致 它的改進點,是加了一個 __marks.mrk 文件,裡面記錄了每個數據塊的偏移 這樣做的一個用處,就是可以準確地切分讀的範圍,從而使用並發讀取成為可能 但是,它是不能支援並發寫的,一個寫操作會阻塞其它讀寫操作 Log 不支援索引,同時因為有一個 __marks.mrk 的冗餘數據,所以在寫入數據時,一旦出現問題,這個表就廢了
應用場景:
同 TinyLog 差不多,它適用的場景也是那種寫一次之後,後面就是只讀的場景,臨時數據用它保存也可以。
drop table if exists test.log; create table test.log (a UInt16, b UInt16) ENGINE = Log; insert into test.log(a,b) values (7,13);
此時/var/lib/clickhouse/data/test/log
保存數據的目錄結構:
├── __marks.mrk ├── a.bin ├── b.bin └── sizes.json
Memory
記憶體引擎,數據以未壓縮的原始形式直接保存在記憶體當中,伺服器重啟數據就會消失 可以並行讀,讀寫互斥鎖的時間也非常短 不支援索引,簡單查詢下有非常非常高的性能表現
應用場景:
a. 進行測試 b. 在需要非常高的性能,同時數據量又不太大(上限大概 1 億行)的場景
Merge
一個工具引擎,本身不保存數據,只用於把指定庫中的指定多個錶鏈在一起。 這樣,讀取操作可以並發執行,同時也可以利用原表的索引,但是,此引擎不支援寫操作 指定引擎的同時,需要指定要鏈接的庫及表,庫名可以使用一個表達式,表名可以使用正則表達式指定
create table test.tinylog1 (id UInt16, name String) ENGINE=TinyLog; create table test.tinylog2 (id UInt16, name String) ENGINE=TinyLog; create table test.tinylog3 (id UInt16, name String) ENGINE=TinyLog; insert into test.tinylog1(id, name) values (1, 'tinylog1'); insert into test.tinylog2(id, name) values (2, 'tinylog2'); insert into test.tinylog3(id, name) values (3, 'tinylog3'); use test; create table test.merge (id UInt16, name String) ENGINE=Merge(currentDatabase(), '^tinylog[0-9]+'); select _table,* from test.merge order by id desc
┌─_table───┬─id─┬─name─────┐ │ tinylog3 │ 3 │ tinylog3 │ │ tinylog2 │ 2 │ tinylog2 │ │ tinylog1 │ 1 │ tinylog1 │ └──────────┴────┴──────────┘
註:
_table 這個列,是因為使用了 Merge 多出來的一個的一個虛擬列
a. 它表示原始數據的來源表,它不會出現在
show table
的結果當中 b.select *
不會包含它
Distributed
與 Merge 類似, Distributed 也是通過一個邏輯表,去訪問各個物理表,設置引擎時的樣子是:
Distributed(remote_group, database, table [, sharding_key])
其中:
remote_group
/etc/clickhouse-server/config.xml中remote_servers參數database
是各伺服器中的庫名table
是表名sharding_key
是一個定址表達式,可以是一個列名,也可以是像 rand() 之類的函數調用,它與 remote_servers 中的 weight 共同作用,決定在 寫 時往哪個 shard 寫
配置文件中的 remote_servers
<remote_servers> <log> <shard> <weight>1</weight> <internal_replication>false</internal_replication> <replica> <host>172.17.0.3</host> <port>9000</port> </replica> </shard> <shard> <weight>2</weight> <internal_replication>false</internal_replication> <replica> <host>172.17.0.4</host> <port>9000</port> </replica> </shard> </log> </remote_servers>
log
是某個 shard 組的名字,就是上面的 remote_group 的值shard
是固定標籤weight
是權重,前面說的 sharding_key 與這個有關。 簡單來說,上面的配置,理論上來看: 第一個 shard 「被選中」的概率是 1 / (1 + 2) ,第二個是 2 / (1 + 2) ,這很容易理解。但是, sharding_key 的工作情況,是按實際數字的「命中區間」算的,即第一個的區間是 [0, 1) 的周期,第二個區間是 [1, 1+2) 的周期。比如把 sharding_key 設置成 id ,當 id=0 或 id=3 時,一定是寫入到第一個 shard 中,如果把 sharding_key 設置成 rand() ,那系統會對應地自己作一般化轉換吧,這種時候就是一種概率場景了。internal_replication
是定義針對多個 replica 時的寫入行為的。 如果為 false ,則會往所有的 replica 中寫入數據,但是並不保證數據寫入的一致性,所以這種情況時間一長,各 replica 的數據很可能出現差異。如果為 true ,則只會往第一個可寫的 replica 中寫入數據(剩下的事「物理表」自己處理)。replica
就是定義各個冗餘副本的,選項有 host , port , user , password 等
看一個實際的例子,我們先在兩台機器上創建好物理表並插入一些測試數據:
create table test.tinylog_d1(id UInt16, name String) ENGINE=TinyLog; insert into test.tinylog_d1(id, name) values (1, 'Distributed record 1'); insert into test.tinylog_d1(id, name) values (2, 'Distributed record 2');
在其中一台創建邏輯表:
create table test.tinylog_d (id UInt16, name String) ENGINE=Distributed(log, test,tinylog_d1 , id); -- 插入數據到邏輯表,觀察數據分發情況 insert into test.tinylog_d(id, name) values (0, 'main'); insert into test.tinylog_d(id, name) values (1, 'main'); insert into test.tinylog_d(id, name) values (2, 'main'); select name,sum(id),count(id) from test.tinylog_d group by name;
註:
邏輯表中的寫入操作是非同步的,會先快取在本機的文件系統上,並且,對於物理表的不可訪問狀態,並沒有嚴格控制,所以寫入失敗丟數據的情況是可能發生的
Null
空引擎,寫入的任何數據都會被忽略,讀取的結果一定是空。
但是注意,雖然數據本身不會被存儲,但是結構上的和數據格式上的約束還是跟普通表一樣是存在的,同時,你也可以在這個引擎上創建視圖
Buffer
- Buffer 引擎,像是Memory 存儲的一個上層應用似的(磁碟上也是沒有相應目錄的)
- 它的行為是一個緩衝區,寫入的數據先被放在緩衝區,達到一個閾值後,這些數據會自動被寫到指定的另一個表中
- 和Memory 一樣,有很多的限制,比如沒有索引
- Buffer 是接在其它表前面的一層,對它的讀操作,也會自動應用到後面表,但是因為前面說到的限制的原因,一般我們讀數據,就直接從源表讀就好了,緩衝區的這點數據延遲,只要配置得當,影響不大的
- Buffer 後面也可以不接任何錶,這樣的話,當數據達到閾值,就會被丟棄掉
一些特點:
- 如果一次寫入的數據太大或太多,超過了 max 條件,則會直接寫入源表。
- 刪源表或改源表的時候,建議 Buffer 表刪了重建。
- 「友好重啟」時, Buffer 數據會先落到源表,「暴力重啟」, Buffer 表中的數據會丟失。
- 即使使用了 Buffer ,多次的小數據寫入,對比一次大數據寫入,也 慢得多 (幾千行與百萬行的差距)
-- 創建源表 create table test.mergetree (sdt Date, id UInt16, name String, point UInt16) ENGINE=MergeTree(sdt, (id, name), 10); -- 創建 Buffer表 -- Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes) create table test.mergetree_buffer as test.mergetree ENGINE=Buffer(test, mergetree, 16, 3, 20, 2, 10, 1, 10000); insert into test.mergetree (sdt, id, name, point) values ('2017-07-10', 1, 'a', 20); insert into test.mergetree_buffer (sdt, id, name, point) values ('2017-07-10', 1, 'b', 10); select * from test.mergetree; select '------'; select * from test.mergetree_buffer;
database
資料庫 table
源表,這裡除了字元串常量,也可以使用變數的。 num_layers
是類似「分區」的概念,每個分區的後面的 min / max 是獨立計算的,官方推薦的值是 16 。 min / max
這組配置薦,就是設置閾值的,分別是 時間(秒),行數,空間(位元組)。
閾值的規則:
是「所有的 min 條件都滿足, 或 至少一個 max 條件滿足」。
如果按上面我們的建表來說,所有的 min 條件就是:過了 3秒,2條數據,1 Byte。一個 max 條件是:20秒,或 10 條數據,或有 10K
Set
Set 這個引擎有點特殊,因為它只用在 IN 操作符右側,你不能對它 select
create table test.set(id UInt16, name String) ENGINE=Set; insert into test.set(id, name) values (1, 'hello'); -- select 1 where (1, 'hello') in test.set; -- 默認UInt8 需要手動進行類型轉換 select 1 where (toUInt16(1), 'hello') in test.set;
注:
Set 引擎表,是全記憶體運行的,但是相關數據會落到磁碟上保存,啟動時會載入到記憶體中。所以,意外中斷或暴力重啟,是可能產生數據丟失問題的。
MergeTree
這個引擎是 ClickHouse 的重頭戲
,它支援一個日期和一組主鍵的兩層式索引
,還可以實時更新數據
。同時,索引的粒度可以自定義,外加直接支援取樣功能
MergeTree(EventDate, (CounterID, EventDate), 8192) MergeTree(EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID)), 8192)
EventDate
一個日期的列名 intHash32(UserID)
取樣表達式 (CounterID, EventDate)
主鍵組(裡面除了列名,也支援表達式),也可以是一個表達式 8192
主鍵索引的粒度
drop table if exists test.mergetree1; create table test.mergetree1 (sdt Date, id UInt16, name String, cnt UInt16) ENGINE=MergeTree(sdt, (id, name), 10); -- 日期的格式,好像必須是 yyyy-mm-dd insert into test.mergetree1(sdt, id, name, cnt) values ('2018-06-01', 1, 'aaa', 10); insert into test.mergetree1(sdt, id, name, cnt) values ('2018-06-02', 4, 'bbb', 10); insert into test.mergetree1(sdt, id, name, cnt) values ('2018-06-03', 5, 'ccc', 11);
此時/var/lib/clickhouse/data/test/mergetree1
的目錄結構:
├── 20180601_20180601_1_1_0 │ ├── checksums.txt │ ├── columns.txt │ ├── id.bin │ ├── id.mrk │ ├── name.bin │ ├── name.mrk │ ├── cnt.bin │ ├── cnt.mrk │ ├── cnt.idx │ ├── primary.idx │ ├── sdt.bin │ └── sdt.mrk -- 保存一下塊偏移量 ├── 20180602_20180602_2_2_0 │ └── ... ├── 20180603_20180603_3_3_0 │ └── ... ├── format_version.txt └── detached
ReplacingMergeTree
1
.在 MergeTree 的基礎上,添加了「處理重複數據」的功能=>實時數據場景 2
.相比 MergeTree ,ReplacingMergeTree 在最後加一個"版本列",它跟時間列配合一起,用以區分哪條數據是"新的",並把舊的丟掉(這個過程是在 merge 時處理,不是數據寫入時就處理了的,平時重複的數據還是保存著的,並且查也是跟平常一樣會查出來的) 3
.主鍵列組用於區分重複的行
-- 版本列 允許的類型是, UInt 一族的整數,或 Date 或 DateTime create table test.replacingmergetree (sdt Date, id UInt16, name String, cnt UInt16) ENGINE=ReplacingMergeTree(sdt, (name), 10, cnt); insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-10', 1, 'a', 20); insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-10', 1, 'a', 30); insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-11', 1, 'a', 20); insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-11', 1, 'a', 30); insert into test.replacingmergetree (sdt, id, name, cnt) values ('2018-06-11', 1, 'a', 10); select * from test.replacingmergetree; -- 如果記錄未執行merge,可以手動觸發一下 merge 行為 optimize table test.replacingmergetree;
┌────────sdt─┬─id─┬─name─┬─cnt─┐ │ 2018-06-11 │ 1 │ a │ 30 │ └────────────┴────┴──────┴─────┘
SummingMergeTree
1
.SummingMergeTree 就是在 merge 階段把數據sum求和 2
.sum求和的列可以指定,不可加的未指定列,會取一個最先出現的值
create table test.summingmergetree (sdt Date, name String, a UInt16, b UInt16) ENGINE=SummingMergeTree(sdt, (sdt, name), 8192, (a)); insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-10', 'a', 1, 20); insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-10', 'b', 2, 11); insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-11', 'b', 3, 18); insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-11', 'b', 3, 82); insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-11', 'a', 3, 11); insert into test.summingmergetree (sdt, name, a, b) values ('2018-06-12', 'c', 1, 35); -- 手動觸發一下 merge 行為 optimize table test.summingmergetree; select * from test.summingmergetree;
┌────────sdt─┬─name─┬─a─┬──b─┐ │ 2018-06-10 │ a │ 1 │ 20 │ │ 2018-06-10 │ b │ 2 │ 11 │ │ 2018-06-11 │ a │ 3 │ 11 │ │ 2018-06-11 │ b │ 6 │ 18 │ │ 2018-06-12 │ c │ 1 │ 35 │ └────────────┴──────┴───┴────┘ 注:
可加列不能是主鍵中的列,並且如果某行數據可加列都是 null ,則這行會被刪除。
AggregatingMergeTree
AggregatingMergeTree 是在 MergeTree 基礎之上,針對聚合函數結果,作增量計算優化的一個設計,它會在 merge 時,針對主鍵預處理聚合的數據 應用於AggregatingMergeTree 上的聚合函數除了普通的 sum, uniq等,還有 sumState , uniqState ,及 sumMerge , uniqMerge 這兩組
1.
聚合數據的預計算 是一種「空間換時間」的權衡,並且是以減少維度為代價的
dim1 |
dim2 |
dim3 |
measure1 |
---|---|---|---|
aaaa |
a |
1 |
1 |
aaaa |
b |
2 |
1 |
bbbb |
b |
3 |
1 |
cccc |
b |
2 |
1 |
cccc |
c |
1 |
1 |
dddd |
c |
2 |
1 |
dddd |
a |
1 |
1 |
假設原始有三個維度,一個需要 count 的指標
dim1 |
dim2 |
dim3 |
measure1 |
---|---|---|---|
aaaa |
a |
1 |
1 |
aaaa |
b |
2 |
1 |
bbbb |
b |
3 |
1 |
cccc |
b |
2 |
1 |
cccc |
c |
1 |
1 |
dddd |
c |
2 |
1 |
dddd |
a |
1 |
1 |
通過減少一個維度的方式,來以 count 函數聚合一次 M
dim2 |
dim3 |
count(measure1) |
---|---|---|
a |
1 |
3 |
b |
2 |
2 |
b |
3 |
1 |
c |
1 |
1 |
c |
2 |
1 |
2.
聚合數據的增量計算
對於 AggregatingMergeTree 引擎的表,不能使用普通的 INSERT 去添加數據,可以用: a.
INSERT SELECT 來插入數據 b.
更常用的,是可以創建一個物化視圖
drop table if exists test.aggregatingmergetree; create table test.aggregatingmergetree( sdt Date , dim1 String , dim2 String , dim3 String , measure1 UInt64 ) ENGINE=MergeTree(sdt, (sdt, dim1, dim2, dim3), 8192); -- 創建一個物化視圖,使用 AggregatingMergeTree drop table if exists test.aggregatingmergetree_view; create materialized view test.aggregatingmergetree_view ENGINE = AggregatingMergeTree(sdt,(dim2, dim3), 8192) as select sdt,dim2, dim3, uniqState(dim1) as uv from test.aggregatingmergetree group by sdt,dim2, dim3; insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'aaaa', 'a', '10', 1); insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'aaaa', 'a', '10', 1); insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'aaaa', 'b', '20', 1); insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'bbbb', 'b', '30', 1); insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'cccc', 'b', '20', 1); insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'cccc', 'c', '10', 1); insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'dddd', 'c', '20', 1); insert into test.aggregatingmergetree (sdt, dim1, dim2, dim3, measure1) values ('2018-06-10', 'dddd', 'a', '10', 1); -- 按 dim2 和 dim3 聚合 count(measure1) select dim2, dim3, count(measure1) from test.aggregatingmergetree group by dim2, dim3; -- 按 dim2 聚合 UV select dim2, uniq(dim1) from test.aggregatingmergetree group by dim2; -- 手動觸發merge OPTIMIZE TABLE test.aggregatingmergetree_view; select * from test.aggregatingmergetree_view; -- 查 dim2 的 uv select dim2, uniqMerge(uv) from test.aggregatingmergetree_view group by dim2 order by dim2;
CollapsingMergeTree
是專門為 OLAP 場景下,一種「變通」存數做法而設計的,在數據是不能改,更不能刪的前提下,通過「運算」的方式,去抹掉舊數據的影響,把舊數據「減」去即可,從而解決"最終狀態"類的問題,比如 當前有多少人在線?
「以加代刪」的增量存儲方式,帶來了聚合計算方便的好處,代價卻是存儲空間的翻倍,並且,對於只關心最新狀態的場景,中間數據都是無用的
CollapsingMergeTree 在創建時與 MergeTree 基本一樣,除了最後多了一個參數,需要指定 Sign 位(必須是 Int8 類型)
create table test.collapsingmergetree(sign Int8, sdt Date, name String, cnt UInt16) ENGINE=CollapsingMergeTree(sdt, (sdt, name), 8192, sign);
作者:darebeat
來源:https://www.jianshu.com/p/a5bf490247ea