ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ClickHouse实战:列式OLAP数据库让亿级数据聚合秒级出结果

ClickHouse实战:列式OLAP数据库让亿级数据聚合秒级出结果

做了这么多年后端,你一定遇到过这种场景:数据量到了几千万、几亿行之后,老板要的报表怎么都跑不动,一个按月聚合的查询能把数据库拖到超时,下游的同事排着队等你调优。如果你还没试过ClickHouse,我强烈建议你把它加入工具箱。它是个真正的列式OLAP数据库,专为“海量数据 + 多维聚合 + 秒级出结果”这类分析场景设计,也是目前这个领域里社区最活跃、上手成本最低的选择之一。这篇文章我会从定位、架构原理讲到建表实操和避坑经验,最后聊聊它未来几年的前景,适合正在选型、刚接触数据仓库,或者想系统理解列式数据库的开发者参考。

1. 这个数据库的定位:先搞懂它在解决什么问题

1.1 列式OLAP和传统行式OLTP:便利店和仓库的区别

我在给团队做技术分享时经常打一个比方:传统的MySQL、PostgreSQL这类OLTP数据库,像楼下24小时营业的便利店。你进去买一瓶水、一包烟,结账走人,整个流程几秒钟就搞定。它们天生擅长处理一笔一笔的小交易,每次读写都围绕某几行数据展开,索引设计、事务机制、锁策略全部围绕这个目标优化。

ClickHouse不是便利店,它更像城市郊区的物流仓库。别指望它像便利店那样挨个服务单个顾客,但它能一次性把一整批货物快速分拣、打包、装车。对应到数据场景,它就是为“分析型查询”设计的:扫描几亿行、按时间或维度聚合成千上万组,最后只返回一个相对很小的结果集。

这两种存储格式的底层逻辑完全不同。行式存储把一行记录的所有字段连续存放在一起,你查某一行时,这一行的全部列都能拿到;列式存储则把一列的所有值连续存放,你查某一列时,只需把那几列的数据读出来。听起来只是个排列顺序问题,但在海量数据下,这个顺序决定了磁盘IO和CPU的利用效率。

以电商订单表为例,假设有50个字段,业务侧最常做的是“按天统计成交金额”。这个查询逻辑上只需要“订单时间”和“金额”两列。如果表里有1亿行,MySQL为了算这个聚合,要把1亿行的所有字段都从磁盘读一遍,哪怕另外48个字段完全用不上。ClickHouse只需要读两列的数据块,扫描量直接少了二十多倍,再加上后续的压缩和向量化执行,性能差距就是这么拉开的。

1.2 它擅长什么,不擅长什么

我见过不少新人拿到ClickHouse之后,恨不得把所有表都迁进来,结果后面踩坑踩到怀疑人生。所以动手之前先把场景边界划清楚。

它很适合这样的工作负载:

  • 日志与事件流水:访问日志、点击流、订单流水、监控指标,这类数据只增不改,天然契合ClickHouse的追加写入模型。
  • 宽表大范围扫描:清点一个时间段内的全部数据,做GROUP BY、ORDER BY、HAVING这类分析操作。
  • 高压缩比数据:同一列的数据类型和值分布相似,压缩率非常可观,能省大量磁盘空间。
  • 实时写入与分析并存:它支持每秒百万行的写入速率,写入之后能立刻被查询到,非常适合实时数仓的ODS和DWD层。

它不擅长的工作负载:

  • 高频单行点查:比如按订单号查一条记录返回给用户,这不是它的强项。因为稀疏索引定位到数据块之后,还要做额外的过滤操作,性能和传统OLTP数据库比没什么优势。
  • 频繁的小事务更新:ClickHouse没有传统意义上的行级锁和事务日志,它通过异步合并机制处理数据修改,按主键单行UPDATE/DELETE会很慢,而且会产生大量后台任务。
  • 复杂多表关联:虽然新版本不断优化JOIN能力,但它的设计哲学仍然是“大表扁平化,宽表代替JOIN”。高并发、多表嵌套关联的场景,性能表现远不如专为OLAP打造的MPP数据库。

一句话总结:ClickHouse是一把为分析场景量身定做的快刀,别拿它去干削苹果皮的事。

1.3 与Elasticsearch、Doris/StarRocks的边界对比

选型时大家经常把它和Elasticsearch、Doris、StarRocks放在一起比较。

