这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 MergeTree 是 ClickHouse 最重要的存储引擎。理解 part、分区、排序键、稀疏索引、后台合并和 TTL,就理解了 ClickHouse 高性能分析的基础。
7.1 存储结构
一张 MergeTree 表的逻辑结构:
table
partition 202608
part all_1_1_0
primary.idx 稀疏主键索引
event_date.bin 列数据
event_type.bin
user_id.bin
amount.bin
count.txt 元信息
每个 part 内数据按 ORDER BY 排序,列文件独立保存,并带有压缩块和校验信息。
7.2 建表示例
CREATE TABLE analytics.events_local
(
event_date Date,
event_time DateTime,
event_id String,
user_id UInt64,
event_type LowCardinality(String),
city_id UInt32,
channel LowCardinality(String),
amount Decimal64(2),
is_deleted UInt8
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_type, city_id, user_id)
TTL event_date + INTERVAL 13 MONTH
SETTINGS index_granularity = 8192;
各元素职责:
| 元素 | 职责 |
|---|---|
| PARTITION BY | 生命周期管理与粗粒度剪枝 |
| ORDER BY | 物理排序、稀疏索引、局部性 |
| TTL | 自动过期数据或列 |
| index_granularity | 稀疏索引粒度 |
| SETTINGS | 引擎行为细节 |
7.3 part 生命周期
INSERT 生成宽 part
|
v
后台 merge 合并同分区 part
|
v
生成更大 part,旧 part 标记 inactive
|
v
超过保留期后物理删除
查看 part:
SELECT
partition,
name,
active,
rows,
bytes_on_disk,
min_block_number,
max_block_number,
level
FROM system.parts
WHERE table = 'events_local' AND active
ORDER BY partition, name;
强制合并:
OPTIMIZE TABLE analytics.events_local PARTITION 202608;
生产上不要频繁执行全表 FINAL 合并。强制合并会占用大量 CPU、IO 和内存。
7.4 分区设计
常用分区:
PARTITION BY toYYYYMM(event_date)
大表也可以按天:
PARTITION BY toDate(event_date)
分区不是越细越好。过多分区会带来:
- 更多目录和元数据;
- 后台合并调度压力;
- 查询计划开销;
- 小文件问题。
建议:
| 场景 | 分区 |
|---|---|
| 保留 1 年,TB 级事件 | 按月或按周 |
| 保留 7 到 30 天,每天量大 | 按天 |
| 小维表或测试表 | 通常不分区 |
| 明确生命周期管理 | 优先时间分区 |
7.5 排序键设计
ORDER BY 决定每个 part 内数据的物理顺序,也决定稀疏索引能否跳过无关 granule。
设计原则:
- 低基数字段放前面;
- 常用等值或范围过滤字段尽量靠前;
- 避免过高基数字段占据前缀;
- 与核心报表维度一致;
- 不追求覆盖所有查询,用投影补齐不同排序。
示例:
ORDER BY (event_date, event_type, city_id, user_id)
查询有效:
SELECT count()
FROM analytics.events_local
WHERE event_date = '2026-08-25'
AND event_type = 'pay'
AND city_id = 1;
查询较弱:
SELECT count()
FROM analytics.events_local
WHERE user_id = 1001;
第二个查询跳过了排序键前缀,需要扫描更多 granule。针对高频用户查询可以额外建投影或物化汇总表。
7.6 稀疏索引
ClickHouse 不是每行一条 B+Tree 索引,而是每隔 index_granularity 行记录一次排序键标记:
granule 0: min_key = (08-01, view, 1, 10)
granule 1: min_key = (08-01, view, 1, 5000)
granule 2: min_key = (08-01, view, 2, 20)
查询时通过 primary.idx 判断哪些 granule 可能包含数据,再读取列块。
查看 granule 信息:
SELECT
table,
primary_key,
index_granularity
FROM system.tables
WHERE database = 'analytics';
index_granularity 一般保持默认。盲目调小会让索引和元数据膨胀,盲目调大可能降低过滤精度。
7.7 跳数索引
跳数索引是二级数据结构,用于跳过不满足条件的 granule:
ALTER TABLE analytics.events_local
ADD INDEX idx_user_bloom user_id TYPE bloom_filter GRANULARITY 3;
ALTER TABLE analytics.events_local
ADD INDEX idx_amount_minmax amount TYPE minmax GRANULARITY 1;
ALTER TABLE analytics.events_local MATERIALIZE INDEX idx_user_bloom;
常见类型:
| 类型 | 适合 |
|---|---|
| minmax | 数值和时间范围 |
| set(N) | 少量离散值 |
| bloom_filter | 等值查询,高基数可选 |
| tokenbf_v1 | token 匹配 |
| ngrambf_v1 | 子串匹配 |
跳数索引有构建成本和存储成本,也会降低写入速度。只给高频过滤条件添加。
7.8 TTL
行 TTL:
ALTER TABLE analytics.events_local
MODIFY TTL event_date + INTERVAL 13 MONTH;
列 TTL:
ALTER TABLE analytics.events_local
MODIFY COLUMN
remark String TTL event_date + INTERVAL 1 MONTH;
查看 TTL 信息:
SELECT database, table, engine_full
FROM system.tables
WHERE table = 'events_local';
TTL 删除通常由后台任务执行,不是精确到秒的实时删除。关键保留期要配合监控和审计确认。
7.9 合并策略与设置
常用设置:
| 设置 | 说明 |
|---|---|
| index_granularity | 稀疏索引粒度 |
| min_bytes_for_wide_part | 宽 part 和紧凑 part 阈值 |
| merge_max_block_size | 合并处理块大小 |
| max_parts_in_total | part 总数保护 |
| ttl_only_drop_parts | 按整 part 删除 TTL 数据 |
按整分区过期可提高删除效率:
ALTER TABLE analytics.events_local
MODIFY SETTING ttl_only_drop_parts = 1;
适用条件:TTL 边界尽量与分区边界对齐。否则效果有限。
7.10 常见故障
Too many parts
SELECT table, count() AS parts, sum(rows) AS rows
FROM system.parts
WHERE active
GROUP BY table
ORDER BY parts DESC;
处理:
- 降低写入频率;
- 增大批次;
- 减少分区数量;
- 检查合并线程和磁盘 IO;
- 扩容或降低写入流量。
查询扫描量过大
SELECT query, read_rows, read_bytes, query_duration_ms
FROM system.query_log
WHERE type = 'QueryFinish'
ORDER BY read_bytes DESC
LIMIT 20;
优先检查分区条件、排序键、投影和预聚合。
part 损坏
查看异常:
SELECT database, table, name, active, reason
FROM system.parts
WHERE not active
ORDER BY modification_time DESC;
处理方式与是否有副本有关。副本表优先从健康副本恢复;单机表需要使用备份恢复,不要盲目删除目录。
本章小结
MergeTree 通过按列存储、part 合并、分区剪枝、排序键稀疏索引和跳数索引,把海量扫描和聚合做成高吞吐流水线。设计时要先确定数据生命周期和主要查询模式,再决定分区、排序键和索引。频繁小写入、过细分区和滥用强制合并都是常见反模式。
思考题
- part、partition、granule 分别是什么?
- 分区和排序键是否都能减少扫描?
- 排序键前缀为什么重要?
- 跳数索引有哪些代价?
- 什么时候使用
ttl_only_drop_parts?