MySQL做报表太慢?引入ClickHouse列式存储,同一个查询快240倍

MySQL做报表太慢?引入ClickHouse列式存储,同一个查询快240倍 大家好我是数据库小学妹 上个月一家零售企业的客户问我。他们的报表系统越跑越慢按地区和月份汇总一次要八分多钟。我查了执行计划。索引用了。JOIN也优化了。但扫描的数据量就是那么大。一千万行数据按地区、按产品、按时间维度汇总MySQL的行式存储天生不擅长这种场景。后来引入了ClickHouse做分析。同一个查询从八分多钟降到两秒。差距不是一个量级。今天我把列式存储的原理、ClickHouse的核心引擎、和MySQL的实际对比写出来。朋友如果你的报表也越跑越慢这篇或许能给你一个新思路。行式存储为什么不适合做报表先看MySQL是怎么存数据的。行式存储 行1: [id1, name张三, region华东, amount100, date2026-01-01] 行2: [id2, name李四, region华南, amount200, date2026-01-02] 行3: [id3, name王五, region华东, amount150, date2026-01-03] ...所有列存在一起。读一行很快。但如果只算amount的总和呢MySQL必须把每一行的所有列都读进内存然后从中提取amount字段。其他列name、region、date也读了但没用到。IO浪费了。数据量小时无所谓。一千万行时这个问题被放大了无数倍。行式存储的设计目标是OLTP在线事务处理。它的优势是增删改快单行查询快事务支持好。报表分析不是它的主场。列式存储是怎么做到快的ClickHouse用的是列式存储。同一个列的数据存在一起。列式存储 id列: [1, 2, 3, 4, 5, ...] name列: [张三, 李四, 王五, 赵六, ...] region列: [华东, 华南, 华东, 华北, ...] amount列: [100, 200, 150, 300, ...] date列: [2026-01-01, 2026-01-02, ...]执行SELECT SUM(amount)时只需要读amount这一列。其他列完全不碰。IO量直接减少了N倍N是列数。这还不是全部。列式存储还有两个杀手锏。数据压缩。同一列的数据类型相同值域相近压缩率极高。amount列全是数字region列重复值多。列存压缩后通常只有行存的1/5到1/10。压缩率对比同一份数据 行式存储MySQL InnoDB约 12GB 列式存储ClickHouse 约 2.4GB 压缩比5:1向量化执行。传统数据库一行一行算。ClickHouse一批一批算。这里面的关键是CPU的SIMD指令。SIMD全称Single Instruction Multiple Data。单指令多数据。普通CPU指令一次处理一个值。SIMD指令一次处理8到16个值。ClickHouse把查询编译成向量化算子每个算子直接调用SIMD指令。传统执行逐行 for row in rows: result row.amount ← 每次处理1个值 向量化执行批量 result SIMD_SUM(amount_chunk) ← 每次处理8-16个值我一开始不理解这有什么用。后来我用perf看了ClickHouse的执行profile发现SUM操作走的是AVX2指令集一个循环处理256位数据。64位的DECIMAL值一次能算4个。这就是为什么列存的SUM能比行存快几十倍。数据挨在一起CPU可以批量加载到向量寄存器里一条指令搞定。但向量化加速有个前提数据必须连续。行式存储里amount字段和其他列混在一起CPU要先跳过无关列才能取到数据。列式存储里amount值连续存放CPU直接批量加载。列存和向量化是天生一对。向量化的加速效果取决于CPU。支持AVX2指令集的CPU向量化SUM可以快4到8倍。理解了列式存储和向量化再来看ClickHouse的表引擎就顺了。ClickHouse有很多表引擎MergeTree家族是最核心的。MergeTree的工作原理是把数据按主键排序后分成多个part数据块每个part独立压缩存储查询时只扫描相关的part。MergeTree数据组织 part_1: [主键范围 1-10000] ← 压缩存储有索引 part_2: [主键范围 10001-20000] part_3: [主键范围 20001-30000] ... 查询 WHERE id BETWEEN 5000 AND 15000 → 只需扫描 part_1 和 part_2 → part_3 及之后的part跳过MergeTree有两个核心特性。主键索引稀疏索引。不是B树而是一个稀疏的标记。每隔一定行数默认8192行ClickHouse叫index_granularity记录一个主键值和它对应的mark文件偏移。查询时先查索引标记通过mark文件定位到具体的数据块只读那个块。8192这个默认值和ClickHouse的存储设计有关——每个granule存8192行数据一个索引标记对应一个granule索引体积小但足以定位。如果有大量点查WHERE id xxx的需求可以调小index_granularity来加密索引代价是索引体积变大。数据合并Merge。新写入的数据先放到一个小part。后台自动把小part合并成大part。合并时重新排序、去重、压缩。这个过程对用户透明。创建一个MergeTree表CREATETABLEorders(order_id UInt64,customer_id UInt64,region String,product_id UInt32,amountDecimal(12,2),order_dateDate,statusUInt8)ENGINEMergeTree()ORDERBY(order_date,region,customer_id)-- 排序键也是主键PARTITIONBYtoYYYYMM(order_date);-- 按月分区ORDER BY在这里不是排序操作是定义主键排序键。数据按这个顺序存储决定了查询能用哪些索引。ORDER BY的字段顺序很关键——把查询最常用的过滤条件放前面这样WHERE条件才能命中索引。我一开始把customer_id放第一位后来发现大多数查询都是按时间范围过滤改成(order_date, region, customer_id)后扫描的granule少了很多。PARTITION BY把数据按月分成独立的分区。查询时如果带了时间范围条件ClickHouse会先用分区裁剪partition pruning跳过不相关的分区只扫描目标分区。查三个月的数据就只读三个分区不碰其他月份。分区粒度要权衡太细按天会导致part数量太多Merge压力大太粗按年裁剪效果差。按月分区是比较稳妥的折中。实际对比同一个查询MySQL vs ClickHouse准备同样的数据。一千万条订单记录。MySQL行式存储-- MySQL 表结构CREATETABLEorders(order_idBIGINTPRIMARYKEY,customer_idBIGINT,regionVARCHAR(20),product_idINT,amountDECIMAL(12,2),order_dateDATE,statusTINYINT,INDEXidx_date_region(order_date,region))ENGINEInnoDB;-- 查询按地区和月份汇总销售额SELECTregion,DATE_FORMAT(order_date,%Y-%m)ASmonth,COUNT(*)ASorder_count,SUM(amount)AStotal_amountFROMordersWHEREorder_date2026-01-01ANDorder_date2027-01-01GROUPBYregion,DATE_FORMAT(order_date,%Y-%m)ORDERBYmonth,region;执行结果12 rows in set (8 min 34 sec)8分34秒。用了索引但GROUP BY需要全表扫描符合条件的数据。ClickHouse列式存储-- ClickHouse 表结构CREATETABLEorders(order_id UInt64,customer_id UInt64,region String,product_id UInt32,amountDecimal(12,2),order_dateDate,statusUInt8)ENGINEMergeTree()ORDERBY(order_date,region)PARTITIONBYtoYYYYMM(order_date);-- 同样的查询SELECTregion,toYYYYMM(order_date)ASmonth,count()ASorder_count,sum(amount)AStotal_amountFROMordersWHEREorder_date2026-01-01ANDorder_date2027-01-01GROUPBYregion,monthORDERBYmonth,region;执行结果12 rows in set. Elapsed: 2.145 sec. Processed 10.00 million rows, 120.00 MB2.1秒。快了约240倍。为什么差距这么大维度MySQL InnoDBClickHouse MergeTree存储方式行式列式查询扫描量读取整行所有列只读取查询涉及的列数据压缩无/低高压缩比5:1执行方式逐行向量化批量IO量约2.5GB约120MB用时8分34秒2.1秒但ClickHouse不是万能的我用了一段时间ClickHouse后发现它有几个明显的短板。不适合高频单行更新。ClickHouse的UPDATE是异步的重写操作。不是原地的行级修改。执行一条UPDATE它要重写整个数据part。更新频率高会导致Merge永远追不上写入。-- ClickHouse的UPDATE异步重写不是原地修改ALTERTABLEordersUPDATEamount600WHEREorder_id1;-- MySQL是原地修改ClickHouse是标记旧行删除插入新行-- 下次Merge时才会真正清理我在一个电商项目里踩过这个坑。运营同事要频繁修改订单状态每改一条就触发一次异步重写。上线两周后小part越来越多后台Merge跟不上写入速度查询时要扫大量小part性能反而变差。后来我把高频变更的字段留在MySQL只把最终状态同步到ClickHouse问题才解决。不支持事务。ClickHouse没有ACID事务。不能做并发写入的一致性保证。它的设计假设是数据批量写入后基本不变。如果往ClickHouse并发写入同一批数据没有任何机制保证一致性。所以写入端要做好幂等设计。JOIN能力有限。ClickHouse能做JOIN但实现方式和MySQL不同。它默认用hash join把右表整个加载到内存里构建哈希表。右表小的时候没问题。右表一大就容易OOM。虽然可以通过join_algorithm参数选择partial_merge_join来控制内存但性能会打折扣。复杂的多表关联查询还是MySQL更擅长。ClickHouse的强项是单表聚合分析不是多表关联。我一开始以为ClickHouse能完全替代MySQL。结果上线后发现它不是万能的——单行查询不擅长并发写入没有一致性保证。ClickHouse解决的是大批量数据算得慢的问题不是所有数据库问题。正确的做法是互补不是替代。MySQL管交易ClickHouse管分析。MySQLOLTP ClickHouseOLAP ↓ ↑ 业务写入 → CDC同步 → 分析查询 在线交易 报表汇总 高频更新 批量分析CDC是这套架构的关键。我用过两种方案Canal Kafka。Canal伪装成MySQL从库监听binlog把变更事件发到Kafka再由消费者写入ClickHouse。延迟通常在秒级。优点是不侵入业务代码MySQL只要开了binlog就能用。缺点是多了一个Kafka组件运维成本高一些。Flink CDC。Flink直接读MySQL的binlog做简单的ETL转换后写入ClickHouse。延迟也很低而且Flink本身能做数据清洗和转换。缺点是需要额外部署Flink集群学习曲线陡。我个人偏好Canal方案组件少链路清晰。如果你的数据同步需求复杂比如需要跨库聚合、格式转换Flink CDC更灵活。说了这么多ClickHouse但列式存储不是它的专利。越来越多的数据库在融合行存和列存。HTAP混合事务/分析处理架构试图用一个引擎同时做好OLTP和OLAP。TiDB的TiFlash是比较典型的例子——写入走TiKV行存通过Raft协议把数据实时同步到TiFlash列存副本分析查询直接走列存。一份数据两个引擎对业务层透明。OceanBase也在做类似的行列混存。国产数据库在HTAP方向上也有探索。有的通过行列混存实现一份数据同时服务交易和分析有的通过存算分离架构实现独立扩展。这条路还在走现阶段的HTAP数据库在纯分析场景的性能和ClickHouse还有差距纯交易场景的事务能力也不如传统行存数据库。但方向是对的——如果一个数据库能同时搞定交易和分析运维复杂度确实能降不少。现阶段行列分离、各司其职仍然是最稳妥的方案。ClickHouse做分析MySQL做交易CDC做同步。等HTAP数据库成熟了可以考虑替换。避坑清单别上来就换数据库。先用EXPLAIN看执行计划确认瓶颈真的是IO和扫描量再考虑引入列存。很多时候加个索引、改个SQL就够了。ClickHouse的ORDER BY字段顺序要认真选。把最常用的查询过滤条件放第一位不然稀疏索引形同虚设。我一开始随便排了个顺序查询性能差了5倍后来改成按实际查询模式排序才解决。CDC同步链路要监控。Canal或Flink CDC偶尔会因为网络抖动或binlog格式问题丢数据。上线后一定要加监控比如对比MySQL和ClickHouse的行数差异发现不一致及时补数据。总结报表慢的根本原因可能不是SQL写错了是存储引擎选错了。行式存储天生不适合大批量聚合分析列式存储才是这个场景的正确工具。MySQL做OLTP没问题加上ClickHouse做OLAP用CDC做数据同步这套架构我亲测有效。但数据库选型不是选一个全能的而是选对的组合。根据业务场景选最合适的引擎比追求一个数据库解决所有问题更务实。朋友你的报表查询慢是怎么解决的加了索引还是换了分析引擎欢迎在评论区聊聊。我是数据库小学妹咱们下篇见