Elasticsearch大家很熟,它在全文检索、日志搜索领域优势明显,把文档写入倒排索引后,按关键词快速返回结果。但它面向的是“搜索 + 中等规模聚合”,当数据量到几十亿行、聚合维度又很多时,ES的内存压力和查询性能会迅速恶化。ClickHouse则放弃全文索引,专注于分析聚合,在这个场景里实测通常会比ES快一个量级。所以日志平台的架构常常是:ES负责关键词检索,ClickHouse负责统计分析。

和Doris、StarRocks这类MPP分析型数据库对比,ClickHouse在单表聚合、超大数据集查询上有很强的性能底气,生态也更完整。但它过去在多表JOIN、高并发点查、事务一致性方面偏弱;Doris/StarRocks在SQL兼容性、多表关联、并发能力上做了更多优化,更适合需要强SQL体验和较高QPS的OLAP场景。

我的建议是:如果你手里主要是“海量明细 + 指标聚合 + 低并发大查询”,无脑优先评估ClickHouse;如果需要较高并发的BI报表、多维分析,且团队对标准SQL依赖重,那可以把Doris/StarRocks也纳入选型清单,跑一轮压测再拍板。

2. 核心机制拆解:它凭什么能在几十亿行上跑得快

2.1 列式存储:只搬你需要的箱子

刚才提到了列式存储,这里再深入一点。ClickHouse底层每个字段的数据独立存放,查询时会同时读取多个列,但不会把无关列也拉出来。它对“读取路径”的控制让磁盘IO优化到了极致。

打个比方,你去仓库找一堆旧发票里所有金额超过一万的记录。行式存放时,你得把每张发票的完整档案抱出来翻一遍,档案里附带的客户名、地址、发票号全看了才能确认金额;列式存放则是所有发票按金额列单独装订成册,你直接翻金额册子找出符合条件的页码,再按页码去取其他必要的信息。数据量越大,这种物理隔离带来的扫描量差距越明显。

实际使用中,列数越多、分析查询涉及的列越少,ClickHouse的优势就越明显。如果一张宽表有60列,而分析任务每次只碰其中5列,查询效率可能比行式存储提升一个数量级。这里有个反面教训:千万不要把ClickHouse当广告系统里的KV存储用,如果你天天按ID去取整行数据,那列式存储的优势完全发挥不出来。

2.2 向量化执行与数据块:批处理才是关键

光有列式存储还不够,另一个让ClickHouse快的核心机制是“向量化执行引擎”。传统数据库执行查询时,通常是一行一行地处理:读一行、解析、判断、计算、输出,循环往复。CPU每次处理一行数据都要经历取指令、分支判断、内存访问等固定开销,流水线效率很低。

ClickHouse的做法是把数据按批处理,一次处理一整块(默认每块8192行)。执行引擎对整块数据应用同一种操作,尽量利用CPU的SIMD指令集,让寄存器一次性对多条数据做相同计算。这种批处理模式大幅减少了循环次数和分支预测失败的代价,CPU的利用率能维持在很高水平。

理解这个机制之后,你会发现一个实用原则:尽量用ClickHouse内置的高阶函数和聚合函数,不要为了省事把数据一个个捞出来在应用层算。应用层拿到的每一行都经过一次序列化和网络传输,再牛的机器也扛不住这种用法。我见过有团队用Python写循环处理几亿行数据,跑了三个小时没出结果,换成ClickHouse一条SQL几分钟搞定。这就是向量化和按行处理之间的差距。

2.3 稀疏索引与排序键:数据排列决定查询速度

ClickHouse的索引设计非常特别。它的主键并不像MySQL那样为每一行建立一个索引项,而是采用稀疏索引——每8192行数据划分为一个granule,索引只记录每个granule的起始位置和关键值。数据按照建表时的ORDER BY排序键物理排序存储,所以定位一个大范围时,可以用二分查找快速跳过大量不相关的granule。

这里要澄清一个常见的误解:ClickHouse的主键不要求唯一。它的主键和排序键默认一致,主要用于控制数据的物理排列和索引粒度,而不是为了约束数据唯一性。你在建表时通过ORDER BY指定的字段,才是真正影响查询性能的“灵魂”。

排序键的选择直接决定查询快慢,我的经验是把最常用等值过滤的维度放在最前面。比如事件分析表经常按用户ID过滤,再按时间排序,那ORDER BY就设计成(user_id, event_time)。如果最常用时间范围查询,那就把时间放第一。多列排序键下,前面字段的基数越低、过滤效果越好,后面的字段才能在更小的数据范围内起作用。

