海量數據存儲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可視化界面:http://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數據類型對比

ClickHouseMysql說明
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

文檔地址:https://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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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提供了更加豐富的函數,具體請查閱官網文檔:點我直達

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