海量數據存儲ClickHouse

ClickHouse介紹

ClickHouse的由來和應用場景

docker安裝

docker run -d --name ybchen_clickhouse --ulimit nofile=262144:262144 \
-p 8123:8123 -p 9000:9000 -p 9009:9009 --privileged=true \
-v /mydata/docker/clickhouse/log:/var/log/clickhouse-server \
-v /mydata/docker/clickhouse/data:/var/lib/clickhouse clickhouse/clickhouse-server:22.2.3.5
  • 默認http埠是8123,tcp埠是9000, 同步埠9009

  • 進入容器內部查看

  • web可視化介面://ip:8123/play

    • 命令

      • 查看資料庫 SHOW DATABASES
      • 查看某個庫下面的全部表 SHOW TABLES IN system
      • 系統資料庫是 ClickHouse 存儲有關 ClickHouse 部署的詳細資訊的地方
    • 默認資料庫最初為空,用於執行未指定資料庫的命令

可視化工具

創建庫

CREATE DATABASE shop

創建表

CREATE TABLE shop.clickstream (
    customer_id String, 
    time_stamp Date, 
    click_event_type String,
    page_code FixedString(20),  
    source_id UInt64
) 
ENGINE = MergeTree()
ORDER BY (time_stamp)
  • ClickHouse 有自己的數據類型,每個表都必須指定一個Engine引擎屬性來確定要創建的表的類型
  • 引擎決定了數據的存儲方式和存儲位置、支援哪些查詢、對並發的支援

插入數據

INSERT INTO shop.clickstream
VALUES ('customer1', '2021-10-02', 'add_to_cart', 'home_enter', 568239 ) 

查詢數據

SELECT * FROM shop.clickstream
SELECT * FROM shop.clickstream WHERE time_stamp >= '2001-11-01'

數據類型

數值類型(整形,浮點數,定點數)

整型

  • 固定長度的整型,包括有符號整型或無符號整型 IntX X是位的意思,1Byte位元組=8bit位
有符號整型範圍
Int8 — [-128 : 127]

Int16 — [-32768 : 32767]

Int32 — [-2147483648 : 2147483647]

Int64 — [-9223372036854775808 : 9223372036854775807]

Int128 — [-170141183460469231731687303715884105728 : 170141183460469231731687303715884105727]

Int256 — [-57896044618658097711785492504343953926634992332820282019728792003956564819968 : 57896044618658097711785492504343953926634992332820282019728792003956564819967]

無符號整型範圍
UInt8 — [0 : 255]

UInt16 — [0 : 65535]

UInt32 — [0 : 4294967295]

UInt64 — [0 : 18446744073709551615]

UInt128 — [0 : 340282366920938463463374607431768211455]

UInt256 — [0 : 115792089237316195423570985008687907853269984665640564039457584007913129639935]

浮點型(存在精度損失問題)

  • 建議儘可能以整型形式存儲數據
  • Float32 – mysql裡面的float類型
  • Float64 – mysql裡面的double類型

Decimal類型

  • 需要要求更高的精度的數值運算,則需要使用定點數

  • 一般金額欄位、匯率、利率等欄位為了保證小數點精度,都使用 Decimal

  • Clickhouse提供了Decimal32,Decimal64,Decimal128三種精度的定點數

    • 用Decimal(P,S)來定義:

      • P代表精度(Precise),表示總位數(整數部分 + 小數部分)
      • S代表規模(Scale),表示小數位數
    • 例子:Decimal(10,2) 小數部分2位,整數部分 8位(10-2)

    • 也可以使用Decimal32(S)、Decimal64(S)和Decimal128(S)的方式來表示

CREATE TABLE shop.clickstream1 (
    customer_id String, 
    time_stamp Date, 
    click_event_type String,
    page_code FixedString(20),  
    source_id UInt64,
    money Decimal(2,1)
) 
ENGINE = MergeTree()
ORDER BY (time_stamp)


INSERT INTO shop.clickstream1
VALUES ('customer1', '2021-10-02', 'add_to_cart', 'home_enter', 568239,2.11 )

字元串類型

UUID

  • 通用唯一標識符(UUID)是由一組32位數的16進位數字所構成,用於標識記錄
61f0c404-5cb3-11e7-907b-a6006ad3dba0
  • 要生成UUID值,ClickHouse提供了 generateuidv4 函數。

  • 如果在插入新記錄時未指定UUID列的值,則UUID值將用零填充

00000000-0000-0000-0000-000000000000
  • 建表和插入例子
CREATE TABLE t_uuid (x UUID, y String) ENGINE=TinyLog

INSERT INTO t_uuid SELECT generateUUIDv4(), 'Example 1'

