MergeTree Part 文件格式

目录

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 边界不对齐)。

流程:

  1. Mark 指向压缩块起始偏移。
  2. 读整块压缩数据,解压到内存 buffer。
  3. 按 granule 内偏移切分列向量片段。
  4. 编解码器链(CODEC(Gorilla, ZSTD) 等)逆序应用。

这与 PG 页内 tuple 完全不同:无「页 = 多列行混合」,而是 列方向压缩块。


六、稀疏主键 primary.idx

primary.idx 存 每个 granule 第一条行的 PRIMARY KEY 列值(扁平数组,按 granule 顺序)。数据按 ORDER BY 物理排序,故 PK 单调,可用二分定位谓词范围。

不是:

是:

自 23.5+ 起 primary.idx 可压缩落盘(compress_primary_key),加载时解压进内存;仍保持「小 enough 常驻内存」设计目标(官方 MergeTree 文档 Primary Keys and Indexes)。


七、分区元数据

PARTITION BY toYYYYMM(d) 时常见:

目录层级: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}

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 | head

primary.idx 按 PK 列类型固定宽度存储;.bin 前字节为压缩块 header(具体格式见 CompressedWriteBuffer 与 CompressionCodecFactory 源码)。无实测 hex 时不解读具体字节。


十三、Detached / Broken Parts

副本场景见 第 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 开放问题

  1. 云盘上 Compact 是否应更激进? 可检验:同 ingest 下 Wide/Compact 切换后读延迟与对象数(入口:CH Data Parts;C-Store projection 代价讨论)。
  2. 自适应 granule(index_granularity_bytes)与固定行 granule 谁更稳? 宽行表上仍有运维争议。

十六、小结

Part = 不可变列文件集合 + 元数据 + 稀疏索引;Granule = 索引与 IO 对齐单位;Mark = granule 到 .bin 的桥梁。读路径永远:PK → granule range → Mark → 压缩块 → 列向量。


上一篇:列存基础

下一篇:压缩与编码


参考资料

核心论文

  1. Stonebraker et al., C-Store: A Column-oriented DBMS, VLDB 2005(projection / 列组;A 级)。
  2. Abadi et al., The Design and Implementation of Modern Column-Oriented Database Systems, FnT DB 2013(DSM/列布局综述;A 级)。

规范 / 源码 / 文档

  1. ClickHouse Documentation, MergeTree table engine — Granules and marks(A 级)。
  2. ClickHouse Documentation, MergeTree settings — index_granularity / min_bytes_for_wide_part(A 级)。
  3. ClickHouse Source, v24.3, IMergeTreeDataPart.cpp、MergeTreeDataPartWide.cpp、MergeTreeMarksLoader.cpp(A 级)。

Part 加载路径

MergeTreeDataPart::loadColumnsChecksumsIndexes()(IMergeTreeDataPart.cpp)依次:

  1. 读 columns.txt 解析列 schema。
  2. 读 checksums.txt 校验各文件。
  3. 读 count.txt 得行数。
  4. 加载 primary.idx 与列 Mark(MergeTreeMarksLoader)。
  5. 可选加载跳数索引 skp_idx_*。

Part 状态机:Temporary → PreActive → Active → Outdated → 删除;merge 产生新 Part 后旧 Part 标记 Outdated。

Wide 格式读列

MergeTreeReaderWide::readRows() 按 Mark Range 对每个请求列:

Compact 格式

小 Part 使用 data.compact.bin + data.compact.mrk3;宽表频繁小 insert 时文件数少,但读单列可能解压同 granule 内其他列——适合窄表或总是读全列的 projection。

文章导航

分类入口
databasestorage
标签入口
#clickhouse#mergetree#part#granule#mark#column-files#wide#compact#24-lts

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。

2026-06-18 · database / storage

【列存引擎内核】列存基础与 ClickHouse 架构

行存 vs 列存的带宽、压缩与向量化三角;ClickHouse Server 进程模型、线程池与 MergeTree 引擎家族地图;src/Storages 与 src/Processors 源码入口。对照 PG 行存与 LSM 写优化路径,版本锚定 ClickHouse 24.x LTS。

2026-06-18 · database / storage

【列存引擎内核】压缩与编码

ClickHouse 列压缩:LZ4、ZSTD、Delta、DoubleDelta、Gorilla 时序编码与列类型关系;CODEC 链顺序、LowCardinality 与 PG TOAST 对照。压缩比须本机实测,本文不编造倍数。

2026-06-18 · database / storage

【列存引擎内核】向量化执行引擎

ClickHouse Block 列向量 batch、IProcessor Pipeline 与 filter/project/aggregate 向量实现;对照 PostgreSQL 火山模型 ExecProcNode。源码入口 src/Processors、src/Columns。24.x LTS。

2026-06-18 · database / storage

【列存引擎内核】查询读取路径

MergeTree SELECT 读路径:Mark Range 定位 Granule、PREWHERE 与 WHERE、Part 级并行与 max_threads。EXPLAIN indexes=1 解读方法。24.x LTS,无伪造 EXPLAIN 输出。


By .