ClickHouse
OLTP
全称 OnLine Transaction Processing,联机事务处理系统, 就是对数据的增删改查等操作。存储的是业务数据,来记录某类业务事件的发生,比如下单、支付、注册、等等。
典型代表有Mysql、 Oracle等数据库,对应的网站、系统应用后端数据库 针对事务进行操作,对响应时间要求高,面向前台应用的,应用比较简单,数据量相对较少,是GB级别的。
面向群体:业务人员
OLAP
当数据积累到一定的程度,需要对过去发生的事情做一个总结分析时,就需要把过去一段时间内产生的数据拿出来进行统计分析,从中获取想要的信息,为公司做决策提供支持,这个就是做OLAP了。
OnLine Analytical Processing,联机分析处理系统,存储的是历史数据,对应的风控平台、BI平台、数据可视化等系统就属于OLAP。
OLAP是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策,并且提供直观易懂的查询结果
典型代表有 Hive、ClickHouse
针对基于查询的分析系统,基础数据来源于生产系统中的操作数据,数据量非常大,常规是TB级别的
面向群体:分析决策人员
OLAP 场景的关键特征
基本业务量到达这个分库分表量级,则离不开数据大屏、推荐系统、画像系统等搭建
需要搭建上面的系统,则离不开海量数据进行存储-分析-统计
海量数据下 TB、PB级别数据存储,靠Mysql进行存储-分析-统计无疑是灾难
• 绝大多数是读请求
• 数据以相当大的批次(> 1000 行)更新,而不是单行更新;或者根本没有更新。
• 已添加到数据库的数据不能修改。
• 对于读取,从数据库中提取相当多的行,但只提取列的一小部分。
• 宽表,即每个表包含着大量的列
• 查询相对较少(通常每台服务器每秒查询数百次或更少)
• 对于简单查询,允许延迟大约 50 毫秒
• 列中的数据相对较小:数字和短字符串(例如,每个 URL 60 个字节)
• 处理单个查询时需要高吞吐量(每台服务器每秒可达数十亿行)
• 事务不是必须的
• 对数据一致性要求低
• 每个查询有一个大表。除了他以外,其他的都很小。
• 查询结果明显小于源数据。换句话说,数据经过过滤或聚合,因此结果适合于单个服务器的 RAM 中
很容易可以看出,OLAP场景与其他通常业务场景(例如,OLTP或K/V)有很大的不同, 因此想要使用OLTP或Key-Value数据库去高效的处理分析查询场景,并不是非常完美的适用方案。例如,使用OLAP数据库去处理分析请求通常要优于使用MongoDB或Redis去处理分析请求。
列式数据库更适合 OLAP 场景的原因
列式数据库更适合于OLAP场景(对于大多数查询而言,处理速度至少提高了100倍),下面详细解释了原因(通过图片更有利于直观理解):
行式
列式
行式存储
传统的OLTP关系型数据库 都是行存储。
一行中的数据在存储介质中以连续存储形式存在。
适合随机的增删改查操作,或者在行中选取所有属性的查询操作,结合索引提升性能。
缺点
查询的全部记录的某几个字段,但由于这些字段在各行数据单元中,而整个行特别大(字段特别多),程序必须不断读取每一条的行记录取对应的字段,使得读取效率大大降低。
例子
找某个订单的话,就很方便,订单的全部信息都能获取
但找全部订单总金额就需要遍历多个数据
列式存储
存储结构化数据时,在底层的存储介质上,数据是以列的方式来组织
存储完若干条记录的首个字段后,再存储这些记录的第二个字段,然后再第三个字段、第四个字段...
查询时只有涉及到的列会被读取,而不需要全表扫描,可以显著减少IO消耗,并降低查询响应时间
缺点
列存储数据更新成本较高,一般适合读多写少的场景,适合 OLAP 分析型系统。
例子
查询全部成交额,只需要把订单金额拿出来即可
行/列存储对比
- 行/列存储描述的是底层存储介质上,数据的组织形式,哪种组织对应哪种业务需求
- 列存储在聚合、统计等操作性能会优于行存储
- 列存储将多行记录的列连续存储在一起,一列接着一列
- 列式存储是同个数据类型,会进行数据压缩率更高,更省空间
- 列存储数据更新成本较高,一般适合读多写少的场景,适合 OLAP 分析型系统
ClickHouse
特点
• 不依赖 Hadoop 生态、安装和维护简单
• 擅长对列的聚合、计数等统计操作性能强劲
• 对列存储和压缩的采用更好的算法,更好节省成本
• 拓展性强,在生产中经过实战测试,从单服务器部署到具有数千个节点的集群的线性水平可扩展性
• 具有企业级安全功能和故障安全机制,可防止数据因应用程序错误和人为错误而损坏
• 支持主流的大部分 SQL 语法和函数
• 吞吐能力强,官方测试支持,支持多种存储引擎,满足多数业务场景
应用场景
• 互联网电商
• 在线教育
• 金融等领域用
• 户行为数据记录和分析
• 搭建数据可视化平台
有谁在用
• 国内:阿里、字节、腾讯 、虎牙、青云、新浪等
• 国外:优步、Ebay、Spotify、思科、等
支持多种部署方式
• Linux:源码部署、Docker 部署
• Window:安装 Docker、安装 Linux 虚拟机
ClickHouse 与 MySQL 的对比
ClickHouse 支持事务的情况
ClickHouse 是一个高性能的列式数据库,主要用于分析和 OLAP(在线分析处理)场景。关于事务支持,ClickHouse 的情况如下:
- 事务支持的现状
不支持传统的 ACID 事务:ClickHouse 不支持传统意义上的 ACID(原子性、一致性、隔离性、持久性)事务。这意味着在 ClickHouse 中,不能像在关系型数据库中那样进行复杂的多行或多表的事务处理。 - 事务特性
单行插入:ClickHouse 支持单行插入操作,但在并发写入时,可能会出现数据不一致的情况。
批量插入:支持批量插入,但在批量插入过程中,如果发生错误,可能会导致部分数据插入成功,部分失败。
表的合并:ClickHouse 使用 MergeTree 引擎进行数据的合并和压缩,这个过程是异步的,可能会影响数据的即时可见性。 - 解决方案
虽然 ClickHouse 不支持传统的事务,但可以通过以下方式来处理数据一致性问题:
使用引擎:选择合适的表引擎(如 ReplacingMergeTree)来处理数据的更新和替换。
数据版本控制:在数据表中添加版本号或时间戳,以便在查询时选择最新的数据。
外部事务管理:在应用层实现事务管理,确保在写入 ClickHouse 之前,数据在其他系统中已经完成事务处理。
Docker 安装
1# 安装并运行Docker。
2yum install docker-io -y
3systemctl start docker
4
5# 检查安装结果。
6docker info
7
8# 启动使用Docker
9systemctl start docker #运行Docker守护进程
10systemctl stop docker #停止Docker守护进程
11systemctl restart docker #重启Docker守护进程
12
13# 修改镜像仓库
14vim /etc/docker/daemon.json
15{
16 "registry-mirrors": ["https://dockerpull.com"]
17}
器容器化部署 ClickHouse
端口
默认 http端口是8123
tcp端口是9000
同步端口9009
Web 可视化界面
http://192.168.10.63:8123/play
命令
默认数据库最初为空,用于执行未指定数据库的命令
命令 | 描述 |
---|---|
SHOW DATABASES |
查看数据库 |
SHOW TABLES IN system |
查看某个库下面的全部表 |
系统数据库是 ClickHouse 存储有关 ClickHouse 部署的详细信息的地方 |
强烈不推荐
- 在任何其他情况下不能将 ClickHouse 服务器暴露给公共互联网
ClickHouse体验平台界面实际上是通过ClickHouse 的HTTP API接口实现的
- 确保它只在私有网络上侦听,并由正确配置的防火墙监控。
docker 部署 ClickHouse
1mkdir -pv /mydata/docker/clickhouse/log
2mkdir -pv /mydata/docker/clickhouse/data
3
4docker run -d --name clickhouse --ulimit nofile=262144:262144 \
5-p 8123:8123 -p 9000:9000 -p 9009:9009 --privileged=true \
6-v /mydata/docker/clickhouse/log:/var/log/clickhouse-server \
7-v /mydata/docker/clickhouse/data:/var/lib/clickhouse clickhouse/clickhouse-server:22.2.3.5
数据库可视化工具 DBeaver 连接 ClickHouse
可视化工具 DBeaver 下载 Windows (zip)
创建数据库和表
1### 创建数据库 soulboy_shop
2CREATE DATABASE soulboy_shop
3
4### 创建表 clickstream
5# ClickHouse 有自己的数据类型,每个表都必须指定一个Engine引擎属性来确定要创建的表的类型
6# 引擎决定了数据的存储方式和存储位置、支持哪些查询、对并发的支持
7
8CREATE TABLE soulboy_shop.clickstream (
9 customer_id String,
10 time_stamp Date,
11 click_event_type String,
12 page_code FixedString(20),
13 source_id UInt64,
14 money Decimal(2,1),
15 is_new Bool
16)
17ENGINE = MergeTree()
18ORDER BY (time_stamp)
19
20### 删除表
21DROP TABLE IF EXISTS soulboy_shop.clickstream
22
23### 查看表结构
24DESCRIBE soulboy_shop.clickstream
25
26### 插入数据
27INSERT INTO soulboy_shop.clickstream
28VALUES ('customer1', '2021-10-02', 'add_to_cart', 'home_enter', 568239, 3.8, 1)
29
30### 查询数据
31SELECT * FROM soulboy_shop.clickstream
32
33### 条件查询数据
34SELECT * FROM soulboy_shop.clickstream WHERE time_stamp >= '2001-11-01'
35
36### 更新和删除
37在OLAP数据库中,可变数据(Mutable data)通常是不被欢迎的,早期ClickHouse是不支持,后来版本才有
38不支持事务,建议批量操作,不要高频率小数据量更新删除
39删除和更新是一个异步操作的过程,语句提交立刻返回,但不一定已经完成了
40
41# 判断是否完成(is_done字段为1代表sql语句执行完成)
42SELECT database, table, command, create_time, is_done FROM system.mutations LIMIT 20
43
44# 更新
45ALTER TABLE soulboy_shop.clickstream UPDATE click_event_type = 'pay' where customer_id = 'customer1';
46
47# 删除
48ALTER TABLE soulboy_shop.clickstream delete where customer_id = 'customer2';
数据类型
- ASCII 码:
一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间
- UTF-8 编码:
一个英文字符等于一个字节,一个中文(含繁体)等于三个字节
数据类型 | 描述 | 补充 |
---|---|---|
整型 | 固定长度的整型,包括有符号整型或无符号整型, IntX X 是位的意思,1Byte 字节=8bit 位 | |
浮点型(存在精度损失问题) | Float32 - MySQL 里面的 float 类型、Float64 - MySQL 里面的 double 类型 | 建议尽可能以整型形式存储数据 |
Decimal 类型 | 需要要求更高的精度的数值运算,则需要使用定点数 ,般金额字段、汇率、利率等字段为了保证小数点精度,都使用 Decimal | Decimal(10,2) 小数部分2位,整数部分 8位(10-2) 整数部分超过了会报错,小数部分超过了会截取丢失 |
字符串类型(UUID) | 通用唯一标识符(UUID)是由一组 32 位数的 16 进制数字所构成,用于标识记录 | 要生成 UUID 值,ClickHouse 提供了 generateuidv4 函数,如果在插入新记录时未指定 UUID 列的值,则 UUID 值将用零填充 |
FixedString(固定字符串类型) | 固定字符串类型(相对少用),类似 MySQL 的 Char 类型,属于定长字符,固定长度 N 的字符串(N 必须是严格的正自然数) | 如果字符串包含的字节数少于 `N’,将对字符串末尾进行空字节填充。如果字符串包含的字节数大于 N,将抛出 Too large value for FixedString(N)异常。当数据的长度恰好为 N 个字节时,FixedString 类型是高效的,在其他情况下,这可能会降低效率 |
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) |
Enum(枚举类型) | 包括 Enum8 和 Enum16 类型,Enum 保存 'string'= integer 的对应关系 | 在 ClickHouse 中,尽管用户使用的是字符串常量,但所有含有 Enum 数据类型的操作都是按照包含整数的值来执行。这在性能方面比使用 String 数据类型更有效。 |
布尔值 | 旧版以前没有单独的类型来存储布尔值。可以使用 UInt8 类型,取值限制为 0 或 1,新增里面新增了 Bool | |
查看所有数据类型
- 不同 CH 版本存在不一样的数据类型,毕竟是火热项目,有些尽管新增但是也很少没用上,记住常用的即可
- case_insensitive 选项为 1 表示大小写不敏感,字段类型不区分大小写
- 为 0 表示大小写敏感,即字段类型需要严格区分大小写
1select * from system.data_type_families
1##################### 整型
2### 有符号整型范围
3Int8 — [-128 : 127]
4
5Int16 — [-32768 : 32767]
6
7Int32 — [-2147483648 : 2147483647]
8
9Int64 — [-9223372036854775808 : 9223372036854775807]
10
11Int128 — [-170141183460469231731687303715884105728 : 170141183460469231731687303715884105727]
12
13Int256 — [-57896044618658097711785492504343953926634992332820282019728792003956564819968 : 57896044618658097711785492504343953926634992332820282019728792003956564819967]
14
15### 无符号整型范围
16UInt8 — [0 : 255]
17
18UInt16 — [0 : 65535]
19
20UInt32 — [0 : 4294967295]
21
22UInt64 — [0 : 18446744073709551615]
23
24UInt128 — [0 : 340282366920938463463374607431768211455]
25
26UInt256 — [0 : 115792089237316195423570985008687907853269984665640564039457584007913129639935]
27
28
29### Decimal类型
30例子:Decimal(10,2) 小数部分2位,整数部分 8位(10-2)
31整数部分超过了会报错,小数部分超过了会截取丢失
32
33
34CREATE TABLE soulboy_shop.clickstream1 (
35 customer_id String,
36 time_stamp Date,
37 click_event_type String,
38 page_code FixedString(20),
39 source_id UInt64,
40 money Decimal(2,1)
41)
42ENGINE = MergeTree()
43ORDER BY (time_stamp)
44
45
46INSERT INTO soulboy_shop.clickstream1
47VALUES ('customer1', '2021-10-02', 'add_to_cart', 'home_enter', 568239,2.11 )
48
49
50### Decimal类型
51CREATE TABLE soulboy_shop.clickstream1 (
52 customer_id String,
53 time_stamp Date,
54 click_event_type String,
55 page_code FixedString(20),
56 source_id UInt64,
57 money Decimal(2,1)
58)
59ENGINE = MergeTree()
60ORDER BY (time_stamp)
61
62INSERT INTO soulboy_shop.clickstream1
63VALUES ('customer1', '2021-10-02', 'add_to_cart', 'home_enter', 568239,2.11 )
64
65### 字符串类型
66通用唯一标识符(UUID)是由一组32位数的16进制数字所构成,用于标识记录
67
68# 建表和插入例子
69CREATE TABLE t_uuid (x UUID, y String) ENGINE=TinyLog
70INSERT INTO t_uuid SELECT generateUUIDv4(), 'Example 1'
71
72### 枚举类型
73包括 Enum8 和 Enum16 类型,Enum 保存 'string'= integer 的对应关系
74在 ClickHouse 中,尽管用户使用的是字符串常量,但所有含有 Enum 数据 类型的操作都是按照包含整数的值来执行。这在性能方面比使用 String 数据类型更有效。
75
76Enum8 用 'String'= Int8 对描述。
77Enum16 用 'String'= Int16 对描述。
78
79# 创建一个带有一个枚举 Enum8('home' = 1, 'detail' = 2, 'pay'=3) 类型的列
80CREATE TABLE t_enum
81(
82 page_code Enum8('home' = 1, 'detail' = 2,'pay'=3)
83)
84ENGINE = TinyLog
85插入, page_code 这列只能存储类型定义中列出的值:'home'或`'detail' 或 'pay'。如果您尝试保存任何其他值,ClickHouse 抛出异常
86
87# 插入成功
88INSERT INTO t_enum VALUES ('home'), ('detail')
89
90# 插入报错
91INSERT INTO t_enum VALUES ('home1')
92
93# 查询
94SELECT * FROM t_enum
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 语法
ClickHouse 语法和常规 SQL 语法类似,多数都是支持的
注意事项
每次更新或者删除,会废弃目标数据的原有分区,而重建新分区
如果只更新一条数据,那么需要重建一个分区,开销大
如果更新100条数据,而这100条可能落在3个分区上,则需重建3个分区
相对来说 一次更新一批数据的整体效率远高于一次更新一行
1### 创建数据库 soulboy_shop
2CREATE DATABASE soulboy_shop
3
4### 创建表 clickstream
5# ClickHouse 有自己的数据类型,每个表都必须指定一个Engine引擎属性来确定要创建的表的类型
6# 引擎决定了数据的存储方式和存储位置、支持哪些查询、对并发的支持
7
8CREATE TABLE soulboy_shop.clickstream (
9 customer_id String,
10 time_stamp Date,
11 click_event_type String,
12 page_code FixedString(20),
13 source_id UInt64,
14 money Decimal(2,1),
15 is_new Bool
16)
17ENGINE = MergeTree()
18ORDER BY (time_stamp)
19
20### 删除表
21DROP TABLE IF EXISTS soulboy_shop.clickstream
22
23### 查看表结构
24DESCRIBE soulboy_shop.clickstream
25
26### 插入数据
27INSERT INTO soulboy_shop.clickstream
28VALUES ('customer1', '2021-10-02', 'add_to_cart', 'home_enter', 568239, 3.8, 1)
29
30### 查询数据
31SELECT * FROM soulboy_shop.clickstream
32
33### 条件查询数据
34SELECT * FROM soulboy_shop.clickstream WHERE time_stamp >= '2001-11-01'
35
36### 更新和删除
37在OLAP数据库中,可变数据(Mutable data)通常是不被欢迎的,早期ClickHouse是不支持,后来版本才有
38不支持事务,建议批量操作,不要高频率小数据量更新删除
39删除和更新是一个异步操作的过程,语句提交立刻返回,但不一定已经完成了
40
41# 判断sql语句是否执行完成(is_done字段为1代表sql语句执行完成)
42SELECT database, table, command, create_time, is_done FROM system.mutations LIMIT 20
43
44# 更新
45ALTER TABLE soulboy_shop.clickstream UPDATE click_event_type = 'pay' where customer_id = 'customer1';
46
47# 删除
48ALTER TABLE soulboy_shop.clickstream delete where customer_id = 'customer2';
分片-分区-副本
类型 | 说明 |
---|---|
数据分区 | 允许查询在指定了分区键的条件下,尽可能的少读取数据 |
数据分片 | 允许多台机器/节点同并行执行查询,实现了分布式并行计算 |
数据副本 | 允许 ClickHouse 节点宕机,依然可以从其他节点查询和获取相同的数据 |
分区
分区是表的分区,把一张表的数据分成N多个区块,分区后的表还是一张表,数据处理还是由自己来完成
PARTITION BY
,指的是一个表按照某一列数据(比如日期)进行分区,不同分区的数据会写入不同的文件中
建表时加入 partition 概念,可以按照对应的分区字段,允许查询在指定了分区键的查询,减少表的扫描范围,提升性能
1create table soulboy_shop.order_merge_tree(
2 id UInt32,
3 sku_id String,
4 out_trade_no String,
5 total_amount Decimal(16,2),
6 create_time Datetime
7) engine =MergeTree()
8 partition by toYYYYMMDD(create_time)
9 order by (id,sku_id)
10 primary key (id);
分片
shard 分片是把 数据库横向扩展(Scale Out)到多个物理节点上的一种有效的方式
复用了数据库的分区概念,相当于在原有的分区下作为第二层分区,ClickHouse会将数据分为多个分片,并且分布到不同节点上,再通过 Distributed 表引擎把数据拼接起来一同使用
Sharding 机制使得 ClickHouse 可以横向线性拓展,构建大规模分布式集群,但需要避免数据倾斜问题(热点数据)
副本
两个相同数据的表, 作 用是为了数据备份与安全,保障数据的高可用性,
即使一台 ClickHouse 节点宕机,那么也可以从其他服务器获得相同的数据
类似 MySQL 主从架构,主节点宕机,从节点也能提供服务
表引擎
MySQL 的表引擎
区别项 | Innodb | myisam |
---|---|---|
事务 | 支持 | 不支持 |
锁粒度 | 行锁,适合高并发 | 表锁,不适合高并发 |
是否默认 | 默认 | 非默认 |
支持外键 | 支持外键 | 不支持 |
适合场景 | 读写均衡,写大于读场景,需要事务 | 读多写少场景,不需要事务 |
全文索引 | 不支持,可以通过插件实现, 更多使用 Elasticsearch | 支持全文索引 |
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 引擎混淆) 本身不存储数据,但可用于同时从任意多个其他的表中读取数据。读是自动并行的,不支持写入(读取时),
那些被真正读取到数据的表的索引(如果有的话)会被使用
MergeTree
- 中文名叫合并树,
MergeTree
系列的引擎被设计用于插入极大量的数据到一张表当中。 - 数据可以以【数据片段】的形式一个接着一个的快速写入,
数据片段在后台按照一定的规则进行合并
。 - 相比在插入时不断修改(重写)已存储的数据,这种策略会高效很多
- 这种数据片段反复合并的特性,也正是合并树名称的由来
MergeTree 特点
特点 | 说明 |
---|---|
主键不唯一 | ClickHouse 不要求主键唯一,所以可以插入多条具有相同主键的行。 |
支持分区键 | 如果指定了【分区键】则可以使用【分区】,可以通过PARTITION 语句指定分区字段,合理使用数据分区,可以有效减少查询时数据文件的扫描范围 ,在相同数据集的情况下 ClickHouse 中某些带分区的操作会比普通操作更快,查询中指定了分区键时 ClickHouse 会自动截取分区数据,这也有效增加了查询性能 |
支持数据副本 | 副本是表级别的不是整个服务器级的,所以服务器里可以同时有复制表和非复制表 |
语法
是否必填 | 选项名 | 说明 |
---|---|---|
必填 | 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) |
1CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
2(
3 name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
4 name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
5 ...
6 INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
7 INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
8) ENGINE = MergeTree()
9ORDER BY expr
10[PARTITION BY expr]
11[PRIMARY KEY expr]
12[SAMPLE BY expr]
13[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
14[SETTINGS name=value, ...]
合并树 MergeTree 建表和数据实操
新的数据写入会有 临时分区产生,不加入已有分区
写入完成后经过一定时间(10 到 15 分钟),ClickHouse 会 自动化执行合并操作,将临时分区的数据合并到已有分区当中
optimize 的合并操作是 在后台执行的,无法预测具体执行时间点,除非是手动执行
1optimize table xxx final;
2optimize table soulboy_shop.order_merge_tree final;
在数据量比较大的情况,尽量不要使用该命令,执行 optimize 要消耗大量时间
1### 创建表 order_merge_tree
2create table soulboy_shop.order_merge_tree(
3 id UInt32,
4 sku_id String,
5 out_trade_no String,
6 total_amount Decimal(16,2),
7 create_time Datetime
8) engine =MergeTree()
9 order by (id,sku_id)
10 partition by toYYYYMMDD(create_time)
11 primary key (id);
12
13### 查看表结构
14DESCRIBE soulboy_shop.order_merge_tree
15
16### 表数据写入
17* 查询
18* 再插入
19* 查询,有多个分区
20* 合并
21* 查询,分区减少
22
23insert into soulboy_shop.order_merge_tree values
24(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
25(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
26(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'),
27(4,'sku_1','54222',2000.3,'2023-04-01 19:00:00'),
28(5,'sku_2','53423',120.2,'2023-04-01 19:00:00'),
29(6,'sku_4','65432',600.01,'2023-04-02 11:00:00');
30
31# 分区合并验证
32docker exec -it clickhouse /bin/bash
33clickhouse-client
34491cfd7182f5 :) SELECT * FROM soulboy_shop.order_merge_tree
35
36SELECT *
37FROM soulboy_shop.order_merge_tree
38
39Query id: 839b0acd-48d1-4689-8956-9ea362d4bd13
40
41┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
42│ 1 │ sku_1 │ aabbcc │ 5600 │ 2023-03-01 16:00:00 │
43│ 2 │ sku_2 │ 23241 │ 4.02 │ 2023-03-01 17:00:00 │
44│ 3 │ sku_3 │ 542323 │ 55.02 │ 2023-03-01 18:00:00 │
45└────┴────────┴──────────────┴──────────────┴─────────────────────┘
46┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
47│ 4 │ sku_1 │ 54222 │ 2000.3 │ 2023-04-01 19:00:00 │
48│ 5 │ sku_2 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
49└────┴────────┴──────────────┴──────────────┴─────────────────────┘
50┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
51│ 6 │ sku_4 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
52└────┴────────┴──────────────┴──────────────┴─────────────────────┘
ReplacingMergeTree
MergeTree 的拓展,该引擎和 MergeTree 的不同之处在它会 删除【排序键值】相同重复项,根据OrderBy字段
数据的去重只会在数据合并期间进行。合并会在后台一个不确定的时间进行,因此你无法预先作出计划。
有一些数据可能仍未被处理,尽管可以调用 OPTIMIZE 语句发起计划外的合并,但请不要依靠它,因为 OPTIMIZE 语句会引发对数据的大量读写
因此,ReplacingMergeTree 适用于在后台清除重复的数据以节省空间,但是它不保证没有重复的数据出现
注意去重访问:如果是有多个分区表,只在分区内部进行去重,不会跨分区
语法
是否必填 | 选项名 | 说明 |
---|---|---|
选填 | ver — 版本列,类型为 UInt*, Date 或 DateTime。可选参数。 | 在数据合并的时候,ReplacingMergeTree 从所有具有相同排序键的行中选择一行留下,如果 ver 列未指定,保留最后一条。如果 ver 列已指定,保留 ver 值最大的版本 |
如何判断数据重复 | 在去除重复数据时,是以 ORDER BY 排序键为基准的,而不是 PRIMARY KEY,若排序字段为两个,则两个字段都相同时才会去重 | |
何时删除重复数据 | 在执行分区合并时触发删除重复数据,optimize 的合并操作是在后台执行的,无法预测具体执行时间点,除非是手动执行 | |
不同分区的重复数据不会被去重 | ReplacingMergeTree() 填入的参数为版本字段,重复数据就会保留版本字段值最大的。如果不填写版本字段,默认保留插入顺序的最后一条数据 | |
删除策略 | ReplacingMergeTree() 填入的参数为版本字段,重复数据就会保留版本字段值最大的。如果不填写版本字段,默认保留插入顺序的最后一条数据 |
1CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
2(
3 name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
4 name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
5 ...
6) ENGINE = ReplacingMergeTree([ver])
7[PARTITION BY expr]
8[ORDER BY expr]
9[SAMPLE BY expr]
10[SETTINGS name=value, ...]
去重合并树 ReplaceMergeTree 建表实操
建表:ver表示的列只能是UInt*,Date和DateTime 类型
删除策略:ReplacingMergeTree() 填入的参数为版本字段,重复数据就会保留版本字段值最大的。 如果不填写版本字段,默认保留插入顺序的最后一条数据
1### 创建表 order_relace_merge_tree
2create table soulboy_shop.order_relace_merge_tree(
3 id UInt32,
4 sku_id String,
5 out_trade_no String,
6 total_amount Decimal(16,2),
7 create_time Datetime
8) engine =ReplacingMergeTree(id)
9 order by (sku_id)
10 partition by toYYYYMMDD(create_time)
11 primary key (sku_id);
12
13 insert into soulboy_shop.order_relace_merge_tree values
14(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
15(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
16(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'),
17(4,'sku_5','54222',2000.3,'2023-04-01 19:00:00'),
18(5,'sku_6','53423',120.2,'2023-04-01 19:00:00'),
19(6,'sku_7','65432',600.01,'2023-04-02 11:00:00');
20
21insert into soulboy_shop.order_relace_merge_tree values
22(11,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
23(21,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
24(31,'sku_3','542323',55.02,'2023-03-01 18:00:00'),
25(41,'sku_5','54222',2000.3,'2023-04-01 19:00:00'),
26(51,'sku_8','53423',120.2,'2023-04-01 19:00:00'),
27(61,'sku_9','65432',600.01,'2023-04-02 11:00:00');
28
29SELECT * FROM soulboy_shop.order_relace_merge_tree;
30
31
32┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
33│ 1 │ sku_1 │ aabbcc │ 5600 │ 2023-03-01 16:00:00 │
34│ 2 │ sku_2 │ 23241 │ 4.02 │ 2023-03-01 17:00:00 │
35│ 3 │ sku_3 │ 542323 │ 55.02 │ 2023-03-01 18:00:00 │
36└────┴────────┴──────────────┴──────────────┴─────────────────────┘
37┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
38│ 11 │ sku_1 │ aabbcc │ 5600 │ 2023-03-01 16:00:00 │
39│ 21 │ sku_2 │ 23241 │ 4.02 │ 2023-03-01 17:00:00 │
40│ 31 │ sku_3 │ 542323 │ 55.02 │ 2023-03-01 18:00:00 │
41└────┴────────┴──────────────┴──────────────┴─────────────────────┘
42┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
43│ 4 │ sku_5 │ 54222 │ 2000.3 │ 2023-04-01 19:00:00 │
44│ 5 │ sku_6 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
45└────┴────────┴──────────────┴──────────────┴─────────────────────┘
46┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
47│ 41 │ sku_5 │ 54222 │ 2000.3 │ 2023-04-01 19:00:00 │
48│ 51 │ sku_8 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
49└────┴────────┴──────────────┴──────────────┴─────────────────────┘
50┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
51│ 6 │ sku_7 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
52└────┴────────┴──────────────┴──────────────┴─────────────────────┘
53┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
54│ 61 │ sku_9 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
55└────┴────────┴──────────────┴──────────────┴─────────────────────┘
56
57# 手工合并
58optimize table soulboy_shop.order_relace_merge_tree final;
59
60SELECT * FROM soulboy_shop.order_relace_merge_tree;
61
62┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
63│ 11 │ sku_1 │ aabbcc │ 5600 │ 2023-03-01 16:00:00 │
64│ 21 │ sku_2 │ 23241 │ 4.02 │ 2023-03-01 17:00:00 │
65│ 31 │ sku_3 │ 542323 │ 55.02 │ 2023-03-01 18:00:00 │
66└────┴────────┴──────────────┴──────────────┴─────────────────────┘
67┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
68│ 41 │ sku_5 │ 54222 │ 2000.3 │ 2023-04-01 19:00:00 │
69│ 5 │ sku_6 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
70│ 51 │ sku_8 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
71└────┴────────┴──────────────┴──────────────┴─────────────────────┘
72┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
73│ 6 │ sku_7 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
74│ 61 │ sku_9 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
75└────┴────────┴──────────────┴──────────────┴─────────────────────┘
SummingMergeTree
聚合引擎 SummingMergeTree,该引擎继承自 MergeTree,区别在于,当合并 SummingMergeTree 表的数据片段时,ClickHouse 会把所有具有 相同 OrderBy 排序键 的行合并为一行,该行包含了被合并的行中具有 数值类型的列的汇总值
。
类似 group by 后使用聚合函数的效果,这个可以 显著的减少存储空间并加快数据查询的速度
推荐将该引擎和 MergeTree 一起使用。例如在准备做数据报表的时候,将完整的数据存储在 MergeTree 表中
,并且 使用SummingMergeTree 来存储聚合数据
,这样做可以避免因为使用不正确的 排序健组合方式而丢失有价值的数据
只需要查询汇总结果,不关心明细数据
设计聚合统计表,字段全部是维度(商品)、度量(销量)或者时间戳即可,非相关的字段可以不添加
获取汇总值,不能直接 select 对应的字段,而 需要使用 sum 进行聚合,因为自动合并的部分可能没进行,会导致一些还没来得及聚合的临时明细数据少
在合并分区的时候按照预先定义的条件聚合汇总数据,将同一分区下的【相同排序】的多行数据汇总合并成一行,既减少了数据行节省空间,又降低了后续汇总查询的开销
语法
是否必填 | 选项名 | 说明 |
---|---|---|
选填 | columns 包含了将要被汇总的列的列名的元组。 | |
所选的【列必须是数值类型】,具有 相同OrderBy排序键 的行合并为一行 |
||
如果没有指定 columns,ClickHouse 会把非维度列且是【数值类型的列】都进行汇总 |
||
跨分区内的相同排序 key 的数据是否会进行合并 | 以数据分区为单位来聚合数据,同一数据分区内相同ORBER BY排序键的数据会被合并汇总 ,而 不同分区之间的数据不会被汇总 ,多个分区内相同 ORBER BY 排序键的数据汇总可以使用 group by ➕ sum(),在原有已汇总的基础上进行最终汇总可以加快数据查询的速度 |
|
如果没有指定聚合字段,会怎么聚合 | 如果没有指定聚合字段,则会用非维度列,且是数值类型字段进行聚合 | |
对于非汇总字段的数据,该保留哪一条 | 如果两行数据除了【ORBER BY 排序键】相同,其他的非聚合字段不相同,在聚合时会【保留最初】的那条数据,新插入的数据对应的那个字段值会被舍弃 |
1(
2name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
3name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
4...
5) ENGINE = SummingMergeTree([columns])
6[PARTITION BY expr]
7[ORDER BY expr]
8[SAMPLE BY expr]
9[SETTINGS name=value, ...]
聚合引擎 SummingMergeTree 建表实操
SummingMergeTree 的参数
- columns 包含了将要被汇总的列的列名的元组。可选参数。
- 所选的【列必须是数值类型】,具有 相同 OrderBy 排序键 的行合并为一行
- 如果没有指定
columns
,ClickHouse 会把非维度列且是【数值类型的列】都进行汇总
1### 创建表 order_summing_merge_tree
2create table soulboy_shop.order_summing_merge_tree(
3 id UInt32,
4 sku_id String,
5 out_trade_no String,
6 total_amount Decimal(16,2),
7 create_time Datetime
8) engine =SummingMergeTree(total_amount)
9 order by (id,sku_id)
10 partition by toYYYYMMDD(create_time)
11 primary key (id);
12
13
14
15### 插入数据
16insert into soulboy_shop.order_summing_merge_tree values
17(1,'sku_1','aabbcc',5600.00,'2023-03-01 16:00:00') ,
18(2,'sku_2','23241',4.02,'2023-03-01 17:00:00'),
19(3,'sku_3','542323',55.02,'2023-03-01 18:00:00'),
20(4,'sku_5','54222',2000.3,'2023-04-01 19:00:00'),
21(5,'sku_6','53423',120.2,'2023-04-01 19:00:00'),
22(6,'sku_7','65432',600.01,'2023-04-02 11:00:00');
23
24### 第一次查询
25491cfd7182f5 :) SELECT * FROM soulboy_shop.order_summing_merge_tree;
26
27SELECT *
28FROM soulboy_shop.order_summing_merge_tree
29
30Query id: 1d03663c-4e48-4a58-b5b8-4196418bce35
31
32┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
33│ 1 │ sku_1 │ aabbcc │ 5600 │ 2023-03-01 16:00:00 │
34│ 2 │ sku_2 │ 23241 │ 4.02 │ 2023-03-01 17:00:00 │
35│ 3 │ sku_3 │ 542323 │ 55.02 │ 2023-03-01 18:00:00 │
36└────┴────────┴──────────────┴──────────────┴─────────────────────┘
37┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
38│ 4 │ sku_5 │ 54222 │ 2000.3 │ 2023-04-01 19:00:00 │
39│ 5 │ sku_6 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
40└────┴────────┴──────────────┴──────────────┴─────────────────────┘
41┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
42│ 6 │ sku_7 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
43└────┴────────┴──────────────┴──────────────┴─────────────────────┘
44
45### 二次插入数据
46insert into soulboy_shop.order_summing_merge_tree values
47(1,'sku_1','aabbccbb',5600.00,'2023-03-01 23:09:00')
48insert into soulboy_shop.order_summing_merge_tree values
49(1,'sku_1','aabbccbb',5600.00,'2023-03-01 23:09:00')
50
51### 二次查询
52491cfd7182f5 :) SELECT * FROM soulboy_shop.order_summing_merge_tree;
53
54SELECT *
55FROM soulboy_shop.order_summing_merge_tree
56
57Query id: 43711bf8-6a7f-4da0-bfbc-0c05882342ec
58
59┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
60│ 1 │ sku_1 │ aabbcc │ 5600 │ 2023-03-01 16:00:00 │
61│ 2 │ sku_2 │ 23241 │ 4.02 │ 2023-03-01 17:00:00 │
62│ 3 │ sku_3 │ 542323 │ 55.02 │ 2023-03-01 18:00:00 │
63└────┴────────┴──────────────┴──────────────┴─────────────────────┘
64┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
65│ 1 │ sku_1 │ aabbccbb │ 5600 │ 2023-03-01 23:09:00 │
66└────┴────────┴──────────────┴──────────────┴─────────────────────┘
67┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
68│ 1 │ sku_1 │ aabbccbb │ 5600 │ 2023-03-01 23:09:00 │
69└────┴────────┴──────────────┴──────────────┴─────────────────────┘
70┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
71│ 4 │ sku_5 │ 54222 │ 2000.3 │ 2023-04-01 19:00:00 │
72│ 5 │ sku_6 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
73└────┴────────┴──────────────┴──────────────┴─────────────────────┘
74┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
75│ 6 │ sku_7 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
76└────┴────────┴──────────────┴──────────────┴─────────────────────┘
77
78### 手工合并数据
79optimize table soulboy_shop.order_summing_merge_tree final;
80
81### 三次查询数据(发现已合并 sku_1 16800 )
82491cfd7182f5 :) SELECT * FROM soulboy_shop.order_summing_merge_tree; 0J9 KI8M
83
84SELECT *
85FROM soulboy_shop.order_summing_merge_tree
86
87Query id: a95f4507-20ca-4771-a186-8a5e46328e82
88
89┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
90│ 1 │ sku_1 │ aabbcc │ 16800 │ 2023-03-01 16:00:00 │
91│ 2 │ sku_2 │ 23241 │ 4.02 │ 2023-03-01 17:00:00 │
92│ 3 │ sku_3 │ 542323 │ 55.02 │ 2023-03-01 18:00:00 │
93└────┴────────┴──────────────┴──────────────┴─────────────────────┘
94┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
95│ 4 │ sku_5 │ 54222 │ 2000.3 │ 2023-04-01 19:00:00 │
96│ 5 │ sku_6 │ 53423 │ 120.2 │ 2023-04-01 19:00:00 │
97└────┴────────┴──────────────┴──────────────┴─────────────────────┘
98┌─id─┬─sku_id─┬─out_trade_no─┬─total_amount─┬─────────create_time─┐
99│ 6 │ sku_7 │ 65432 │ 600.01 │ 2023-04-02 11:00:00 │
100└────┴────────┴──────────────┴──────────────┴─────────────────────┘
101
102### 查询汇总数据(查询时聚合)
103select sku_id,sum(total_amount) from soulboy_shop.order_summing_merge_tree group by sku_id
ClickHouse 高可用集群分片-副本
ClickHouse 的副本
- 两个相同数据的表, 作用是为了数据备份与安全,
保障数据的高可用性
- 副本是
表级别的
,不是整个服务器级的,服务器里可以同时有复制表和非复制表
。 - 副本不依赖分片,
每个分片有它自己的独立副本
。
副本写入流程
- 复制是
多主异步
- 数据会
先插入到执行该语句的服务器上,然后被复制到其他服务器
。 - 由于它是
异步
的,在其他副本上最近插入的数据会有一些延迟
。如果部分副本不可用,则数据在其可用时再写入
- 副本可用的情况下,则【延迟时长】是
通过网络传输、压缩数据块所需的时间。
- 默认情况下,
INSERT 语句仅等待一个副本写入成功后返回
,如果数据只成功写入一个副本,而后该副本所在的服务器不再存在,则存储的数据会丢失 - 要
启用数据写入多个副本才确认返回
,使用insert_quorum
进行配置,但是会影响性能
- 对于
INSERT 和 ALTER 语句
操作数据的会在压缩的情况下被复制
,而CREATE,DROP,ATTACH,DETACH 和 RENAME 语句只会在单个服务器上执行,不会被复制
。
ReplicatedMergeTree(副本合并树表引擎)
- 如果有
两个副本
的话,相当于分布在两台clickhosue节点中的两个表
- 这个
两个表具有协调功能
,无论是哪个表执行insert或者alter操作,都会同步到另外一张表,副本就是相互同步数据的表
副本同步需要借助zookeeper实现数据的同步, 副本节点会在zk上进行监听,但数据同步过程是不经过zk的
- zookeeper 要求 3.4.5 以及以上版本
语法
是否必填 | 选项名 | 说明 |
---|---|---|
必填 | zoo_path | zk 中该表的路径,可自定义名称,同一张表的同一分片的不同副本,要定义相同的路径。 |
ZooKeeper 中该表的路径:对每个可复制表都要是唯一的,不同分片上的表要有不同的路径 。 |
/clickhouse/tables/{shard}/{table_name} |
|
必填 | replica_name | zk 中的该表的副本名称,同一张表的同一分片的不同副本,要定义不同的名称。 |
ZooKeeper 中的该表的副本名称:同一张表的同一分片的不同副本,要定义不同的名称 |
tb_order_01 |
|
1CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
2(
3 name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
4 name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
5 ...
6 INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
7 INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
8) ENGINE = ReplicatedMergeTree('{zoo_path}', '{replica_name}')
9ORDER BY expr
10[PARTITION BY expr]
11[PRIMARY KEY expr]
12
13
14### 示例
15
16CREATE TABLE tb_order
17(
18 EventDate DateTime,
19 CounterID UInt32,
20 UserID UInt32
21) ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/tb_order', 'tb_order_01')
22PARTITION BY toYYYYMM(EventDate)
23ORDER BY (CounterID, EventDate, intHash32(UserID))
24SAMPLE BY intHash32(UserID)
只有 MergeTree 系列里的表可支持副本
支持副本的引擎 |
---|
ReplicatedMergeTree |
ReplicatedSummingMergeTree |
ReplicatedReplacingMergeTree |
ReplicatedAggregatingMergeTree |
ReplicatedCollapsingMergeTree |
ReplicatedVersionedCollapsingMergeTree |
ReplicatedGraphiteMergeTree |
ClickHouse 高可用架构准备
高可用架构需要 ZooKeeper
副本同步需要借助 ZooKeeper 实现数据的同步,副本节点会在ZooKeeper上进行监听,但数据同步过程是不经过ZooKeeper的
环境准备
角色 | ip 地址 |
---|---|
ZooKeeper(Docker 部署) | 192.168.10.60 |
ClickHouse(RPM 包部署) | 192.168.10.63 |
ClickHouse_2(RPM 包部署) | 192.168.10.64 |
Docker 部署 ZooKeeper
1docker run -d --name zookeeper -p 2181:2181 -t zookeeper:3.7.0
RPM 包部署 ClickHouse(版本:ClickHouse 22.1.2.2)
依赖包 |
---|
clickhouse-client-22.1.2.2-2.noarch.rpm |
clickhouse-common-static-22.1.2.2-2.x86_64.rpm |
clickhouse-common-static-dbg-22.1.2.2-2.x86_64.rpm |
clickhouse-server-22.1.2.2-2.noarch.rpm |
安装 ClickHouse 22.1.2.2
1# 配置DNS域名解析
2vim /etc/hosts
3192.168.10.63 ClickHouse
4192.168.10.64 ClickHouse2
5
6# 在2个节点分别上传rpm包至如下路径
7[root@ClickHouse tmp]# pwd
8/tmp
9[root@ClickHouse tmp]# ls
10clickhouse-client-22.1.2.2-2.noarch.rpm
11clickhouse-common-static-22.1.2.2-2.x86_64.rpm
12clickhouse-common-static-dbg-22.1.2.2-2.x86_64.rpm
13clickhouse-server-22.1.2.2-2.noarch.rpm
14
15# 安装(默认回车)
16[root@ClickHouse tmp]# rpm -ivh *.rpm
17
18# 启动
19systemctl start clickhouse-server
20
21# 停止
22systemctl stop clickhouse-server
23
24# 重启
25systemctl restart clickhouse-server
26
27# 状态查看
28sudo systemctl status clickhouse-server
29
30# 查看端口占用,如果命令不存在 yum install -y lsof
31lsof -i :8123
32
33# 关闭防火墙
34systemctl stop firewalld
35
36# 查看日志 、错误日志
37tail -f /var/log/clickhouse-server/clickhouse-server.log
38tail -f /var/log/clickhouse-server/clickhouse-server.err.log
39
40# 开启远程访问,取消下面的注释
41vim /etc/clickhouse-server/config.xml
42
43# 编辑配置文件
44<listen_host>0.0.0.0</listen_host>
45
46# 重启
47systemctl restart clickhouse-server
ClickHouse 可视化工具连接
ClickHouse 副本配置实操
【每个节点】都需要配置
1# 进入配置目录
2cd /etc/clickhouse-server
3
4# 编辑配置文件(找到zookeeper节点,增加下面的,如果有多个zookeeper则按照顺序加即可)
5vim /etc/clickhouse-server/config.xml
6<zookeeper>
7 <node>
8 <host>192.168.10.60</host>
9 <port>2181</port>
10 </node>
11</zookeeper>
12
13# 重启
14systemctl restart clickhouse-server
15
16# 状态查看
17sudo systemctl status clickhouse-server
18
19# 查看端口占用,如果命令不存在 yum install -y lsof
20lsof -i :8123
21
22# 查看日志 、错误日志
23tail -f /var/log/clickhouse-server/clickhouse-server.log
24tail -f /var/log/clickhouse-server/clickhouse-server.err.log
【每个机器】上创建表
副本只能同步数据,不能同步表结构,需要在每台机器上手动建表
1# 节点一(192.168.10.63),zookeeper路径一致,副本名称不一样
2CREATE TABLE tb_product
3(
4 userId UInt32
5) ENGINE = ReplicatedMergeTree('/clickhouse/tables/1/tb_product', 'product-replica-1')
6ORDER BY (userId)
7
8# 节点二(192.168.10.64),zookeeper路径一致,副本名称不一样
9CREATE TABLE tb_product
10(
11 userId UInt32
12) ENGINE = ReplicatedMergeTree('/clickhouse/tables/1/tb_product', 'product-replica-2')
13ORDER BY (userId)
14
15# 查询zk配置
16select * from system.zookeeper where path='/';
17
18ClickHouse2 :) select * from system.zookeeper where path='/'
19
20Query id: c0f47288-9774-4bae-943e-64e9c210d22c
21
22┌─name───────┬─value─┬─czxid─┬─mzxid─┬───────────────ctime─┬───────────────mtime─┬─version─┬─cversion─┬─aversion─┬─ephemeralOwner─┬─dataLength─┬─numChildren─┬─pzxid─┬─path─┐
23│ zookeeper │ │ 0 │ 0 │ 1970-01-01 08:00:00 │ 1970-01-01 08:00:00 │ 0 │ -2 │ 0 │ 0 │ 0 │ 2 │ 0 │ / │
24│ clickhouse │ │ 4 │ 4 │ 2024-10-04 13:22:30 │ 2024-10-04 13:22:30 │ 0 │ 2 │ 0 │ 0 │ 0 │ 2 │ 27 │ / │
25└────────────┴───────┴───────┴───────┴─────────────────────┴─────────────────────┴─────────┴──────────┴──────────┴────────────────┴────────────┴─────────────┴───────┴──────┘
【任意节点】插入和查询数据
在任意节点插入数据,在另外一个节点观察数据是否可以同步
1# ClickHouse1插入数据
2clickhouse-client --host=192.168.10.64
3INSERT into tb_product values(1),(2),(3)
4
5# ClickHouse2查询数据
6clickhouse-client --host=192.168.10.64
7ClickHouse2 :) select * from tb_product
8
9SELECT *
10FROM tb_product
11
12Query id: df0236ee-b358-44fc-b93f-7d1048e6f3dc
13
14┌─userId─┐
15│ 1 │
16│ 2 │
17│ 3 │
18└────────┘
高可用架构之分片和 Distributed 表引擎
什么是 ClickHouse 的副本
两个相同数据的表, 作 用是为了数据备份与安全,保障数据的高可用性,
什么是 ClickHouse 的分片
- 数据分片-允许
多台机器/节点同并行执行查询
,实现了分布式并行计算
- 分片间的数据是不同的,
不同的服务器存储同一张表的不同部分
,作用是为了水平切分表,缓解单节点的压力
分布式表引擎 Distributed
Distributed 表引擎主要是用于分布式,自身不存储任何数据
,数据都分散存储在某一个分片上,能够 自动路由数据
至集群中的各个节点,需要和其他数据表引擎一起协同工作
一张 分布式表
底层会对应多个 分片数据表
,由具体的分片表存储数据,分布式表
与 分片表
是一对多的关系
分布式表主要有 本地表(xxx_local)
和 分布式表(xxx_all)
两部分组成
语法
是否必填 | 选项名 | 说明 |
---|---|---|
必填 | cluster | 集群名称,与集群配置中的自定义名称相对应,比如: soulboy_shard |
必填 | database | 数据库名称 |
必填 | table | 本地表名称 |
选填 | sharding_key | 用于分片的 key 值,在写入的数据 Distributed表引擎会依据分片key的规则,将数据分布到各个节点的本地表 ,通常是:user_id 等业务字段、rand()随机函数等规则 |
1CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
2(
3name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
4name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
5...
6) ENGINE = Distributed(cluster, database, table[, sharding_key[, policy_name]])
7[SETTINGS name=value, ...]
多分片表查询
本着 谁执行谁负责
的原则,向 ClickHouse 发起分布式表执行 SELECT * FROM distributed_table
它会转为如下形式 SELECT * FROM local_table
,先 执行本地分片
,再 发送远端各分片执行
最后 合并结果为临时表返回
分片配置详解 config.xml
/etc/clickhouse-server/config.xml
属性名 | 说明 | 是否必填 |
---|---|---|
logs | 这里定义了一个名为 logs 的集群,它由 两个分片 组成,每个分片包含两个副本 。 |
必填 |
weight | 写数据时分片权重。 默认: 1 | 选填 |
internal_replication | 是否只将数据写入其中一个副本。默认值:false(将数据写入所有副本) | 选填 |
replica | 定义副本,每个分片包含两个副本 |
必填 |
priority | 负载均衡副本的优先级,请参见(load_balancing 设置)。默认值:1(值越小优先级越高) | 选填 |
host | 远程服务器地址。可以域名、IPv4 或 IPv6,如果指定域名,则服务在启动时发起一个 DNS 请求,并且请求结果会在服务器运行期间一直被记录。如果 DNS 请求失败,则服务不会启动。如果你修改了 DNS 记录,则需要重启服务。 | 必填 |
port | 息传递的 TCP 端口(「tcp_port」配置通常设为 9000)。不要跟 http_port 混淆 | 必填 |
user | 用于连接远程服务器的用户名。默认值:default。该用户必须有权限访问该远程服务器。访问权限配置在 users.xml 文件中。更多信息,请查看«访问权限»部分。 | 选填 |
password | 用于连接远程服务器的密码。默认值:空字符串。 | 选填 |
secure | 是否使用 ssl 进行连接,设为 true 时,通常也应该设置 port = 9440。服务器也要监听 9440 并有正确的证书。 | 选填 |
compression | 是否使用数据压缩。默认值:true。 | 选填 |
1<remote_servers>
2 <logs>
3 <shard>
4 <!-- 可选的。写数据时分片权重。 默认: 1. -->
5 <weight>1</weight>
6 <!-- 可选的。是否只将数据写入其中一个副本。默认值:false(将数据写入所有副本)。 -->
7 <internal_replication>false</internal_replication>
8
9 <replica>
10<!-- 可选的。负载均衡副本的优先级,请参见(load_balancing 设置)。默认值:1(值越小优先级越高)。 -->
11 <priority>1</priority>
12 <host>example01-01-1</host>
13 <port>9000</port>
14 </replica>
15
16 <replica>
17 <host>example01-01-2</host>
18 <port>9000</port>
19 </replica>
20 </shard>
21
22 <shard>
23 <weight>2</weight>
24 <internal_replication>false</internal_replication>
25
26 <replica>
27 <host>example01-02-1</host>
28 <port>9000</port>
29 </replica>
30
31 <replica>
32 <host>example01-02-2</host>
33 <secure>1</secure>
34 <port>9440</port>
35 </replica>
36 </shard>
37 </logs>
38</remote_servers>
【每个机器】上配置
/etc/clickhouse-server/config.xml
1# 编辑配置文件
2vim /etc/clickhouse-server/config.xml
3
4<!-- 2_Shard 1_Replica -->
5 <cluster_2shards_1replicas>
6 <!-- shard1 -->
7 <shard>
8 <replica>
9 <host>192.168.10.63</host>
10 <port>9000</port>
11 </replica>
12 </shard>
13
14 <!-- shard2 -->
15 <shard>
16 <replica>
17 <host>192.168.10.64</host>
18 <port> 9000</port>
19 </replica>
20 </shard>
21
22 </cluster_2shards_1replicas>
23
24
25# 每个节点重启ClickHouse
26systemctl restart clickhouse-server
27
28# 判断配置是否成功(重启ClickHouse后能查询到) 2个节点都可以查看到
29clickhouse-client --host=192.168.10.64
30select * from system.clusters
31
32┌─cluster─────────────────────────────────────────┬─shard_num─┬─shard_weight─┬─replica_num─┬─host_name─────┬─host_address──┬─port─┬─is_local─┬─user────┬─default_database─┬─errors_count─┬─slowdowns_count─┬─estimated_recovery_time─┐
33│ cluster_2shards_1replicas │ 1 │ 1 │ 1 │ 192.168.10.63 │ 192.168.10.63 │ 9000 │ 0 │ default │ │ 0 │ 0 │ 0 │
34│ cluster_2shards_1replicas │ 2 │ 1 │ 1 │ 192.168.10.64 │ 192.168.10.64 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
35│ test_cluster_one_shard_three_replicas_localhost │ 1 │ 1 │ 1 │ 127.0.0.1 │ 127.0.0.1 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
36│ test_cluster_one_shard_three_replicas_localhost │ 1 │ 1 │ 2 │ 127.0.0.2 │ 127.0.0.2 │ 9000 │ 0 │ default │ │ 0 │ 0 │ 0 │
37│ test_cluster_one_shard_three_replicas_localhost │ 1 │ 1 │ 3 │ 127.0.0.3 │ 127.0.0.3 │ 9000 │ 0 │ default │ │ 0 │ 0 │ 0 │
38│ test_cluster_two_shards │ 1 │ 1 │ 1 │ 127.0.0.1 │ 127.0.0.1 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
39│ test_cluster_two_shards │ 2 │ 1 │ 1 │ 127.0.0.2 │ 127.0.0.2 │ 9000 │ 0 │ default │ │ 0 │ 0 │ 0 │
40│ test_cluster_two_shards_internal_replication │ 1 │ 1 │ 1 │ 127.0.0.1 │ 127.0.0.1 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
41│ test_cluster_two_shards_internal_replication │ 2 │ 1 │ 1 │ 127.0.0.2 │ 127.0.0.2 │ 9000 │ 0 │ default │ │ 0 │ 0 │ 0 │
42│ test_cluster_two_shards_localhost │ 1 │ 1 │ 1 │ localhost │ ::1 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
43│ test_cluster_two_shards_localhost │ 2 │ 1 │ 1 │ localhost │ ::1 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
44│ test_shard_localhost │ 1 │ 1 │ 1 │ localhost │ ::1 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
45│ test_shard_localhost_secure │ 1 │ 1 │ 1 │ localhost │ ::1 │ 9440 │ 0 │ default │ │ 0 │ 0 │ 0 │
46│ test_unavailable_shard │ 1 │ 1 │ 1 │ localhost │ ::1 │ 9000 │ 1 │ default │ │ 0 │ 0 │ 0 │
47│ test_unavailable_shard │ 2 │ 1 │ 1 │ localhost │ ::1 │ 1 │ 0 │ default │ │ 0 │ 0 │ 0 │
48└─────────────────────────────────────────────────┴───────────┴──────────────┴─────────────┴───────────────┴───────────────┴──────┴──────────┴─────────┴──────────────────┴──────────────┴─────────────────┴─────────────────────────┘
建表实操
1# 【选一个节点】 创建好本地表后,在1个节点创建,会自动同步到其他节点
2create table default.soulboy_order on cluster cluster_2shards_1replicas
3(id Int8,name String) engine =MergeTree order by id;
4
5
6# 【选一个节点】 创建分布式表名 soulboy_order_all,在1个节点创建,会自动同步到其他节点
7create table soulboy_order_all on cluster cluster_2shards_1replicas (
8id Int8,name String
9)engine = Distributed(cluster_2shards_1replicas,default, soulboy_order,hiveHash(id));
10
11# 通过分布式表插入
12insert into soulboy_order_all values(1,'里昂'),(2,'笨象'),(3,'巨象'),(4,'程书远');
13
14# 【任意节点查询-分布式,全部数据】
15SELECT * from soulboy_order_all
16┌─id─┬─name─┐
17│ 1 │ 里昂 │
18│ 3 │ 巨象 │
19└────┴──────┘
20┌─id─┬─name───┐
21│ 2 │ 笨象 │
22│ 4 │ 程书远 │
23└────┴────────┘
24
25# 【ClickHouse1查询,部分数据】
26SELECT * from soulboy_order
27┌─id─┬─name───┐
28│ 2 │ 笨象 │
29│ 4 │ 程书远 │
30└────┴────────┘
31
32# 【ClickHouse2查询,部分数据】
33SELECT * from soulboy_order
34┌─id─┬─name─┐
35│ 1 │ 里昂 │
36│ 3 │ 巨象 │
37└────┴──────┘
向集群中写入数据的两种方法
写入数据到集群的方式 | 说明 |
---|---|
本地表数据表 INSERT➕ 分布式表 SELECT | 自已指定要将哪些数据写入哪些服务器,并直接在每个分片上执行写入。在分布式表上«查询»,在数据表上 INSERT。可以使用任何分片方案,对于复杂业务特性的需求,数据可以完全独立地写入不同的分片 。 |
分布式表 INSERT | 在这种情况下,分布式表会 跨服务器分发插入数据 。为了写入分布式表,必须要配置分片键(最后一个参数)。如果只有一个分片,则写操作在没有分片键的情况下也能工作,这种情况下分片键没有意义 。每个分片都可以在配置文件中定义权重。 默认情况下,权重等于 1。 数据依据分片权重按比例分发到分片上,如果有两个分片,第一个分片的权重是9,而第二个分片的权重是10,则发送 9 / 19 的行到第一个分片, 10 / 19 的行到第二个分片。 |
SpringBoot2.X 整合 ClickHouse 实操
HTTP 接口允许您在任何编程语言的任何平台上使用 ClickHouse。我们使用它在 Java 和 Perl 以及 shell 脚本中工作。在其他部门中,HTTP 接口用于 Perl、Python 和 Go。HTTP 接口比原生接口 受到更多的限制,但它具有更好的兼容性。
在线创建项目
start.spring.io
引入依赖
1<!-- Spring Web -->
2 <dependency>
3 <groupId>org.springframework.boot</groupId>
4 <artifactId>spring-boot-starter-web</artifactId>
5 </dependency>
6 <!-- lombok -->
7 <dependency>
8 <groupId>org.projectlombok</groupId>
9 <artifactId>lombok</artifactId>
10 <optional>true</optional>
11 </dependency>
12 <!-- test -->
13 <dependency>
14 <groupId>org.springframework.boot</groupId>
15 <artifactId>spring-boot-starter-test</artifactId>
16 <scope>test</scope>
17 </dependency>
18 <!-- clickhouse-jdbc -->
19 <dependency>
20 <groupId>ru.yandex.clickhouse</groupId>
21 <artifactId>clickhouse-jdbc</artifactId>
22 <version>0.1.55</version>
23 </dependency>
24 <!--mybatis plus-->
25 <dependency>
26 <groupId>com.baomidou</groupId>
27 <artifactId>mybatis-plus-boot-starter</artifactId>
28 <version>3.4.0</version>
29 </dependency>
application.properties
1server.port=8080
2
3spring.datasource.driver-class-name=ru.yandex.clickhouse.ClickHouseDriver
4spring.datasource.url=jdbc:clickhouse://192.168.10.63:8123/default
5
6mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
7
8logging.level.root=INFO
ClickHouse 项目数据统计需求分析
需求背景
访问商品日志记录
数据,经过 Flink实时计算轻度聚合
之后,存储到 ClickHouse
需求 | 说明 |
---|---|
统计商品访问 pv 数 | 指定商品,统计指定日期范围内,各个省市的访问的pv数量,如7天内有各个城市访问某个商品分布 |
商品访问量波动图 | 指定商品,多天内的访问曲线变化图,如,1~7号商品访问量波动图 |
![]() |
前端显示图标组件(Charts)
库表和数据准备
需求 | 说明 |
---|---|
统计商品访问 pv 数 | 指定商品,统计指定日期范围内,各个省市的访问的pv数量,如7天内有各个城市访问某个商品分布 |
商品访问量波动图 | 指定商品,多天内的访问曲线变化图,如,1~7号商品访问量波动图 |
建表语句
1CREATE TABLE default.visit_stats
2(
3 `product_id` UInt64,
4 `is_new` UInt16,
5 `province` String,
6 `city` String,
7 `pv` UInt32,
8 `visit_time` DateTime
9)
10ENGINE = MergeTree()
11PARTITION BY toYYYYMMDD(visit_time)
12ORDER BY (
13 product_id,
14 is_new,
15 province,
16 city
17 );
插入数据
1### 0代表老用户,1代表新用户,PV是访问量(轻度聚合后)、访问时间
2INSERT into visit_stats values
3('1','1','广东','广州',14323,'2023-01-01 12:11:13'),
4('1','0','广东','广州',4232,'2023-02-12 16:16:13'),
5('1','1','广东','佛山',54323,'2023-03-06 16:11:13'),
6('1','0','广东','东莞',42341,'2023-03-02 16:12:13'),
7('1','1','广东','梅州',52422,'2023-03-09 12:11:13'),
8
9('2','1','广东','广州',14323,'2021-03-01 12:11:13'),
10('2','0','广东','深圳',425232,'2023-04-12 16:16:13'),
11('2','1','广东','佛山',543323,'2022-06-06 16:11:13'),
12('2','0','广东','东莞',42341,'2021-05-02 16:12:13'),
13('2','1','广东','梅州',52422,'2022-01-09 12:11:13'),
14
15('3','1','北京','北京',13132,'2023-01-01 12:11:13'),
16('3','0','广东','广州',533232,'2022-02-16 16:16:13'),
17('4','1','浙江','杭州',663643,'2023-12-06 12:11:13'),
18('4','0','广东','东莞',4142,'2023-11-02 16:12:13'),
19('5','1','湖南','长沙',52123,'2022-01-09 12:11:13'),
20('4','0','湖南','衡阳',4142,'2024-05-02 16:12:13'),
21('5','1','广东','中山',52123,'2024-01-09 12:11:13'),
22
23('2','1','上海','上海',14323,'2021-03-01 12:11:13'),
24('5','0','浙江','宁波',425232,'2023-04-12 16:16:13'),
25('3','1','广东','佛山',543323,'2022-06-06 16:11:13'),
26('2','0','湖南','长沙',42341,'2021-05-02 16:12:13'),
27('2','1','广东','深圳',52422,'2022-01-09 12:11:13')
查询
1SELECT * FROM visit_stats;
函数编写统计 SQL
函数
ClickHouse 中至少存在两种类型的函数 - 常规函数(它们称之为«函数»)和聚合函数。 常规函数的工作就像分别为每一行执行一次函数计算一样(对于每一行,函数的结果不依赖于其他行)。 聚合函数则从各行累积一组值(即函数的结果以来整个结果集)。
函数 | 功能描述 |
---|---|
sum(pv) | 求和 |
select toYear(toDateTime('2024-12-11 11:12:13')) | 年格式 |
select toYYYYMMDD(toDateTime('2024-12-11 11:12:13')) | 日期格式化 |
select toYYYYMMDDhhmmss(toDateTime('2024-12-11 11:12:13')) | 日期时间格式化 |
select toDayOfWeek(toDateTime('2024-12-11 11:12:13')) | 周格式化,1~7,当前时间是本周第几天,下面是周三结果是 3,周日结果是 7 |
select toHour(toDateTime('2024-12-11 11:12:13')) | 小时格式化,提取时间里面的小时,比如 2023-12-29 10:05:10,格式化后是【10】点 |
select toMinute(toDateTime('2024-12-11 11:12:13')) | 分钟格式化,提取时间里面的分钟,比如 2023-12-29 10:05:10,格式化后是【5】分钟 |
select toSecond(toDateTime('2024-12-11 11:12:13')) | 秒格式化,提取时间里面的秒 |
select now() | 获取当前日期时间 |
select today() | 获取当前日期 |
SELECT if(cond, then, else) | 如果条件 cond 的计算结果为非零值,则返回表达式 then 的结果,并且跳过表达式 else 的结果。如果 cond 为零或 NULL,则将跳过 then 表达式的结果,并返回 else 表达式的结果。可以用于统计新老用户 ,例子:SELECT if(1, plus(3, 3), plus(6, 8)) |
select lower('SOULBOYCLASS') | 小写转换 |
select upper('java') | 大写转换 |
select concat('我','在小滴课堂','摸鱼') | 字符串拼接(不能双引号) |
select max(pv), min(pv), avg(pv) from visit_stats | 最大、最小、平均值 |
SELECT protocol('https://abc1024.pub') | URL 协议提取,结果 https |
SELECT domain('https://abc1024.pub') | URL 域名提取,结果 abc1024.pub |
select path('https://abc1024.pub/api/v1/pay?video_id=2&accountNo=999') | URL 路径 path,结果 /api/v1/pay |
统计需求
某个商品在一定的时间范围内(天级别),各地区(城市级别)访问分布
1# 连接
2clickhouse-client --host=192.168.10.63
3
4# 某个商品在一定的时间范围内(天级别),各地区(城市级别)访问分布
5select province,city, sum(pv) pv_count
6from visit_stats where product_id =1
7and toYYYYMMDD(visit_time) BETWEEN '20200101' and '20241212'
8group by province,city order by pv_count desc
9
10┌─province─┬─city─┬─pv_count─┐
11│ 广东 │ 佛山 │ 54323 │
12│ 广东 │ 梅州 │ 52422 │
13│ 广东 │ 东莞 │ 42341 │
14│ 广东 │ 广州 │ 18555 │
15└──────────┴──────┴──────────┘
16
17# 全部商品在一定的时间范围内(天级别),各地区(城市级别)访问分布
18select province,city, sum(pv) pv_count
19from visit_stats where toYYYYMMDD(visit_time) BETWEEN '20200101' and '20241212'
20group by province,city order by pv_count desc
21
22┌─province─┬─city─┬─pv_count─┐
23│ 广东 │ 佛山 │ 1140969 │
24│ 浙江 │ 杭州 │ 663643 │
25│ 广东 │ 广州 │ 566110 │
26│ 广东 │ 深圳 │ 477654 │
27│ 浙江 │ 宁波 │ 425232 │
28│ 广东 │ 梅州 │ 104844 │
29│ 湖南 │ 长沙 │ 94464 │
30│ 广东 │ 东莞 │ 88824 │
31│ 广东 │ 中山 │ 52123 │
32│ 上海 │ 上海 │ 14323 │
33│ 北京 │ 北京 │ 13132 │
34│ 湖南 │ 衡阳 │ 4142 │
35└──────────┴──────┴──────────┘
统计需求
某个商品,多天内的访问曲线图(天级别)
1### 某个商品,多天内的访问曲线图(天级别)
2select
3 toYYYYMMDD(visit_time) date_time_str,
4 sum(pv) pv_count
5 from visit_stats
6 where product_id = 1 and toYYYYMMDD(visit_time) BETWEEN '20200101' and '20241212'
7 group by date_time_str ORDER BY date_time_str desc
8
9┌─date_time_str─┬─pv_count─┐
10│ 20230309 │ 52422 │
11│ 20230306 │ 54323 │
12│ 20230302 │ 42341 │
13│ 20230212 │ 4232 │
14│ 20230101 │ 14323 │
15└───────────────┴──────────┘
SpringBoot2.X 整合 ClickHouse 基础模块搭建
响应工具类 JsonData(封装响应结果给前端)
1import lombok.AllArgsConstructor;
2import lombok.Data;
3import lombok.NoArgsConstructor;
4
5@Data
6@AllArgsConstructor
7@NoArgsConstructor
8public class JsonData {
9
10 /**
11 * 状态码 0 表示成功
12 */
13
14 private Integer code;
15 /**
16 * 数据
17 */
18 private Object data;
19 /**
20 * 描述
21 */
22 private String msg;
23
24
25 /**
26 * 成功,不传入数据
27 *
28 * @return
29 */
30 public static JsonData buildSuccess() {
31 return new JsonData(0, null, null);
32 }
33
34 /**
35 * 成功,传入数据
36 *
37 * @param data
38 * @return
39 */
40 public static JsonData buildSuccess(Object data) {
41 return new JsonData(0, data, null);
42 }
43
44 /**
45 * 失败,传入描述信息
46 *
47 * @param msg
48 * @return
49 */
50 public static JsonData buildError(String msg) {
51 return new JsonData(-1, null, msg);
52 }
53
54
55 /**
56 * 自定义状态码和错误信息
57 *
58 * @param code
59 * @param msg
60 * @return
61 */
62 public static JsonData buildCodeAndMsg(int code, String msg) {
63 return new JsonData(code, null, msg);
64 }
65
66
67}
Model 层
VisitStatsDO
类
1import lombok.Data;
2
3@Data
4public class VisitStatsDO {
5
6 /**
7 * 商品
8 */
9 private Long productId;
10
11 /**
12 * 访问时间
13 */
14 private String visitTime;
15
16
17 /**
18 * 1是新访客,0是老访客
19 */
20 private Integer isNew;
21
22 /**
23 * 访问量
24 */
25 private Integer pv;
26
27 /**
28 * 省份
29 */
30 private String province;
31
32 /**
33 * 城市
34 */
35 private String city;
36
37 /**
38 * ========度量值=========
39 */
40 private Long pvCount=0L;
41
42 /**
43 * 时间的字符串映射,天、小时
44 */
45 private String dateTimeStr;
46}
Mapper 接口
VisitStatsMapper
接口类
1import com.baomidou.mybatisplus.core.mapper.BaseMapper;
2import net.xdclass.demo.model.VisitStatsDO;
3import org.apache.ibatis.annotations.Mapper;
4import org.apache.ibatis.annotations.Param;
5
6import java.util.List;
7
8@Mapper
9public interface VisitStatsMapper extends BaseMapper<VisitStatsDO> {
10
11 /**
12 * 统计总条数
13 * @param productId
14 * @return
15 */
16 int countTotal(@Param("productId") long productId);
17
18 /**
19 * 分页查询
20 * @param productId
21 * @param from
22 * @param size
23 * @return
24 */
25 List<VisitStatsDO> pageVisitRecord(@Param("productId") Long productId , @Param("from") int from, @Param("size") int size);
26
27
28
29 /**
30 * 查询地区访问分布
31 * @param productId
32 * @param startTime
33 * @param endTime
34 * @return
35 */
36 List<VisitStatsDO> queryRegionTrendWithMultiDay(@Param("productId")Long productId, @Param("startTime") String startTime, @Param("endTime")String endTime);
37
38 /**
39 * 查询访问趋势,多天内
40 * @param productId
41 * @param startTime
42 * @param endTime
43 * @return
44 */
45 List<VisitStatsDO> queryVisitTrendWithMultiDay(@Param("productId")Long productId, @Param("startTime") String startTime, @Param("endTime")String endTime);
46}
VisitStatsMapper.xml
D:\Project\ClickHouse\src\main\resources\mapper\VisitStatsMapper.xml
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3<mapper namespace="net.xdclass.demo.mapper.VisitStatsMapper">
4
5 <!-- 通用查询映射结果 -->
6 <resultMap id="BaseResultMap" type="net.xdclass.demo.model.VisitStatsDO">
7 <result column="product_id" property="productId"/>
8 <result column="is_new" property="isNew"/>
9 <result column="province" property="province"/>
10 <result column="city" property="city"/>
11 <result column="pv_count" property="pvCount"/>
12 <result column="visit_time" property="visitTime"/>
13 <result column="date_time_str" property="dateTimeStr"/>
14 </resultMap>
15
16 <!-- 通用查询结果列 -->
17 <sql id="Base_Column_List">
18 product_id,is_new,province,city,pv,visit_time
19 </sql>
20
21 <!-- 统计总条数 -->
22 <select id="countTotal" resultType="java.lang.Integer">
23 select count(1) from visit_stats where product_id=#{productId}
24 </select>
25
26 <!-- 分页查找 -->
27 <select id="pageVisitRecord" resultMap="BaseResultMap">
28 select
29 <include refid="Base_Column_List"/>
30 from visit_stats where product_id=#{productId}
31 order by visit_time desc limit #{from},#{size}
32 </select>
33
34 <!-- 某个商品,多天内的访问曲线图(天级别): 按城市查询 -->
35 <select id="queryRegionTrendWithMultiDay" resultMap="BaseResultMap">
36 select province ,city, sum(pv) pv_count from visit_stats
37 where product_id = #{productId} and toYYYYMMDD(visit_time) BETWEEN #{startTime} and #{endTime}
38 GROUP BY province ,city order by pv_count desc
39 </select>
40
41 <!-- 某个商品,多天内的访问曲线图(天级别): 按时间查询 -->
42 <select id="queryVisitTrendWithMultiDay" resultMap="BaseResultMap">
43 select toYYYYMMDD(visit_time) date_time_str,sum(pv) pv_count from visit_stats
44 where product_id = #{productId} and toYYYYMMDD(visit_time) BETWEEN #{startTime} and #{endTime}
45 GROUP BY date_time_str order by date_time_str desc
46 </select>
47
48</mapper>
Service 层
VisitStatsService
接口
1import net.xdclass.demo.controller.request.VisitRecordPageRequest;
2import net.xdclass.demo.controller.request.VisitTrendQueryRequest;
3import net.xdclass.demo.model.VisitStatsDO;
4
5import java.util.List;
6import java.util.Map;
7
8public interface VisitStatsService {
9
10 /**
11 * 分页查询
12 * @param pageRequest
13 * @return
14 */
15 Map<String, Object> pageVisitRecord(VisitRecordPageRequest pageRequest);
16
17
18 /**
19 * 某个商品,多天内的访问曲线图(天级别) 按城市 or 时间
20 * @param queryRequest
21 * @return
22 */
23 List<VisitStatsDO> queryVisitTrend(VisitTrendQueryRequest queryRequest);
24}
VisitStatsServiceImpl
实现类
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.stereotype.Service;
3import java.util.HashMap;
4import java.util.List;
5import java.util.Map;
6
7@Service
8public class VisitStatsServiceImpl implements VisitStatsService {
9
10 @Autowired
11 private VisitStatsMapper visitStatsMapper;
12
13 /**
14 * 分页查询
15 * @param pageRequest
16 * @return
17 */
18 @Override
19 public Map<String, Object> pageVisitRecord(VisitRecordPageRequest pageRequest) {
20 Map<String, Object> data = new HashMap<>(3);
21 //获取商品id
22 long productId = pageRequest.getProductId();
23 //当前页数
24 int page = pageRequest.getPage();
25 //每页多少条
26 int size = pageRequest.getSize();
27 //统计总记录数
28 int count = visitStatsMapper.countTotal(productId);
29 //分页查询
30 int from = (page - 1) * size;
31 List<VisitStatsDO> visitStatsDOS = visitStatsMapper.pageVisitRecord(productId, from, size);
32 //将信息封装到Map中返回
33 data.put("total", count); //总记录数
34 data.put("current_page", page); //当前页
35 data.put("data", visitStatsDOS); //当前页数据
36 //计算总页数
37 int totalPage;
38 if (count % size == 0) {
39 totalPage = count / size;
40 } else {
41 totalPage = count / size + 1;
42 }
43 data.put("total_page", totalPage);
44 return data;
45 }
46
47 /**
48 * 某个商品,多天内的访问曲线图(天级别) 支持按照城市 or 时间 查询
49 * @param queryRequest
50 * @return
51 */
52 @Override
53 public List<VisitStatsDO> queryVisitTrend(VisitTrendQueryRequest queryRequest) {
54 long productId = queryRequest.getProductId();
55 String type = queryRequest.getType();
56 List<VisitStatsDO> list = null;
57
58 //判断type 城市或者时间
59 if (type.equalsIgnoreCase("region")) {
60 list = visitStatsMapper.queryRegionTrendWithMultiDay(productId,queryRequest.getStartTime(),queryRequest.getEndTime());
61 } else if (type.equalsIgnoreCase("day")) {
62 list = visitStatsMapper.queryVisitTrendWithMultiDay(productId,queryRequest.getStartTime(),queryRequest.getEndTime());
63 }
64 return list;
65 }
66}
Controller 层
分页查询参数接收类 VisitRecordPageRequest
1import lombok.Data;
2
3@Data
4public class VisitRecordPageRequest {
5
6 //商品id
7 private long productId;
8
9 //显示当前第几页
10 private int page;
11
12 //每页显示多少条
13 private int size;
14}
分页查询参数接收类 VisitTrendQueryRequest
1import lombok.Data;
2
3@Data
4public class VisitTrendQueryRequest {
5
6 private long productId;
7
8 private String startTime;
9
10 private String endTime;
11
12 /**
13 * 查询类型: 时间分布 or 城市分布
14 */
15 private String type;
16}
DataController
类
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.web.bind.annotation.RequestBody;
3import org.springframework.web.bind.annotation.RequestMapping;
4import org.springframework.web.bind.annotation.RestController;
5
6import java.util.List;
7import java.util.Map;
8
9@RestController
10@RequestMapping("/api/v1/data")
11public class DataController {
12
13 @Autowired
14 private VisitStatsService visitStatsService;
15
16 /**
17 * 分页查询
18 * @param pageRequest
19 * @return
20 */
21 @RequestMapping("page")
22 public JsonData queryVisitRecord(@RequestBody VisitRecordPageRequest pageRequest) {
23 Map<String, Object> map = visitStatsService.pageVisitRecord(pageRequest);
24 return JsonData.buildSuccess(map);
25 }
26
27
28 /**
29 * 某个商品,多天内的访问曲线图(天级别)
30 * @param queryRequest
31 * @return
32 */
33 @RequestMapping("trend")
34 public JsonData queryVisitTrend(@RequestBody VisitTrendQueryRequest queryRequest){
35 List<VisitStatsDO> list = visitStatsService.queryVisitTrend(queryRequest);
36 return JsonData.buildSuccess(list);
37 }
38
39
40}
测试结果
分页测试
http://192.168.10.88:8080/api/v1/data/page
某个商品,多天内的访问曲线图(天级别) 按照城市查询
http://192.168.10.88:8080/api/v1/data/trend
某个商品,多天内的访问曲线图(天级别) 按照时间查询
http://192.168.10.88:8080/api/v1/data/trend
建表和引擎选择归纳
主要是数据仓库用比较多,一般都是 MergeTree系列引擎
,没有最好,根据业务选择最合适的
-
MergeTree
- 去重合并树引擎
ReplacingMergeTree
- 可以保证幂等性处理
- 聚合合并树引擎
SummingMergeTree
- 可以进行预聚合,性能更高
- 重复作业会导致数据累计
- 去重合并树引擎
-
Log 系列
- 说明:最小功能的轻量级引擎,当需要快速写入许多小表并在以后整体读取它们时效果最佳,即一次写入多次查询,使用大量的小表存储数据,性能会很低,生产环境使用少,练习为主
- 这些引擎是为了需要写入许多小数据量(少于一百万行)的表的场景而开发的
- 将数据存储在磁盘上,不支持索引,数据写入时追加写到文件末尾
- 该引擎没有并发控制,不允许同时读写,当向表中写入数据时,这张表的查询会被阻塞,直至写入完成
- 不支持原子写,如果写过程中断,可能会损坏表数据
- 包括:TinyLog、Log、StripLog
-
主键选择
- 主键可以重复,也可以是多列的组合
- 如果未指明主键,则使用 order by 字段作为主键
- 业务里面 where 条件常用的列,频率高的字段排前面
-
排序键选择
- 业务里面 where 条件常用的列,频率高的字段排前面
-
分区键选择
- 常规根据时间进行分区,分区字段类型建议是 DateTime 类型,不要是 Long 类型
- 不能分区粒度太细,比如秒级别分区键,则插入很容易报错,
常见注意事项和异常问题排查
-
注意点一
- 查询要使用的列,避免 select * 全部字段,浪费 IO 资源
-
注意点二
- 避免大量的小批量数据,插入更新操作,会导致分区过多
- 每次插入 1 条,产生了一个分区,大量写入产生大量临时分区和合并操作浪费资源
- 避免大量的小批量数据,插入更新操作,会导致分区过多
-
注意点三
- JOIN 操作时一定要把数据量小的表放在右边,无论是 Left Join 、Right Join 还是 Inner Join,右表中的每一条记录到左表中查找该记录是否存在,所以右表必须是小表
-
注意点四
- 批量写入数据时,控制每个批次的数据中涉及到的分区的数量,无序的数据导致涉及的分区太多,建议写入之前最好对需要导入的数据进行排序
-
注意点五
- 写入分布式表还是本地表? 建议:数据量不大,写入本地表和分布式表都行
1分布式表不存储数据,本地表存储数据的表
2
3大量数据,日新增500万行以上,分布式表在写入时会在本地节点生成临时数据,会产生写放大,所以会对CPU及内存造成一些额外消耗,服务器merge的工作量增加, 导致写入速度变慢;
4
5数据写入默认是异步的(可以开启同步写但性能会影响),先在分布式表所在的机器进行落盘, 然后异步的发送到本地表所在机器进行存储,中间没有一致性的校验,短时间内可能造成不一致,且如果分布式表所在机器时如果机器出现down机, 会存在数据丢失风险。
6
7建议大数据量尽量少使用分布式表进行写操作,可以根据业务规则均衡的写入本地表;
8
9必须用的话尽量只用读,因为写分布式表对性能影响非常大
-
注意点六
- 单 SQL 查询可以压榨 CPU ,但并发多条查询则不是很强
- 一个分区查询占据一个 CPU,业务需要查询表的多个分区可以多个 CPU 并行处理
-
注意点七
- 没有完整的事务支持,不支持 Transaction
- OLAP 类业务由于数据量非常大,建议数据批量写入,尽量避免数据更新或少更新
-
注意点八
- 在分布式模式下,ClickHouse 会将数据分为多个分片,并且分布到不同节点上,有哪种分片策略
- ClickHouse 提供了丰富的 sharding 策略,让业务可以根据实际需求选用
- random 随机分片:写入数据会被随机分发到分布式集群中的某个节点上
- constant 固定分片:写入数据会被分发到固定一个节点上
- hash column value 分片:按照某一列的值进行 hash 分片
常见异常问题
- 错误码 300,Too many parts
1写入频率过快,使用了不合理的分区键导致总的 part 数目太多,好比-soulboy,直接拿精确到秒的 timestamp 来作为分区键来进行分区,GG了。
- 错误码 252,Too many partitions for single INSERT block (more than 100)
1同一批次写入里包括大于100个分区值,clickhouse认为这样会存在性能问题
2
3让数据是按照天/小时分区的,一批数据里的日期跨度为一年,单次插入可能产生365个分区,导致后台异步合并数据出现问题,也避免跨度过大
4
5解决方案:单批次写入数据,要控制写入分区过多
6
7参数:max_partitions_per_insert_block 限制单个插入块中的最大分区数,默认是100
ClickHouse 高性能查询原因剖析-稀疏索引
密集索引
在密集索引中,数据库中的每个键值都有一个索引记录,可以加快搜索速度,但需要更多空间来存储索引记录本身,索引记录
包含 键值
和 指向磁盘上实际记录的指针
,索引项是连续的。
稀疏索引
在稀疏索引中,不会为每个关键字创建索引记录,而是 为数据记录文件的每个存储块设一个键-指针对
,存储块意味着 块内存储单元连续
案例
- MySQL 的 MyISAM 引擎里面, 使用均为
稀疏索引
; - MySQL 的 Innodb 引擎里面,如果有主键,则主键为
密集索引
- Kafka 里面的索引文件也是采用
稀疏索引
进行构造消息索引 - ClickHouse 的合并树 MergeTree 引擎是
稀疏索引
,默认 index_granularity 设置 8192,新版本的提供了自适应粒度大小的特性
1建表语句最后加这个,可以调整
2SETTINGS index_granularity = 8192
总结
- ClickHouse 一级索引就是【稀疏索引】,可以
大幅减少索引占用的空间
- 默认的
索引力度8192
,假如 1 亿行数据只需要存储 12208 行索引,占用空间小,clickhouse 中 ` 一级索引的数据是常驻内存的,取用速度极快 - 稀疏索引所占空间小,并且
插入和删除时所需维护的开销也小
,缺点是速度比密集索引慢一点
- 默认的
- 密集索引
空间占用多
,比稀疏索引更快的定位一条记录
,缺点就是会占用较多的空间
- 不变思想:
时间换空间、空间换时间
ClickHouse 高性能写入剖析-LSM-Tree 数据结构
磁盘顺序读写和随机读写的性能差距大概是 1 千到 5 千倍之间
- 连续 I/O 顺序读写,磁头几乎不用换道,或者换道的时间很短,性能很高,比如 0.03 * 2000 MB /s
- 随机 I/O 随机读写,会导致磁头不停地换道,造成效率的极大降低,0.03MB/s
ClickHouse 中的 MergeTree 也是类 LSM 树的思想
- 充分利用了
磁盘顺序写的特性,实现高吞吐写能力
,数据写入后定期在后台Compaction
- 在数据导入时全部是顺序 append 写,在后台合并时也是多个段 merge sort 后顺序写回磁盘
- 官方公开 benchmark 测试显示能够达到
50MB-200MB/s的写入吞吐能力
,按照每行100Byte估算,大约相当于50W-200W条/s的写入速度
什么是 LSM-Tree
全称 Log-Structured Merge-Tree 日志结构合并树,但不是树,而是 利用磁盘顺序读写能力,实现一个多层读写的存储结构
- 是一种分层,有序,面向磁盘的数据结构,核心思想是
利用了磁盘批量的顺序写要远比随机写性能高出很多
- 大大
提升了数据的写入能力
,但会牺牲部分读取性能为代价
- HBase、LevelDB、ClickHouse 这些 NoSQL 存储都是
采用的类LSM树结构
- 在 NoSQL 系统里非常常见,基本已经成为必选方案,
为了解决快速读写的问题去设计的
分两个部分理解
Log-Structured
日志结构的,打印日志是一行行往下写,不需要更改,只需要在后边追加就好了Merge-tree
,合并就是把多个合成一个,自上而下
LSM-tree 是一个多层结构,就更一个喷泉树一样,上小下大
- 专门为 key-value 存储系统设计的,最主要的就两个个功能
- 写入 put(k,v)
- 查找 get(k)得到 v
- 首先是内存的 C0 层,保存了所有最近写入的 (k,v),这个
内存结构是有序的
,且可以随时原地更新
,同时支持随时查询
- 接下去的 C1 到 Cn 层都在磁盘上,
每一层都是一个有序的存储结构
- 降低一点读性能,
通过牺牲小部分读性能,换来高性能写
写入流程
- put 操作,首先追加到写前日志(Write Ahead Log),然后加到 C0 层
- 当 C0 层的数据达到一定大小,就把 C0 层 和 C1 层合并,这个过程就是 Compaction(合并)
- 合并出来的新的 C1 会顺序写磁盘,替换掉原来的 C1
- 当 C1 层达到一定大小,会和下层继续合并,合并后删除旧的,留下新的
查询流程
- 最新的数据在 C0 层,最老的数据在 Cn 层
- 查询也是先查 C0 层,如果没有要查的数据,再查 C1,逐层查下去直到最后一层(通过牺牲小部分读性能,换来高性能写)
缺点
- 读放大:读取数据时实际
读取的数据量大于真正的数据量
,在 LSM 树中需要先在C0查看当前key是否存在,不存在继续从Cn层中寻找
- 写放大:写入数据时实际
写入的数据量大于真正的数据量
,在 LSM 树中写入时可能触发 Compact 操作,导致实际写入的数据量远大于该 key 的数据量