--- 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: 每批次写入的数据大小,用于自适应索引间隔,默认 10MB,0 表示无视数据大小 - 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 /ch/ssd0 1073741824 /ch/ssd1 /ch/hdd0 2147483648 /ch/hdd1 /ch/hdd2 disk_hot_0 disk_hot_1 1073741824 disk_hot_0 disk_hot_1 disk_cold_0 disk_cold_1 disk_cold_2 0.2 ``` - 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 排序