www.colben.cn/content/post/ch-mergetree.md
2021-11-14 14:32:08 +08:00

389 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "ClickHouse 表引擎之 MergeTree(合并树)"
date: 2020-10-06T19:55:00+08:00
lastmod: 2020-10-07T22:54:00+08:00
tags: []
categories: ["clickhouse"]
---
# 简介
- 支持主键索引、数据分区、数据副本、数据采样、ALTER 操作
- 扩展表引擎丰富,生产环境中大多使用该表引擎
- 数据以片段形式写入磁盘,后台定期合并片段到各分区相应片段
# 数据表
- 建表语句
```sql
CREATE TABLE [IF NOT EXISTS] [db_name.]table_name(
...
) ENGINE = MergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...];
```
- PARTITION BY: 分区键,选填,支持单字段、多字段和表达式,默认生成一个 all 分区
- ORDER BY: 排序键,必填,支持单列和元组(包含多列)
- PRIMARY KEY: 主键,选填,默认与排序键相同,允许重复数据
- SAMPLE BY: 抽样,选填,该配置需在主键中同时声明
- SETTINGS: 其他参数,选填,示例如下
- index_granularity: 索引粒度,默认 8192通常不需要修改
- index_granularity_bytes: 每批次写入的数据大小,用于自适应索引间隔,默认 10MB0 表示无视数据大小
- enable_mixed_granularity_parts: 自适应索引间隔,默认开启
- merge_with_ttl_timeout: TTL 合并间隔时间,默认 86400(1天)
- storage_policy: 数据在硬盘上的存储策略
# 数据文件
- 目录和文件
```
table_name # 表名目录
|___ partition_1 # 分区目录
|___ checksums.txt # 校验文件,二进制,记录该分区目录中其他文件的大小和哈希值
|___ columns.txt # 列信息文件,明文,记录该分区下的列字段信息
|___ count.txt # 计数文件,明文,记录该分区总行数
|___ primary.txt # 一级索引文件,二进制,存放稀疏索引
|___ {column_name}.bin # 列数据文件,默认 LZ4 压缩
|___ {column_name}.mrk # 列标记文件,二进制,记录对应数据文件(.bin)中的数据偏移量
|___ {column_name}.mrk2 # 如果表使用了自适应索引间隔,那么对应的列字段标记文件以 .mrk2 命令
|___ partition.dat # 保存当前分区表达式的值,二进制
|___ minmax_{column_name}.idx # 保存当前分区字段对应原始数据的最小和最大值,二进制
|___ skp_idx_{column_name}.idx # 二级索引(跳数索引)文件
|___ skp_idx_{column_name}.mrk # 二级索引(跳数索引)列的标记文件
```
# 数据分区
## 分区 ID
- 单字段分区 ID 生成规则
类型 | 样例数据 | 分区表达式 | 分区 ID
---- | ---- | ---- | ----
无分区键 | - | 无 | all
整型 | 18,19,20 | PARTITION BY Age | 分区1: 18分区2: 19分区3: 20
整型 | 'A0', 'A1', 'A2' | PARTITION BY length(Code) | 分区1: 2
日期 | 2020-10-05, 2020-10-06 | PARTITION BY EventTime | 分区1: 20201005分区2: 20201006
日期 | 2020-09-25, 2020-10-06 | PARTITION BY toYYYYMM(EventTime) | 分区1: 202009分区2: 202010
其他 | 'www.colben.cn' | PARTITION BY URL | 分区1: {128 位 Hash 算法}
- 多字段(元组)分区时, 先按单字段生成对应 ID再用 "-" 拼接
## 分区目录
- 分区目录命名: PartitionID_MinBlockNum_MaxBlockNum_Level例如 202010_1_1_0
- PartitionID: 分区 ID
- MinBlockNum: 最小数据块编号,**表内全局累加**,从 1 开始
- MaxBlockNum: 最大数据块编号,**表内全局累加**,从 1 开始
- Level: 分区合并次数,从 0 开始
- 不同批次写入的数据,即使分区相同,也会存储在不同目录中
- 后台在默认 10-15 分钟后自动合并分区相同的多个目录,也可以手动执行 optimize 语句
- 合并成功后,旧分区目录被置为非激活状态,在默认 8 分钟后被后台删除
- 合并后新目录的命名规则:
- MinBlockNum: 所有合并目录中的最小 MinBlockNum
- MaxBlockNum: 所有合并目录中的最大 MaxBlockNum
- Level: 所有合并目录中的最大 Level 值并加 1
# 数据索引
- 常驻内存
- 一级索引是稀疏索引,间隔 index_granularity (默认 8192) 行数据生成一条索引记录
- 二级索引又称跳数索引,有数据的聚合信息构建而成,在 CREATE 语句中定义如下:
```sql
INDEX index_name expr TYPE index_type(...) GRANULARITY granularity
-- GRANULARITY 指定一行跳数索引聚合的数据段(index_granularity 区间)的个数
```
- 跳数索引类型
- minmax: 记录一段数据内的最小值和最大值
```sql
INDEX index_name ID TYPE minmax GRANULARITY 5
```
- set: 记录字段或表达式的无重复取值
```sql
INDEX index_name (length(ID)) TYPE set(100) GRANULARITY 5
-- 每个数据段(index_granularity 区间)内最多记录 100 条 set 索引记录
```
- ngrambf_v1: 只支持 String 和 FixedString只能提升 in、notIn、like、equals 和 notEquals 性能
```sql
INDEX index_name (ID, Code) TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 5;
-- 3: token 长度,把数据切割成长度为 3 的短语
-- 256: 布隆过滤器大小
-- 2: 哈希函数个数
-- 0: 哈希函数随机种子
```
- tokenbf_v1: ngrambf_v1 变种,按照非 字母和数字 自动分割
```sql
INDEX index_name ID TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5;
-- 注意传参时不需要指定 token 长度
```
# 数据存储
- 按列独立存储
- 默认 LZ4 压缩
- 按照 order by 排序
- 以数据压缩块形式写入 .bin 文件,规则如下:
- 单批次数据 < 64KB继续获取下一批数据
- 64KB <= 单批次数据 <= 1MB直接生成压缩数据块
- 单批次数据 > 1MB按照 1MB 大小截断并生成数据块,剩余数据继续按前面规则执行
# 数据标记
- 使用 LRU 策略缓存
- 每一行标记数据记录的是一个数据片段在 .bin 文件中的读取位置
# 数据写入
- 生成分区目录,合并分区相同的目录
- 按照 index_granularity 索引粒度,生成一级索引、二级索引、数据标记文件和数据压缩文件
# 数据查询
- 借助分区、索引、数据标记来缩小扫描范围
- 如果未指定查询条件或条件未匹配到索引MergeTree 仍可借助数据标记多线程读取压缩数据块
# 数据 TTL
## TTL 机制
- TTL 信息保存在分区目录中的 ttl.txt 中
- 支持的时间单位: SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR
- 触发 TTL 删除过期数据
- 后台分区合并
- merge_with_ttl_timeout 合并频率,默认 86400 秒
- 手动执行 OPTIMIZE 语句
- 合并分区时TTL 全部到期的数据分区不会参与合并
- 控制全局 TTL 合并任务
```sql
-- 启动
SYSTEM START TTL MERGES;
-- 停止
SYSTEM STOP TTL MERGES;
```
## 列级别 TTL
- 到达时间时,列数据被还原为对应数据类型的默认值
- 主键字段不能被声明 TTL
- 声明列级别 TTL
```sql
CREATE TABLE table_name(
id String,
create_time DateTime,
code String TTL create_time + INTERVAL 10 SECOND,
type UInt8 TTL create_time + INTERVAL 16 SECOND
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(create_time)
ORDER BY id;
```
- 修改列级别 TTL
```sql
ALTER TABLE table_name MODIFY COLUMN code String TTL create_time + INTERVAL 1 DAY;
```
## 表级别 TTL
- 到达时间时,删除过期的数据行
- 声明表级别 TTL
```sql
CREATE TABLE table_name(
id String,
create_time DateTime,
code String TTL create_time _ INTERVAL 1 MINUTE,
type UInt8
) ENGINE = MergeTree
PARTITION BY toYYYYMM(create_time)
ORDER BY create_time
TTL create_time + INTERVAL 1 DAY;
```
- 修改表级别 TTL
```sql
ALTER TABLE table_name MODIFY TTL create_time + INTERVAL 3 DAY;
```
# 存储策略
- 最小移动单元是数据分区
- 三大策略: 默认、JBOD、HOT/COLD
## 默认策略
- 无需配置,所有分区自动保存至 config.xml 中的 path 目录下
## JOB 策略
- 适用于多磁盘无 RAID 场景
- INSERT 或 MERGE 产生的新分区轮询写入各磁盘,类似 RAID0
- 磁盘故障时,丢掉相应数据,需要副本机制保障数据可靠性
## HOT/COLD 策略
- 适用于已挂载不同类型磁盘的场景
- 把磁盘划分到 HOT 和 COLD 两个区域HOT 使用 SSD注重性能CODE 使用 HDD注重经济
- 单个区域内可应用 JBOD 策略
## 配置策略
- 配置示例
```xml
<storage_configuration>
<disks>
<!-- 磁盘名称,全局唯一 -->
<disk_hot_0>
<!-- 存储目录,注意 clickhouse 用户有权限读写该目录 -->
<path>/ch/ssd0</path>
<!-- 磁盘预留空间,选填 -->
<keep_free_space_bytes>1073741824</keep_free_space_bytes>
</disk_hot_0>
<disk_hot_1>
<path>/ch/ssd1<path>
</disk_hot_1>
<disk_cold_0>
<path>/ch/hdd0<path>
<keep_free_space_bytes>2147483648</keep_free_space_bytes>
</disk_cold_0>
<disk_cold_1>
<path>/ch/hdd1<path>
</disk_cold_1>
<disk_cold_2>
<path>/ch/hdd2<path>
</disk_cold_2>
</disks>
<policies>
<!-- 策略名称,全局唯一 -->
<policy_jbod_0>
<volumes>
<!-- 卷名称,全局唯一 -->
<volume_jbod_0>
<!-- 指定该卷内使用的磁盘 -->
<disk>disk_hot_0</disk>
<disk>disk_hot_1</disk>
<!-- 单个 disk 中一个分区的最大存储阈值,选填 -->
<!-- 超出阈值后,该分区的其他数据会写入下一个该卷内下一个 disk -->
<max_data_part_size_bytes>1073741824</max_data_part_size_bytes>
</volume_jbod_0>
</volumes>
</policy_jbod_0>
<policy_hot_cold_0>
<volumes>
<volume_hot_0>
<disk>disk_hot_0</disk>
<disk>disk_hot_1</disk>
</volume_hot_0>
<volume_cold_0>
<disk>disk_cold_0</disk>
<disk>disk_cold_1</disk>
<disk>disk_cold_2</disk>
</volume_cold_0>
</volumes>
<!-- 卷可用空间因子,默认 0.1,选填 -->
<!-- 如果当前卷可用空间小于 20%,则数据会自动写入下一个卷 -->
<move_factor>0.2</move_factor>
</policy_hot_cold_0>
</policies>
</storage_configuration>
```
- clickhouse 用户需要有权限读写各存储目录
- 存储配置不支持动态更新
- 存储磁盘系统表: system.disks
- 存储策略系统表: system.storage_policies
- 移动分区到其他 disk
```sql
ALTER TABLE table_name MOVE PART 'part_name' TO DISK 'disk_name';
```
- 移动分区到其他 volume
```sql
ALTER TABLE table_name MOVE PART 'part_name' TO VOLUME 'volume_name';
```
# ReplacingMergeTree
- 依据 ORDER BY 字段去重
- 合并分区时,**以分区为单位**删除重复数据
- 声明
```sql
ENGINE = ReplacingMergeTree(version_column)
```
- version_column 选填,指定一个 UInt\*、Date 或 DateTime 字段作为版本号
- 未指定 version_column 时,保留同一组重复数据中的最后一行
- 指定 version_column 时,保留同一组重复数据中该字段取值最大的一行
# SummingMergeTree
- 场景: 用户只需要汇总结果,不关心明细
- 依据 ORDER BY 字段聚合
- 合并分区时,触发条件聚合,**以分区为单位**把同一分组下的多行数据汇总成一行
- 声明:
```sql
ENGINE = SummingMergeTree((col1,col2, ...))
```
- col1、col2 选填,不可指定主键,指定被 SUM 汇总的数值类型字段
- 未指定任何汇总字段时,默认汇总所有非主键的数值类型字段
- 非汇总字段保留同组内的第一行数据
- 汇总嵌套字段时,字段名需以 Map 为后缀,默认嵌套字段中第一列作为聚合 Key其他以 \*Key、\*Id、\*Type 未后缀名的列会和第一列组成复合 Key
# AggregatingMergeTree
- 预先计算聚合数据,二进制格式存入表中,空间换时间,可看成是 SummingMergeTree 的*升级版*
- 依据 ORDER BY 字段聚合
- 使用 AggregationFunction 字段类型定义聚合函数和字段
- 分区合并时,触发**以分区为单位**的合并计算
- 非汇总字段保留同组内的第一行数据
- 写数据时调用 \*State 函数,查询时调用 \*Merge 函数
- 一般用作物化视图的表引擎,与普通 MergeTree 搭配使用,示例如下
- 创建明细数据表,俗称底表
```sql
CREATE TABLE table_name(
id String,
city String,
code String,
value Uint32
) ENGINE = MergeTree()
PARTITION BY city
ORDER BY (id, city);
```
- 创建物化视图
```sql
CREATE MATERIALIZED VIEW view_name
ENGINE = AggregatingMergeTree()
PARTITION BY city
ORDER BY (id, city)
AS SELECT
id,
city,
uniqState(code) AS code,
sumState(value) AS value
FROM table_name
GROUP BY id, city;
```
- 使用常规 SQL 面向底表增加数据
- 面向物化视图查询
```sql
SELECT id, sumMerge(value), uniqMerge(code) FROM agg_view GROUP BY id,city;
```
# CollapsingMergeTree
- 以增代删
- 声明
```sql
ENGINE = CollapsingMergeTree(sign)
```
- 定义 sign 标记字段Int8 类型1 代表有效,-1 代表无效
- 依据 ORDER BY 字段作为数据唯一性依据
- 规则
- 如果 sign=1 比 sign=-1 多一行,则保留最后一行 sign=1 的数据
- 如果 sign=-1 比 sign=1 多一行,则保留第一行 sign=-1 的数据
- 如果 sign=-1 和 sign=1 一样多,且最后一行是 sign=1则保留第一行 sign=-1 和最后一行 sign=1 的数据
- 如果 sign=-1 和 sign=1 一样多,且最后一行是 sign=-1则不保留任何数据
- 其他情况打印告警日志
- 合并分区时,触发**以分区为单位**的数据折叠
- 严格要求数据写入顺序,只有先写入 sign=1再写入 sign=-1才能正常折叠
# VersionedCollapsingMergeTree
- 与 CollapsingMergeTree 类似,但对数据写入顺序没有要求
- 声明
```sql
ENGINE = VersionedCollapsingMergeTree(sign, ver)
```
- ver 是 UInt8 类型的版本号字段
- 每个分区内的数据都以 ORDER BY column_name, ver DESC 排序