ClickHouse 列式存储与海量数据实时 OLAP 实战

MySQL 扛住了交易订单的实时写入,却在” yesterday 全站 GMV 按省份聚合”这种分析查询里反复超时。ClickHouse 就是为这种”海量数据、复杂聚合、秒级返回”场景而生的列式 OLAP 数据库。本文把 ClickHouse 最容易被问、最容易踩坑的原理与设计讲清楚。

一、为什么需要 ClickHouse:OLTP 与 OLAP 的冲突

MySQL / PostgreSQL 擅长的是 OLTP(联机事务处理),行存、强事务、点查写入快。BI / 监控 / 埋点分析需要的是 OLAP(联机分析处理),列存、高吞吐扫描、聚合快。两者设计目标不同,硬用 MySQL 做分析会遇到三大瓶颈:

维度 OLTP(MySQL) OLAP(ClickHouse)
数据组织 行存储,一次读一整行 列存储,只读需要的列
写入模型 面向单行增删改,事务强一致 面向批量追加,最终一致
典型查询 SELECT * WHERE id = ? SELECT province, sum(amount) FROM ... GROUP BY province
索引结构 B+Tree,适合点查 稀疏索引 + 排序键,适合范围/聚合
扩展方式 主从读写分离 分片 + 副本,水平扩展

ClickHouse 不是 MySQL 的替代品,而是分析型负载的专用引擎。当单表过亿、查询以”宽表聚合 + 多维过滤”为主、对事务一致性要求不高时,才应该把它请出来。

二、核心概念速览

ClickHouse 的名词和 MySQL 不一样,先把概念对齐:

概念 含义 类比
MergeTree 最常用表引擎,支持排序、分区、副本 InnoDB 之于 MySQL
列族 / Column 数据按列独立压缩存储 CSV 的每一列
排序键(Order Key) 定义数据在磁盘上的物理顺序 聚簇索引
主键(Primary Key) 通常与排序键一致,用于稀疏索引 逻辑主键
分区键(Partition Key) 按时间/维度把数据切到不同目录 MySQL 分区表
Granule 最小读取单元,默认 8192 行 页 / Page
Part 一次写入产生一个不可变数据块 LSM-Tree 的 SSTable
Merge 后台把多个 Part 合并成更大 Part LSM 的 Compaction

注意:ClickHouse 的 PRIMARY KEY 不保证唯一,它只是声明排序键并构建稀疏索引。真正的去重要用 ReplacingMergeTree 或在写入前完成。

三、列式存储:为什么聚合查询这么快

列式存储是 ClickHouse 快的根本原因。假设一条订单记录有 50 个字段,分析查询通常只关心 order_amount、province、dt 三个字段:

1
2
行存读取:把整行 50 个字段全部读到内存,再丢弃 47 个
列存读取:只把 order_amount、province、dt 三列加载到内存
优势 列式存储的表现
I/O 裁剪 只读查询涉及的列,磁盘 I/O 与字段数成正比
压缩比高 同一列数据类型相同、重复值多,字典 / RLE 压缩效果显著
向量化执行 整批数据在 CPU SIMD 指令里流水线处理,减少分支跳转
数据局部性 同列数据物理连续,CPU Cache 命中率高

宽表不是 ClickHouse 的敌人,反而是它的朋友。 业务上常用的大宽表在列存里每一列都是独立文件,加字段不影响旧列的读取效率。

四、MergeTree 表引擎:数据的物理组织

