clickhouse使用心得

clickhouse目前用在實時BI後臺,只要數據穩定落庫了,出報表很快,臨時查詢也很快,在使用過程中,對它的一些優點和不足也是深有體會,這裏總結一下,不能做到面面俱到,但儘可能詳細的介紹實際應用需要注意的問題和應用技巧。

我們是通過編寫Flink程序,消費kafka數據,將數據清洗,擴充維度,然後落在clickhouse裏面,半年以來,Flink程序很少出問題,數據落庫也很穩定。對於clickhouse,使用的是騰訊雲的clickhouse服務,有副本的集羣,中間擴充了幾次磁盤,服務也是挺穩定的,整體看來,整個BI後臺,都能穩定的提供數據報表。爲了書寫方便,接下來clickhouse用ck縮寫。

ck裏面引用mysql外部數據表

通常需要在ck裏面要用mysql裏面的表,比如mysql裏面存在一張維表,我們需要根據id查詢出某個名稱,這個時候,不需要把數據導一份過來,就可以把mysql表映射到ck裏面,或者直接整個mysql數據庫映射到ck某個庫裏面,就能操作mysql這個數據庫所有表,使用sql語法關聯查詢mysql和ck的表。

MySQL引擎用於將遠程的MySQL服務器中的表映射到ClickHouse中,並允許您對錶進行INSERT和SELECT查詢,以方便您在ClickHouse與MySQL之間進行數據交換。

創建數據庫

CREATE DATABASE [IF NOT EXISTS] db_name [ON CLUSTER cluster]
ENGINE = MySQL('host:port', ['database' | database], 'user', 'password')

比如,我們在mysql裏面創建一張表:

mysql> USE test;
Database changed

mysql> CREATE TABLE `mysql_table` (
    ->   `int_id` INT NOT NULL AUTO_INCREMENT,
    ->   `float` FLOAT NOT NULL,
    ->   PRIMARY KEY (`int_id`));
Query OK, 0 rows affected (0,09 sec)

mysql> insert into mysql_table (`int_id`, `float`) VALUES (1,2);
Query OK, 1 row affected (0,00 sec)

mysql> select * from mysql_table;
+------+-----+
| int_id | value |
+------+-----+
|      1 |     2 |
+------+-----+
1 row in set (0,00 sec)

我們去ck裏面創建一個數據庫,跟mysql這個數據庫關聯起來。

CREATE DATABASE mysql_db ENGINE = MySQL('localhost:3306', 'test', 'my_user', 'user_password')

這樣就在ck裏面創建了一個mysql_db,這個數據庫跟mysql的test數據庫是映射在一起了,我們在ck裏面直接查詢:

SELECT * FROM mysql_db.mysql_table

┌─int_id─┬─value─┐
│      1 │     2 │
└────────┴───────┘

數據庫引擎可以是mysql,也可以是其它數據庫,比如sqlite、PostgreSQL,更多可以查閱官方文檔:

https://clickhouse.com/docs/zh/engines/database-engines

ck帶副本的分佈式表

帶副本的分佈式表,就是分佈式表,並且單個part也是有副本的,剛開始我們建表時候,也是花了一些時間,回憶下當時的問題主要有以下:

1) 帶副本的分佈式表的創建問題,怎麼創建?

開始我們也是創建錯了,發現數據不完整,每次只有一半,後來得知騰訊雲的服務是帶副本的分佈式集羣,創建表也需要帶副本的分佈式表,不然數據有丟失,建表分2步,語句如下:

-- 第一步:創建本地表,這個表會在每個機器節點上面創建,不要漏了on cluster cluster_name
CREATE TABLE test.table_local on cluster default_cluster
(
    `id` Int32,
    `uid` String,
    `name` String,
    `time` Int32,
    `create_at` DateTime
)
ENGINE = ReplicatedMergeTree()
PARTITION BY toYYYYMM(create_at)
ORDER BY id;

-- 第二步:創建分佈式表
CREATE TABLE test.table_all on cluster default_cluster as test.table_local 
ENGINE = Distributed('default_cluster', 'test', 'table_local', sipHash64(id));

參數說明:

ReplicatedMergeTree:帶副本的表引擎;

PARTITION BY:數據分區方式;

sipHash64(id):分佈式表在每個節點上面的數據分發方式;

具體可以看官方文檔,地址:

https://clickhouse.com/docs/zh/engines/table-engines/mergetree-family/replication

後文,都已這張表爲例。

2)分佈式表,插入數據要每個節點都執行插入操作嗎?

不需要,用標準sql語法插入分佈式表即可,比如:

insert into test.table_all values(.....)

3)分佈式表的更新刪除操作,與mysql相同嗎?

不相同,只能說是相似,按照模板來使用即可,alter table語法:

ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr

比如:

alter table test.table_local on cluster default_cluster update name = 'name' where id = 10000

注意:更新操作,需要用本地表test.table_local,不能用分佈式表。

刪除操作也是一樣的:

alter table test.table_local on cluster default_cluster delete where id = 10000