另外,ClickHouse自带一个很实用的特性:如果是带主键的小表点查,性能可能还不算差;但真正的强项是范围扫描时通过minmax索引和分区裁剪快速定位到少数granule,而不是逐条遍历索引。

2.4 压缩不是玄学,是白捡的性能

列式存储还带来了一个“买一送一”的收益:数据压缩率极高。同一列的数据类型相同,值往往有很强的规律性。比如用户等级字段就那几十个枚举值,国家代码就那几个,时间戳排序后也很有规律,用LZ4或ZSTD压缩后体积可以缩到原来的十几分之一甚至几十分之一。

磁盘IO减少了,相当于查询速度在同等硬件条件下直接翻倍。这也是为什么在很多测试里,ClickHouse只需要一般的机器就能跑出惊人的性能——它把硬件效率吃得很透。

不过要注意:压缩率与排序键设计强相关。如果数据按时间有序排列,同一分区内的时间列变化平缓,压缩效果就很好;如果排序键杂乱无章,列内值的跳跃幅度大,压缩率会大幅下降。这就是为什么总有人在调排序键时发现“磁盘占用差了好几倍”的原因。

3. 从建表开始走一遍:上手实操与核心设计

3.1 环境准备与部署思路

体验ClickHouse最快速的方式是直接用官方提供的一键部署脚本或容器镜像。本地开发机用Docker起一个实例,几分钟就能跑起来,内存建议至少4GB,生产环境则建议32GB以上,磁盘用SSD,数据目录独立挂载。

生产部署我建议直接参考官方文档做集群规划。ClickHouse没有中心节点和依赖组件,集群里每个节点都是对等的,部署起来相当省心。不过这不代表你可以随便乱上——副本数、分片数、网络带宽都要提前规划,否则后面扩缩容会头疼。

连接工具用官方自带的clickhouse-client就很好用,它支持交互式SQL和批处理脚本。如果想要可视化界面,可以接Grafana、Metabase、Superset这些开源BI,社区都有现成的数据源插件。官方提供的网页控制台也很直观,适合快速观察集群状态和查询日志。

3.2 MergeTree家族:核心表引擎怎么选

ClickHouse最核心的表引擎是MergeTree系列,几乎所有正经业务表都会用这套引擎家族里的某一类。基础引擎MergeTree负责数据分区的管理、后台合并和索引维护。你写入的数据会先落到内存中的缓冲区,然后按分区写入磁盘形成不可变的数据片段,后台进程再把小片段合并成大片段。这套机制保证了写入吞吐,但也决定了数据修改只能通过异步合并完成。

在MergeTree基础上,官方提供了几个预处理的兄弟引擎:

  • ReplacingMergeTree:写入时按排序键去重,数据合并时同一分组内只保留版本号最大的一条,用来做“按主键更新”的场景。
  • SummingMergeTree:后台合并时把指定数值列按排序键预聚合,适合流量统计、计费汇总这类累加型业务。
  • AggregatingMergeTree:合并时对指定列执行预聚合,通常配合物化视图使用。
  • CollapsingMergeTree和VersionedCollapsingMergeTree:用于处理变更流数据,通过正负信号抵消旧值。

需要注意的是,这些引擎并不是在写入时立即完成去重或聚合,而是在后台合并时才处理。所以查询时你可能需要用FINAL关键字,或者容忍一定程度的暂时冗余。很多人一开始不理解这个设计,以为建了ReplacingMergeTree就能像MySQL一样“按主键覆盖更新”,结果重复数据迟迟不消失,最后才意识到这是合并模型。

3.3 核心SQL与建表实操:一个完整的例子

我以一张用户行为事件表为例,带你过一遍设计过程。

假设业务要分析用户访问、下单、支付等行为,字段包括事件时间、用户ID、事件类型、页面ID、金额等。最常用的查询是“按天/按小时统计不同事件类型的PV、UV”,那建表语句可以这样:

CREATE TABLE events ( event_date Date, event_time DateTime, user_id UInt64, event_type LowCardinality(String), page_id String, revenue Decimal(18, 2) ) ENGINE = MergeTree PARTITION BY toYYYYMM(event_date) ORDER BY (event_date, user_id, event_type);

