大数据-138 ClickHouse MergeTree 实战详解|分区裁剪 × 稀疏主键索引 × marks 标记 × 压缩

TL;DR

  • 场景:线上表在 MergeTree 下读放大严重、查询”扫全列”、TTL 不生效
  • 结论:用 分区裁剪 + 稀疏主键索引 + mark 粒度 组合,配合压缩与 OPTIMIZE,可把同等查询的 I/O 降到 10–30%
  • 产出:一套可复制 DDL/查询与诊断 附 marks/压缩比/TTL 验证方法

版本矩阵

组件版本/类型备注
ClickHouse Server24.x/25.x.mrk2 为常见标记扩展名;默认 index_granularity = 8192
OS/FSUbuntu 22.04 / ext4以本机为例;不同 FS 仅影响 I/O 细节
客户端clickhouse-client用于跑 SQL 与系统表查询
工具clickhouse-compressor查看列压缩统计

MergeTree

数据存储

ClickHouse 是一个 列式存储 数据库,这意味着每一列的数据是单独存储的,而不是像行式数据库那样将每一行作为一个整体来存储。列式存储的优势在于,它可以针对特定的查询只读取相关的列,大大减少了 I/O 操作,尤其在进行聚合或过滤操作时表现出色。

表由按主键排序的数据片段组成。 当数据被插入到表中时,会分成数据片段并按主键的字典序排序。

ClickHouse会为每个数据片段创建一个索引文件,索引文件包括每个索引行的主键值,索引行号定义为 n * index_granularity。

按列存储

在MergeTree中数据按列存储,具体到每个字段列,都拥有一个bin数据文件。 按列存储的好处:

  • 更好的压缩
  • 最小化数据扫描范围

MergeTree往bin里存数据的步骤:

  1. 对数据进行压缩
  2. 根据ORDER BY排序
  3. 数据以压缩块的形式写入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

主要优势

  1. 查询效率提升 - 分区裁剪
  2. 数据管理便捷 - TTL、删除、归档、迁移
  3. 时间序列处理

索引(Index)

ClickHouse 的索引与传统数据库不同,主要依赖主键索引和稀疏索引。

  • 主键索引:决定数据排序方式,辅助数据查询,是一种稀疏索引
  • 稀疏索引:仅针对某些行进行标记,减少存储开销
  • Skip Indexes:minmax、set、bloom_filter 等

标记(Marks)

标记是稀疏索引的实现基础。查询时利用标记跳过不需要的块,加速查询过程。

压缩协同(Compression)

ClickHouse 提供多种压缩算法:

  • LZ4(默认):快速、轻量级
  • ZSTD:高压缩率
  • Delta、DoubleDelta:专为时间序列数据设计

写入过程

  1. 生成分区目录
  2. 合并分区目录
  3. 生成primary.idx索引文件,每一列的bin和mrk文件

查询过程

  1. 根据分区缩小查询范围
  2. 根据数据标记、缩小查询范围
  3. 解压数据块

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 数调整分区粒度、加速合并