MergeTree 的数据单元是 Part——一次 insert
block 或一次 merge 输出的
不可变目录。查询、merge、副本同步、mutation
都围绕 Part 展开。搞清
columns.txt、.bin、.mrk2、primary.idx
各干什么,是读 system.parts、诊断 parts
过多、理解 PREWHERE 剪枝的前提。
本文对照 ClickHouse 24.x 官方文档 MergeTree table
engine 与源码
src/Storages/MergeTree/IMergeTreeDataPart*。本环境未安装
ClickHouse,实验步骤可复现,正文不粘贴未执行的命令输出。
一、Part 在表生命周期中的位置
flowchart LR
INS[INSERT block] --> SORT[内存按 ORDER BY 排序]
SORT --> NEW[新 Part 目录]
NEW --> ACTIVE[Active Parts 可读]
ACTIVE --> MERGE[Background Merge]
MERGE --> BIGGER[更大 Part]
BIGGER --> OUT[旧 Part Outdated 删除]
| 概念 | 含义 |
|---|---|
| Partition | PARTITION BY 表达式决定的逻辑分桶,对应
data/db/table/ 下子目录或前缀 |
| Part | 分区内一段连续排序数据,目录名含块号与 level,如
202406_1_1_0 |
| Block | 内存 insert 缓冲,落盘成 Part |
同一分区可同时存在 多个 Active Part——读查询合并多 Part 结果(类似 LSM 多层 SST,见 LSM 概览)。
二、Part 目录结构(Wide 格式)
典型 Wide Part(列独立文件),路径形如:
/var/lib/clickhouse/data/<db>/<table>/202406_1_1_0/
├── checksums.txt
├── columns.txt
├── count.txt
├── primary.idx
├── event_date.bin
├── event_date.mrk2
├── user_id.bin
├── user_id.mrk2
├── value.bin
├── value.mrk2
├── minmax_event_date.idx # 若定义 PARTITION BY
└── skp_idx_<name>.idx / .mrk2 # 若定义跳数索引
flowchart TB
PART[Part 目录]
PART --> META[columns.txt / count.txt / checksums.txt]
PART --> PK[primary.idx 稀疏主键]
PART --> COL1[event_date.bin + .mrk2]
PART --> COL2[user_id.bin + .mrk2]
PK --> G1[Granule 0]
PK --> G2[Granule 1]
COL1 --> G1
COL1 --> G2
2.1 columns.txt
文本格式列清单:名称、类型、默认表达式、编解码器。Part 创建时冻结 schema;ALTER ADD COLUMN 产生新 Part 或 mutation,旧 Part 无新列(读时补默认值)。
2.2 checksums.txt
各文件大小与 hash(如 CityHash128)。副本 fetch、启动加载时校验完整性。
2.3 count.txt
Part 总行数,\(O(1)\) 元数据;与 granule 数、Mark 数一致推导关系见 §四。
三、Granule:最小读索引单元
官方定义:Granule 是 ClickHouse SELECT 时不进一步拆分的最小数据集;每个 granule 含整数行,首行 PK 值写入索引。
控制参数(MergeTree settings):
| Setting | 作用 |
|---|---|
index_granularity |
Mark 间最大行数,默认 8192 |
index_granularity_bytes |
自适应 granule 字节上限,默认 10 MiB |
enable_mixed_granularity_parts |
启用混合粒度 |
行宽极大时,单 granule 可能 少于 8192 行甚至 1
行(单行大于
index_granularity_bytes)。
granule 数估算(理想均匀):
\[ G pprox \left\lceil rac{ ext{rows}}{ ext{index\_granularity}} ight ceil \]
自适应粒度下用实际 Mark
数为准(system.parts.primary_key_bytes_in_memory
等)。
四、Mark
文件:.mrk / .mrk2 /
.mrk3
Mark 把 逻辑 granule 映射到 列
.bin 字节位置。
| 扩展名 | 布局 | 每 Mark 内容 |
|---|---|---|
.mrk |
旧固定粒度 Wide | 压缩块偏移 + granule 在块内偏移 |
.mrk2 |
现代 Wide(自适应) | 上述 + granule 行数 |
.mrk3 |
Compact Part | 指向 data.compact.bin |
读路径:primary.idx 定 granule 下标 → 各列
.mrk2 同下标 → 读 .bin 压缩块 →
解压 → 取 granule 行范围。
sequenceDiagram
participant Q as Query
participant PK as primary.idx
participant M as column.mrk2
participant B as column.bin
Q->>PK: PK 谓词 → granule range
Q->>M: 下标 g_lo..g_hi
M->>B: 压缩块偏移
B->>Q: 解压列向量
源码:MergeTreeMarksLoader.cpp、MergeTreeMarkType.h。
五、.bin
列数据与压缩块
每个 .bin 由 多个压缩块
串联;一个压缩块可含 多个 granule
的列数据(列存按列压缩,块边界与 granule 边界不对齐)。
流程:
- Mark 指向压缩块起始偏移。
- 读整块压缩数据,解压到内存 buffer。
- 按 granule 内偏移切分列向量片段。
- 编解码器链(
CODEC(Gorilla, ZSTD)等)逆序应用。
这与 PG 页内 tuple 完全不同:无「页 = 多列行混合」,而是 列方向压缩块。
六、稀疏主键
primary.idx
primary.idx 存 每个 granule
第一条行的 PRIMARY KEY 列值(扁平数组,按 granule
顺序)。数据按 ORDER BY 物理排序,故 PK
单调,可用二分定位谓词范围。
不是:
- 唯一索引
- 行指针(不含 offset 到行)
是:
- granule 级剪枝入口,配合 Mark 读列
自 23.5+ 起 primary.idx
可压缩落盘(compress_primary_key),加载时解压进内存;仍保持「小
enough 常驻内存」设计目标(官方 MergeTree 文档
Primary Keys and Indexes)。
七、分区元数据
PARTITION BY toYYYYMM(d) 时常见:
partition.dat:分区值minmax_<col>.idx:Part 内该列 min/max,用于 partition pruning
目录层级:detached/ 存放 DETACH
的 Part;mutation 产生带 mut
后缀的中间态 Part。
八、Wide vs Compact
| Wide | Compact | |
|---|---|---|
| 列文件 | 每列 .bin + .mrk2 |
data.compact.bin + .mrk3 |
| 触发 | Part 较大(默认 ≥10 MiB) | 小 Part |
| 读少列 | 只读目标列文件 | 可能解压 granule 内多列 |
| merge 后 | 常合并为 Wide | 小 Part 合并 |
Settings:min_bytes_for_wide_part、min_rows_for_wide_part(MergeTree
settings)。
九、Part 命名与 level
目录名模式:{partition_id}_{min_block}_{max_block}_{level}
level:merge 代数,越大表示经历越多 merge。- 同分区多个 Part 的 block 号区间不重叠(merge 归并后生成新区间)。
system.parts
字段:name、rows、bytes_on_disk、level、data_version
等。
十、源码:IMergeTreeDataPart
| 符号 | 文件 | 职责 |
|---|---|---|
IMergeTreeDataPart |
IMergeTreeDataPart.h |
Part 抽象:加载、列、索引 |
MergeTreeDataPartWide |
MergeTreeDataPartWide.cpp |
Wide 布局 |
MergeTreeDataPartCompact |
MergeTreeDataPartCompact.cpp |
Compact 布局 |
MergeTreeDataPartWriter |
MergeTreeDataPartWriter*.cpp |
写 Part |
MergeTreeMarksLoader |
MergeTreeMarksLoader.cpp |
加载 Mark |
MergeTreeIndexGranularity |
MergeTreeIndexGranularity.cpp |
granule 边界 |
加载流程见附录(loadColumnsChecksumsIndexes)。
十一、实验:clickhouse-local 观察 Part
本环境未安装 ClickHouse。 读者在 24.x 执行:
mkdir -p /tmp/ch-local && cd /tmp/ch-local
clickhouse-local --path ./ch_data --multiquery <<'SQL'
CREATE TABLE t (
d Date,
id UInt64,
s String,
v Float64
) ENGINE = MergeTree()
ORDER BY (d, id);
INSERT INTO t
SELECT '2024-06-01', number, concat('s', toString(number)), number / 3.0
FROM numbers(50000);
SQL查找 Part 路径(因 --path 而异):
find ./ch_data -name 'columns.txt' | head -5
find ./ch_data -name '*.mrk2' | head -5对照 §二
列表检查文件是否齐全。请在本机执行后自行记录
ls 输出,此处不伪造。
系统表(server 模式):
SELECT name, part_type, rows, bytes_on_disk, primary_key_bytes_in_memory
FROM system.parts
WHERE database = currentDatabase() AND table = 't' AND active;part_type 为 Wide 或
Compact。
十二、与 hex 对照(方法)
若需二进制层验证:
# 仅说明方法,未在本环境执行
xxd -l 256 path/to/column.bin | head
xxd -l 128 path/to/primary.idx | headprimary.idx 按 PK
列类型固定宽度存储;.bin 前字节为压缩块
header(具体格式见 CompressedWriteBuffer 与
CompressionCodecFactory 源码)。无实测
hex 时不解读具体字节。
十三、Detached / Broken Parts
DETACH PART:Part 移到detached/,不参与查询。- 校验失败:
checksums.txt不匹配 → Part 标记 broken,需ATTACH或从副本拉取。
副本场景见 第 8 篇。
十四、工程坑点
14.1 小 insert → 海量 Part
每个 insert block 可产生新 Part;秒级 thousands insert →
merge 跟不上 → parts_to_delay_insert /
parts_to_throw_insert(第 6、15 篇)。
14.2 宽表 + Compact
频繁小 batch 宽表可能长期 Compact,读单列仍解压整
granule——调 min_bytes_for_wide_part 或增大
batch。
14.3 跳数索引文件
skp_idx_* 与列 Mark 对齐
granule;定义不当几乎无剪枝(第 7 篇)。
十五、学术谱系:DSM · C-Store Projection · MergeTree Part
| 阶段 | 文献 / 概念 | 与本篇 Part 的关系 |
|---|---|---|
| 早期 | DSM(Decomposition Storage Model) | 同列连续存放;Part 内每列 .bin |
| 2005 | C-Store projection(列组) | 一组列共排序、可冗余;CH 默认
整表一排序键,可选
PROJECTION |
| 2005+ | 不可变列段 / RS | Part 写入后只读,靠 merge 归并(第 6 篇) |
| 24.x | Wide vs Compact Part | 宽表「每列一文件」vs 小 Part 打包——论文少谈的工程分叉 |
谱系结论:C-Store 用多 projection 换查询;MergeTree 用 单 Part 多列文件 + 稀疏 Mark 换运维简单。Wide/Compact 是 文件粒度 优化,不是第二套存储引擎。
15.1 工程间隙
| 论文假设 | ClickHouse Part |
|---|---|
| 列组预先设计、少变更 | DDL 加列产生新列文件;旧 Part 兼容读 |
| 理想大段顺序读 | 小 insert → 海量小 Part(§14.1) |
| 本地磁盘大块 IO | 对象存储上小文件 / 高延迟(第 16 篇) |
15.2 开放问题
- 云盘上 Compact 是否应更激进? 可检验:同 ingest 下 Wide/Compact 切换后读延迟与对象数(入口:CH Data Parts;C-Store projection 代价讨论)。
- 自适应
granule(
index_granularity_bytes)与固定行 granule 谁更稳? 宽行表上仍有运维争议。
十六、小结
Part = 不可变列文件集合 + 元数据 +
稀疏索引;Granule = 索引与 IO 对齐单位;Mark =
granule 到 .bin 的桥梁。读路径永远:PK
→ granule range → Mark → 压缩块 → 列向量。
上一篇:列存基础
下一篇:压缩与编码
参考资料
核心论文
- Stonebraker et al., C-Store: A Column-oriented DBMS, VLDB 2005(projection / 列组;A 级)。
- Abadi et al., The Design and Implementation of Modern Column-Oriented Database Systems, FnT DB 2013(DSM/列布局综述;A 级)。
规范 / 源码 / 文档
- ClickHouse Documentation, MergeTree table engine — Granules and marks(A 级)。
- ClickHouse Documentation, MergeTree settings —
index_granularity/min_bytes_for_wide_part(A 级)。 - ClickHouse Source, v24.3,
IMergeTreeDataPart.cpp、MergeTreeDataPartWide.cpp、MergeTreeMarksLoader.cpp(A 级)。
Part 加载路径
MergeTreeDataPart::loadColumnsChecksumsIndexes()(IMergeTreeDataPart.cpp)依次:
- 读
columns.txt解析列 schema。 - 读
checksums.txt校验各文件。 - 读
count.txt得行数。 - 加载
primary.idx与列 Mark(MergeTreeMarksLoader)。 - 可选加载跳数索引
skp_idx_*。
Part 状态机:Temporary →
PreActive → Active →
Outdated → 删除;merge 产生新 Part 后旧 Part
标记 Outdated。
Wide 格式读列
MergeTreeReaderWide::readRows() 按 Mark
Range 对每个请求列:
- 读
.mrk2得压缩块偏移与 granule 内偏移。 CachedCompressedReadBuffer读.bin压缩块。- 解码器链(
ISerialization+CompressionCodec)还原列片段。
Compact 格式
小 Part 使用 data.compact.bin +
data.compact.mrk3;宽表频繁小 insert
时文件数少,但读单列可能解压同 granule
内其他列——适合窄表或总是读全列的 projection。
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【列存引擎内核】列存基础与 ClickHouse 架构
行存 vs 列存的带宽、压缩与向量化三角;ClickHouse Server 进程模型、线程池与 MergeTree 引擎家族地图;src/Storages 与 src/Processors 源码入口。对照 PG 行存与 LSM 写优化路径,版本锚定 ClickHouse 24.x LTS。
【列存引擎内核】压缩与编码
ClickHouse 列压缩:LZ4、ZSTD、Delta、DoubleDelta、Gorilla 时序编码与列类型关系;CODEC 链顺序、LowCardinality 与 PG TOAST 对照。压缩比须本机实测,本文不编造倍数。
【列存引擎内核】向量化执行引擎
ClickHouse Block 列向量 batch、IProcessor Pipeline 与 filter/project/aggregate 向量实现;对照 PostgreSQL 火山模型 ExecProcNode。源码入口 src/Processors、src/Columns。24.x LTS。
【列存引擎内核】查询读取路径
MergeTree SELECT 读路径:Mark Range 定位 Granule、PREWHERE 与 WHERE、Part 级并行与 max_threads。EXPLAIN indexes=1 解读方法。24.x LTS,无伪造 EXPLAIN 输出。