ClickHouse index_granularity 详解
什么是 index_granularity
index_granularity
是ClickHouse中一个重要的性能配置参数,它定义了索引的粒度(granularity),即每多少个数据行会创建一个索引标记(index mark)。
基本概念
1. 索引标记的工作原理
数据行: [1, 2, 3, ..., 8192] [8193, 8194, ..., 16384] [16385, 16386, ...]
索引标记: ↑标记1 ↑标记2 ↑标记3
- 每8192行数据创建一个索引标记
- 每个标记存储该段数据的边界信息(最小值、最大值等)
- 查询时先通过索引标记快速定位到可能包含目标数据的段
2. 索引标记的创建依据
索引标记是根据ORDER BY子句中定义的列来创建的:
CREATE TABLE blocks (block_number UInt64,timestamp DateTime,block_hash String
) ENGINE = MergeTree()
ORDER BY (block_number, timestamp) -- 索引标记基于这两列创建
SETTINGS index_granularity = 8192;
索引标记存储的信息:
- 最小值:该段数据中ORDER BY列的最小值
- 最大值:该段数据中ORDER BY列的最大值
- 行数:该段包含的行数
- 数据块位置:指向实际数据块的指针
3. 索引查找过程示例
-- 查询区块号范围
SELECT * FROM blocks WHERE block_number BETWEEN 10000 AND 20000;
索引查找过程:
- 检查索引标记1:max=8192 < 10000,跳过
- 检查索引标记2:min=8193, max=16384,与查询范围重叠,需要读取
- 检查索引标记3:min=16385 > 20000,跳过
- 只读取索引标记2对应的数据块
ORDER BY 对索引的影响
1. 不同ORDER BY的影响
单列排序:
ORDER BY block_number
-- 索引标记基于block_number创建
-- 适合按区块号查询
多列排序:
ORDER BY (block_number, timestamp)
-- 索引标记基于(block_number, timestamp)组合创建
-- 适合复合查询条件
你的表结构:
ORDER BY (start_block, end_block)
-- 索引标记基于(start_block, end_block)组合创建
-- 适合按区块范围查询
2. 索引效率的关键因素
ORDER BY列的选择:
-- 好的选择:经常用于WHERE条件的列
ORDER BY (block_number, timestamp)-- 不好的选择:很少用于查询的列
ORDER BY (random_column, unused_column)
列的顺序:
-- 好的顺序:选择性高的列在前
ORDER BY (block_number, timestamp) -- block_number选择性高-- 不好的顺序:选择性低的列在前
ORDER BY (status, block_number) -- status选择性低
查询模式匹配(左侧匹配原则):
-- 表结构
ORDER BY (start_block, end_block, status)-- 高效查询(遵循左侧匹配)
WHERE start_block = 12345 -- ✅ 使用第1列
WHERE start_block = 12345 AND end_block = 12350 -- ✅ 使用第1、2列
WHERE start_block = 12345 AND end_block = 12350 AND status = 'finished' -- ✅ 使用第1、2、3列
WHERE start_block BETWEEN 1000 AND 2000 -- ✅ 范围查询也遵循左侧匹配-- 低效查询(违反左侧匹配)
WHERE end_block = 12350 -- ❌ 跳过第1列,直接使用第2列
WHERE status = 'finished' -- ❌ 跳过第1、2列,直接使用第3列
WHERE start_block = 12345 AND status = 'finished' -- ❌ 使用第1、3列,跳过第2列
左侧匹配原则详解
1. 基本原理
ClickHouse的索引遵循左侧匹配原则,类似于MySQL的复合索引。查询时必须从左到右使用ORDER BY中的列,不能跳过中间的列。
2. 与MySQL的对比
-- MySQL复合索引:(start_block, end_block, status)
-- 遵循左侧匹配原则-- ClickHouse ORDER BY:(start_block, end_block, status)
-- 同样遵循左侧匹配原则
3. 索引标记的工作原理
-- 表结构:ORDER BY (start_block, end_block, status)-- 索引标记存储:
标记1: start_block_min=1, start_block_max=1000, end_block_min=1, end_block_max=1000, status_min='init', status_max='pending'
标记2: start_block_min=1001, start_block_max=2000, end_block_min=1001, end_block_max=2000, status_min='init', status_max='pending'
4. 查询效率分析
-- ✅ 高效查询:WHERE start_block = 1500
-- 可以快速定位到标记2,因为start_block在范围内-- ❌ 低效查询:WHERE end_block = 1500
-- 无法有效使用索引,因为索引标记的end_block范围重叠
-- 需要扫描多个标记或全表扫描
5. 实际应用示例
-- 你的block_tasks表
CREATE TABLE block_tasks (start_block UInt64,end_block UInt64,status String
) ENGINE = MergeTree()
ORDER BY (start_block, end_block)
SETTINGS index_granularity = 8192;-- 高效查询模式
SELECT * FROM block_tasks WHERE start_block = 12345; -- ✅ 使用第1列
SELECT * FROM block_tasks WHERE start_block BETWEEN 1000 AND 2000; -- ✅ 范围查询第1列
SELECT * FROM block_tasks WHERE start_block = 12345 AND end_block = 12350; -- ✅ 使用第1、2列-- 低效查询模式
SELECT * FROM block_tasks WHERE end_block = 12350; -- ❌ 跳过第1列
SELECT * FROM block_tasks WHERE status = 'finished'; -- ❌ 跳过第1、2列
6. 最佳实践
-- 设计ORDER BY时要考虑查询模式
-- 将最常用于WHERE条件的列放在前面
ORDER BY (最常用的列, 次常用的列, 较少用的列)-- 例如:如果经常按start_block查询,偶尔按start_block+end_block查询
ORDER BY (start_block, end_block) -- 好的设计-- 如果经常按end_block查询,偶尔按start_block查询
ORDER BY (end_block, start_block) -- 更好的设计
默认值和推荐值
- 默认值:8192
- 推荐值:8192(ClickHouse官方推荐)
- 常见值:1024, 4096, 8192, 16384, 65536
不同值的影响
index_granularity | 内存使用 | 查询速度 | 写入速度 | 适用场景 |
---|---|---|---|---|
1024 (小) | 高 | 慢 | 快 | 频繁更新,查询较少 |
4096 (中小) | 较高 | 较慢 | 较快 | 平衡型应用 |
8192 (默认) | 中等 | 快 | 中等 | 通用场景,推荐 |
16384 (大) | 较低 | 中等 | 较慢 | 批量写入,查询频繁 |
65536 (很大) | 低 | 中等 | 慢 | 大批量数据,查询较少 |
为什么选择 8192
1. 数学优势
- 2的幂次方:8192 = 2^13,便于计算机处理
- 内存对齐:与系统页面大小(8KB)匹配
- 缓存友好:适合CPU缓存行大小
2. 性能平衡
- 查询性能:能够快速定位到相关数据段
- 内存使用:不会创建过多索引标记
- 写入性能:在批量插入时保持良好的性能
3. 经验优化
- ClickHouse团队经过大量测试得出的最佳值
- 适用于大多数应用场景
- 平衡了各种性能指标
实际应用示例
1. 区块数据表
CREATE TABLE blocks (block_number UInt64,timestamp DateTime,block_hash String
) ENGINE = MergeTree()
ORDER BY (block_number, timestamp)
SETTINGS index_granularity = 8192;
查询示例:
-- 查询特定区块范围
SELECT * FROM blocks WHERE block_number BETWEEN 1000000 AND 1001000;
性能分析:
- 100万行数据,8192粒度,约122个索引标记
- 查询时只需检查122个标记,然后读取对应的8192行数据段
- 相比全表扫描,性能提升显著
2. 交易数据表
CREATE TABLE transactions (tx_hash String,block_number UInt64,from_address String,to_address String
) ENGINE = MergeTree()
ORDER BY (block_number, tx_hash)
SETTINGS index_granularity = 8192;
3. 日志数据表
CREATE TABLE logs (block_number UInt64,log_index UInt32,address String,data String
) ENGINE = MergeTree()
ORDER BY (block_number, log_index)
SETTINGS index_granularity = 8192;
如何选择合适的值
1. 数据量考虑
-- 小表(<10万行):使用较小值
SETTINGS index_granularity = 1024;-- 中等表(10万-1000万行):使用默认值
SETTINGS index_granularity = 8192;-- 大表(>1000万行):使用较大值
SETTINGS index_granularity = 16384;
2. 查询模式考虑
-- 频繁点查询:使用较小值
SETTINGS index_granularity = 4096;-- 范围查询为主:使用默认值
SETTINGS index_granularity = 8192;-- 批量查询:使用较大值
SETTINGS index_granularity = 16384;
3. 写入模式考虑
-- 频繁小批量写入:使用较小值
SETTINGS index_granularity = 4096;-- 批量写入:使用默认值
SETTINGS index_granularity = 8192;-- 大批量写入:使用较大值
SETTINGS index_granularity = 32768;
性能测试示例
1. 查询性能对比
-- 测试不同粒度的查询性能
-- 数据量:1000万行
-- 查询:WHERE block_number BETWEEN 1000000 AND 1001000-- index_granularity = 1024
-- 索引标记数:约9766个
-- 查询时间:较慢-- index_granularity = 8192
-- 索引标记数:约1221个
-- 查询时间:快-- index_granularity = 65536
-- 索引标记数:约153个
-- 查询时间:中等
2. 内存使用对比
-- 索引标记内存使用估算
-- 每个标记约占用几十字节-- 1000万行数据:
-- 1024粒度:约9766个标记,内存使用高
-- 8192粒度:约1221个标记,内存使用中等
-- 65536粒度:约153个标记,内存使用低
最佳实践
1. 默认配置
-- 大多数情况下使用默认值
SETTINGS index_granularity = 8192;
2. 特殊场景调整
-- 高并发点查询
SETTINGS index_granularity = 4096;-- 大批量数据写入
SETTINGS index_granularity = 16384;-- 内存受限环境
SETTINGS index_granularity = 32768;
3. 监控和调优
-- 查看表设置
SELECT * FROM system.tables WHERE name = 'your_table';-- 监控查询性能
SELECT * FROM system.query_log
WHERE query LIKE '%your_table%'
ORDER BY event_time DESC;
常见问题
Q: 如何确定最佳值?
A: 通过性能测试,监控查询时间和内存使用,选择平衡点。
Q: 可以动态调整吗?
A: 不可以,需要在创建表时设置,修改需要重建表。
Q: 不同表可以使用不同值吗?
A: 可以,每个表可以独立设置。
Q: 设置过小会有什么问题?
A: 内存使用增加,写入性能下降。
Q: 设置过大会有什么问题?
A: 查询时需要扫描更多数据,查询性能下降。
总结
index_granularity = 8192
是ClickHouse的推荐配置,它在查询性能、内存使用和写入性能之间达到了很好的平衡。
关键要点
- 索引基于ORDER BY列:索引标记是根据ORDER BY子句中定义的列创建的
- 左侧匹配原则:查询时必须从左到右使用ORDER BY中的列,不能跳过中间的列
- 查询匹配很重要:WHERE条件应该尽量使用ORDER BY列,以获得最佳性能
- 列顺序影响效率:选择性高的列应该放在ORDER BY的前面
- 默认值适用大多数场景:8192是经过优化验证的最佳选择
Web3数据收集项目应用
- 区块数据:使用默认值8192,适合范围查询
- 交易数据:使用默认值8192,平衡查询和写入
- 日志数据:使用默认值8192,适合批量查询
- 任务管理:ORDER BY (start_block, end_block) 配合8192粒度,高效支持任务查询
除非有特殊需求,否则建议使用默认值8192,这是经过优化验证的最佳选择。