一、核心概念解析
1. Granule(颗粒) - 索引的跳读单元
定义:连续的 N 行数据组成的逻辑处理单位
-
作用:ClickHouse 最小索引粒度,决定"这组数据是否需要被扫描"
-
默认值:
index_granularity = 8192行 -
关键特性:
- 与列类型、压缩方式无关,纯粹按行数划分
- 不是物理文件,也不是压缩块,是逻辑上的"数据块"
- 查询时按 granule 为单位进行筛选,避免全表扫描
2. Block(块) - 压缩/解压单元
定义:Granule 内部的物理压缩单位
-
作用:控制数据压缩和解压的粒度,优化 CPU/IO 效率
-
特点:
- 一个 Granule 通常包含多个 Block
- 每个 Block 独立压缩后写入
.bin文件 - 解压时按 Block 为单位,减少内存占用
| 概念 | 作用 | 层级 |
|---|---|---|
| Granule | 索引判断:要不要读? | 逻辑层 |
| Block | 压缩优化:怎么读更快? | 物理层 |
3. .bin 文件 - 列数据存储
定义:每个列独立的数据文件
- 格式:
{列名}.bin(如uid.bin,ts.bin) - 内容:该列的所有数据,按 Block 组织并压缩存储
- 特点:列式存储的核心,读取时只需访问相关列的文件
4. .mrk 文件 - 偏移量映射表
定义:Granule 到 .bin 文件位置的映射索引
- 作用:快速定位到特定 Granule 在
.bin文件中的起始位置 - 机制:存储每个 Granule 在
.bin文件的字节偏移量 - 优势:避免顺序扫描整个
.bin文件,实现随机访问
5. primary.idx - 主键稀疏索引
定义:Granule 级别的稀疏索引文件
-
内容:记录每个 Granule 的主键(ORDER BY 键)最小值
-
特性:
- 常驻内存,查询时快速判断 Granule 相关性
- 稀疏存储,只存 Granule 起始值,内存占用可控
- 用于快速过滤不相关的 Granule
二、架构关系全景图
查询执行流程:
┌─────────────────────────────────────────────┐
│ 1. primary.idx(内存)判断相关 Granule │
│ ↓ │
│ 2. .mrk 文件获取 .bin 偏移量 │
│ ↓ │
│ 3. .bin 文件读取压缩数据 │
│ ↓ │
│ 4. 按 Block 解压数据 │
│ ↓ │
│ 5. 返回最终结果 │
└─────────────────────────────────────────────┘
存储层级关系:
primary.idx(Granule索引)
↓
.mrk(位置映射)
↓
.bin(压缩数据)
↓
Block(解压单元)
↓
行数据
三、实战示例详解
示例表结构
CREATE TABLE user_events
(
event_time DateTime, -- 事件时间
user_id UInt64, -- 用户ID
city String, -- 城市
action String -- 用户行为
)
ENGINE = MergeTree
ORDER BY (event_time) -- 主键排序
SETTINGS index_granularity = 8192; -- 每8192行一个Granule
场景:表中有 16,384 行数据
Granule 划分:
Granule 0 → 行 0 ~ 8191 (事件时间: 2024-01-01 ~ 2024-01-01 23:59)
Granule 1 → 行 8192 ~ 16383 (事件时间: 2024-01-02 ~ 2024-01-02 23:59)
1. primary.idx 索引工作流程
索引内容:
Granule编号 | event_time最小值
-----------|------------------
0 | 2024-01-01 00:00:00
1 | 2024-01-02 00:00:00
查询示例:
-- 查询2024年1月2日的数据
SELECT count(*) FROM user_events
WHERE event_time >= '2024-01-02 00:00:00';
索引判断过程:
- 读取 primary.idx(内存中)
- Granule 0: 最大值 < '2024-01-02' → 跳过
- Granule 1: 最小值 ≥ '2024-01-02' → 需要读取
- 结果:只需处理 Granule 1 的 8192 行,跳过 50% 的数据
2. .mrk 文件定位机制
假设 user_id.bin 文件布局:
文件偏移量 | 数据内容
--------------|------------------
0 ~ 119 KB | Granule 0 的所有Block
120 KB ~ 结尾 | Granule 1 的所有Block
对应的 user_id.mrk 内容:
Granule编号 | .bin文件偏移量
-----------|---------------
0 | 0
1 | 120 KB
定位过程:
- 确定需要读取 Granule 1
- 查询
user_id.mrk,获取偏移量 120 KB - 直接
seek(120 KB)到指定位置 - 优势:避免扫描前 120 KB 无关数据
3. .bin 文件的 Block 组织
Granule 1 的实际存储结构:
Granule 1 (8192行)
├── Block A: 2000行,压缩大小 15 KB
├── Block B: 2000行,压缩大小 14 KB
├── Block C: 2000行,压缩大小 16 KB
└── Block D: 2192行,压缩大小 17 KB
(总计: 62 KB 压缩数据)
Block 级别的优化:
- 并行解压:多个 Block 可同时解压,利用多核CPU
- 按需读取:查询部分列时只解压相关Block
- 内存友好:避免一次性解压整个Granule
| 对应关系 | 条件 | 示例 | 说明 |
|---|---|---|---|
| 多对一 | granule 内数据 < 64KB | JavaEnable (1B) | 多个 granule 的标记对应同一个压缩 block |
| 一对一 | granule 内数据 64KB ~ 1MB | URLHash (8B) | 每个 granule 的标记对应一个 block |
| 一对多 | granule 内数据 > 1MB | URL (String) | 一个 granule 标记对应多个 block |
四、完整查询流程示例
查询:统计特定日期各城市的用户活跃数
SELECT city, count(DISTINCT user_id) as active_users
FROM user_events
WHERE event_time BETWEEN '2024-01-02 09:00' AND '2024-01-02 17:00'
GROUP BY city;
执行步骤详解:
步骤1:主键索引过滤
1. 加载 primary.idx 到内存
2. 找到 event_time 包含 '2024-01-02' 的 Granule
3. 确定只有 Granule 1 需要处理
→ 过滤掉 50% 的 Granule
步骤2:定位数据位置
1. 读取 event_time.mrk → Granule 1 偏移量: 80 KB
2. 读取 user_id.mrk → Granule 1 偏移量: 120 KB
3. 读取 city.mrk → Granule 1 偏移量: 45 KB
→ 获得各列数据的精确位置
步骤3:读取并解压数据
1. 跳转到 .bin 文件的指定偏移量
2. 按 Block 读取压缩数据:
- event_time.bin: 读取 4个Block,共 20 KB
- user_id.bin: 读取 4个Block,共 62 KB
- city.bin: 读取 4个Block,共 85 KB
3. 并行解压各个 Block
→ 只处理实际需要的压缩数据
步骤4:执行查询计算
1. 应用时间范围过滤: '2024-01-02 09:00' ~ '2024-01-02 17:00'
2. 按 city 分组,统计去重 user_id
3. 返回最终结果
五、性能优化要点
1. 索引设计最佳实践
-
主键顺序:将高筛选度的列放在 ORDER BY 前面
-
Granule 大小:根据数据特性调整
index_granularity- 默认 8192 适合大多数场景
- 查询频繁且数据量大 → 可适当增大
- 点查询多 → 可适当减小
2. 存储优化策略
- 列选择原则:只查询需要的列,避免全列读取
- 压缩效果:Block 大小影响压缩率,通常 64KB~1MB 较优
- 预排序:数据按主键有序写入,提升索引效率
3. 查询性能影响
优化前(全表扫描):
读取: 所有 Granule → 所有 .bin 文件 → 全量解压
耗时: 100%
优化后(索引跳读):
读取: 相关 Granule → 部分 .bin → 部分解压
耗时: 20%~50%(视过滤条件)
六、总结对比
| 组件 | 作用 | 类比 | 关键优势 |
|---|---|---|---|
| Granule | 逻辑跳读单位 | 书的章节 | 快速判断相关性 |
| primary.idx | 稀疏索引 | 目录页 | 内存常驻,过滤快 |
| .mrk文件 | 位置映射 | 页码索引 | 精确跳转,避免扫描 |
| .bin文件 | 列数据存储 | 章节内容 | 列式存储,高效压缩 |
| Block | 压缩单元 | 段落 | 并行解压,内存友好 |
核心价值:
- 减少 IO:通过索引跳过无关数据
- 降低 CPU:按需解压,列式处理
- 内存高效:稀疏索引常驻内存
- 查询快速:多级跳读,避免全扫描
更详细的请移步:www.cnblogs.com/traditional…