FixedString固定字元串類型(相對少用)

  • 類似MySQL的Char類型,屬於定長字元,固定長度 N 的字元串(N 必須是嚴格的正自然數)

  • 如果字元串包含的位元組數少於`N』,將對字元串末尾進行空位元組填充。

  • 如果字元串包含的位元組數大於N,將拋出Too large value for FixedString(N)異常。

  • 當數據的長度恰好為N個位元組時,FixedString類型是高效的,在其他情況下,這可能會降低效率

    • 應用場景

      • ip地址二進位表示的IP地址
      • 語言程式碼(ru_RU, en_US … )
      • 貨幣程式碼(USD, RUB … 

String 字元串類型

  • 字元串可以任意長度的。它可以包含任意的位元組集,包含空位元組
  • 字元串類型可以代替其他 DBMSs中的 VARCHAR、BLOB、CLOB 等類型
  • ClickHouse 沒有編碼的概念,字元串可以是任意的位元組集,按它們原本的方式進行存儲和輸出

時間類型

  • Date

    • 日期類型,用兩個位元組存儲,表示從 1970-01-01 (無符號) 到當前的日期值,支援字元串形式寫入
    • 上限是2106年,但最終完全支援的年份為2105
  • DateTime

    • 時間戳類型。用四個位元組(無符號的)存儲 Unix 時間戳,支援字元串形式寫入
    • 時間戳類型值精確到秒
    • 值的範圍: [1970-01-01 00:00:00, 2106-02-07 06:28:15]
  • DateTime64

    • 此類型允許以日期(date)加時間(time)的形式來存儲一個時刻的時間值,具有定義的亞秒精度
    • 值的範圍: [1925-01-01 00:00:00, 2283-11-11 23:59:59.99999999] (注意: 最大值的精度是8)

枚舉類型

  • 包括 Enum8 和 Enum16 類型,Enum 保存 'string'= integer 的對應關係

  • 在 ClickHouse 中,儘管用戶使用的是字元串常量,但所有含有 Enum 數據類型的操作都是按照包含整數的值來執行。這在性能方面比使用 String 數據類型更有效。

    • Enum8 用 'String'= Int8 對描述。
    • Enum16 用 'String'= Int16 對描述。
  • 創建一個帶有一個枚舉 Enum8('home' = 1, 'detail' = 2, 'pay'=3) 類型的列

CREATE TABLE t_enum
(
    page_code Enum8('home' = 1, 'detail' = 2,'pay'=3)
)
ENGINE = TinyLog
  • 插入, page_code 這列只能存儲類型定義中列出的值:'home'或`’detail’ 或 ‘pay’。如果您嘗試保存任何其他值,ClickHouse 拋出異常
#插入成功
INSERT INTO t_enum VALUES ('home'), ('detail')

#插入報錯
INSERT INTO t_enum VALUES ('home1')

#查詢
SELECT * FROM t_enum

布爾值

  • 舊版以前沒有單獨的類型來存儲布爾值。可以使用 UInt8 類型,取值限制為 0 或 1
  • 新增裡面新增了Bool
CREATE TABLE shop.clickstream2 (
    customer_id String, 
    time_stamp Date, 
    click_event_type String,
    page_code FixedString(20),  
    source_id UInt64,
    money Decimal(2,1),
    is_new Bool
) 
ENGINE = MergeTree()
ORDER BY (time_stamp)

DESCRIBE shop.clickstream2

INSERT INTO shop.clickstream2
VALUES ('customer1', '2021-10-02', 'add_to_cart', 'home_enter', 568239, 3.8,1)

查看當前版本支援的數據類型

select * from system.data_type_families
  • case_insensitive 選項為1 表示大小寫不敏感,欄位類型不區分大小寫
  • 為0 表示大小寫敏感,即欄位類型需要嚴格區分大小寫
  • 裡面很多數據類型,記住常用的即可

Mysql數據類型對比

ClickHouse Mysql 說明
UInt8 UNSIGNED TINYINT  
Int8 TINYINT  
UInt16 UNSIGNED SMALLINT  
Int16 SMALLINT  
UInt32 UNSIGNED INT, UNSIGNED MEDIUMINT  
Int32 INT, MEDIUMINT  
UInt64 UNSIGNED BIGINT  
Int64 BIGINT  
Float32 FLOAT  
Float64 DOUBLE  
Date DATE  
DateTime DATETIME, TIMESTAMP  
FixedString BINARY  

常見SQL語法和注意事項

官方文檔

創建表

CREATE TABLE shop.clickstream3 (
    customer_id String, 
    time_stamp Date, 
    click_event_type String,
    page_code FixedString(20),  
    source_id UInt64,
    money Decimal(2,1),
    is_new Bool
) 
ENGINE = MergeTree()
ORDER BY (time_stamp)

查看錶結構

DESCRIBE shop.clickstream3

查詢

SELECT * FROM shop.clickstream3

插入

INSERT INTO shop.clickstream3
VALUES ('customer2', '2021-10-02', 'add_to_cart', 'home_enter', 568239,2.1, False ) 

更新和刪除

  • 在OLAP資料庫中,可變數據(Mutable data)通常是不被歡迎的,早期ClickHouse是不支援,後來版本才有

  • 不支援事務,建議批量操作,不要高頻率小數據量更新刪除

  • 刪除和更新是一個非同步操作的過程,語句提交立刻返回,但不一定已經完成了

    • 判斷是否完成
