大数据-138 ClickHouse MergeTree 实战详解|分区裁剪 × 稀疏主键索引 × marks 标记 × 压缩
TL;DR
- 场景:线上表在 MergeTree 下读放大严重、查询”扫全列”、TTL 不生效
- 结论:用 分区裁剪 + 稀疏主键索引 + mark 粒度 组合,配合压缩与 OPTIMIZE,可把同等查询的 I/O 降到 10–30%
- 产出:一套可复制 DDL/查询与诊断 附 marks/压缩比/TTL 验证方法
版本矩阵
| 组件 | 版本/类型 | 备注 |
|---|---|---|
| ClickHouse Server | 24.x/25.x | .mrk2 为常见标记扩展名;默认 index_granularity = 8192 |
| OS/FS | Ubuntu 22.04 / ext4 | 以本机为例;不同 FS 仅影响 I/O 细节 |
| 客户端 | clickhouse-client | 用于跑 SQL 与系统表查询 |
| 工具 | clickhouse-compressor | 查看列压缩统计 |
MergeTree
数据存储
ClickHouse 是一个 列式存储 数据库,这意味着每一列的数据是单独存储的,而不是像行式数据库那样将每一行作为一个整体来存储。列式存储的优势在于,它可以针对特定的查询只读取相关的列,大大减少了 I/O 操作,尤其在进行聚合或过滤操作时表现出色。
表由按主键排序的数据片段组成。 当数据被插入到表中时,会分成数据片段并按主键的字典序排序。
ClickHouse会为每个数据片段创建一个索引文件,索引文件包括每个索引行的主键值,索引行号定义为 n * index_granularity。
按列存储
在MergeTree中数据按列存储,具体到每个字段列,都拥有一个bin数据文件。 按列存储的好处:
- 更好的压缩
- 最小化数据扫描范围
MergeTree往bin里存数据的步骤:
- 对数据进行压缩
- 根据ORDER BY排序
- 数据以压缩块的形式写入bin文件
压缩数据块
压缩数据块由两部分组成:
- 头信息(固定使用9位字节表示)
- 压缩数据
头信息格式:CompressionMethod_CompressedSize_UnccompressedSize
可以使用如下命令查看压缩情况:
cd /var/lib/clickhouse/data/default/mt_table/202407_1_1_0
clickhouse-compressor --stat data.bin out.log
压缩数据块生成规则(默认8192的索引粒度):
- 如果单批次数据 x < 64k,则继续读下一个批次,找到 size > 64k 则生成下一个数据块
- 如果单批次数据 64k < x < 1M 则直接生成下一个数据块
- 如果 x > 1M,则按照 1M 切分数据,剩下的数据继续按照上述规则执行
数据标记
在 ClickHouse 中,mark 是索引的一部分,用于标记数据文件中数据块的开始位置。标记帮助快速定位需要查询的数据块。
标记包含以下信息:
- 块开始的位置
- 块中每列的最小值和最大值
- 其他元数据信息
标记的粒度可以通过配置 index_granularity 来控制。标记粒度越小,标记文件占用的空间越大,但查询性能也会越好。
.mrk文件:将索引primary.idx和数据文件.bin建立映射关系。
- 数据标记和索引区间是对齐的,根据索引区间的下标编号,就能找到数据标记
- 每一个[Column].bin都有一个[Column].mrk与之对应
- .mrk文件记录数据在bin文件中的偏移量
分区、索引、标记和压缩协同
分区(Partition)
ClickHouse 的分区机制是一种将大型表数据分割成独立逻辑段的存储策略。每个分区相当于表的一个独立子集。
分区键定义:
- 单列分区:
PARTITION BY toYYYYMM(date_column) - 多列分区:
PARTITION BY (toYYYYMM(date_column), city) - 表达式分区:
PARTITION BY sipHash64(user_id) % 4
主要优势:
- 查询效率提升 - 分区裁剪
- 数据管理便捷 - TTL、删除、归档、迁移
- 时间序列处理
索引(Index)
ClickHouse 的索引与传统数据库不同,主要依赖主键索引和稀疏索引。
- 主键索引:决定数据排序方式,辅助数据查询,是一种稀疏索引
- 稀疏索引:仅针对某些行进行标记,减少存储开销
- Skip Indexes:minmax、set、bloom_filter 等
标记(Marks)
标记是稀疏索引的实现基础。查询时利用标记跳过不需要的块,加速查询过程。
压缩协同(Compression)
ClickHouse 提供多种压缩算法:
- LZ4(默认):快速、轻量级
- ZSTD:高压缩率
- Delta、DoubleDelta:专为时间序列数据设计
写入过程
- 生成分区目录
- 合并分区目录
- 生成primary.idx索引文件,每一列的bin和mrk文件
查询过程
- 根据分区缩小查询范围
- 根据数据标记、缩小查询范围
- 解压数据块
MergeTree的TTL
TTL:time to live 数据存活时间,可以设置在表上或列上。
-- TTL 设置列
CREATE TABLE ttl_table_v1 (
id String,
create_time DateTime,
code String TTL create_time + INTERVAL 10 SECOND,
type UInt8 TTL create_time + INTERVAL 10 SECOND
) ENGINE = MergeTree
PARTITION BY toYYYYMM(create_time)
ORDER BY id;
-- 插入数据
INSERT INTO ttl_table_v1 VALUES
('A0000', now(), 'c1', 1),
('A0000', now() + INTERVAL 10 MINUTE, 'c1', 1);
-- 手动触发合并
OPTIMIZE TABLE ttl_table_v1 FINAL;
-- TTL 设置表
CREATE TABLE tt1_table_v2 (
id String,
create_time DateTime,
code String,
type UInt8
) ENGINE = MergeTree
PARTITION BY toYYYYMM(create_time)
ORDER BY create_time
TTL create_time + INTERVAL 1 DAY;
-- 修改TTL
ALTER TABLE tt1_table_v1 MODIFY TTL create_time + INTERVAL + 3 DAY;
可复现简易脚本
-- 1) 建表:月分区 + 典型主键
CREATE TABLE mt_demo (
counter_id UInt32,
dt DateTime,
city LowCardinality(String),
v Float64
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(dt)
ORDER BY (counter_id, dt)
SETTINGS index_granularity = 8192;
-- 2) 造数:1000 万行
INSERT INTO mt_demo
SELECT
randUniform(1, 50000) AS counter_id,
now() - randUniform(0, 86400*90) AS dt,
concat('c', toString(randUniform(1, 5000))) AS city,
randCanonical() AS v
FROM numbers(10000000);
-- 3) 典型查询:裁剪 + 跳读
SELECT avg(v)
FROM mt_demo
WHERE counter_id = 42 AND dt BETWEEN now()-86400 AND now();
-- 4) 观察系统表:分区/part/marks/压缩
SELECT partition, name, rows, bytes_on_disk
FROM system.parts WHERE table = 'mt_demo' AND active;
SELECT partition, name, column, marks, data_compressed_bytes
FROM system.parts_columns WHERE table = 'mt_demo' AND active
ORDER BY partition, name, column;
分区策略与维护
-- 查看分区/行数/空间
SELECT partition, rows, disk_size FROM system.parts WHERE table='mt_demo' AND active;
-- 单分区优化
OPTIMIZE TABLE mt_demo PARTITION 202510 FINAL;
-- 移动/删除/分离
ALTER TABLE mt_demo DROP PARTITION 202409;
ALTER TABLE mt_demo DETACH PARTITION 202409;
错误速查
| 症状 | 可能根因 | 快速定位方法 | 处理方案 |
|---|---|---|---|
| WHERE 命中但几乎全表扫 | 主键不含过滤列 / 粒度过大 | EXPLAIN AST/PIPELINE、看 read_rows | 调整主键与 index_granularity,或加 skip index |
| TTL 没生效 | 数据未到期 / 未触发合并 | 用”过去时间”造数 + OPTIMIZE FINAL | 等待后台合并或人工触发 |
| 磁盘飙升 | 小分区/小 part 过多 | system.parts 看 part 数 | 调整分区粒度、加速合并 |