ClickHouse 亿级数据实时分析:列式存储与查询优化

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;排序键和分区要按查询模式设计;用物化视图把热查询预聚掉。把它接进你的日志与指标管道,实时分析能力立刻上一个台阶。

上一篇 GitLab CI/CD 实战:缓存、制品与多环境部署
下一篇 数据库连接池耗尽:HikariCP 连接泄漏排查实录