MergeTree 是 ClickHouse 的默认与核心引擎。建表示例:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE order_events
(
dt Date,
ts DateTime,
order_id UInt64,
user_id UInt64,
province LowCardinality(String),
amount Decimal(18, 2)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(dt)
ORDER BY (province, dt, order_id);

关键设计:

  • PARTITION BY:按月份把数据拆到不同目录,删除旧分区时只需删目录,极快。
  • ORDER BY:决定 Part 内部物理顺序,是稀疏索引的依据,也是查询剪枝的关键。
  • 不可变 Part:每次批量写入产生一个 Part,后续由后台线程合并,写入性能极高。
  • 稀疏索引:每 8192 行(一个 Granule)记录一次边界值,索引体积小,适合顺序扫描。

排序键一旦确定就很难修改。设计原则是:把最常出现在 WHERE、JOIN、GROUP BY 中的列放在前面,让稀疏索引和压缩都能发挥最大作用。

五、分区与排序键:查询剪枝的艺术

同样的数据,不同的排序键,查询性能可能差几十倍。优秀的设计遵循:

设计原则 说明
高频过滤列放排序键前面 如 WHERE province = 'Zhejiang', province 应靠前
时间列放分区键 toYYYYMMDD(dt) 或 toYYYYMM(dt),方便 TTL 和按天删除
基数低的列用 LowCardinality 如 province、channel,显著降低内存和磁盘占用
大基数列只做最后排序 如 user_id、order_id,用于保证 Part 内有序即可
1
2
3
-- 常见埋点日志表设计
ORDER BY (app_id, event_type, dt, user_id);
PARTITION BY toYYYYMMDD(dt);

这样 WHERE app_id = 'xxx' AND event_type = 'pay' AND dt >= '2026-10-01' 能同时命中分区剪枝和索引剪枝。

六、向量化执行与 SQL 解析

ClickHouse 的执行引擎是向量化(Vectorized)的:数据以块(Block)为单位处理,每列用紧凑数组存储,函数调用批量作用在数组上。相比传统的”一次处理一行”的火山模型,向量化大幅减少虚函数调用和缓存失效。

1
2
3
4
5
6
7
8
9
10
SELECT 
province,
count() AS order_count,
sum(amount) AS total_amount,
avg(amount) AS avg_amount
FROM order_events
WHERE dt >= today() - 7
GROUP BY province
ORDER BY total_amount DESC
LIMIT 100;

对于这条查询,ClickHouse 只会读取 dt、province、amount 三列,按 Granule 批量过滤、分组、聚合,最后在节点间做二次聚合。

七、物化视图与投影:空间换时间

物化视图(Materialized View)是 ClickHouse 里的”预聚合”利器。你可以为原始明细表建立一张汇总表,写入明细时自动把聚合结果同步过去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 1. 创建目标预聚合表
CREATE TABLE order_events_by_province
(
dt Date,
province LowCardinality(String),
pv UInt64,
amount AggregateFunction(sum, Decimal(18, 2))
)
ENGINE = SummingMergeTree()
ORDER BY (dt, province);

-- 2. 创建物化视图,写入时自动聚合
CREATE MATERIALIZED VIEW mv_order_events_by_province
TO order_events_by_province
AS SELECT
dt,
province,
count() AS pv,
sumState(amount) AS amount
FROM order_events
GROUP BY dt, province;

查询时直接查 order_events_by_province 就能得到 province 维度的聚合结果,避免扫描明细。

物化视图不是”自动索引”,它是一份独立的数据。适合查询模式固定、可接受最终一致的场景,比如日报、实时监控大盘。

八、MergeTree 家族与典型引擎

MergeTree 不是单一引擎,而是一个家族。根据业务场景选引擎,能少走很多弯路:

引擎 特点 典型场景
MergeTree 基础引擎,支持排序、分区、副本 绝大多数分析表
ReplacingMergeTree 按排序键去重,保留版本最新 需要幂等写入的表
SummingMergeTree 对数值列自动求和合并 预聚合指标表
AggregatingMergeTree 对聚合函数状态合并 复杂预聚合(UV、分位数)
CollapsingMergeTree 用 Sign 字段做”行级撤销” CDC / 状态变更场景
VersionedCollapsingMergeTree 带版本号的 Collapsing 需要版本控制的 CDC
Log 引擎 轻量、无索引、追加写入 临时日志、测试

九、ClickHouse 与 Elasticsearch / Doris / StarRocks 怎么选

维度 ClickHouse Elasticsearch Doris / StarRocks
定位 列式 OLAP 搜索引擎 实时数仓 / 湖仓
写入模式 批量追加最优 近实时索引 近实时导入
查询强项 宽表聚合、Ad-hoc 全文检索、日志搜索 多表关联、复杂数仓
多表 Join 弱(推荐大宽表) 不支持复杂 Join 强,支持分布式 Join
更新删除 弱(异步 Merge) 实时 支持 Upsert
生态 监控 / 埋点领域成熟 ELK 栈 国内实时数仓首选

一句话:日志检索选 Elasticsearch,复杂数仓建模选 Doris/StarRocks,宽表聚合和高吞吐分析选 ClickHouse。

十、生产避坑清单

坑 现象 正确姿势
单表不分区 删历史数据慢、查询扫描全表 按 toYYYYMMDD(dt) 分区
排序键乱写 查询命中不了索引,全表扫描 高频过滤列放前面
频繁小批量写 Part 爆炸,Merge 跟不上 攒批写入,每秒至少几千条
用行存思维更新 ALTER UPDATE 极慢 用 ReplacingMergeTree / 重分区写新表
不做 TTL 磁盘无限增长 TTL dt + INTERVAL 90 DAY
超大 Join 内存溢出或极慢 提前打成大宽表,或换 StarRocks
单节点 磁盘/CPU 瓶颈 用分片 + 副本搭建集群
不监控 Merge 后台负载不可见 盯 system.parts、system.merges

总结

ClickHouse 的设计哲学很清晰:用列式存储减少 I/O,用稀疏索引和分区做剪枝,用向量化执行压榨 CPU,用不可变 Part 换取极致写入吞吐。它不是万能数据库,但在”大宽表 + 海量数据 + 复杂聚合”的分析场景里几乎没有对手。掌握好排序键、分区键、MergeTree 家族与物化视图这四条主线,再结合生产避坑清单,就能把 ClickHouse 稳定地跑在监控、埋点、BI 数仓的第一线。

下一篇可以沿着”ClickHouse 集群分片与副本部署”或”ClickHouse 接入 Kafka/Flink 实时数仓”继续深入——把”算”和”存”真正打通,实时分析才算落地。