
一、为什么 MySQL 在 OLAP 场景慢先看一个典型宽表聚合查询SELECT region, SUM(amount), AVG(price) FROM orders WHERE create_time 2026-01-01 GROUP BY region;假设orders表有 1 亿行、30 个字段。MySQL 是行存Row Store数据按行连续存放磁盘上的存储行存 ┌──────────────────────────────────────────────────────┐ │ row1: [id, name, region, amount, price, ..., 30列] │ │ row2: [id, name, region, amount, price, ..., 30列] │ │ row3: ... │ └──────────────────────────────────────────────────────┘执行这条查询时MySQL 即使有索引也得把每行完整读进内存只为取region/amount/price三个字段——其余 27 个字段的 IO 全是浪费。更糟的是行存压缩率低相邻字段类型不同难以压缩数据量大磁盘 IO 成为瓶颈。这就是 MySQL 快不起来的根因为点查询OLTP设计的存储硬扛分析查询OLAP。二、ClickHouse 核心设计列存 向量化2.1 列存Column StoreClickHouse 把每个列单独存储为一个文件磁盘上的存储列存 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ region │ │ amount │ │ price │ │ create_time│ │ 北京 │ │ 100.5 │ │ 9.9 │ │ 2026-01... │ │ 上海 │ │ 88.0 │ │ 12.3 │ │ 2026-01... │ │ 北京 │ │ 200.0 │ │ 15.0 │ │ ... │ └──────────┘ └──────────┘ └──────────┘ └────────────┘查询只需要读region/amount/price三个文件其他 27 列完全不碰。IO 量直接降到约 1/10。而且同一列数据类型相同压缩率极高LZ4 / ZSTD 通常能压到 3-10 倍进一步减少 IO。2.2 向量化执行Vectorized Execution传统数据库的火山模型Volcano Model是一行一行处理for each row: eval_filter(row) # 一次处理 1 行 if pass: eval_agg(row)火山模型每行都有一次虚函数调用next()CPU 分支预测失败多、指令缓存命中率低。ClickHouse 改为按批次Block默认 8192 行批量处理for each block (8192 rows): eval_filter(block) # 一次处理 8192 行 eval_agg(block)批量处理让 CPU 能从逐行解释切换到批量计算配合 SIMD 指令单指令多数据并行吞吐陡增。三、向量化执行原理3.1 批量处理 减少虚函数ClickHouse 的函数如plus、sum都接受整个列IColumn而非单个值。调用一次就处理整列cpp// 简化示意向量化加法 void FunctionPlus::execute(ColumnVectorFloat64 a, ColumnVectorFloat64 b, ColumnVectorFloat64 res) { const auto a_data a.getData(); // 整列引用 const auto b_data b.getData(); auto res_data res.getData(); size_t n a.size(); for (size_t i 0; i n; i) // 循环体极简易 SIMD 化 res_data[i] a_data[i] b_data[i]; }编译器尤其 Clang -O3会把上面的循环自动向量化成 AVX2 指令一次算 4 个 double; 向量化后示意4 个 double 一次加法 vmovupd ymm0, [arax] vaddpd ymm0, ymm0, [brax] vmovupd [resrax], ymm03.2 SIMD 加速ClickHouse 在热点路径大量手写 SIMDSSE2/AVX2/AVX-512例如过滤WHERE_mm_cmplt_pd批量比较生成位掩码聚合SUM向量内hadds指令水平相加解码批量解压列数据实测 AVX2 相比标量循环在整数聚合上能快 3-5 倍。3.3 零拷贝与内存布局列数据用连续std::vector存储缓存友好Block 在算子间传递是引用不复制字符串列用StringRef指针长度避免拷贝四、源码走读IColumn 与向量化数据流4.1 IColumn 接口ClickHouse 所有列都实现IColumn抽象基类cpp// src/Columns/IColumn.h核心方法示意 class IColumn { public: virtual size_t size() const 0; virtual Field operator[](size_t n) const 0; // 取第 n 行 virtual void insert(const Field ) 0; // 插入一行 // 向量化关键批量操作而不是逐行 virtual ColumnPtr filter(const Filter , ...) const; // 整列过滤 virtual ColumnPtr permute(...) const; };ColumnVectorT是数值列的具体实现内部就是PODArrayT连续内存。4.2 一次查询的执行流SQL → Parser → QueryPlan │ ▼ ┌─────────────────────────────────────┐ │ Pipeline: │ │ Source(读列存文件) │ │ → Expression(向量化过滤) │ │ → Aggregating(按 block 聚合) │ │ → Sink(输出) │ └─────────────────────────────────────┘ 每个算子一次处理一个 Block8192 行Aggregator类把每个 block 的聚合结果 hash 到AggregatedData中最后 merge。全程列在 Block 里流动不拆成单行。五、实测1 亿行聚合对比5.1 环境项目配置数据量1 亿行订单表30 字段CPU16 核 Intel Xeon (AVX2)内存64 GB磁盘NVMe SSD查询单表聚合 过滤 GROUP BY5.2 结果同一查询3 次取中位数引擎建表查询耗时扫描数据量备注MySQL 8.0 (InnoDB)行存 二级索引18.4 s全表 8.2 GB索引帮不上 GROUP BY 全扫MySQL 8.0 (列式插件)—6.1 s—仍非原生向量化PostgreSQL行存12.7 s8.2 GB类似行存瓶颈ClickHouseMergeTree 列存0.18 s压缩后 0.9 GB快 ~100xClickHouse (跳数索引)加minmax索引0.07 s0.3 GB再快 2.5x差距来自三处叠加列存只扫描 3/30 列IO ×0.1、压缩后体积 ×0.1、向量化 SIMDCPU ×5~10。5.3 插入性能引擎批量插入 1 亿行说明MySQL520 s行级事务 二级索引维护开销大ClickHouse95 s顺序写列文件索引异步建六、索引设计MergeTree 的稀疏索引ClickHouse 不用 B 树而是MergeTree 的稀疏主键索引主键 ORDER BY (region, create_time) 决定数据物理排序 每 8192 行index_granularity记录一个稀疏索引条目 ┌───────────────────────────────────────────┐ │ granule 0: region北京, time2026-01-01 │ │ granule 1: region北京, time2026-01-02 │ │ ... │ │ granule N: region上海, time2026-03-xx │ └───────────────────────────────────────────┘稀疏索引只存每块的边界值体积很小能快速跳过不匹配的 granule。跳数索引Skip Index进一步加速sqlALTER TABLE orders ADD INDEX idx_region minmax(region) TYPE minmax GRANULARITY 4;minmax索引记录每个 granule 内region的最小/最大值查询WHERE region北京时直接跳过不含北京的 granule。七、实战建表与查询7.1 建表sqlCREATE TABLE orders ( id UInt64, user_id UInt32, region LowCardinality(String), -- 低基数列用字典编码 amount Float64, price Float64, create_time DateTime, /* 其他字段... */ INDEX idx_region minmax(region) TYPE minmax GRANULARITY 4 ) ENGINE MergeTree() PARTITION BY toYYYYMM(create_time) -- 按月分区利于 TTL 和剪枝 ORDER BY (region, create_time) -- 主键排序决定稀疏索引 SETTINGS index_granularity 8192;7.2 写入sql-- 批量插入避免单行 INSERT用文件/流式批量 INSERT INTO orders SELECT * FROM system_numbers LIMIT 100000000;7.3 查询sqlSELECT region, SUM(amount) AS total, AVG(price) AS avg_price FROM orders WHERE create_time 2026-01-01 GROUP BY region ORDER BY total DESC FORMAT PrettyCompact;八、调优参数参数默认值建议作用index_granularity8192保持 8192稀疏索引粒度max_threads核数设为 CPU 核数并行度max_insert_block_size1048576调大减少小批量写入use_uncompressed_cache0内存够时设 1热数据不解压缓存merge_tree压缩LZ4ZSTD 更省空间牺牲少量 CPU 换 IOprefer_column_compression—开启列级压缩常见坑ORDER BY 选错没把高频过滤列放前面稀疏索引失效JOIN 大表ClickHouse JOIN 弱于聚合尽量用PREWHERE 字典表单行高频 INSERT会触发疯狂小 part 合并写入垮掉用SELECT *列存优势尽失九、总结与选型建议ClickHouse 比 MySQL 快 100 倍的本质不是优化得好而是存储模型和执行模型根本不同列存让分析查询只扫需要的列 高压缩比向量化让 CPU 批量处理 SIMD 并行稀疏索引 跳数索引精准剪枝选型判断报表/日志/埋点/时序分析/实时数仓 → ClickHouse高并发事务下单、转账、账户→ 仍用 MySQL/PostgreSQL需要强 JOIN 事务一致性的分析 → 考虑 Doris/StarRocks下一篇预告ClickHouse 负责算得快但数据从哪来下期我们拆解 Flink CDC 实时入湖 ClickHouse 的管道设计打通实时数仓最后一公里。往期回顾Meta Muse Glimmer 30B 部署实测单卡 4090 跑通本地多模态 AgentDFlash 让推理飙到 233 tok/sIceberg vs Hudi vs Delta Lake数据湖三引擎深度压测与选型SenseNova U1.5 Lite 部署实测8B 单卡跑通 4K 生图Apache 2.0 免费商用