clickhouse
通过列式存储、MergeTree 和向量化执行分析海量追加写数据
- S:业务数据量爆炸式增长,日志、埋点、监控指标动辄百亿、千亿行,业务方希望对这些数据做实时多维分析、即席查询和报表。
- C:传统的行式 OLTP 数据库(MySQL、PostgreSQL)面向事务设计,做大规模聚合扫描时 IO 放大严重、CPU 利用率低;Hadoop/Spark 这类离线方案延迟在分钟到小时级,无法满足交互式分析的体验;自研列存又成本高、生态弱。
- Q:有没有一种数据库,既能像列式存储那样高效压缩与扫描海量数据,又能像 SQL 数据库那样易用,并且查询延迟做到亚秒级?
- A:ClickHouse —— 通过列式存储 + 向量化执行 + MPP 并行 + LSM 结构的 MergeTree,在单机上就能跑出 GB/s 级别的扫描吞吐,对百亿行数据的聚合查询通常在秒级返回,且兼容标准 SQL,是当下 OLAP 场景的事实标准之一。
高性能、列式、面向 OLAP 的 SQL 数据库
ClickHouse 是一个列式 OLAP 数据库,主要用于大规模数据分析、实时报表、日志分析、用户行为分析、监控指标分析等场景。
从海量明细数据里,快速算聚合结果。
大量插入,少量更新,频繁聚合查询。
大量追加写 少量修改删除 频繁聚合查询
ClickHouse 的核心不是“一个很快的 SQL 数据库”,而是把 OLAP 查询转化成对 sorted + compressed + immutable columnar data 的高度并行扫描。
实现原理
核心思想:让 CPU 始终在对连续内存块做紧凑的、SIMD 友好的计算。
一句话:少读 + 顺序读 + 批量算 + 多核并行 + 多机并行。
- 列式存储:每列单独成文件,类型一致带来高压缩比(LZ4/ZSTD 常见 5×~20×),查询只读用到的列。
- MergeTree 引擎:数据按主键排序写入不可变 Part,后台持续 Merge 成大 Part(类 LSM Compaction)。
- 稀疏主键索引:每
index_granularity(默认 8192 行)一条索引项常驻内存,配合分区裁剪、Skip Index(minmax/bloom 等)大量跳过数据。 - 向量化执行:算子间传递 Block(默认 65536 行),按列循环 + SIMD + LLVM JIT,避开 Volcano 一次一行的开销。
- MPP 并行:单机按 Part/Granule 多线程扫描;分布式表两阶段聚合(本地 partial → 全局 merge)减少跨节点数据。
- 写入路径:append-only 大批次写一次成一个 Part,小批量会产生大量小 Part 拖垮 Merge;update/delete 是异步 Mutation,重写整段 Part。
- 副本与一致性:
ReplicatedMergeTree借 ZooKeeper/Keeper 异步多副本复制;事务能力弱(仅单 Part 原子),不适合 OLTP。
1.先理解列式数据库和 OLAP。 2.学会 MergeTree 建表。 3.重点理解 PARTITION BY 和 ORDER BY。 4.掌握常用聚合函数:count、uniq、sum、avg、quantile。 5.学会数据导入:CSV、JSON、Kafka、MySQL CDC。 6.学会物化视图做预聚合。 7.再看 Projection、Dictionary、分布式表、集群部署。
表设计核心:
- ENGINE = MergeTree 决定表的存储能力
- PARTITION BY ... 数据怎么分区管理
- ORDER BY ... 数据在磁盘上怎么排序 直接影响查询性能
MergeTree 适合大批量写入、大规模查询、后台自动整理数据的表引擎。
| 项目 | PARTITION BY | ORDER BY |
|---|---|---|
| 主要作用 | 数据分区管理 | 数据排序和索引 |
| 常见字段 | 时间 | 查询过滤字段 |
| 是否越细越好 | 不是 | 也不是,需匹配查询 |
| 常见写法 | toYYYYMM(time) |
(type, time, id) |
| 影响 | 删除、裁剪、管理 | 查询性能核心 |
- MergeTree 是 ClickHouse 最常用表引擎。
- PARTITION BY 通常按时间分区,比如月或天。
- 分区不要按 user_id、uuid、request_id 这类高基数字段。
- ORDER BY 是查询性能核心,不是结果排序。
- ORDER BY 要根据最常用 WHERE 条件设计。
聚合
| 函数 | 作用 | 常见用途 |
|---|---|---|
count() |
统计行数 | PV、请求量、订单量 |
uniq() |
去重计数 | UV、去重设备数 |
sum() |
求和 | 销售额、流量 |
avg() |
平均值 | 平均耗时、平均订单金额 |
min() |
最小值 | 最早时间、最低价格 |
max() |
最大值 | 最晚时间、最高金额 |
quantile() |
分位数 | P95、P99 延迟 |
topK() |
高频值 | 热门页面、热门城市 |
countIf() |
条件计数 | 错误数、点击数 |
sumIf() |
条件求和 | 已支付金额、退款金额 |
写入
- 原则 1:批量写入 为大批量插入优化
- 原则 2:控制小 part 后台会合并 part,但如果小批次太多,后台合并压力会很大
- 原则 3:尽量按分区写入
- 原则 4:数据类型要对齐
- 原则 5:失败要能重试 写入链路最好设计成:应用 → 队列 → 消费者 → ClickHouse
物化视图:如何把慢查询变成快查询
把查询时的聚合计算,提前放到写入时完成。
Projection:表内投影,如何优化不同查询模式
同一张 ClickHouse 表内部,额外存了一份按另一种方式组织的数据(表内副本)
- 物化视图是
另一张显式目标表 - Projection 是
同一张表内部的隐藏优化结构
| 对比项 | Materialized View | Projection |
|---|---|---|
| 存储位置 | 单独目标表 | 原表内部 |
| 查询方式 | 通常查目标表 | 仍然查原表 |
| 是否透明 | 不透明,需要你知道目标表 | 比较透明,优化器自动选择 |
| 适合 | 预聚合、ETL、复杂转换 | 多种排序、部分预聚合 |
| 是否支持 Join 定义 | 支持复杂一些 | Projection 定义不支持 Join |
| 是否支持 WHERE 过滤定义 | 可以 | Projection 定义不支持 WHERE |
| TTL 灵活性 | 目标表可单独 TTL | 不能和源表用不同 TTL |
| 链式处理 | 可以链式物化视图 | 不支持链式 |
Dictionary 高性能 Key-Value 查询结构,常用来做维度表查询
一种来自内部或外部数据源的 key-value 表示,用于低延迟 lookup 查询,常用于优化 Join 或数据 enrichment
把维表变成一个快速查找结构,查询时用 key 去取属性,而不是每次做完整 Join
| 场景 | 更适合 |
|---|---|
| 事实表很大,维表较小 | Dictionary |
| 只需要根据 key 查几个字段 | Dictionary |
| 维表更新不需要秒级实时 | Dictionary |
| 复杂多条件 Join | Join |
| 多对多关系 | Join |
| 需要 Join 后保留大量维表字段 | 宽表或 Join |
| 维度字段查询极高频 | Dictionary 或宽表 |
Join 与宽表设计
宽表 就是把查询常用字段提前合并到一张事实表里
ClickHouse 偏向:查询时少 Join,写入时多处理,尽量让查询阶段扫一张大宽表。
ClickHouse Join 的一个重要规则:小表放右边, 很多 Join 算法会基于右表构建内存结构
| 场景 | 推荐方案 |
|---|---|
| 高频固定报表 | 宽表 |
| 事实表巨大,维表较小,按 key 查属性 | Dictionary |
| 维表很小 | Join 也可以 |
| 临时分析 | Join |
| 多对多关系 | Join 或中间明细表 |
| 维度变化慢,按事件发生时口径统计 | 宽表 |
| 维度变化快,按当前口径统计 | Dictionary 或 Join |
| 查询需要极低延迟 | 宽表或预聚合 |
| 查询字段很多,每次都要补很多维度 | 宽表 |
| 维表太大无法放内存 | Join / Direct Dictionary / 宽表重建 |
TTL 与冷热数据管理
自动删除过期数据 自动冷热分层 自动清理大字段 辅助数据生命周期管理
自动删除过期数据 自动冷热分层 自动清理大字段 辅助数据生命周期管理
MergeTree 变体
ReplacingMergeTree / SummingMergeTree / AggregatingMergeTree
普通 MergeTree 存明细;ReplacingMergeTree 存最后一版;SummingMergeTree 存可累加指标;AggregatingMergeTree 存聚合状态。
| 引擎 | 主要用途 |
|---|---|
ReplacingMergeTree |
去重、保留最新版本 |
SummingMergeTree |
自动把数值列求和 |
AggregatingMergeTree |
存储聚合状态,适合复杂预聚合 |
为什么 数据写进去后,不会像 MySQL 那样频繁原地更新,而是通过后台 merge 慢慢合并数据。 也就是在merge时候做一些操作
数据更新与删除
追加写入 → 后台合并 → 查询时读取列式数据
我要写入一个新事实,让查询或后台 merge 得到新结果
不像 MySQL 那样频繁原地更新:ClickHouse 面向 OLAP,大量数据顺序写入、压缩、列式扫描更重要;原地更新会破坏这种高吞吐写入和列式存储优势。
性能优化基础
让查询少读数据,而不是读完以后算得更快
核心抽象: 从磁盘上很多有序数据块中,尽量少拿块; 优化(哪些块不用读)
第一步:看 query_log read_rows,read_bytes 第二步:看 SQL 访问模式 第三步:看表结构 第四步:EXPLAIN indexes = 1 第五步:决定优化手段
看执行计划、读行数、索引命中、避免全表扫
数据压缩与类型设计
LowCardinality、Enum、Decimal、Nullable 使用建议
| 类型 | 特点 | 适合 |
|---|---|---|
LowCardinality(String) |
灵活,自动字典编码 | 低基数但会变化的字符串 |
Enum8 / Enum16 |
严格,值集合固定 | 稳定状态机、固定枚举 |
String |
最通用,但可能重 | 高基数或自由文本 |
分布式表与集群
三层概念:本地表、分片 shard、副本 replica、Distributed 表 shard 存不同部分的数据;replica 是相同数据的副本;Distributed 表本身不存数据,而是把查询转发到各个 shard,并汇总结果。
- shard:分片,解决容量和吞吐扩展
- replica:副本,解决高可用和读扩展
- Distributed 表:提供一个统一查询入口
| 概念 | 数据关系 | 解决问题 |
|---|---|---|
| shard | 不同 shard 存不同数据 | 扩容、分摊计算、分摊写入 |
| replica | 同一 shard 的多个副本存相同数据 | 高可用、容灾、读扩展 |
| 组件 | 负责什么 |
|---|---|
Distributed |
跨 shard 查询和写入路由 |
ReplicatedMergeTree |
同一个 shard 内的副本复制 |
ON CLUSTER |
在多个节点执行 DDL |
Keeper / ZooKeeper |
协调副本元数据和复制状态 |
ClickHouse 集群 = 多个本地 MergeTree 表 + Distributed 统一入口 + shard 分片 + replica 副本。
副本与高可用
副本主要解决:高可用,容灾,读扩展,故障恢复
ClickHouse 的复制是“表级别”的,同一个 server 上可以同时有复制表和非复制表。
这张表使用了 ReplicatedMergeTree 才有复制能力。
Keeper :副本复制的协调者 / 元数据日志中心 主要存:复制元数据,副本状态,复制队列,分布式 DDL 相关信息
ClickHouse 的复制更适合这样理解:面向 OLAP 的异步副本复制 更关注:高吞吐写入,大规模分析,最终一致,可用性;而不是:每次写后读都强一致
system.replicas 是监控副本状态的核心系统表
Kafka 接入
ClickHouse 有一个 Kafka 表引擎,把 Kafka topic 暴露成一张 ClickHouse 表
查询 Kafka Engine 表时,本质是在消费 Kafka 消息
你查一次
↓
ClickHouse 消费一批消息
↓
consumer offset 往前推进
↓
下次可能读不到同一批消息
Kafka Engine 表更适合搭配:Materialized View;让 MV 持续消费 Kafka,然后写入真正的 MergeTree 表
ClickHouse 接 Kafka,最常见是三张表:
- Kafka Engine 表:负责读 Kafka
- MergeTree 目标表:负责存真实数据
- Materialized View:负责把 Kafka 表的数据写入MergeTree目标表
Kafka topic: user_events
↓
kafka_events_queue -- Kafka Engine 表,不长期存数据
↓
mv_kafka_to_events -- Materialized View,消费并写入
↓
events_local -- MergeTree 表,真正存数据
MySQL 同步到 ClickHouse
MySQL 是行级更新的 OLTP 源库,ClickHouse 是追加优先的 OLAP 分析库,中间通常靠 CDC 把变更流转成可分析数据
MySQL binlog 也是一种事实日志 CDC 是把数据库变更日志变成事件流 ClickHouse 消费这个事件流,生成分析表
CDC: Change Data Capture 变更数据捕获:捕获源数据库里发生了什么变化 CDC 把数据库内部的变化,变成外部系统可以消费的事件流。
MySQL binlog
↓
解析 INSERT / UPDATE / DELETE
↓
生成变更事件
↓
写入 Kafka 或直接写入 ClickHouse
Debezium 读取 MySQL binlog ,把 INSERT / UPDATE / DELETE 变成 Kafka 消息 Flink 清洗 / join / 聚合 / 宽表化 DataX 定时批量抽取 Airbyte 快速搭建数据同步
Debezium 偏“捕获变更” Flink 偏“处理变更” DataX 偏“批量搬运” Airbyte 偏“连接器化同步”
| 场景 | 方案 |
|---|---|
| 简单单表同步 | Airbyte / DataX / ClickPipes / connector |
| MySQL binlog 进入 Kafka | Debezium |
| 多表 join、清洗、聚合 | Flink CDC |
| 离线批量导入 | DataX / Spark / 自研批处理 |
| 高度定制同步语义 | 自研 consumer |
| 需要复杂错误处理和回放 | Kafka + Flink / 自研 |
监控与运维
system 表:小 part、慢 merge、副本延迟、磁盘快满,是 ClickHouse 运维四大高频问题
| 问题 | 主要看哪里 |
|---|---|
| 慢查询 | system.query_log |
| 当前正在跑的查询 | system.processes |
| 表的 part 是否过多 | system.parts |
| merge 是否跟不上 | system.merges |
| 副本是否延迟 | system.replicas |
| 复制队列是否堆积 | system.replication_queue |
| 磁盘空间 | system.disks |
| 表大小 | system.parts |
| mutation 是否卡住 | system.mutations / system.merges |
| Kafka 消费是否异常 | Kafka consumer lag + ClickHouse logs / Kafka 表状态 |
实战项目:用户行为分析系统
从建表、导入、预聚合到报表查询完整走一遍
评论