ClickHouse 2026 深度实战:当列式存储遇见 AI 时代——从 MergeTree 引擎到 PB 级实时分析,构建下一代数据基础设施的完全指南
2026 年,数据基础设施的战场已经从“能不能存得下”转移到“能不能查得快、用得省”。ClickHouse 这个源自 Yandex _metrica 的列式数据库,在实时分析、可观测性、AI 特征工程等场景里已经不再是“可选项”,而是很多团队的核心依赖。这篇文章不会只讲概念,而是带着你从存储引擎、查询执行、分布式架构到生产调优,完整走一遍 ClickHouse 的实战落地。
一、背景:为什么 2026 年我们还要认真聊 ClickHouse?
数据量还在涨。一个中等规模的互联网产品,每天产生的日志、事件、指标数据轻松就能到 TB 级别。传统关系型数据库在这种场景下早就力不从心,Hadoop 生态太重、实时性不够,而各种“新一代数仓”又往往在易用性和性能之间做取舍。
ClickHouse 的定位非常清晰:单表查询性能极致、SQL 原生、部署简单、生态开放。它不是万能数据库,但在 OLAP(联机分析处理)场景下,几乎是你能拿到的性价比最高的方案之一。
2026 年的几个趋势让 ClickHouse 更值得关注:
- AI 驱动的可观测性:LLM 应用、Agent 系统的调用链、成本、延迟指标爆炸式增长,需要极低的查询延迟和极高的压缩比。
- 实时特征工程:推荐、风控、广告系统越来越多地把特征计算下沉到 ClickHouse,用物化视图和投影做流式聚合。
- 云原生与存算分离:ClickHouse Cloud、S3 backed MergeTree 让 PB 级数据成本大幅下降。
- 开源生态爆发:围绕 ClickHouse 的工具链(如 cerberus 将 Prometheus/Loki/Tempo 查询转发到 ClickHouse)正在成熟。
如果你是后端、大数据、SRE 或 AI 基础设施工程师,理解 ClickHouse 已经从“加分项”变成“必选项”。
二、核心概念:先搞懂 ClickHouse 为什么快
2.1 列式存储:不只是“按列存”
传统行式数据库把一行数据连续存放,适合 OLTP 的短事务查询。但分析查询通常只读取少数几列,行式存储会把无关列也读进内存,浪费 I/O 和 CPU。
ClickHouse 把同一列的数据连续存放。这样做有三个直接好处:
- 只读需要的列:
SELECT user_id, amount FROM orders只会读取这两列,而不是整行。 - 更好的压缩:同一列的数据类型相同,值域分布有规律,压缩率通常是行式的 5-10 倍。
- 向量化执行:CPU 可以一次性处理一批同类型的值,配合 SIMD 指令大幅提升效率。
ClickHouse 的列存不是简单的“列文件”,而是把数据切分成 part(数据片段),每个 part 内部按列组织,并带有稀疏主键索引、统计信息(min/max/count)等元数据。
2.2 MergeTree 引擎家族:ClickHouse 的灵魂
MergeTree 是 ClickHouse 最核心的表引擎。名字里的“Merge”指的是后台会不断合并小的 part,减少文件数量、优化查询性能。
CREATE TABLE orders
(
`order_id` UInt64,
`user_id` UInt64,
`amount` Decimal(18, 2),
`status` LowCardinality(String),
`created_at` DateTime,
`event_date` Date DEFAULT toDate(created_at)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, created_at)
SETTINGS index_granularity = 8192;
这段 SQL 里藏着几个关键设计:
ORDER BY:定义了数据的物理排序键,同时也是稀疏主键索引。查询时如果过滤条件包含排序前缀,可以大幅跳过不需要的 granule。PARTITION BY:数据按分区目录存放。按时间分区是最常见的做法,方便生命周期管理和按时间范围剪枝。SETTINGS index_granularity = 8192:每个 granule 默认包含 8192 行。这是稀疏索引的粒度,调小会增大索引体积,调大可能降低过滤精度。LowCardinality(String):对于重复值多的字符串列,ClickHouse 会用字典编码,通常能省 3-5 倍空间并提升查询速度。
MergeTree 的变体很多,生产中最常用的包括:
| 引擎 | 适用场景 |
|---|---|
| MergeTree | 单表分析,默认选择 |
| ReplacingMergeTree | 需要按版本去重,如最新用户状态 |
| SummingMergeTree | 预聚合指标,如 PV/UV 汇总 |
| AggregatingMergeTree | 复杂聚合状态,如漏斗、留存 |
| ReplicatedMergeTree | 分布式高可用,配合 ZooKeeper/Keeper |
| VersionedCollapsingMergeTree | 需要支持修改、删除的历史数据 |
2.3 向量化执行与 SIMD
ClickHouse 的查询执行是按列批处理的。一个算子一次处理 8192 行同一列的数据,而不是一行一行处理。这带来的好处:
- CPU 缓存命中率高;
- 可以充分利用 SIMD 指令(AVX2/AVX-512);
- 函数调用开销被摊薄到整个批次。
比如计算 sum(amount),ClickHouse 不是遍历每一行,而是批量读取 amount 列的压缩块,解压缩后一次性求和。对于聚合函数,还会利用状态合并(aggregate function states)来减少内存占用。
2.4 稀疏索引:不是 B+ 树
ClickHouse 没有传统意义上的 B+ 树索引。它使用的是稀疏主键索引(primary.idx),每个 granule 只在索引里记录一个标记值。查询时通过二分查找定位到候选 granule,再通过列的统计信息做进一步剪枝。
这种设计决定了 ClickHouse 的索引策略:
- 最适合:等值、范围查询命中排序前缀;
- 不太适合:高基数列的随机点查;
- 可以通过跳数索引(data skipping indexes)补强:如 minmax、set、bloom_filter、tokenbf_v1 等。
ALTER TABLE orders
ADD INDEX idx_status status TYPE bloom_filter GRANULARITY 3;
Bloom filter 跳数索引对 WHERE user_id IN (...) 或 WHERE url = '...' 这类高基数点查特别有效。
三、架构分析:单机、分布式与云原生
3.1 单机架构:极简起点
单机 ClickHouse 就是一个进程 + 一份数据目录。你可以用 Docker 一分钟跑起来:
docker run -d --name clickhouse-server \
-p 8123:8123 -p 9000:9000 \
--ulimit nofile=262144:262144 \
clickhouse/clickhouse-server:latest
单机模式下,数据按 database/table/part 的层级存放在 /var/lib/clickhouse/data/。每个 part 是一个目录,里面包含各列的压缩文件(.bin)、标记文件(.mrk)和索引文件。
单机的极限通常取决于:
- 磁盘 IOPS(NVMe SSD 是刚需);
- 内存大小(用于缓存、排序、聚合);
- CPU 核心数(向量化执行需要多核)。
对于日均 TB 级写入、查询 QPS 不高的场景,单机 ClickHouse 往往能撑很久。
3.2 分布式架构:分片与副本
当单机不够用时,ClickHouse 提供了**分布式表(Distributed)**机制:
-- 先在每个分片建本地表
CREATE TABLE orders_local ON CLUSTER 'my_cluster' (...)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/orders', '{replica}')
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, created_at);
-- 再建一张分布式表做路由
CREATE TABLE orders_distributed ON CLUSTER 'my_cluster' AS orders_local
ENGINE = Distributed('my_cluster', 'default', 'orders_local', rand());
这里有几个要点:
ReplicatedMergeTree依赖 ZooKeeper 或 ClickHouse Keeper 做副本协调;Distributed表本身不存数据,只是把查询路由到各个分片的本地表;- 分片键(
rand())决定数据写入哪个分片,通常用user_id或sipHash64(order_id)保证数据分布均匀; - 分布式查询默认会广播到所有分片,结果在查询节点合并。
分布式架构的写入有两种模式:
- 同步写入(insert_distributed_sync=1):写入分布式表时,等所有分片 ack 再返回。可靠性高,但延迟大。
- 异步写入(默认):数据先写到分布式表的本地队列,后台再批量转发到各分片。吞吐高,但有小概率丢失。
3.3 查询生命周期:一条 SQL 是怎么执行的?
理解 ClickHouse 的查询生命周期,对调优至关重要:
- 解析与优化:SQL 解析成 AST,经过类型检查、常量折叠、谓词下推等优化;
- 索引剪枝:根据
WHERE条件和稀疏索引,确定需要读取的 part 和 granule; - 读取列数据:从磁盘读取目标列的压缩块,解压缩成列块;
- 过滤与计算:应用过滤条件、表达式计算、聚合;
- 合并结果:如果是分布式查询,各分片返回部分结果,由查询节点二次聚合;
- 返回客户端。
ClickHouse 的 EXPLAIN 可以帮我们查看执行计划:
EXPLAIN SELECT user_id, sum(amount)
FROM orders
WHERE event_date >= '2026-06-01'
GROUP BY user_id;
3.4 云原生与存算分离:S3-backed MergeTree
2026 年,越来越多的团队把 ClickHouse 部署在云上。ClickHouse Cloud 提供了全托管服务,而自托管方案里最值得关注的是 S3-backed MergeTree(也称 MergeTree over S3 或 SharedMergeTree)。
核心思路:热数据放在本地 SSD,温冷数据自动下沉到 S3 对象存储。这样可以用极低的成本保留 PB 级历史数据,同时保证近期数据的查询性能。
<storage_configuration>
<disks>
<s3_disk>
<type>s3</type>
<endpoint>https://bucket.s3.amazonaws.com/clickhouse/</endpoint>
<access_key_id>AKIA...</access_key_id>
<secret_access_key>...</secret_access_key>
</s3_disk>
</disks>
<policies>
<tiered>
<volumes>
<hot>
<disk>default</disk>
<max_data_part_size_bytes>1073741824</max_data_part_size_bytes>
</hot>
<cold>
<disk>s3_disk</disk>
</cold>
</volumes>
<move_factor>0.2</move_factor>
</tiered>
</policies>
</storage_configuration>
配合 TTL 策略,可以让数据在 7 天后自动移动到 S3:
ALTER TABLE orders
MODIFY TTL event_date + INTERVAL 7 DAY TO VOLUME 'cold',
event_date + INTERVAL 365 DAY DELETE;
四、代码实战:从 0 到 1 搭建实时分析 pipeline
4.1 环境准备:Docker Compose 一键启动
version: '3.8'
services:
clickhouse:
image: clickhouse/clickhouse-server:latest
ports:
- "8123:8123"
- "9000:9000"
volumes:
- ./data:/var/lib/clickhouse
- ./config:/etc/clickhouse-server/config.d
ulimits:
nofile:
soft: 262144
hard: 262144
kafka:
image: confluentinc/cp-kafka:latest
ports:
- "9092:9092"
environment:
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
4.2 建表:一个典型的事件表
CREATE TABLE events
(
`event_id` UUID,
`user_id` UInt64,
`event_type` LowCardinality(String),
`platform` LowCardinality(String),
`country` LowCardinality(String),
`properties` String, -- JSON 字符串,按需解析
`amount` Nullable(Decimal(18, 2)),
`event_time` DateTime64(3),
`event_date` Date DEFAULT toDate(event_time)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (user_id, event_time, event_type)
TTL event_date + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;
4.3 批量写入:INSERT 与 ClickHouse 客户端
ClickHouse 对写入非常友好,支持:
- 标准
INSERT INTO ... VALUES(小数据量测试用); INSERT INTO ... FORMAT CSV/JSONEachRow/Parquet(批量导入);clickhouse-client本地文件导入;- Kafka 引擎、S3 表函数等外部数据源。
Python 示例:
import clickhouse_connect
client = clickhouse_connect.get_client(host='localhost', port=8123)
rows = [
('uuid-1', 10001, 'click', 'ios', 'CN', '{"page":"home"}', 0, '2026-06-21 14:30:00.000'),
('uuid-2', 10002, 'purchase', 'android', 'US', '{"item":"book"}', 29.99, '2026-06-21 14:31:00.000'),
]
client.insert('events', rows, columns=[
'event_id','user_id','event_type','platform','country','properties','amount','event_time'
])
生产环境建议:
- 攒批写入,批次大小 10k-100k 行;
- 使用异步插入(
async_insert=1, wait_for_async_insert=0)提升吞吐; - 同一个分区内避免过多小 part,否则后台 merge 压力大。
4.4 查询实战:从简单到复杂
-- 1. 最近 7 天各平台事件量
SELECT platform, count() AS pv
FROM events
WHERE event_date >= today() - 7
GROUP BY platform
ORDER BY pv DESC;
-- 2. 用户漏斗:注册 -> 浏览 -> 加购 -> 支付
SELECT
sumIf(1, event_type = 'register') AS register_count,
sumIf(1, event_type = 'browse') AS browse_count,
sumIf(1, event_type = 'add_cart') AS add_cart_count,
sumIf(1, event_type = 'purchase') AS purchase_count
FROM events
WHERE event_date >= '2026-06-01';
-- 3. 用户分群:最近 30 天消费金额 Top 100
SELECT user_id, sum(amount) AS total_amount
FROM events
WHERE event_type = 'purchase'
AND event_date >= today() - 30
GROUP BY user_id
ORDER BY total_amount DESC
LIMIT 100;
-- 4. 时间序列聚合:每 5 分钟的事件数
SELECT
toStartOfFiveMinute(event_time) AS ts,
count() AS cnt
FROM events
WHERE event_date = today()
GROUP BY ts
ORDER BY ts;
4.5 物化视图:预聚合的杀手锏
物化视图是 ClickHouse 做实时聚合的核武器。它会在后台自动把源表数据聚合到目标表。
-- 目标表:按小时预聚合的指标
CREATE TABLE events_hourly
(
`event_date` Date,
`hour` UInt8,
`platform` LowCardinality(String),
`event_type` LowCardinality(String),
`pv` UInt64,
`uv` UInt64,
`total_amount` Decimal(38, 2)
)
ENGINE = SummingMergeTree()
ORDER BY (event_date, hour, platform, event_type);
-- 物化视图定义
CREATE MATERIALIZED VIEW events_hourly_mv
TO events_hourly
AS
SELECT
event_date,
toHour(event_time) AS hour,
platform,
event_type,
count() AS pv,
uniqExact(user_id) AS uv,
sum(amount) AS total_amount
FROM events
GROUP BY event_date, hour, platform, event_type;
查询预聚合表时,延迟通常只有原始表的 1/10 甚至 1/100。注意:
SummingMergeTree会在后台合并时对数值列求和;uniqExact的结果需要正确聚合,通常用AggregateFunction配合AggregatingMergeTree更灵活。
4.6 与 Kafka 集成:实时流写入
CREATE TABLE events_kafka
(
`event_id` UUID,
`user_id` UInt64,
`event_type` LowCardinality(String),
`platform` LowCardinality(String),
`country` LowCardinality(String),
`properties` String,
`amount` Nullable(Decimal(18, 2)),
`event_time` DateTime64(3)
)
ENGINE = Kafka
SETTINGS
kafka_broker_list = 'localhost:9092',
kafka_topic_list = 'events',
kafka_group_name = 'clickhouse-consumer',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 4;
-- 消费到目标表
CREATE MATERIALIZED VIEW events_kafka_mv
TO events
AS SELECT * FROM events_kafka;
这套组合是 ClickHouse 实时数仓的经典模式:Kafka 做缓冲解耦,ClickHouse 做实时分析,物化视图做预聚合。
五、性能优化:让 ClickHouse 飞起来
5.1 表设计三板斧
- 排序键尽量贴近查询过滤条件:如果你的查询总是
WHERE user_id = ? AND event_date >= ?,那排序键就应该是(user_id, event_date, ...)。 - 分区粒度要适中:按月分区适合数据量大、按时间查询多的场景;按天分区适合数据量小、需要精确 TTL 的场景。分区太多会拖慢写入和元数据管理。
- 类型要用到极致:能用
UInt就别用String;重复字符串用LowCardinality;固定长度用FixedString;枚举用Enum;UUID 用UUID类型。
5.2 索引与跳数索引
稀疏主键索引是免费的,但只对排序前缀有效。如果查询模式多样,补充跳数索引:
ALTER TABLE events
ADD INDEX idx_user_id user_id TYPE minmax GRANULARITY 4,
ADD INDEX idx_event_type event_type TYPE set(100) GRANULARITY 4;
minmax:适合范围查询;set:适合低基数列的等值查询;bloom_filter:适合高基数列的点查;tokenbf_v1/ngrambf_v1:适合字符串搜索。
5.3 查询优化技巧
- 避免
SELECT *:只读需要的列。 - 大表 join 要小表:ClickHouse 的 join 会把小表广播到所有分片,大表 join 大表容易 OOM。
- 用
PREWHERE代替WHERE:ClickHouse 会自动把适合过滤的列放到 PREWHERE,但显式控制有时更高效。 - 限制聚合粒度:不要一次性
GROUP BY高基数列,可以先做粗粒度聚合,再做细粒度查询。 - 利用
LIMIT BY/TOP K:ClickHouse 的LIMIT n BY category非常适合分组 TopN。
5.4 写入优化
- 攒批:单条 INSERT 性能极差,目标是每次 10k-100k 行;
- 控制并发:写入并发过高会产生大量小 part,merge 压力爆炸;
- 使用
async_insert:小流量场景下开启异步插入,由服务端攒批; - 避免跨分区写入:一次 INSERT 包含多个月份的数据会导致多个分区同时产生 part。
SET async_insert = 1, wait_for_async_insert = 0;
INSERT INTO events VALUES ...
5.5 监控与运维
ClickHouse 暴露了大量指标,建议通过 system 数据库和 Prometheus exporter 监控:
-- 查看 part 数量
SELECT table, partition, count() AS parts
FROM system.parts
WHERE active
GROUP BY table, partition
ORDER BY parts DESC;
-- 查看慢查询
SELECT query, query_duration_ms, read_rows, read_bytes
FROM system.query_log
WHERE type = 'QueryFinish'
ORDER BY query_duration_ms DESC
LIMIT 20;
-- 查看 merge 压力
SELECT *
FROM system.merges;
关键监控指标:
ClickHouseAsyncMetrics_DiskDataBytes/MemoryTracking:资源使用;ClickHouseMetrics_PartsActive/ClickHouseMetrics_Merge:part 和 merge 健康度;ClickHouseProfileEvents_Query/QueryTimeMicroseconds:查询性能;ClickHouseErrorCodes:错误码分布。
六、高级场景:ClickHouse 在 AI 与可观测性中的实战
6.1 LLM 成本与延迟分析
LLM 应用的日志通常包含:模型名、token 输入/输出、延迟、成本、用户请求、响应摘要。ClickHouse 非常适合做这类分析:
CREATE TABLE llm_logs
(
`ts` DateTime64(3),
`model` LowCardinality(String),
`provider` LowCardinality(String),
`input_tokens` UInt32,
`output_tokens` UInt32,
`latency_ms` UInt32,
`cost_usd` Decimal(18, 6),
`request_id` UUID,
`status` LowCardinality(String)
)
ENGINE = MergeTree()
ORDER BY (model, ts)
PARTITION BY toYYYYMMDD(ts);
-- 实时监控每分钟成本和 token 吞吐
SELECT
toStartOfMinute(ts) AS minute,
model,
count() AS calls,
sum(input_tokens + output_tokens) AS tokens,
sum(cost_usd) AS cost,
avg(latency_ms) AS avg_latency
FROM llm_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY minute, model
ORDER BY minute, model;
6.2 可观测性三件套:指标、日志、Trace
结合 cerberus 等工具,ClickHouse 可以替换 Prometheus 远端存储、Loki 日志存储、Tempo Trace 存储。优势:
- 单点存储,减少组件数量;
- SQL 查询,统一分析体验;
- 高压缩比,长期存储成本低;
- 高性能聚合,适合大盘和告警。
6.3 特征工程:实时用户画像
用 ClickHouse 做特征工程,关键是把原始事件流通过物化视图转换成特征表:
CREATE TABLE user_features
(
`user_id` UInt64,
`feature_date` Date,
`last_7d_purchase_cnt` UInt32,
`last_7d_purchase_amount` Decimal(18, 2),
`last_30d_active_days` UInt32,
`category_preference` String
)
ENGINE = ReplacingMergeTree()
ORDER BY (user_id, feature_date);
CREATE MATERIALIZED VIEW user_features_mv TO user_features AS
SELECT
user_id,
today() AS feature_date,
sumIf(1, event_type = 'purchase' AND event_date >= today() - 7) AS last_7d_purchase_cnt,
sumIf(amount, event_type = 'purchase' AND event_date >= today() - 7) AS last_7d_purchase_amount,
uniqIf(event_date, event_date >= today() - 30) AS last_30d_active_days,
topK(3)(event_type) AS category_preference
FROM events
GROUP BY user_id;
七、总结与展望
ClickHouse 2026 年的生态位已经非常清晰:它不是在所有场景下都最优,但在实时分析、可观测性、AI 特征工程这三大场景里,几乎没有更好的开源替代方案。
它的核心优势可以总结为:
- 列式存储 + 向量化执行:查询速度极致;
- MergeTree 引擎家族:灵活应对去重、聚合、版本控制等需求;
- SQL 原生:学习成本低,生态兼容;
- 分布式与云原生:从单机到 PB 级集群都能平滑扩展;
- 开放生态:Kafka、S3、Iceberg、Prometheus、Grafana 无缝集成。
未来 1-2 年,ClickHouse 可能会在以下几个方向继续发力:
- 更强的存算分离:SharedMergeTree 和 ClickHouse Cloud 会成为主流部署形态;
- AI 原生能力:向量检索、Embedding 存储、与 LLM 的更好集成;
- 更轻量的边缘部署:在端侧、IoT 网关里做本地分析;
- 与数据湖更深融合:Iceberg / Delta Lake 表函数、开放表格式支持。
如果你正在做数据平台、实时数仓、可观测性或 AI 基础设施,ClickHouse 值得你花一周时间深入实践。从建表、写入、查询到物化视图、分布式、性能调优,每一个环节都有明确的最佳实践,也有明确的取舍逻辑。
希望这篇长文能帮你少踩几个坑,多榨一点性能。有问题欢迎交流。