SELECT database, table, command, create_time, is_done FROM system.mutations LIMIT 20

更新

ALTER TABLE shop.clickstream3 UPDATE  click_event_type = 'pay' where customer_id = 'customer2'; 

刪除

ALTER TABLE shop.clickstream3 delete where customer_id = 'customer2'; 

分片-分區-副本

什麼是ClickHouse的分區

  • 分區是表的分區,把一張表的數據分成N多個區塊,分區後的表還是一張表,數據處理還是由自己來完成

  • PARTITION BY,指的是一個表按照某一列數據(比如日期)進行分區,不同分區的數據會寫入不同的文件中

  • 建表時加入partition概念,可以按照對應的分區欄位,允許查詢在指定了分區鍵的條件下,儘可能的少讀取數據

create table shop.order_merge_tree( 
    id UInt32,
    sku_id String,
    out_trade_no String,
    total_amount Decimal(16,2), 
    create_time Datetime
) engine =MergeTree()
  partition by toYYYYMMDD(create_time) 
  order by (id,sku_id)
  primary key (id);

注意

  • 不是所有的表引擎都可以分區,合併樹(MergeTree) 系列的表引擎才支援數據分區,Log系列引擎不支援

什麼是ClickHouse的分片

  • Shard 分片是把資料庫橫向擴展(Scale Out)到多個物理節點上的一種有效的方式
  • 復用了資料庫的分區概念,相當於在原有的分區下作為第二層分區,ClickHouse會將數據分為多個分片,並且分布到不同節點上,再通過 Distributed 表引擎把數據拼接起來一同使用
  • Sharding機制使得ClickHouse可以橫向線性拓展,構建大規模分散式集群,但需要避免數據傾斜問題

什麼是ClickHouse的副本

  • 兩個相同數據的表, 作用是為了數據備份與安全,保障數據的高可用性,
  • 即使一台 ClickHouse 節點宕機,那麼也可以從其他伺服器獲得相同的數據
  • 類似Mysql主從架構,主節點宕機,從節點也能提供服務

總結

  • 數據分區-允許查詢在指定了分區鍵的條件下,儘可能的少讀取數據
  • 數據分片-允許多台機器/節點同並行執行查詢,實現了分散式並行計算

ClickHouse常見引擎

Log系列

  • 最小功能的輕量級引擎,當需要快速寫入許多小表並在以後整體讀取它們時效果最佳,一次寫入多次查詢

  • 種類

    • TinyLog、StripLog、Log

MergeTree系列

  • CLickhouse最強大的表引擎,有多個不同的種類

  • 適用於高負載任務的最通用和功能最強大的表引擎,可以快速插入數據並進行後續的後台數據處理

  • 支援主鍵索引、數據分區、數據副本等功能特性和一些其他引擎不支援的其他功能

  • 種類

    • MergeTree、ReplacingMergeTree
    • SummingMergeTree、AggregatingMergeTree
    • CollapsingMergeTree、VersionedCollapsingMergeTree、GraphiteMergeTree

外部存儲引擎系列

  • 能夠直接從其它的存儲系統讀取數據,例如直接讀取 HDFS 的文件或者 MySQL 資料庫的表,這些表引擎只負責元數據管理和數據查詢

  • 種類

    • HDFS、Mysql
    • Kafka、JDBC

其他特定引擎

  • Memory

    • 原生數據直接存儲記憶體,性能高,重啟則消失,讀寫不會阻塞,不支援索引,主要是測試使用
  • Distributed

    • 分散式引擎本身不存儲數據, 但可以在多個伺服器上進行分散式查詢。 讀是自動並行的。讀取時,遠程伺服器表的索引(如果有的話)會被使用
  • File

    • 數據源是以 Clickhouse 支援的一種輸入格式(TabSeparated,Native等)存儲數據的文件
    • 從 ClickHouse 導出數據到文件,將數據從一種格式轉換為另一種格式
  • Merge

    • Merge 引擎 (不要跟 MergeTree 引擎混淆) 本身不存儲數據,但可用於同時從任意多個其他的表中讀取數據。讀是自動並行的,不支援寫入,讀取時,那些被真正讀取到數據的表的索引(如果有的話)會被使用
  • Set、Buffer、Dictionary等

ClickHouse的MergeTree表引擎

特點

  • ClickHouse 不要求主鍵唯一,所以可以插入多條具有相同主鍵的行。

  • 如果指定了【分區鍵】則可以使用【分區】,可以通過PARTITION 語句指定分區欄位,合理使用數據分區,可以有效減少查詢時數據文件的掃描範圍

  • 在相同數據集的情況下 ClickHouse 中某些帶分區的操作會比普通操作更快,查詢中指定了分區鍵時 ClickHouse 會自動截取分區數據,這也有效增加了查詢性能

  • 支援數據副本和數據取樣

    • 副本是表級別的不是整個伺服器級的,所以伺服器里可以同時有複製表和非複製表。
    • 副本不依賴分片,每個分片有它自己的獨立副本

