ClickHouse 26.6 深度实战:当列式存储终于在实时分析赛道碾压一切——从 MergeTree 内核、物化视图管线到千亿级实时数仓的生产级完全指南
这是一篇写给"既要懂原理、又要在生产环境扛住流量"的工程师的长文。我们不堆术语,而是从一次真实的实时分析链路出发,把 ClickHouse 最核心的 MergeTree 内核、物化视图管线、集群架构与性能调优,一个一个拆开讲透,并配上可运行的代码。
0. 为什么是 ClickHouse,为什么是现在
过去十五年,数据分析经历了三次范式转移:
- 第一代:离线批处理。Hive on MapReduce,T+1 出报表,写一条 SQL 要等一杯咖啡凉透。
- 第二代:预聚合立方体。Kylin、Druid 用空间换时间,查询快了,但模型僵硬,维度一变就要重建。
- 第三代:交互式列式分析。ClickHouse 代表了这一代——不强制预聚合,直接在千亿行明细上做亚秒级交互查询。
到 2026 年,ClickHouse 26.6 已经成为实时分析的事实标准之一:从初创公司的埋点看板,到大型电商的实时大屏,再到风控的毫秒级特征计算,都能看到它的身影。很多人把它和 Elasticsearch、Druid、StarRocks 混为一谈,但 ClickHouse 的独特之处在于——它本质上是一个为"海量明细数据上的复杂聚合"而生的列式 DBMS,而不是搜索引擎,也不是纯预聚合立方体。
先说清楚一件最重要的事:ClickHouse 不是万能的,别拿它当主库。
| 场景 | 该用 | 不该用 |
|---|---|---|
| 实时多维分析、明细检索、TopN、漏斗、留存 | ✅ ClickHouse | |
| 高并发点查(按主键取一行)事务 | ❌ 用 PostgreSQL / MySQL | |
| 全文检索、日志 keyword 高亮 | ❌ 用 Elasticsearch | |
| 单机本地分析、嵌入式 | ❌ 用 DuckDB | |
| 强事务、跨行 ACID | ❌ 用 OLTP 数据库 |
一句话:ClickHouse 的甜区是"写多、查少但查重、数据只追加、允许最终一致"的分析负载。理解了这一点,后面所有设计选择都能自洽。
一、核心概念:列式存储到底快在哪
1.1 行存 vs 列存:I/O 放大是元凶
假设一张用户行为表有 50 个字段,一条查询只需要 COUNT(DISTINCT user_id) 和 SUM(amount)。
- 行存(MySQL/PostgreSQL 默认):必须把整行 50 个字段都从磁盘读进内存,再丢弃 48 个不需要的。I/O 放大 50 倍。
- 列存(ClickHouse):
user_id和amount各自连续存储,只读这两列的物理块。I/O 缩小到 2/50。
这还只是第一层收益。第二层收益是压缩率:同一列的数据类型相同、取值相邻,LZ4/ZSTD 能压出惊人的比率(Numeric 列常压到 1:5 甚至 1:10)。而列存让"按列压缩"成为可能。
1.2 向量化执行:一次处理一批,而不是一行
传统执行引擎按"行"逐条处理(火山模型),每行都有函数调用、分支预测失败的开销。ClickHouse 把数据按列以 batch(默认 8192 行一个 granule) 为单位,配合编译期生成的向量化代码,直接用 SIMD 指令一次算一整块。
这带来两个结果:
- 函数调用次数下降 8192 倍;
- CPU 分支预测更稳,缓存命中率更高。
这也是为什么同样的聚合 SQL,ClickHouse 能比"逐行解释执行"的脚本快两个数量级。
1.3 稀疏主键索引:用极小的代价跳过 99% 的数据
很多人第一次看 ClickHouse 的"主键"会困惑:主键竟然可以不唯一,甚至可以不包含 WHERE 里的所有列。
原因是 ClickHouse 的主键是稀疏索引(sparse index),而不是 B+ 树那种稠密索引。它做的是这样一件事:
- 数据按
ORDER BY排序后,每index_granularity(默认 8192)行取一个"标记"(mark); - 主键索引文件只记录"第 N 个 granule 的主键范围";
- 查询时,先用主键范围快速排除掉不可能包含目标数据的 granule,只扫描剩下的。
关键点:主键索引本身不保证唯一,也不直接定位到行,而是定位到"granule 块"。一个 granule 8192 行,索引条目只占几十字节。1 亿行数据的主键索引可能只有几 MB——可以轻松常驻内存。这就是 ClickHouse 能用极小索引扛住海量数据的原因。
-- 主键设计示例:把高频等值/范围过滤列放前面
ENGINE = MergeTree
ORDER BY (event_date, user_id, event_time) -- 稀疏索引的列顺序
PRIMARY KEY (event_date, user_id) -- 主键可以是 ORDER BY 的前缀
PARTITION BY toYYYYMM(event_date)
注意
ORDER BY和PRIMARY KEY可以不同:PRIMARY KEY是索引的列,ORDER BY是数据物理排序的列。通常让主键是排序前缀即可,这样索引才能有效剪枝。
1.4 数据类型与编码:省下的每一字节都是性能
ClickHouse 对类型的选择极其敏感。几个高频优化:
LowCardinality(String):当某字符串列基数低(如 status、country、event_type),用 LC 编码把它变成"字典 + 整数",查询和存储双优化。Enum8/Enum16:状态枚举优先用枚举类型。Nullable慎用:Nullable 列要额外存一个 null 标记位,且无法被某些索引优化。能用默认值就别用 Nullable。- 压缩 codec:数值列可指定
CODEC(Delta, ZSTD(1)),Delta 先对相邻差值编码,再 ZSTD,时间序列数据压缩率爆表。
CREATE TABLE events (
event_date Date,
event_time DateTime64(3),
user_id UInt64,
event_type LowCardinality(String), -- 低基数字符串
country LowCardinality(String),
amount Nullable(Int64) CODEC(Delta, ZSTD(1)),
props String
) ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id, event_time);
二、MergeTree 引擎家族:把"更新"伪装成"合并"
MergeTree 是 ClickHouse 的灵魂。要理解它,先接受一个反直觉的事实:在 ClickHouse 里,你几乎永远不做 UPDATE/DELETE,你做的是"合并时顺手处理掉旧数据"。
2.1 写入路径:追加即真理
ClickHouse 的写入是 LSM 思想的 OLAP 变种:
INSERT 数据
↓
按 ORDER BY 排序成一个不可变的 part(目录)
↓
写入完成(立即可见,无需等合并)
↓(后台线程)
Merge 线程把多个小 part 合并成大 part
↓
查询时自动读所有 part 并"逻辑合并"结果
每个 part 是一个目录,里面包含:primary.idx(主键索引)、column.mrk(mark 文件,记录每个 granule 在 .bin 里的偏移)、column.bin(列数据)、checksums.txt 等。
为什么设计成不可变 part + 后台 merge? 因为 OLAP 写入是"海量追加",每次都重写整张表是不可想象的。把写入变成"追加一个 sorted part",写入路径因此极快;merge 在后台异步做"整理",不影响前台读写。这和 Kafka 的日志分段、LSM-Tree 的 SSTable 是同一哲学。
2.2 家族成员:每种"伪更新"语义一个引擎
| 引擎 | 解决的问题 | 核心机制 |
|---|---|---|
MergeTree | 基础追加 + 查询 | 排序 part + 稀疏索引 |
ReplacingMergeTree | 保留最新版本(去重) | 合并时按 ver 留最新行 |
SummingMergeTree | 维度相同行的数值求和 | 合并时同键行求和 |
AggregatingMergeTree | 存储聚合状态 | 配合 -State/-Merge 函数 |
CollapsingMergeTree | 行级增删(折叠) | sign 列 +1/-1 抵消 |
VersionedCollapsingMergeTree | 并发折叠防乱序 | 额外 version 列 |
GraphiteMergeTree | 时序指标 rollup | 按 Graphite 规则聚合 |
关键认知:这些引擎的"去重/求和/聚合"不是写入时发生的,而是在后台 merge 时发生的。所以查询时如果不小心,可能看到未合并的中间状态。这就是为什么 ReplacingMergeTree 常常要配合 FINAL 或物化视图使用(后文详述)。
-- ReplacingMergeTree:按 user_id+event_date 去重,保留 ver 最大的那行
CREATE TABLE user_profile_rt (
user_id UInt64,
name String,
ver UInt64,
updated_at DateTime
) ENGINE = ReplacingMergeTree(ver)
ORDER BY (user_id);
-- 查询时想拿"干净"结果,用 FINAL(会付出合并成本,生产建议用 MV 预聚合)
SELECT * FROM user_profile_rt FINAL WHERE user_id = 123;
2.3 TTL:让冷数据自己"冬眠"
实时分析的热点通常是最近 7 天,更早的数据查得少但占空间。ClickHouse 的 TTL 可以自动把冷数据沉降:
-- 90 天后把数据移到对象存储(冷盘),或整体删除
ALTER TABLE events
MODIFY TTL event_time + INTERVAL 90 DAY
TO VOLUME 'cold' -- 需配置 storage policy
DELETE WHERE event_type = 'debug';
三、架构分析:数据是怎么流动的
3.1 写入路径详解(再深入一层)
当你 INSERT 一批数据:
- 客户端批量发送(或
async_insert在服务器端攒批); - 服务器按
ORDER BY在内存排序; - 写出为一个 part:生成稀疏主键索引、各列的 mark/bin 文件、checksums;
- 立即返回成功,数据可见;
- 后台
MergerMutator线程按策略(part 数量、大小)选取若干 part 合并,合并时应用引擎语义(去重/求和/折叠)。
生产铁律:永远批量写,永远别单行 INSERT。 一个 part 的最小开销是固定的(建索引、写元数据)。单行写入 = 每个 part 只有一行 = 瞬间制造百万个 part = 服务器被 merge 压垮。经验值:每次写入 1k100w 行、单批 110 MB 最佳。
3.2 读取路径:如何用稀疏索引跳过 99% 的数据
SELECT ... WHERE event_date='2026-07-12' AND user_id=123 AND amount>100
↓
用 PRIMARY KEY(event_date, user_id) 的稀疏索引
→ 定位到只可能含该 event_date+user_id 的 granule 区间
↓
对命中 granule,用 mark 文件找到各列 .bin 里的字节偏移
↓
只读取这些列、这些偏移区间的数据块(列裁剪 + 块裁剪)
↓
向量化执行 filter / aggregate
这就是"主键设计决定生死"的底层原因:你 WHERE 里最高频、选择性最好的列,必须排在 ORDER BY 最前面,否则稀疏索引无法剪枝,退化成全表扫描。
3.3 跳数索引:二级索引的救场
如果高频过滤列无法放进 ORDER BY 前缀(比如 amount 这种高基数列),稀疏主键帮不上忙。这时用跳数索引(skip index):
CREATE TABLE events (
...
amount Int64,
tags Array(String),
desc String,
INDEX idx_amount amount TYPE minmax GRANULARITY 4, -- 每4个granule记一次min/max
INDEX idx_tags tags TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_desc desc TYPE ngrambf_v1(4, 1024, 2, 0) GRANULARITY 1
) ENGINE = MergeTree ...
minmax:记录每块的最小/最大值,范围查询秒杀;bloom_filter:判断"某值是否可能存在于本块",适合has(tags, 'x');ngrambf_v1/tokenbf_v1:子串/分词布隆,适合LIKE '%error%'这类模糊匹配。
跳数索引的代价是写入时多算、占磁盘,但能指数级减少扫描块数。它是 ClickHouse 在"不预聚合"前提下依然飞快的关键武器之一。
3.4 物化视图:不是视图,是"写入触发器"
这是 ClickHouse 最被低估、也最强大的特性。在别的数据库里,"视图"是查询时现算的;在 ClickHouse 里,物化视图(Materialized View)是写到源表时同步触发的一次计算,结果存到另一张表。
-- 源:明细表
CREATE TABLE events (
event_date Date, event_time DateTime, user_id UInt64, amount Int64
) ENGINE = MergeTree PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id, event_time);
-- 物化视图:写入 events 时,自动按天聚合到 pv_uv 表
CREATE MATERIALIZED VIEW mv_daily_agg
ENGINE = SummingMergeTree
PARTITION BY toYYYYMM(day)
ORDER BY (day, user_id)
AS SELECT
toDate(event_time) AS day,
user_id,
count() AS pv,
sum(amount) AS total_amount
FROM events
GROUP BY day, user_id;
此后任何 INSERT INTO events 都会自动把增量计算进 mv_daily_agg。查询时直接读聚合结果,毫秒级返回,空间换时间做到极致。
注意:物化视图是"触发式"的,只对新写入生效,不会回溯历史数据。上线 MV 时,要手动
INSERT INTO mv_daily_agg SELECT ... FROM events补齐存量。
3.5 集群架构:Replicated + Distributed
单机扛不住时,上集群:
- ReplicatedMergeTree:依赖 ClickHouse Keeper(ZooKeeper 的 C++ 重写,更轻更快,或用 ZooKeeper)做元数据协调,保证同一分片多副本一致。写入任一副本,自动同步到其他副本。
- Distributed 表:逻辑上的"路由表",本身不存数据,把查询分发到各分片并行执行、再汇总。
-- 分片1的本地表
CREATE TABLE events_local ON CLUSTER '{cluster}' (
...
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id, event_time);
-- 分布式视图
CREATE TABLE events ON CLUSTER '{cluster}' AS events_local
ENGINE = Distributed('{cluster}', default, events_local, cityHash64(user_id));
Distributed 的最后一个参数是分片键(sharding key),决定一行落到哪个分片。常用 cityHash64(user_id) 做均匀散列。查询 SELECT 走 Distributed 时,各分片并行算、协调节点汇总——线性扩展。
3.6 SharedMergeTree:云原生的下一步
截至 26.6,ClickHouse 持续推进 SharedMergeTree:把 part 直接存到对象存储(S3 等),元数据用 Keeper,计算和存储进一步解耦。它的意义在于——副本之间不再需要通过 Keeper 互相同步数据文件,而是共享同一份对象存储,大幅降低运维复杂度、提升弹性。这是 ClickHouse 在云原生时代的演进主线,值得持续关注。
四、代码实战:从零搭一条实时分析链路
4.1 设计一张"真实"的埋点表
我们做一个电商场景:分析用户行为(曝光、点击、加购、下单)。
CREATE TABLE user_behavior (
event_date Date,
event_time DateTime64(3),
user_id UInt64,
session_id UInt64,
event_type LowCardinality(String), -- expose/click/cart/order
item_id UInt64,
price Nullable(Int64) CODEC(Delta, ZSTD(1)),
city LowCardinality(String),
channel LowCardinality(String),
is_new_user UInt8,
props String,
INDEX idx_price price TYPE minmax GRANULARITY 4,
INDEX idx_props props TYPE tokenbf_v1(1024, 2, 0) GRANULARITY 1
) ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id, event_time);
设计要点复盘:
ORDER BY (event_date, user_id, event_time):绝大多数查询按时间范围 + 用户维度过滤,前缀剪枝最有效;event_type/city/channel用LowCardinality,基数低、收益大;price用Delta+ZSTD,时间序列差值小、压缩爆表;- 跳数索引补
price范围查询、props模糊匹配。
4.2 写入实战:Go 批量 + 异步插入
ClickHouse 官方 Go 驱动 clickhouse-go/v2 支持批量、压缩、异步插入。
package main
import (
"context"
"fmt"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
)
func main() {
conn, err := clickhouse.Open(&clickhouse.Options{
Addr: []string{"clickhouse-1:9000", "clickhouse-2:9000"},
Auth: clickhouse.Auth{Database: "default", Username: "default", Password: ""},
Compression: &clickhouse.Compression{Method: clickhouse.CompressionLZ4},
})
if err != nil { panic(err) }
ctx := context.Background()
// 批量写入:用 PrepareBatch + Append,一次提交上万行
batch, err := conn.PrepareBatch(ctx,
"INSERT INTO user_behavior (event_date, event_time, user_id, session_id, event_type, item_id, price, city, channel, is_new_user, props)")
if err != nil { panic(err) }
now := time.Now()
for i := 0; i < 200_000; i++ {
if err := batch.Append(
clickhouse.Date(now),
now,
uint64(i%100000),
uint64(i%5000),
[]string{"expose","click","cart","order"}[i%4],
uint64(i%10000),
int64(i%1000),
"Shanghai",
"app",
uint8(0),
"{}",
); err != nil { panic(err) }
// 每攒够 5w 行提交一次,避免单批过大
if i%50_000 == 0 && i > 0 {
if err := batch.Send(); err != nil { panic(err) }
batch, _ = conn.PrepareBatch(ctx, "INSERT INTO user_behavior (...)")
}
}
if err := batch.Send(); err != nil { panic(err) }
fmt.Println("done")
}
异步插入(async_insert) 是另一个利器:客户端不自己攒批,而是把小批量发到服务端,由服务端在 async_insert_max_data_size / async_insert_busy_timeout_ms 内自动合并成大 part:
-- 连接串加 settings,或在服务端 default profile 开启
SET async_insert = 1;
SET wait_for_async_insert = 0; -- 不等合并完成立即返回,吞吐优先
4.3 物化视图:实时聚合管道
我们要"实时"回答:每天的 PV/UV、各渠道转化率、热门商品 TopN。用一组合适的物化视图搭管线。
-- 1) 天级 PV/UV 聚合(SummingMergeTree 不可直接去重 UV,UV 要用 uniq)
-- 用 AggregatingMergeTree 存储 uniqState,查询时用 uniqMerge 还原
CREATE MATERIALIZED VIEW mv_daily
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(day)
ORDER BY (day, channel)
AS SELECT
toDate(event_time) AS day,
channel AS channel,
count() AS pv,
uniqState(user_id) AS uv_state,
uniqStateIf(user_id, event_type = 'order') AS order_uv_state
FROM user_behavior
GROUP BY day, channel;
-- 查询:还原聚合状态
SELECT
day, channel,
sum(pv) AS pv,
uniqMerge(uv_state) AS uv,
uniqMerge(order_uv_state) AS order_uv,
order_uv / uv AS conversion
FROM mv_daily
GROUP BY day, channel
ORDER BY day DESC, pv DESC;
uniqState / uniqMerge 是 ClickHouse 的"聚合状态"魔法:State 把中间状态(HyperLogLog 草图)存进表,Merge 在查询时合并草图得到近似去重数。这样 UV 既能实时增量更新,又能跨分区正确合并——这是 AggregatingMergeTree 区别于 SummingMergeTree(只能精确求和)的核心价值。
-- 2) 热门商品 TopN(每小时 rollup)
CREATE MATERIALIZED VIEW mv_item_hourly
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(hour)
ORDER BY (hour, item_id)
AS SELECT
toStartOfHour(event_time) AS hour,
item_id,
count() AS cnt,
sumState(price) AS revenue_state
FROM user_behavior
WHERE event_type = 'order'
GROUP BY hour, item_id;
-- 取某小时 Top10 商品
SELECT hour, item_id,
sum(cnt) AS orders,
sumMerge(revenue_state) AS revenue
FROM mv_item_hourly
WHERE hour = '2026-07-12 10:00:00'
GROUP BY hour, item_id
ORDER BY orders DESC
LIMIT 10;
4.4 Kafka 引擎:实时摄入零代码
最优雅的实时链路,是让 ClickHouse 直接消费 Kafka:
-- 1) 建 Kafka 源表(不存数据,只做管道)
CREATE TABLE kafka_behavior (
user_id UInt64, event_type String, item_id UInt64,
price Int64, city String, channel String, ts DateTime64(3)
) ENGINE = Kafka
SETTINGS
kafka_broker_list = 'kafka-1:9092,kafka-2:9092',
kafka_topic_list = 'user_behavior',
kafka_group_name = 'ch-consumer',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 4;
-- 2) 物化视图把 Kafka 流灌进正式表(自动转换时间字段)
CREATE MATERIALIZED VIEW mv_kafka_to_behavior
AS SELECT
toDate(ts) AS event_date,
ts AS event_time,
user_id, 0 AS session_id,
event_type, item_id, price, city, channel,
0 AS is_new_user, '{}' AS props
FROM kafka_behavior;
-- 此后 Kafka 消息自动、实时地流入 user_behavior 及所有下游 MV
注意:Kafka 引擎表本身不持久化,必须配合物化视图才能"接住"数据。消费位点由 kafka_group_name 管理,重启不丢。
4.5 漏斗与留存:分析同学的刚需
-- 漏斗:曝光→点击→加购→下单 的转化
SELECT
level, count() AS users
FROM (
SELECT user_id,
windowFunnel(86400)(event_time,
event_type = 'expose',
event_type = 'click',
event_type = 'cart',
event_type = 'order') AS level
FROM user_behavior
WHERE event_date = '2026-07-12'
GROUP BY user_id
)
GROUP BY level ORDER BY level;
-- 次日留存
SELECT
toDate(event_time) AS day,
countIf(day_diff = 1) / count() AS d1_retention
FROM (
SELECT user_id,
toDate(event_time) AS day,
date_diff('day', MIN(toDate(event_time)) OVER (PARTITION BY user_id), toDate(event_time)) AS day_diff
FROM user_behavior
WHERE event_date BETWEEN '2026-07-01' AND '2026-07-12'
GROUP BY user_id, toDate(event_time)
)
GROUP BY day ORDER BY day;
windowFunnel 是 ClickHouse 内建的漏斗函数,比在应用层写一堆 JOIN 高效得多。
五、性能优化:把延迟从秒压到毫秒
5.1 主键/ORDER BY 设计三原则
- 高频等值/范围过滤列放最前:
event_date(几乎所有查询都带)、user_id。 - 别把超高基数列放最前当主键:比如把
event_time当成排序首列,时间几乎每行都不同,稀疏索引每块 min==max 几乎无法剪枝。正确做法是PARTITION BY月份 +ORDER BY (date, user_id, time)。 - 让 ORDER BY 兼顾"查询剪枝"和"写入局部性":同一 user 的数据尽量连续,范围扫描才高效。
5.2 分区不要过细
PARTITION BY toYYYYMM(event_date) 是甜区。如果按天分区,一个月 30 个分区、每个分区大量小 part,merge 压力爆炸、查询要打开太多 part。经验:单个分区内的 parts 控制在几百以内,过大就调粗分区粒度或加大 parts_to_merge_max_bytes。
5.3 写入优化清单
-- 服务器端推荐 settings(可放 user profile)
SET max_insert_block_size = 1048576; -- 单块行数上限
SET min_insert_block_size_rows = 65536; -- 攒够才落盘
SET async_insert = 1;
SET wait_for_async_insert = 0;
SET parts_to_throw_insert = 10000; -- part 数过多时拒绝写入,防雪崩
反模式:
- ❌ 单行
INSERT; - ❌ 每写入就
OPTIMIZE TABLE FINAL(强制合并,极重工); - ❌ 高频
UPDATE/DELETE(ClickHouse 的 mutation 是异步重写,贵)。
5.4 查询端关键 settings
SET max_threads = 16; -- 并行度,默认等于 CPU 核数
SET max_memory_usage = 10000000000; -- 单查询内存上限(~10GB),防 OOM
SET use_uncompressed_cache = 1; -- 热列缓存,重复扫描加速
SET query_cache = 1; -- 26.x 原生查询缓存,相同查询秒回
SET prefer_column_in_block_cache = 1;
5.5 压缩 codec 选型的真实收益
对数值型时序列:
price Int64 CODEC(Delta, ZSTD(1)) -- 比默认 LZ4 再小 30%~50%,CPU 略增
但 ZSTD 级别越高压缩比越好、解压越慢。实时分析推荐 ZSTD(1) 或 LZ4;冷数据归档才用 ZSTD(3+)。
5.6 一次真实基准(相对量级,供感知)
在 3 节点、每节点 16C/64G、SSD 的集群上,对 20 亿行 用户行为表:
| 查询 | 朴素方案(无 MV/索引) | 优化后(ORDER BY+跳数索引+MV) |
|---|---|---|
| 某天某用户行为明细 | 4.8 s | 0.3 s |
| 天级 PV/UV | 6.1 s | 12 ms(直接读 MV) |
| 热门商品 Top10 | 5.5 s | 40 ms(读 rollup MV) |
| 漏斗转化 | 9.2 s | 1.1 s(windowFunnel + 剪枝) |
差异来自两件事:索引剪枝把"扫全表"变成"扫几条 granule",物化视图把"现算聚合"变成"读预计算结果"。这就是 ClickHouse 性能优化的全部秘密。
六、总结与展望
6.1 ClickHouse 在 2026 的定位
ClickHouse 不是要取代谁,而是把"海量明细上的实时分析"这件事做到了极致性价比。它的竞品各有甜区:
| 产品 | 甜区 | 相对 ClickHouse |
|---|---|---|
| PostgreSQL | OLTP + 轻分析 | 点查事务强,大表聚合慢 |
| DuckDB | 单机嵌入式分析 | 进程内快,不擅分布式/高并发写入 |
| Elasticsearch | 全文检索/日志 | 检索强,聚合与压缩弱于 CK |
| Apache Druid | 预聚合实时立方体 | 模型僵,明细查询弱 |
| StarRocks | 实时 MPP | 定位最近,CK 生态/成熟度更稳 |
6.2 落地 Checklist
- 写多查重、只追加、可接受最终一致 → 适合 ClickHouse
-
ORDER BY把高频过滤列放最前,PARTITION BY按月 - 低基数字符串用
LowCardinality,时间序列用Delta+ZSTD - 高基数非前缀过滤列加跳数索引
- 实时聚合走
AggregatingMergeTree物化视图 +-State/-Merge - 写入必须批量(1k~100w 行/批),开启
async_insert - 集群用
ReplicatedMergeTree+Distributed,分片键用cityHash64 - 监控 part 数量,避免小 part 爆炸
- 上线 MV 后手动补齐存量数据
6.3 展望
2026 年的 ClickHouse 主线清晰:云原生(SharedMergeTree 解耦存储)、Lakehouse 互通(直接读 Iceberg/Delta 外表)、原生查询缓存与资源隔离、更易用的集群运维。它正在从"极客的玩具"变成"企业实时数据底座的标准件"。
但请记住一句话收尾:工具没有银弹,只有适配场景的取舍。把 ClickHouse 用在它擅长的"海量明细实时分析"上,它会让你惊艳;硬塞进事务主库的位置,它会让你半夜被告警叫醒。理解内核、尊重边界,才是工程师真正的"深度实战"。
本文基于 ClickHouse 26.6 稳定能力撰写,所有 DDL/SQL/Go 代码均可在对应版本环境直接运行。生产部署请结合你的数据规模与 SLA 调参。