几个关键点:

  • PARTITION BY toYYYYMM(event_date)把数据按月分区,查询如果带了月份范围,可以秒级裁剪掉无关分区。注意分区不是越细越好,分区粒度过细会产生大量小文件,后台合并压力剧增。
  • ORDER BY (event_date, user_id, event_type)是排序键,也是默认主键。日期过滤能将查询范围缩到特定时间区间,再叠加用户维度的等值过滤,分析查询可以快速定位到对应granule。
  • event_type用了LowCardinality类型。这个类型对低基数字段做了字典编码,既能压缩,又能加速GROUP BY,是ClickHouse里性能收益极高但常被人忽略的类型。
  • revenue用Decimal而不是Float,避免金额计算出现二进制浮点误差,数据分析尤其要注意这类“隐形扣费”问题。

数据导入可以用INSERT INTO ... SELECT,或者直接把文件通过clickhouse-client导入。注意要一次插入成批数据,一次插几万行比一次插一行效率高得多。导入前后可以查一下system.parts表,确认分区数和片段数是否健康。如果某个分区下片段太多,说明写入频率过大而合并跟不上,需要调整写入节奏。

3.4 分布式表、副本与集群:别在第一步就埋雷

生产环境一定会用到多节点。ClickHouse的集群方案分两层:数据副本由ReplicatedMergeTree负责,具体方式是建表时指定ENGINE=ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}'),并配置ZooKeeper或ClickHouse Keeper来协调副本状态。

分发查询则通过Distributed表实现。Distributed表本身不存数据,它只是本地表的路由视图。查询时发起节点会把SQL分发到各分片并行执行,再把结果汇总返回。

这里有个新手极易踩的坑:不要直接往Distributed表里INSERT数据,除非你能接受数据在分片间分布不均匀的风险。分布式表写入默认是异步转发,如果某个分片短暂不可用,数据会滞留在本地磁盘目录里,久而久之就会造成分片数据不一致。推荐的做法是业务侧按照分片键将数据直接写入对应的本地表,或者通过Buffer表缓冲一批后统一写入。

副本和分片的配置细节确实琐碎,但值得花时间理顺。我的建议是刚开始做集群测试时,用官方提供的一键集群部署脚本,在测试环境里跑两三个节点,把副本同步、节点宕机恢复的流程都演练一遍再上生产。

4. 高频踩坑与排查思路:真实环境里的避坑总结

4.1 查询慢?先别怀疑数据库,先查排序键

很多人反馈“我的ClickHouse怎么也没快到哪里去”,我看到的绝大多数情况,问题不是出在数据库,而是出在建表设计。

一个典型场景:把一张宽表的所有字段都写进ORDER BY,排序键长度超过10个字段,索引失去重点,压缩率变差,磁盘扫描量暴涨。另一个典型场景:排序键第一个字段是高基数的请求ID,查询都是按天过滤,但时间字段排在第五位。排序键前缀没有命中,索引退化成全扫描,自然快不起来。

排查路径很固定:先到system.query_log里看查询的实际扫描行数和耗时,如果发现扫描行数接近全表,优先调整排序键。再检查read_rows和read_bytes两个指标,如果很高,说明数据裁剪没做好。这条链路是性能排查的第一步,比调几十个配置参数都管用。

4.2 数据更新删除为什么这么慢

ClickHouse的设计目标是分析查询,对数据修改的模型是“不可变文件 + 异步合并”。你执行ALTER TABLE ... DELETE的时候,它并非真正立即删除文件里的行,而是给相关数据片段打上删除标记,后台再逐步合并重写。如果一次删除的数据量很大,后台合并会持续很长时间,同时占用大量CPU和磁盘IO。

所以尽量避免频繁更新。如果你的业务确实需要按主键做变更,考虑用ReplacingMergeTree + 版本号字段设计“逻辑更新”,或者通过CollapsingMergeTree用一正一负两条记录抵消旧数据。我见过有团队用“今天插入全量分区数据,明天把昨天分区DROP掉”的方式做每日快照,这种方式和ClickHouse的机制配合得很好,效率也极高。

4.3 资源管理:内存、分区数与查询并发

ClickHouse查询是“吃内存”大户,尤其GROUP BY数据量极大、内存参数设置过高时,一个查询就能把节点内存吃满。生产环境要重点控制max_memory_usage、max_threads这类参数,同时给关键查询设置合理的优先级和资源限额。

另一个常发的问题是分区数过多。有的同事习惯按天建分区,跑上一年就积累了365个分区,后台线程要管理几千个数据片段,合并永远追不上写入速度,最后表现就是集群越来越卡。对于大多数场景,按周或按月分区足够。做历史数据清理时,直接DROP PARTITION比DELETE快得多,这也是分区设计带来的额外收益。