語法 

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 (CounterID, EventDate) 。如果沒有使用 PRIMARY KEY 顯式指定的主鍵,ClickHouse 會使用排序鍵作為主鍵
    • 如果不需要排序,可以使用 ORDER BY tuple()
  • 【選填】PARTITION BY — 分區鍵 ,可選項

    • 要按月分區,可以使用表達式 toYYYYMM(date_column) ,這裡的 date_column 是一個 Date 類型的列, 分區名的格式會是 "YYYYMM"
    • 分區的好處是降低掃描範圍提升速度,不填寫默認就使用一個分區
  • 【選填】PRIMARY KEY -主鍵,作為數據的一級索引,但是不是唯一約束,和其他資料庫區分

    • 如果要 選擇與排序鍵不同的主鍵,在這裡指定,可選項,
    • 默認情況下主鍵跟排序鍵(由 ORDER BY 子句指定)相同。
    • 大部分情況下不需要再專門指定一個 PRIMARY KEY
  • PRIMARY KEY 主鍵必須是 order by 欄位的前綴欄位。

    • 主鍵和排序欄位這兩個屬性只設置一個時,另一個默認與它相同, 當兩個都設置時,PRIMARY KEY必須為ORDER BY的前綴
    • 比如ORDER BY (CounterID, EventDate),那主鍵需要是(CounterID )或 (CounterID, EventDate)

合併樹MergeTree實戰

建表

create table shop.order_merge_tree( 
    id UInt32,
    sku_id String,
    out_trade_no String,
    total_amount Decimal(16,2), 
    create_time Datetime
) engine =MergeTree()
  order by (id,sku_id)
  partition by toYYYYMMDD(create_time) 
  primary key (id);