4)分佈式表,添加列,修改列的類型

-- 添加列
ALTER TABLE test.table_local ON CLUSTER default_cluster ADD COLUMN product String;

-- 修改列
ALTER TABLE test.table_local on cluster default_cluster MODIFY COLUMN uid Int64;

可以看到,ck帶副本的表與標準sql語法的區別在於使用了alter table和on cluster關鍵字,使用時候,套用模板即可。其它的一些DDL操作可以看具體官方文檔:

https://clickhouse.com/docs/zh/sql-reference/statements/alter

寫性能

ck提倡低頻、大批量寫,每秒鐘只寫幾次,每次插入上萬、十萬條數據,這是比較合適的。因爲如果稍微瞭解一下底層原理就知道,ck會間隔合併數據塊,不宜頻繁寫入導致頻繁合併,影響性能。

在使用Flink導入數據的過程中,需要攢數據,批量寫,我們通過Flink窗口函數積累數據,每次寫5秒鐘的一批數據。記得剛開始使用ck的時候,開發沒注意這些,運維就說要批量寫,後來基本就統一了。

添加索引需要注意

ck裏面有一級稀疏索引,和二級跳數索引,二級索引是基於一級索引的,有時候一張表建完了,寫入數據,我們發現查詢需要用到一些字段,需要加索引,語句:

-- 添加索引
alter table test.table_local on cluster default_cluster add index index_uid uid type minmax(100) GRANULARITY 2;

-- 使索引生效,對歷史數據也生效索引
ALTER TABLE test.table_local MATERIALIZE index index_uid;

也是用的alter table格式,這裏需要注意的是,索引是在插入數據時候建立的,新建索引對歷史數據是不生效的,需要讓歷史數據也生效。

數據去重

ReplacingMergeTree引擎表會刪除排序鍵值相同的重複項,排序鍵值就是建表時候跟在order by後面的字段。ck對更新不友好,性能很差,於是可以利用這個引擎,每次只管寫入,不需要更新,ck會自動幫我們保存最新版本。建表語句如下:

CREATE TABLE test.test_local on cluster default_cluster (
    `id` UInt64,
    `type` Int32,
    `username` String,
    `password` String,
    `phone` String COMMENT '手機號賬戶',
    `nick` String,
    `mobile` String,
    `insert_time` DateTime DEFAULT '2023-07-31 00:00:00'
) ENGINE = ReplicatedReplacingMergeTree()
partition by dt
order by id;

CREATE TABLE test.test_all on cluster default_cluster as test.test_local ENGINE = Distributed('default_cluster', 'test', 'test_local', sipHash64(id));

insert_time字段需要有,放在最後,便於ck根據時候保留最新數據。

數據的去重只會在數據合併期間進行。合併會在後臺一個不確定的時間進行,因此你無法預先作出計劃。有一些數據可能仍未被處理。通常使用OPTIMIZE 語句手動觸發,比如今天程序異常停止了,我啓動了程序, 大概率會有多個版本數據,這個時候需要手動合併一下:

OPTIMIZE table test.test_local on cluster default_cluster final;

這樣會觸發數據合併,這個過程耗費性能,正常情況下,如果沒有多版本數據,不需要觸發合併。如果沒有觸發,查詢數據時候,會有多個版本,需要final關鍵字,查詢時候合併一下,如果查詢很多,將非常耗費性能,這個時候可以選擇定期合併。

select * from test.test_all final where id = 10000

對於這種多個版本的表,有時候也是可以避開final的,比如去重,可以select distinct id from table,而不需要select distinct id from table final,這個final是可以省的,等等。

分佈式表的刪除

需要刪除本地表和分佈式表:

drop table test.test_local on cluster default_cluster;
drop table test.test_all on cluster default_cluster;

複雜數據類型map

有些業務數據某個字段是json類型,並且key數量不定,這個時候需要將其在ck裏面定義爲map類型,包容性好。

CREATE TABLE test.test_local2 on cluster default_cluster (
    `id` UInt64,
    `data` Map(String, String),
) ENGINE = ReplicatedMergeTree()
partition by dt
order by id;

CREATE TABLE test.test_all2 on cluster default_cluster as test.test_local2 ENGINE = Distributed('default_cluster', 'test', 'test_local2', sipHash64(id));

data Map(String, String) :這就定義了一個map類型字段,查詢時候可以這樣查:

select data['key'] from test.test_all2 limit 5

對於json,ck也是可以解的。

select JSONExtractRaw('{"a": "hello", "b": [-100, 200.0, 300]}', 'b')
-- 結果
'[-100, 200.0, 300]'

select JSONExtractString('{"a": "hello", "b": [-100, 200.0, 300]}', 'a')
-- 結果
'hello'

更詳細官方文檔:https://clickhouse.com/docs/zh/sql-reference/functions/json-functions

關於成本

相比較而言,ck是能節省成本的,運維是這麼說的。經常擴容磁盤,而計算性能只擴容了一次。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章