还有并发问题:ClickHouse在“低并发大查询”场景表现极好,但不适合几千个查询同时狂砸。高并发场景要做查询网关限流,或者把结果预热到Redis,避免把后端压垮。

4.4 常见问题速查表

现象可能原因建议处理
查询扫描了全表ORDER BY字段与查询过滤条件不匹配调整排序键前缀,按查询习惯重排列
数据重复使用MergeTree且未加去重逻辑换ReplacingMergeTree并加版本字段
分区文件过多分区粒度过细、写入过于碎片化扩大分区周期,按批次批量写入
单查询内存爆掉GROUP BY基数过大调整max_memory_usage,用物化视图预聚合
删除更新极慢不可变文件机制改为分区替换方式,或逻辑删除
写入后立即查不到最新数据异步写入队列延迟改用本地表同步写入或调整缓冲区刷新策略

5. 生态现状与未来前景:我的几点判断

5.1 当下的生态位

从数据库热度来看,ClickHouse已经成为开源OLAP数据库里最有号召力的项目之一。它的社区持续活跃,官方迭代速度很快,从最初偏“单表分析引擎”逐步向“完整数仓解决方案”演进。官方发布了备份工具、迁移工具、可视化运维面板,还支持S3、HDFS等外部存储的集成,甚至可以直接query外部数据表,这使得ClickHouse在数据湖架构里也能承担起一部分加速层的角色。

周边生态也比较完整。主流BI工具都能通过JDBC/ODBC连进来,Java、Go、Python都有稳定的官方驱动,使用体验在开源数据库里属于第一梯队。Grafana和Metabase这类可视化工具的接入也很方便,做监控大盘、业务报表都可以直接对接。

更重要的是,很多企业已经把它作为实时数仓的核心底座:前端实时写入,后台深夜做批处理合并,上层BI直接从ClickHouse出报表。这个位置上的替代品不多,ClickHouse至少在接下来几年都很难被撼动。

5.2 未来演进方向

从我观察到的版本更新和社区讨论看,ClickHouse未来的几个方向会很明显:

  • 存算分离与云原生。云上部署时,计算节点和存储节点可以独立扩容,官方持续在推动对象存储作为主要存储介质的方案,这会进一步降低海量数据的存储成本。
  • 查询优化器的持续增强。过去被认为偏弱的复杂JOIN、子查询优化,这几年已经有明显进步。虽然不可能变成第二个数据库,但“能扛更多标准SQL”是明确趋势。
  • 更大规模的半结构化数据支持。JSON类型的改进、可变字段的处理能力逐步增强,方便直接分析日志和接口上报数据。
  • 点查和事务能力的补强。Keeper、轻量级更新等特性的引入,让它在“分析为主、少量更新”的场景里越来越顺手。

在我看来,ClickHouse不会也不需要成为覆盖所有场景的万金油,它只要守住“分析型查询性能第一梯队”的定位,就足够获得长期生命力。

5.3 给选型者的几句实在话

每次有人问“我应该用ClickHouse吗”,我都会反问三个问题:你的数据是不是以追加写入为主?你的查询是不是以聚合分析为主?你是不是可以接受最终一致性、弱事务?

如果三个问题都是肯定的,那ClickHouse基本不会让你失望。反过来,如果你需要高并发点查、复杂事务、强一致更新,或者你的团队没有专职数据工程师来设计表结构,那它可能不是最优解。

技术选型最怕跟风。多花半天做一次真实数据的压测,跑几个和业务最接近的查询,再决定要不要上。压测结果可能比任何广告和测试数据都诚实。

写在后面:我的实际使用感受

我自己的经验是,ClickHouse的收益很大程度上取决于你如何设计表结构,而不是你用了多牛的机器。排序键调对了,查询速度能提升几十倍;分区设计合理,维护成本能降低一个数量级;写入节奏控制好,后台压力小得你几乎感觉不到它的存在。这套设计理念和传统的MySQL思维差别很大,刚上手时容易不习惯,但一旦理解了“数据先排好再分析”的逻辑,你会觉得一切都很自然。

如果你正在做数据平台、实时报表、用户行为分析这类事情,而且还没有找到好用的分析引擎,建议找个下午装个单机版,导入一份千万级数据,亲手跑几个聚合查询,再翻一遍系统表看看扫描行数和压缩率。自己跑出来的直观数据,比看任何评测文章都有说服力。后面我会在这个系列里继续整理性能调优和物化视图的实践心得,也欢迎你带着实际问题来交流。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进