insert into shop.order_merge_tree values 
(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(4,'sku_1','54222',2000.3,'2023-04-01 19:00:00'), 
(5,'sku_2','53423',120.2,'2023-04-01 19:00:00'), 
(6,'sku_4','65432',600.01,'2023-04-02 11:00:00');

進入容器

docker exec -it 35dad2d981d5 /bin/bash

分區合併驗證

新的數據寫入會有臨時分區產生,不之間加入已有分區

寫入完成後經過一定時間(10到15分鐘),ClickHouse會自動化執行合併操作,將臨時分區的數據合併到已有分區當中

optimize的合併操作是在後台執行的,無法預測具體執行時間點,除非是手動執行

通過手工合併( optimize table xxx final; ) 

在數據量比較大的情況,盡量不要使用該命令,執行optimize要消耗大量時間
create table shop.order_merge_tree( 
    id UInt32,
    sku_id String,
    out_trade_no String,
    total_amount Decimal(16,2), 
    create_time Datetime
) engine =MergeTree()
  order by (id,sku_id)
  partition by toYYYYMMDD(create_time) 
  primary key (id);




insert into shop.order_merge_tree values 
(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(4,'sku_1','54222',2000.3,'2023-04-01 19:00:00'), 
(5,'sku_2','53423',120.2,'2023-04-01 19:00:00'), 
(6,'sku_4','65432',600.01,'2023-04-02 11:00:00');

docker exec -it 35dad2d981d5 /bin/bash


 clickhouse-client

 optimize table shop.order_merge_tree final;

不使用分區演示

create table shop.order_merge_tree22( 
    id UInt32,
    sku_id String,
    out_trade_no String,
    total_amount Decimal(16,2), 
    create_time Datetime
) engine =MergeTree()
  order by (id,sku_id)

  primary key (id);
  
 insert into shop.order_merge_tree22 values 
(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(4,'sku_1','54222',2000.3,'2023-04-01 19:00:00'), 
(5,'sku_2','53423',120.2,'2023-04-01 19:00:00'), 
(6,'sku_4','65432',600.01,'2023-04-02 11:00:00');

總結

  • 使用過分區鍵,會通過分區鍵,將數據落到不同分區中,提升查詢效率
  • 沒使用過分區鍵,數據全部落一個分區中

合併樹ReplacingMergeTree實戰

介紹

  • MergeTree的拓展,該引擎和 MergeTree 的不同之處在它會刪除【排序鍵值】相同重複項,根據OrderBy欄位

  • 數據的去重只會在數據合併期間進行。合併會在後台一個不確定的時間進行,因此你無法預先作出計劃。

  • 有一些數據可能仍未被處理,儘管可以調用 OPTIMIZE 語句發起計劃外的合併,但請不要依靠它,因為 OPTIMIZE 語句會引發對數據的大量讀寫

  • 因此,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]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

參數

  • ver — 版本列。類型為 UInt*Date 或 DateTime。可選參數。

    在數據合併的時候,ReplacingMergeTree 從所有具有相同排序鍵的行中選擇一行留下:

    • 如果 ver 列未指定,保留最後一條。
    • 如果 ver 列已指定,保留 ver 值最大的版本
  • 如何判斷數據重複

    • 在去除重複數據時,是以ORDER BY排序鍵為基準的,而不是PRIMARY KEY
    • 若排序欄位為兩個,則兩個欄位都相同時才會去重
  • 何時刪除重複數據

    • 在執行分區合併時觸發刪除重複數據,optimize的合併操作是在後台執行的,無法預測具體執行時間點,除非是手動執行
  • 不同分區的重複數據不會被去重

    • ReplacingMergeTree是以分區為單位刪除重複數據的,在相同的數據分區內重複的數據才會被刪除,而不同數據分區之間的重複數據依然不能被刪除的
  • 刪除策略

    • ReplacingMergeTree() 填入的參數為版本欄位,重複數據就會保留版本欄位值最大的。
    • 如果不填寫版本欄位,默認保留插入順序的最後一條數據

建表

  • ver表示的列只能是UInt*,Date和DateTime 類型

  • 刪除策略

    • ReplacingMergeTree() 填入的參數為版本欄位,重複數據就會保留版本欄位值最大的。
    • 如果不填寫版本欄位,默認保留插入順序的最後一條數據
create table shop.order_relace_merge_tree( 
    id UInt32,
    sku_id String,
    out_trade_no String,
    total_amount Decimal(16,2), 
    create_time Datetime
) engine =ReplacingMergeTree(id)
  order by (sku_id)
  partition by toYYYYMMDD(create_time) 
  primary key (sku_id);
insert into shop.order_relace_merge_tree values 
(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(4,'sku_5','54222',2000.3,'2023-04-01 19:00:00'), 
(5,'sku_6','53423',120.2,'2023-04-01 19:00:00'), 
(6,'sku_7','65432',600.01,'2023-04-02 11:00:00');


insert into shop.order_relace_merge_tree values 
(11,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(21,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(31,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(41,'sku_5','54222',2000.3,'2023-04-01 19:00:00'), 
(51,'sku_8','53423',120.2,'2023-04-01 19:00:00'), 
(61,'sku_9','65432',600.01,'2023-04-02 11:00:00');
docker exec -it 35dad2d981d5 /bin/bash


 clickhouse-client

SELECT  * FROM  shop.order_relace_merge_tree

 optimize table shop.order_relace_merge_tree final;

演示

create table shop.order_relace_merge_tree( 
    id UInt32,
    sku_id String,
    out_trade_no String,
    total_amount Decimal(16,2), 
    create_time Datetime
) engine =ReplacingMergeTree(id)
  order by (sku_id)
  partition by toYYYYMMDD(create_time) 
  primary key (sku_id);

insert into shop.order_relace_merge_tree values 
(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(4,'sku_5','54222',2000.3,'2023-04-01 19:00:00'), 
(5,'sku_6','53423',120.2,'2023-04-01 19:00:00'), 
(6,'sku_7','65432',600.01,'2023-04-02 11:00:00');


insert into shop.order_relace_merge_tree values 
(11,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(21,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(31,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(41,'sku_5','54222',2000.3,'2023-04-01 19:00:00'), 
(51,'sku_8','53423',120.2,'2023-04-01 19:00:00'), 
(61,'sku_9','65432',600.01,'2023-04-02 11:00:00');

docker exec -it 35dad2d981d5 /bin/bash


 clickhouse-client

SELECT  * FROM  shop.order_relace_merge_tree

 optimize table shop.order_relace_merge_tree final;

合併樹SummingMergeTree實戰

介紹

  • 該引擎繼承自 MergeTree,區別在於,當合併 SummingMergeTree 表的數據片段時,ClickHouse 會把所有具有 相同OrderBy排序鍵 的行合併為一行,該行包含了被合併的行中具有數值類型的列的匯總值。

  • 類似group by的效果,這個可以顯著的減少存儲空間並加快數據查詢的速度

  • 推薦將該引擎和 MergeTree 一起使用。例如在準備做數據報表的時候,將完整的數據存儲在 MergeTree 表中,並且使用 SummingMergeTree 來存儲聚合數據,可以使避免因為使用不正確的 排序健組合方式而丟失有價值的數據

  • 只需要查詢匯總結果,不關心明細數據

  • 設計聚合統計表,欄位全部是維度、度量或者時間戳即可,非相關的欄位可以不添加

  • 獲取匯總值,不能直接 select 對應的欄位,而需要使用 sum 進行聚合,因為自動合併的部分可能沒進行,會導致一些還沒來得及聚合的臨時明細數據少

語法

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = SummingMergeTree([columns])
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

參數

  • columns 包含了將要被匯總的列的列名的元組。可選參數。
  • 所選的【列必須是數值類型】,具有 相同OrderBy排序鍵 的行合併為一行
  • 如果沒有指定 columns,ClickHouse 會把非維度列且是【數值類型的列】都進行匯總

建表

create table shop.order_summing_merge_tree( 
    id UInt32,
    sku_id String,
    out_trade_no String,
    total_amount Decimal(16,2), 
    create_time Datetime
) engine =SummingMergeTree(total_amount)
  order by (id,sku_id)
  partition by toYYYYMMDD(create_time) 
  primary key (id);
insert into shop.order_summing_merge_tree values 
(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'), 
(4,'sku_5','54222',2000.3,'2023-04-01 19:00:00'), 
(5,'sku_6','53423',120.2,'2023-04-01 19:00:00'), 
(6,'sku_7','65432',600.01,'2023-04-02 11:00:00');


insert into shop.order_summing_merge_tree values 
(1,'sku_1','aabbccbb',5600.00,'2023-03-01 23:09:00')
select sku_id,sum(total_amount) from shop.order_summing_merge_tree group by sku_id
 optimize table shop.order_summing_merge_tree final;

演示

總結

  • SummingMergeTree是根據什麼對數據進行合併的

    • 【ORBER BY排序鍵相同】作為聚合數據的條件Key的行中的列進行匯總,將這些行替換為包含匯總數據的一行記錄
  • ** 跨分區內的相同排序key的數據是否會進行合併**

    • 以數據分區為單位來聚合數據,同一數據分區內相同ORBER BY排序鍵的數據會被合併匯總,而不同分區之間的數據不會被匯總
  • 如果沒有指定聚合欄位,會怎麼聚合

    • 如果沒有指定聚合欄位,則會用非維度列,且是數值類型欄位進行聚合
  • 對於非匯總欄位的數據,該保留哪一條

    • 如果兩行數據除了【ORBER BY排序鍵】相同,其他的非聚合欄位不相同,在聚合時會【保留最初】的那條數據,新插入的數據對應的那個欄位值會被捨棄
  • 在合併分區的時候按照預先定義的條件聚合匯總數據,將同一分區下的【相同排序】的多行數據匯總合併成一行,既減少了數據行節省空間,又降低了後續匯總查詢的開銷

Clickhouse高可用集群搭建

部署zookeeper

  • 副本同步需要藉助zookeeper實現數據的同步, 副本節點會在zk上進行監聽,但數據同步過程是不經過zk的
  • zookeeper要求3.4.5以及以上版本
docker run -d --name ybchen_zookeeper -p 2181:2181 -t zookeeper:3.7.0

部署Clickhouse

機器 公網 私網 hostname
機器-1 47.116.143.16 172.16.0.103 ybchen-1
機器-2 101.132.27.2 172.16.0.108 ybchen-2

Linux機器安裝ClickHouse,版本:ClickHouse 22.1.2.2-2

文檔地址://clickhouse.com/docs/zh/getting-started/install

#各個節點上傳到新建文件夾
/usr/local/software/*

#安裝
sudo rpm -ivh *.rpm

#啟動
systemctl start clickhouse-server

#停止
systemctl stop clickhouse-server

#重啟
systemctl restart clickhouse-server

#狀態查看
sudo systemctl status clickhouse-server

#查看埠佔用,如果命令不存在 yum install -y lsof
lsof -i :8123


#查看日誌 
tail -f /var/log/clickhouse-server/clickhouse-server.log
tail -f /var/log/clickhouse-server/clickhouse-server.err.log


#開啟遠程訪問,取消下面的注釋
vim /etc/clickhouse-server/config.xml

#編輯配置文件
<listen_host>0.0.0.0</listen_host>

#重啟
systemctl restart clickhouse-server


# 增加dns解析
sudo vim /etc/hosts

172.16.0.103    ybchen-1
172.16.0.108    ybchen-2
  • 網路安全組記得開放http埠是8123,tcp埠是9000, 同步埠9009

高可用集群架構-ClickHouse副本配置

#進入配置目錄
cd /etc/clickhouse-server

#編輯配置文件
sudo vim config.xml

#找到zookeeper節點,增加下面的,如果有多個zk則按照順序加即可

<zookeeper>
          <node>
                <host>172.16.0.103</host>
                <port>2181</port>
          </node>
</zookeeper>


#重啟
systemctl restart clickhouse-server

########建表#######
#節點一,zk路徑一致,副本名稱不一樣
CREATE TABLE tb_product
(
    userId UInt32
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/1/tb_product', 'product-replica-1')
ORDER BY (userId)

#節點二,zk路徑一致,副本名稱不一樣
CREATE TABLE tb_product
(
    userId UInt32
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/1/tb_product', 'product-replica-2')
ORDER BY (userId)

注意
  副本只能同步數據,不能同步表結構,需要在每台機器上手動建表
#插入和查詢數據
INSERT into tb_product values(1),(2),(3)

select * from tb_product

#查詢zk配置
select * from system.zookeeper where path='/'

表引擎-數據副本

ReplicatedMergeTree 

語法 

  • zoo_path — zk 中該表的路徑,可自定義名稱,同一張表的同一分片的不同副本,要定義相同的路徑
  • replica_name —  zk 中的該表的副本名稱,同一張表的同一分片的不同副本,要定義不同的名稱
CREATE TABLE tb_order
(
    EventDate DateTime,
    CounterID UInt32,
    UserID UInt32
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/tb_order', 'tb_order_01')
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID))
SAMPLE BY intHash32(UserID)

表引擎-分散式引擎

語法

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = Distributed(cluster, database, table[, sharding_key[, policy_name]])
[SETTINGS name=value, ...]
  • cluster:集群名稱,與集群配置中的自定義名稱相對應,比如 xdclass_shard。

  • database:資料庫名稱

  • table:本地表名稱

  • sharding_key:可選參數,用於分片的key值,在寫入的數據Distributed表引擎會依據分片key的規則,將數據分布到各個節點的本地表

    • user_id等業務欄位、rand()隨機函數等規則

分片配置 config.xml

<remote_servers>
    <logs>
        <!-- 分散式查詢的伺服器間集群密碼
             默認值:無密碼(將不執行身份驗證)

             如果設置了,那麼分散式查詢將在分片上驗證,所以至少:
             - 這樣的集群應該存在於shard上
             - 這樣的集群應該有相同的密碼。

             而且(這是更重要的),initial_user將作為查詢的當前用戶使用。
        -->
        <!-- <secret></secret> -->
        <shard>
            <!-- 可選的。寫數據時分片權重。 默認: 1. -->
            <weight>1</weight>
            <!-- 可選的。是否只將數據寫入其中一個副本。默認值:false(將數據寫入所有副本)。 -->
            <internal_replication>false</internal_replication>
            <replica>
                <!-- 可選的。負載均衡副本的優先順序,請參見(load_balancing 設置)。默認值:1(值越小優先順序越高)。 -->
                <priority>1</priority>
                <host>example01-01-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-01-2</host>
                <port>9000</port>
            </replica>
        </shard>
        <shard>
            <weight>2</weight>
            <internal_replication>false</internal_replication>
            <replica>
                <host>example01-02-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-02-2</host>
                <secure>1</secure>
                <port>9440</port>
            </replica>
        </shard>
    </logs>
</remote_servers>

這裡定義了一個名為』logs』的集群,它由兩個分片組成,每個分片包含兩個副本。 分片是指包含數據不同部分的伺服器(要讀取所有數據,必須訪問所有分片)。 副本是存儲複製數據的伺服器(要讀取所有數據,訪問任一副本上的數據即可)。

集群名稱不能包含點號。

每個伺服器需要指定 hostport,和可選的 userpasswordsecurecompression 的參數:

  • host – 遠程伺服器地址。可以域名、IPv4或IPv6。如果指定域名,則服務在啟動時發起一個 DNS 請求,並且請求結果會在伺服器運行期間一直被記錄。如果 DNS 請求失敗,則服務不會啟動。如果你修改了 DNS 記錄,則需要重啟服務。
  • port – 消息傳遞的 TCP 埠(「tcp_port」配置通常設為 9000)。不要跟 http_port 混淆。
  • user – 用於連接遠程伺服器的用戶名。默認值:default。該用戶必須有許可權訪問該遠程伺服器。訪問許可權配置在 users.xml 文件中。更多資訊,請查看«訪問許可權»部分。
  • password – 用於連接遠程伺服器的密碼。默認值:空字元串。
  • secure – 是否使用ssl進行連接,設為true時,通常也應該設置 port = 9440。伺服器也要監聽 <tcp_port_secure>9440</tcp_port_secure> 並有正確的證書。
  • compression – 是否使用數據壓縮。默認值:true。

配置實戰

機器

服務類型 公網 私網 hostname
clickhouse-1 47.116.143.16 172.16.0.103 ybchen-1
clickhouse-2 101.132.27.2 172.16.0.108 ybchen-2
zookeeper 47.116.143.16 172.16.0.103 ybchen-1

每台機器上的配置

#進入配置目錄
cd /etc/clickhouse-server

#編輯配置文件
sudo vim config.xml

<!-- 2shard 1replica -->
    <cluster_2shards_1replicas>
        <!-- shard1  -->
        <shard>
            <replica>
                <host>172.16.0.103</host>
                <port>9000</port>
            </replica>
        </shard>
        
        <!-- shard2  -->
        <shard>
            <replica>
                <host>172.16.0.108</host>
                <port> 9000</port>
            </replica>
        </shard>
        
    </cluster_2shards_1replicas>


    <zookeeper>
          <node >
                <host>172.16.0.103</host>
                <port>2181</port>
          </node>
    </zookeeper>

重啟 

systemctl restart clickhouse-server

判斷是否配置成功

  • 重啟ClickHouse前查詢,查不到對應的集群名稱,重啟ClickHouse後能查詢到
select * from system.clusters

建表

 

#【選一個節點】創建好本地表後,在1個節點創建,會在其他節點都存在
create table default.ybchen_order on cluster cluster_2shards_1replicas
(id Int8,name String) engine =MergeTree order by id;


#【選一個節點】創建分散式表名 ybchen_order_all,在1個節點創建,會在其他節點都存在
create table ybchen_order_all on cluster cluster_2shards_1replicas (
id Int8,name String
)engine = Distributed(cluster_2shards_1replicas,default, ybchen_order,hiveHash(id));

#分散式表插入
insert into ybchen_order_all values(1,'老陳'),(2,'老王'),(3,'老李'),(4,'老趙');

#【任意節點查詢-分散式,全部數據】
SELECT * from ybchen_order_all

#【任意本地節點查詢,部分數據】
SELECT * from ybchen_order

SpringBoot整合Clickhouse 

數據準備

CREATE TABLE default.visit_stats
(
    `product_id` UInt64,
    `is_new` UInt16,
    `province` String,
    `city` String,
    `pv` UInt32,
    `visit_time` DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(visit_time)
ORDER BY (
 product_id,
 is_new,
 province,
 city
 );

創建工程

 

添加依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ybchen</groupId>
    <artifactId>ybchen-clickhouse</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ybchen-clickhouse</name>
    <description>SpringBoot整合Clickhouse</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- clickhouse -->
        <dependency>
            <groupId>ru.yandex.clickhouse</groupId>
            <artifactId>clickhouse-jdbc</artifactId>
            <version>0.3.2</version>
        </dependency>


        <!--mybatis plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

配置類

server.port=8888
spring.datasource.driver-class-name=ru.yandex.clickhouse.ClickHouseDriver
spring.datasource.url=jdbc:clickhouse://47.116.143.16:8123/default
# 帳號
#spring.datasource.username=
# 密碼
#spring.datasource.password=

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
logging.level.root=INFO

實體類

package com.ybchen.model;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @Description:
 * @Author:chenyanbin
 * @CreateTime: 2022-05-14 21:51
 * @Version:1.0.0
 */
@Data
@TableName("visit_stats")
public class VisitStatsDO {
    /**
     * 商品
     */
    private Long productId;

    /**
     * 1是新訪客,0是老訪客
     */
    private Integer isNew;

    /**
     * 省份
     */
    private String province;

    /**
     * 城市
     */
    private String city;

    /**
     * 訪問量
     */
    private Integer pv;

    /**
     * 訪問時間
     */
    private String visitTime;
}

Mapper

package com.ybchen.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ybchen.model.VisitStatsDO;

public interface VisitStatsMapper extends BaseMapper<VisitStatsDO> {
}

Controller

package com.ybchen.controller;

import com.ybchen.request.VisitRecordListRequest;
import com.ybchen.service.VisitService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @Description:
 * @Author:chenyanbin
 * @CreateTime: 2022-05-14 21:53
 * @Version:1.0.0
 */
@RestController
@RequestMapping("/api/v1/data")
public class DataController {
    @Autowired
    VisitService visitService;

    @PostMapping("list")
    public Map<String,Object> queryVisitRecordList(
            @RequestBody VisitRecordListRequest request
    ) {
        return visitService.queryVisitRecordList(request);
    }
}
package com.ybchen.request;

import lombok.Data;

/**
 * @Description:
 * @Author:chenyanbin
 * @CreateTime: 2022-05-14 21:54
 * @Version:1.0.0
 */
@Data
public class VisitRecordListRequest {
    private Long productId;
    private int page;
    private int size;
}

Service

package com.ybchen.service;

import com.ybchen.request.VisitRecordListRequest;

import java.util.Map;

public interface VisitService {
    /**
     * 查詢訪問記錄
     * @param request
     * @return
     */
    Map<String, Object> queryVisitRecordList(VisitRecordListRequest request);
}
package com.ybchen.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ybchen.mapper.VisitStatsMapper;
import com.ybchen.model.VisitStatsDO;
import com.ybchen.request.VisitRecordListRequest;
import com.ybchen.service.VisitService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description:
 * @Author:chenyanbin
 * @CreateTime: 2022-05-14 21:55
 * @Version:1.0.0
 */
@Service
@Slf4j
public class VisitServiceImpl implements VisitService {
    @Autowired
    VisitStatsMapper visitStatsMapper;

    @Override
    public Map<String, Object> queryVisitRecordList(VisitRecordListRequest request) {
        Map<String, Object> dataMap = new HashMap<>(3);
        IPage<VisitStatsDO> pageInfo = new Page<>(request.getPage(), request.getSize());
        IPage<VisitStatsDO> visitStatsDOIPage = visitStatsMapper.selectPage(
                pageInfo,
                new LambdaQueryWrapper<VisitStatsDO>()
                        .eq(VisitStatsDO::getProductId, request.getProductId())
        );
        /**
         * 分頁記錄列表
         */
        dataMap.put("current_data",visitStatsDOIPage.getRecords());
        /**
         * 當前分頁總頁數
         */
        dataMap.put("total_page",visitStatsDOIPage.getPages());
        /**
         * 總記錄數
         */
        dataMap.put("total_record",visitStatsDOIPage.getTotal());
        return dataMap;
    }
}

測試

Clickhouse語法

  Clickhouse語法和mysql差不多,只不過Clickhouse提供了更加豐富的函數,具體請查閱官網文檔:點我直達