ClickHouse 列式存储的查询性能优势

FreeGuideOnline 最新 2026-07-09

什么是列式存储

列式存储是一种将数据按列而非按行组织在磁盘上的存储模式。传统关系型数据库(如 MySQL、PostgreSQL)普遍采用行式存储,同一行的所有字段值在物理上相邻排列。而在列式存储中,同一列的所有值被连续存储,不同列的数据则分别独立存放。

以一张用户行为日志表为例:

时间戳 用户ID 事件类型 省份
10:00 1001 点击 北京
10:01 1002 浏览 上海
10:02 1003 购买 广州
  • 行式存储[10:00, 1001, 点击, 北京] [10:01, 1002, 浏览, 上海] [10:02, 1003, 购买, 广州]
  • 列式存储:时间戳列文件 [10:00, 10:01, 10:02],用户ID列文件 [1001, 1002, 1003],事件类型列文件 [点击, 浏览, 购买],省份列文件 [北京, 上海, 广州]

ClickHouse 的列式存储实现

ClickHouse 将每一列的数据独立存储为 .bin 数据文件,同时为每列生成对应的 .mrk 标记文件,记录数据块在文件中的偏移位置及压缩编码信息。这种设计天然支持以下能力:

  • 查询时可以只读取涉及到的列,其他列完全不被扫描
  • 每一列可以使用最适合其数据特征的编码与压缩算法(如 LZ4、ZSTD、Delta、Gorilla 等)
  • 按列压缩大幅提升压缩比,相同数据量占用更少的磁盘空间和 IO 带宽

查询性能优势的核心原理

1. 消除不必要的 I/O

分析型查询通常只关注部分列,例如统计各省份的事件总量:

SELECT 省份, count() FROM logs GROUP BY 省份;

行式存储会读取整行全部字段,包括时间戳、用户ID、事件类型。而 ClickHouse 列式存储仅需扫描“省份”这一列的压缩文件,数据读取量减少 80%~90%。在高基数、宽表(上百列)的场景下,这种优势呈倍数放大。

2. 极高的数据压缩率

同一列的数据类型相同且值域集中,存在大量重复或相似模式。ClickHouse 利用此特性极大压缩数据,典型做法包括:

  • Delta 编码:存储相邻值的差值,适用于递增或变化缓慢的时序数据
  • 字典编码:为低基数枚举值(如省份、事件类型)构建全局字典,列内仅存字典索引
  • 通用压缩:在编码后的数据上再次应用 LZ4 或 ZSTD 无锁压缩

实际案例中,日志表的省份列压缩比可达 10:1 到 50:1,时间戳列借助 Delta 编码后的压缩比常超过 100:1。更小的数据体积直接减少了磁盘 IO 和内存占用,使查询更快。

3. 向量化执行与 CPU 高效利用

现代 CPU 支持单指令多数据(SIMD)操作,一次处理小块连续数据。列式存储让同一列的大量数据在内存中连续排列,ClickHouse 能直接以批量向量方式处理:

  • 一次加载数百个省份值到 CPU 寄存器
  • 用 SIMD 指令批量比较、聚合,避免逐行解释执行的开销
  • 配合数据块压缩,解压过程也被向量化,充分利用 CPU 缓存及流水线

这使得 ClickHouse 在处理数十亿行数据的聚合查询时,CPU 效率远高于传统行存引擎。

4. 避免全表扫描的索引机制

列式存储结合 稀疏索引 进一步降低扫描成本。ClickHouse 默认按排序键进行数据排序和分块存储,每个数据块(Granule,默认 8192 行)记录其键列的最小最大值。执行带有条件过滤的查询时:

SELECT avg(金额) FROM 订单表 WHERE 日期 = '2025-03-31';

ClickHouse 会读取日期列对应的标记文件,快速跳过不包含目标日期的数据块,只解压并处理相关块。这种索引仅依赖排序键列,无需像 B-Tree 那样为每行建立索引指针,写入和查询成本都极其低廉。

典型场景性能对比

在标准的星形分析查询测试(TPC-H Q1)中,ClickHouse 单机比行式数据库快 100~1000 倍不等,核心差距正来源于列式存储带来的 IO 减少与向量化计算的结合。

一个 1TB 的营销事件宽表(包含 200 列),执行以下查询:

SELECT 渠道, countDistinct(用户ID)
FROM 事件表
WHERE 日期 BETWEEN '2025-03-01' AND '2025-03-31'
  AND 事件类型 = '下单'
GROUP BY 渠道;
  • 行式数据库:需要完整扫描整个 1TB 数据(即使索引存在,仍需回表读取其他列),查询耗时超过 10 分钟
  • ClickHouse:仅读取 日期事件类型渠道用户ID 四列,压缩后实际读取量可能不到 50GB,配合块跳过和向量化,查询往往在 2~5 秒 内完成

为何列式存储不适合 OLTP 事务

列式存储的优势集中于读密集型分析,但在频繁单行更新的场景中会暴露劣势:

  • 更新一行需要修改多个列文件,产生大量随机 IO
  • 同一行的数据物理分离,无法在单次 IO 中完整取出
  • 行级事务和 MVCC 实现复杂

因此 ClickHouse 被设计为近乎无更新的追加写分析引擎,而 OLTP 场景仍需选择行存数据库。

上手验证:观察列式存储的实际行为

你可以用以下步骤在 ClickHouse 中直观感受列存特性。

  1. 创建测试表并写入数据:
CREATE TABLE test_wide (
  id UInt32,
  name String,
  age UInt8,
  score Float64,
  remark String
) ENGINE = MergeTree()
ORDER BY id;
  1. 插入数亿行随机数据(可使用 numbers 表函数快速生成)。

  2. 执行查询并观察系统表 system.query_logread_rowsread_bytes 的变化:

-- 仅查两列
SELECT avg(score), max(age) FROM test_wide;
-- 再查全部列
SELECT * FROM test_wide LIMIT 10;

你会明确看到:前者读取的字节数远小于后者,证明只读所需列的事实。

总结

ClickHouse 的列式存储通过“按列读取、极限压缩、向量化处理、稀疏索引” 四大机制,将分析查询的性能推向极致。理解其原理后,建表时应遵循以下最佳实践:

  • 按查询需求设排序键,让稀疏索引发挥最大过滤效果
  • 避免在宽表中读取不需要的列
  • 适当选用编码方式进一步减小磁盘占用量
  • 将同一业务的明细与聚合分离(物化视图),减少查询扫描量

掌握列式存储本质,你就可以在设计数据模型和 SQL 时做出对查询效率更友好的决策。