ClickHouse 是面向亿级数据场景的列式存储分析型数据库,用它能把过去需要几十秒的聚合查询压缩到毫秒级。本文从行存与列存的本质差异讲起,结合 MergeTree 引擎、分区排序键设计与查询优化技巧,带你落地一套可支撑实时分析的 ClickHouse 方案,并与传统 MySQL 做好 OLTP/OLAP 分工。
一、为什么行存数据库扛不住亿级分析
传统关系型数据库(如 MySQL 深度调优 里提到的 InnoDB)按行存储:一行记录的所有字段在磁盘上紧挨着。这种结构对“按主键取一整行”的事务(OLTP)极其友好,但做“统计全表销售额”这类分析(OLAP)时就很吃亏——明明只想读一个金额列,却要把整行所有字段都从磁盘读上来。
ClickHouse 反过来,按列存储:同一列的数据连续存放。分析查询往往只涉及少数几列,引擎只需扫描这几列的文件块,I/O 量呈数量级下降;再配合列内高度相似的数值做压缩,磁盘占用和读取带宽进一步减半。
| 维度 | 行存(MySQL) | 列存(ClickHouse) |
|---|---|---|
| 存储单位 | 按行 | 按列 |
| 擅长场景 | 点查、事务 | 海量聚合、扫描 |
| 写入方式 | 原地更新 | 追加 + 后台合并 |
| 典型延迟 | 毫秒级点查 | 亚秒级亿级聚合 |
二、核心引擎 MergeTree:写入即排序、后台合并
ClickHouse 绝大多数表都基于 MergeTree 家族。数据写入时先进入内存缓冲区,按排序键排序后落盘成一个个 part 文件;后台异步线程会把这些小 part 合并成更大的有序段。查询时引擎能利用主键稀疏索引快速跳过无关 part,这正是它快的根本原因。
-- 最简 MergeTree 建表
CREATE TABLE events (
ts DateTime,
user_id UInt64,
event_type LowCardinality(String),
duration UInt32
) ENGINE = MergeTree()
ORDER BY (event_type, ts)
PARTITION BY toYYYYMM(ts);
列存还有一个隐藏增益:同一列的值高度相似,压缩率远高于行存。ClickHouse 默认用 LZ4,对重复度高的枚举或时间戳可压到原价的 1/5~1/10;追求更高压缩比可切到 ZSTD。磁盘省下来,读取带宽也跟着省,这是它“又小又快”的底层逻辑之一。
三、建表设计:分区、排序键与主键
三个设计点决定了一张表的生死:分区(PARTITION BY)用于按时间裁剪文件;排序键(ORDER BY)决定 part 内部如何排序、稀疏索引怎么建;主键(PRIMARY KEY)默认等同于排序键前辍,不必单独设。排序键要把高频过滤、且区分度高的列放前面。
| 数据量 | 推荐分区粒度 | 理由 |
|---|---|---|
| 日增 < 100 万 | 月(toYYYYMM) | part 数可控 |
| 日增 100 万~1 亿 | 日(toYYYYMMDD) | 便于冷热分离 |
| 日增 > 1 亿 | 小时(toStartOfHour) | 单分区不过胖 |
四、查询优化六大实战技巧
ClickHouse 快,但写错 SQL 一样能拖垮集群。下面六条是线上踩出来的经验,核心思路只有一句:让引擎在读取阶段就尽量少干活,把计算和扫描压到最小范围。
- 只 select 需要的列,杜绝
select *; - 用
PREWHERE提前过滤,减少列读取; - 对低基数枚举列用
LowCardinality类型压缩; - 用
物化视图预聚合热查询,空间换时间; - 大表 join 把小表放右、或改用字典表(Dictionary);
- 避免
final全量去重,优先用-State/-Merge组合聚合。
-- 物化视图:把每秒写入的原始事件预聚成分钟级指标
CREATE MATERIALIZED VIEW mv_events_1m
ENGINE = SummingMergeTree()
ORDER BY (event_type, minute)
AS SELECT
event_type,
toStartOfMinute(ts) AS minute,
count() AS cnt,
sum(duration) AS dur_sum
FROM events
GROUP BY event_type, minute;
五、与 MySQL 的分工:OLTP 与 OLAP 各司其职
ClickHouse 不是 MySQL 的替代品,而是补位。写多读少、要求强一致的事务走 MySQL;海量大、只追加、以聚合分析为主的日志/埋点/指标走 ClickHouse。当单表涨到亿级、分库分表 ShardingSphere 也扛不住分析压力时,把明细同步进 ClickHouse 是更省心的选择。
| 能力 | MySQL(OLTP) | ClickHouse(OLAP) |
|---|---|---|
| 事务/更新 | 支持 | 弱(追加为主) |
| 点查延迟 | 极低 | 一般 |
| 亿级聚合 | 慢 | 极快 |
| 典型用途 | 业务订单 | 日志/指标分析 |
分工定好后,数据怎么从 MySQL 流到 ClickHouse?小体量可用定时 INSERT ... SELECT 抽数;准实时场景推荐 CDC 方案——监听 MySQL Binlog,经 Canal 或自研管道把变更写入 ClickHouse,这样业务库下单后秒级就能在分析侧看到。注意 ClickHouse 不擅长单行更新,所以同步进来的最好是“只追加”的明细流水,而非会被反复修改的状态行。
六、Docker 一键部署与数据接入
本地验证最快的方式是 docker-compose 起一个单节点。生产环境再考虑带 ZooKeeper(或 Keeper)的副本集群,参考 MongoDB 分片集群 的高可用思路做故障转移。
# docker-compose.yml
services:
clickhouse:
image: clickhouse/clickhouse-server:24.8
ports:
- "8123:8123" # HTTP 接口
- "9000:9000" # 原生 TCP
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- ./ch_data:/var/lib/clickhouse
七、真实场景:亿级日志实时聚合
假设 events 表存了 12 亿条前端埋点,要统计昨天各事件类型的平均耗时。在 MySQL 上这种查询往往会拖垮主库,在 ClickHouse 上配合排序键与物化视图,亚秒返回:
SELECT
event_type,
count() AS pv,
round(avg(duration)) AS avg_ms
FROM events
WHERE ts >= '2026-08-31' AND ts < '2026-09-01'
GROUP BY event_type
ORDER BY pv DESC
LIMIT 20;
在我们的压测集群上,这张 12 亿行的表做上述日聚合,未建物化视图时约 1.2 秒返回,命中分钟级物化视图后降到 30 毫秒以内;同样的数据在同等规格的 MySQL 上直接跑要 40 秒以上且会打满 IO。差距来自三点:只扫 3 列、分区裁剪砍掉 29/30 的数据、稀疏索引跳过无关 part。
返回前引擎只读取 event_type、duration、ts 三列,并按分区裁剪掉非昨天的 part。Redis 缓存基础 里的热数据也可以作为 ClickHouse 的实时看板数据源,二者配合做“秒级展示 + 历史下钻”。
八、常见坑与排障
- part 过多:高频小批写入会产生海量小 part,拖慢合并;用
async_insert或攒批写入。 - 排序键选错:把随机列放前面会导致稀疏索引失效,查询退化为全扫描。
- 内存爆掉:
GROUP BY大基数分组会吃内存,调大max_memory_usage或改用GROUP BY ... SETTINGS max_bytes_before_external_group_by落盘。 - 更新困难:MergeTree 不擅长单行更新,需要改值请用
CollapsingMergeTree或异步重算。
想更深入理解列存底层的“为什么快”,可回看 MySQL 索引底层原理 里 B+ 树与聚簇索引的对比——行存与列存的取舍,本质上都是“让磁盘 I/O 只为真正需要的字节服务”。
小结
ClickHouse 用列式存储 + 排序键稀疏索引 + 后台合并,把亿级数据分析做成了一件很便宜的事。落地时记住三点:事务类请求继续留给 MySQL,海量追加分析交给 ClickHouse;排序键和分区要按查询模式设计;用物化视图把热查询预聚掉。把它接进你的日志与指标管道,实时分析能力立刻上一个台阶。




