ClickHouse 深度实战:当实时 OLAP 撞上次世代列式引擎——MergeTree、稀疏索引与向量化执行如何重写数据分析心智模型
关键词:ClickHouse、MergeTree、列式存储、向量化执行、稀疏索引、物化视图、跳过索引、ClickHouse Keeper、实时数仓、OLAP
写在前面:过去两年,DuckDB 让"进程内 OLAP"火了一把,但当你面对的是每秒几十万条写入、PB 级历史、亚秒级聚合的生产场景时,单机嵌入式引擎就不够看了。这篇文章从工程师视角,把 ClickHouse 这台"为分析而生的机器"彻底拆开:它不是又一个 SQL 数据库,而是一套关于"如何用 CPU 缓存、如何用磁盘、如何用内存"的极端工程哲学。读完你应该能回答三个问题——为什么它快、什么时候该用、怎么把它用对。
一、背景:为什么我们需要"为分析而生"的数据库
1.1 行存与列存:一次彻底的逆向思维
传统关系型数据库(MySQL、PostgreSQL)是为**事务(OLTP)设计的:一条订单、一行用户、一次更新。它们的存储是按行(row)**连续的——一条记录的 id、name、price、ctime 紧挨着躺在磁盘上。这对"取出完整一条记录"极其友好,但对"统计全天 GMV"是灾难:为了算一个 sum(price),数据库必须把整张表每一行的所有列都从磁盘读进内存,再丢掉 99% 用不上的字段。
ClickHouse 的底层假设完全反过来:分析查询几乎永远只碰少数列、但要扫极多行。于是它把同一列的数据连续存储:
行存(MySQL): [id=1,name=A,price=10] [id=2,name=B,price=20] [id=3,name=C,price=30] ...
列存(ClickHouse):
id 列: 1, 2, 3, 4, 5 ...
name列: A, B, C, D, E ...
price列:10,20,30,40,50 ...
列存带来两个直接红利:
- 只读取需要的列,I/O 量随查询列数线性下降;
- 同列数据类型一致,压缩率极高。价格列
10,20,30...这种有序整数,配上 delta + LZ4/ZSTD,轻松达到 5:1 甚至 20:1 的压缩比。更少的字节 = 更少的磁盘 I/O = 更快的查询。
1.2 OLTP 与 OLAP:两种完全不同的负载
| 维度 | OLTP(MySQL) | OLAP(ClickHouse) |
|---|---|---|
| 典型操作 | 单行 INSERT/UPDATE/DELETE | 批量 INSERT + 大范围聚合 SELECT |
| 数据新鲜度 | 强一致、实时 | 近实时(秒级可查) |
| 事务 | 必须 ACID | 不需要跨行事务 |
| 索引 | B+ 树行级索引 | 稀疏主键 + 跳过索引 |
| 写入模式 | 随机小写入 | 顺序大批量写入 |
| Join | 频繁 | 能避免就避免 |
ClickHouse 明确放弃了传统事务和行级 UPDATE/DELETE 的高频能力,把这些"负担"换成极致的扫描与聚合性能。这不是缺陷,而是取舍(trade-off)——它把"分析"这件事做到了工程极限。
1.3 ClickHouse 从哪来
ClickHouse 诞生于俄罗斯搜索巨头 Yandex 的内部项目 Yandex.Metrica(类似 Google Analytics),用来支撑每秒百万级的网站埋点写入与交互式多维分析。2016 年开源,2021 年成立独立公司 ClickHouse Inc.。今天的 v26.x(v26.2 为 2026 年初发布的稳定大版本)已经是一个成熟的、云原生友好的分析型 DBMS。
1.4 它解决的真实痛点
- MySQL 跑聚合:
SELECT city, COUNT(*) FROM events GROUP BY city在 10 亿行上,MySQL 可能要分钟级,ClickHouse 亚秒。 - Elasticsearch 太贵:ES 的倒排索引为全文检索而生,做数值聚合时内存与存储成本陡增;ClickHouse 用列存 + 跳过索引把成本压到 ES 的零头。
- Hadoop/Spark 太重:离线批处理 T+1,等不起;ClickHouse 是实时的,写入即可查。
二、核心概念:ClickHouse 到底是怎么"快"的
2.1 向量化执行(Vectorized Execution):别一次算一个数
传统数据库执行 price * 1.1 是逐行的:取一个数 → 乘 → 存,再取下个。现代 CPU 的厉害之处在于 SIMD(单指令多数据) 和深层流水线——一次对一批数据做同样的运算才能喂饱它。
ClickHouse 的执行引擎是列式、批处理的:它把数据按 block(默认 65536 行)成批流经算子,每个函数调用一次处理一整列。这带来:
- 函数调用开销被摊薄(每 65536 行才调用一次);
- 内存访问是顺序的,CPU cache 命中率高;
- 可充分利用现代编译器的自动向量化(配合
-msse4.2/avx指令集)。
心智模型:ClickHouse 的查询像一条流水线工厂,数据是一箱一箱的零件(block),每个工人(函数)对整箱做同一种操作,而不是把单个零件传来传去。
2.2 MergeTree:一切的起点
MergeTree 是 ClickHouse 的核心表引擎家族。它的设计极其优雅:写入即追加,合并在后台。
- 数据写入时,先落到内存,再 flush 成一个个 part(数据 part,旧称"分区块") 文件;
- 后台线程异步把这些小 part merge(合并) 成大 part,过程类似 LSM-Tree 的 compaction;
- 查询时,ClickHouse 会并行扫描相关 part,并跳过明显不匹配的 part。
CREATE TABLE events
(
event_date Date,
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
url String,
duration UInt32,
price Nullable(Float64)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date) -- 按月分区
ORDER BY (event_type, user_id, event_time) -- 排序键 = 主键
TTL event_date + INTERVAL 180 DAY; -- 180 天后自动清理冷数据
几个关键点:
ORDER BY就是主键(primary key 默认等于 ORDER BY,除非显式指定PRIMARY KEY)。它决定数据在 part 内的物理排序,是稀疏索引的基础。PARTITION BY决定 part 的粗粒度切分。查询带上分区条件能直接跳过整批 part(partition pruning)。TTL让冷数据自动过期,避免手动删表的运维噩梦。
2.3 稀疏主键索引(Sparse Primary Index):用极小代价定位数据
和 MySQL 的 B+ 树(每行一条索引项)不同,ClickHouse 的主键是稀疏的:
- 数据按
index_granularity(默认 8192 行)切成一个个 granule(颗粒); - 主键索引只为每个 granule 的第一行记录一条标记(mark);
- 查询时,先在主键上做"粗糙"二分,定位到可能包含目标值的 granule 区间,再读这些 granule 做精确过滤。
主键 (event_type, user_id, event_time) 的 mark 文件(每个 granule 一条):
('click', 1001, 2026-07-01 08:00) -> mark #0
('click', 1001, 2026-07-01 10:00) -> mark #1
...
这意味着主键索引本身极小(数据量的 1/8192 量级),即使百亿行,索引也能轻松放进内存。这是 ClickHouse 能"扫全表也很便宜"的根本原因之一。
⚠️ 常见误区:ClickHouse 的主键不等于唯一约束。它不保证唯一,只是排序与索引依据。去重要靠
ReplacingMergeTree或FINAL/GROUP BY。
2.4 MergeTree 家族:用引擎表达业务语义
ClickHouse 把很多"业务聚合逻辑"直接下沉到引擎层:
| 引擎 | 解决什么 | 核心机制 |
|---|---|---|
MergeTree | 最通用 | 后台 merge |
ReplacingMergeTree | 去重最新版本(CDC 场景) | 按 ver 列保留最新 |
SummingMergeTree | 自动预求和 | 相同排序键的行在 merge 时相加 |
AggregatingMergeTree | 预聚合(配合物化视图) | 存聚合函数中间态(-State/-Merge) |
CollapsingMergeTree | 行级"抵消"(±1 标记) | 成对折叠 |
VersionedCollapsingMergeTree | 带版本的折叠 | 支持乱序到达 |
例如,统计每个用户的累计消费,用 SummingMergeTree 可以让后台 merge 自动把同一 (user_id) 的多条记录求和,查询时直接读"已半聚合"的数据:
CREATE TABLE user_revenue
(
user_id UInt64,
date Date,
revenue AggregateFunction(sum, Float64) -- 注意这里存的是"聚合态"
)
ENGINE = AggregatingMergeTree
ORDER BY (user_id, date);
2.5 压缩:被严重低估的性能开关
ClickHouse 每列可单独指定 codec。默认是 LZ4,但选对 codec 收益巨大:
CREATE TABLE metrics
(
ts DateTime CODEC(Delta, ZSTD), -- 时间戳:先 delta 再 ZSTD,压缩比惊人
host LowCardinality(String), -- 低基数字段:字典编码
value Float64 CODEC(Gorilla, ZSTD), -- 浮点监控值:Gorilla 专治时序
labels String
)
ENGINE = MergeTree
ORDER BY (host, ts);
Delta:对递增的时间戳/ID,差值很小,压缩率飙升;Gorilla:Facebook 为时序浮点设计的 codec,相邻值差异编码;LowCardinality:把host这种取值有限的列做字典化,既省空间又加速分组。
三、架构分析:从单机到集群
3.1 节点角色与分布式表
ClickHouse 的集群是无中心(shared-nothing)的:每个节点都是对等的计算+存储单元,没有主节点。横向扩展靠分片(shard),高可用靠副本(replica)。
分布式的核心是两个引擎:
-- 1) 本地表:真正存数据的表(每个分片一份)
CREATE TABLE events_local ON CLUSTER '{cluster}' AS events
ENGINE = ReplicatedMergeTree('/clickhouse/{shard}/tables/events', '{replica}')
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_type, user_id, event_time);
-- 2) 分布式表:一张"虚拟"的路由表,把查询分发到各分片
CREATE TABLE events_dist AS events_local
ENGINE = Distributed('{cluster}', default, events_local, cityHash64(user_id));
Distributed 引擎本身不存数据,它根据分片键 cityHash64(user_id) 把写入路由到对应分片,查询时并行下发到所有分片再汇总。对应用层来说,读写 events_dist 就像操作一张表。
3.2 ClickHouse Keeper:ZooKeeper 的 Raft 替代
老版本 ClickHouse 依赖 ZooKeeper 做副本协调,但 ZK 是 JVM 写的、运维复杂、有单点瓶颈。新版本用 ClickHouse Keeper——一个用 C++ 实现、基于 Raft 共识协议、消息格式兼容 ZK 的组件,直接嵌在 ClickHouse 生态里,延迟更低、吞吐更高、运维更省心。
<!-- keeper 配置片段 -->
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id>
<raft_configuration>
<server><id>1</id><hostname>ck-keeper-1</hostname><port>9234</port></server>
<server><id>2</id><hostname>ck-keeper-2</hostname><port>9234</port></server>
<server><id>3</id><hostname>ck-keeper-3</hostname><port>9234</port></server>
</raft_configuration>
</keeper_server>
3.3 写入路径:别小看"怎么写"
ClickHouse 对大批量顺序写极其友好,但对单行随机写很糟糕。生产实践:
async_insert:客户端把小写入先攒成一批,服务端异步落盘,大幅降低 part 数量爆炸;max_insert_block_size:控制单次插入的 block 大小;Buffer表引擎:在内存中缓冲写入,周期性 flush,削峰填谷;- Kafka 表引擎:直接
SELECT消费 Kafka topic 写入,做实时管道。
-- 用 Kafka 引擎把埋点实时灌进 ClickHouse
CREATE TABLE kafka_events
(
user_id UInt64,
event_type String,
payload String
)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'kafka:9092',
kafka_topic_list = 'raw_events',
kafka_group_name = 'ch_consumer',
kafka_format = 'JSONEachRow';
-- 物化视图把 Kafka 流实时落到 MergeTree
CREATE MATERIALIZED VIEW mv_events TO events_local AS
SELECT user_id, event_type, now() AS event_time, ...
FROM kafka_events;
3.4 ClickStack:把可观测性也吞进来
2025–2026 年 ClickHouse 力推 ClickStack——把 ClickHouse 与可视化(类 Grafana)打包成开源可观测性栈,用一份列存同时搞定 logs、metrics、traces。它背后是 ClickHouse 用同一套引擎承载"可观测性三件套"的能力:高写入吞吐、廉价存储、灵活聚合。对工程师的意义是:你的业务数仓和监控栈可以用同一个数据库,少一套技术栈、少一份运维。
四、代码实战:从零跑起来
4.1 用 Docker Compose 起一个单节点
# docker-compose.yml
version: '3.8'
services:
clickhouse:
image: clickhouse/clickhouse-server:25.8
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: ""
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ports:
- "8123:8123" # HTTP 接口
- "9000:9000" # 原生 TCP 接口
volumes:
- ./ch_data:/var/lib/clickhouse
- ./config.xml:/etc/clickhouse-server/config.d/override.xml:ro
ulimits:
nofile:
soft: 262144
hard: 262144
docker compose up -d
# 连进去
docker exec -it $(docker ps -qf ancestor=clickhouse/clickhouse-server) clickhouse-client
4.2 建表与设计排序键(这是性能的地基)
设计原则:把"最常被等值/范围过滤、且区分度高"的列放 ORDER BY 前面,让稀疏索引最大化和分区裁剪配合。
CREATE TABLE default.web_events
(
event_date Date DEFAULT toDate(event_time),
event_time DateTime64(3) CODEC(Delta, ZSTD),
user_id UInt64,
session_id UUID,
event_type LowCardinality(String),
country LowCardinality(FixedString(2)),
device LowCardinality(String),
url String,
duration_ms Nullable(UInt32),
props JSON, -- 半结构化字段,无需预先定义 schema
revenue Nullable(Float64) CODEC(ZSTD)
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(event_date) -- 按天分区,便于 TTL 与裁剪
ORDER BY (event_type, country, event_time, user_id)
TTL event_date + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;
注意 JSON 类型(2024 后趋于成熟的原生 JSON 列):它把 JSON 内部字段自动展开为子列做列存,兼顾灵活性与性能,不用再手动 String 然后 JSONExtract 全表解析。
4.3 写入:三种典型姿势
-- 姿势 1:直接 INSERT VALUES(小批量)
INSERT INTO web_events
(event_time, user_id, event_type, country, url, duration_ms, props)
VALUES
(now(), 10001, 'page_view', 'CN', '/home', 1200, '{"utm":"x","ab":"B"}'),
(now(), 10002, 'click', 'US', '/buy', 300, '{"utm":"y"}');
-- 姿势 2:从文件批量导入(超快)
INSERT INTO web_events
SELECT * FROM file('events.jsonl', 'JSONEachRow');
-- 姿势 3:HTTP 接口灌入(数据管道常用)
curl 'http://localhost:8123/?query=INSERT%20INTO%20web_events%20FORMAT%20JSONEachRow' \
-d '{"event_time":"2026-07-15 10:00:00","user_id":1,"event_type":"click","country":"CN","url":"/p"}'
4.4 物化视图:把聚合提前算好
这是 ClickHouse 最有"爽点"的特性之一。与其每次查询都 GROUP BY 十亿行,不如在数据写入时增量预聚合。
-- 明细表
CREATE TABLE events_raw
(
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
revenue Float64
)
ENGINE = MergeTree
ORDER BY (event_type, event_time);
-- 预聚合表:每分钟每类型的总额与次数
CREATE TABLE events_by_min
(
event_date Date,
minute DateTime,
event_type LowCardinality(String),
cnt AggregateFunction(count),
sum_rev AggregateFunction(sum, Float64)
)
ENGINE = AggregatingMergeTree
ORDER BY (event_type, minute);
-- 物化视图:写入 raw 时自动流入聚合表
CREATE MATERIALIZED VIEW mv_events_by_min
TO events_by_min
AS SELECT
toDate(event_time) AS event_date,
toStartOfMinute(event_time) AS minute,
event_type,
countState() AS cnt,
sumState(revenue) AS sum_rev
FROM events_raw
GROUP BY event_date, minute, event_type;
-- 查询时"合并"聚合态
SELECT
event_type,
sumMerge(sum_rev) AS total_revenue,
countMerge(cnt) AS total_cnt
FROM events_by_min
WHERE minute >= now() - INTERVAL 1 HOUR
GROUP BY event_type;
物化视图是写入时触发、对应用透明的。应用只管写 events_raw,查询读 events_by_min 即可获得数量级提速。
4.5 跳过索引(Skipping Index):让查询"跳过"无关数据
稀疏主键只能沿 ORDER BY 列加速。对 url 这种不在排序键里的字符串做过滤,就要靠跳过索引:
ALTER TABLE web_events
ADD INDEX idx_url url TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4;
-- 全文检索:inverted index(2024 后走向成熟)
ALTER TABLE web_events
ADD INDEX idx_url_inv url TYPE inverted GRANULARITY 1;
-- 查某 URL 的访问
SELECT count() FROM web_events WHERE url LIKE '%checkout%';
-- ClickHouse 先读跳过索引,直接排除不含该 token 的 granule
tokenbf_v1:基于 Bloom Filter,适合"是否包含某 token"的成员判断;inverted:真正倒排索引,支持hasToken()、分词全文检索,替代 ES 的轻量方案;set(1000)、minmax、ngrambf_v1等各有所长。
4.6 Projection:同一份数据的"另一种排序"
Projection 是表上的辅助物理视图——它按不同排序键额外存一份数据,查询优化器自动选择用主表还是投影。
ALTER TABLE web_events
ADD PROJECTION proj_by_user
(
SELECT * ORDER BY (user_id, event_time)
);
-- 触发后台物化(已有数据需要物化)
ALTER TABLE web_events MATERIALIZE PROJECTION proj_by_user;
-- 按 user_id 过滤的查询会自动走 proj_by_user,快得飞起
SELECT * FROM web_events WHERE user_id = 10001;
Projection 与物化视图的区别:投影共享同一张表的写入语义、自动维护、对查询透明,适合"同一份数据需要多种访问顺序"的场景。
4.7 用 Python / Go 读写(真实应用代码)
# pip install clickhouse-driver
from clickhouse_driver import Client
client = Client(host='localhost', port=9000, database='default')
# 批量写入(注意用 executemany / INSERT 的列格式)
rows = [
(10001, 'click', 19.9),
(10002, 'view', None),
]
client.execute(
"INSERT INTO events_raw (user_id, event_type, revenue) VALUES",
rows
)
# 查询
result = client.execute(
"SELECT event_type, count() FROM events_raw GROUP BY event_type"
)
for event_type, cnt in result:
print(event_type, cnt)
// go get github.com/ClickHouse/clickhouse-go/v2
package main
import (
"context"
"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{"localhost:9000"},
Auth: clickhouse.Auth{Database: "default"},
})
if err != nil { panic(err) }
ctx := context.Background()
batch, _ := conn.PrepareBatch(ctx, "INSERT INTO events_raw")
for i := 0; i < 1000; i++ {
_ = batch.Append(
time.Now(),
uint64(i),
"click",
9.9,
)
}
_ = batch.Send()
}
4.8 查询示例:分析语句长什么样
-- 各国 TOP 页面的 PV 与平均停留,最近 7 天
SELECT
country,
url,
count() AS pv,
avg(duration_ms) AS avg_dur,
quantile(0.95)(duration_ms) AS p95_dur
FROM web_events
WHERE event_date >= today() - 7
AND event_type = 'page_view'
GROUP BY country, url
ORDER BY pv DESC
LIMIT 20;
-- 漏斗:从浏览到下单的转化率
SELECT
countIf(event_type = 'page_view') AS views,
countIf(event_type = 'add_cart') AS carts,
countIf(event_type = 'purchase') AS purchases,
round(purchases / views, 4) AS cvr
FROM web_events
WHERE event_date = today();
五、性能优化:把 ClickHouse 用对
5.1 ORDER BY / PRIMARY KEY 是第一杠杆
经验法则:
- 等值过滤高频的列放最前(最大程度 prune);
- 范围过滤(时间)通常紧随其后;
- 避免把超高基数列(如
user_id)放太前——它会让 granule 内数据过于分散,稀疏索引退化; - 排序键不宜过长(影响 merge 成本与索引体积)。
5.2 分区粒度:别太细也别太粗
- 按天/月分区是常态;
- 太细(按小时)→ part 数量爆炸,merge 压力大;
- 太粗(按年)→ 分区裁剪失效,单 part 过大。
5.3 跳过索引的选型决策树
- 等值/集合成员判断 →
set/bloom_filter; - 字符串包含/token →
tokenbf_v1→ 真全文 →inverted; - 数值/日期范围过滤且不在主键 →
minmax/ngrambf_v1; - 前缀匹配 →
ngrambf_v1。
5.4 物化视图 vs Projection vs 普通表
| 手段 | 透明性 | 维护成本 | 适用 |
|---|---|---|---|
| 物化视图 | 需改写查询目标表 | 中(独立表) | 跨表聚合、异构目标 |
| Projection | 完全透明 | 低(随主表) | 同表多排序维度 |
| 预聚合表(手动) | 需改写 | 高 | 极致可控 |
原则:能靠排序键/投影解决的,别上物化视图;需要跨维度重算的,再上物化视图。
5.5 关键 session/query settings
-- 调优时常用的几个开关(可在查询前 SET,或写进 users.xml)
SET max_threads = 16; -- 并行度,通常 = 核数
SET max_memory_usage = 10000000000;-- 单查询内存上限(~10GB)
SET use_uncompressed_cache = 1; -- 热数据解压缓存
SET max_insert_block_size = 1048576;
SET async_insert = 1; -- 客户端异步攒批
SET optimize_read_in_order = 1; -- 利用排序键做有序读,省排序
SET query_profiler_real_time_period_ns = 1000000; -- 开启剖析
5.6 查询剖析:别靠猜,看 Pipeline
-- 看执行计划(是否读了正确的索引/投影)
EXPLAIN PLAN actions=1, indexes=1
SELECT * FROM web_events WHERE user_id = 10001;
-- 看算子流水线(vectorized pipeline)
EXPLAIN PIPELINE SELECT count() FROM web_events;
-- 查慢查询日志
SELECT
query_duration_ms,
read_rows,
memory_usage,
query
FROM system.query_log
WHERE type = 'QueryFinish'
AND query_duration_ms > 1000
ORDER BY query_duration_ms DESC
LIMIT 10;
-- 实时看正在跑的查询
SELECT * FROM system.processes ORDER BY elapsed DESC;
system.query_log、system.merges、system.parts、system.query_thread_log 是排查问题的四件套。
5.7 压缩 codec 的 ROI
监控 system.columns 里每列的压缩比,对高基数列盲目上 ZSTD(1) 可能反而拖慢写入;时序类用 Delta, ZSTD 几乎稳赚。LowCardinality 对状态/国家/设备这种列是免费午餐。
5.8 常见反模式(踩坑清单)
- 单行高频 INSERT → part 爆炸,merge 跟不上;老老实实批量 +
async_insert。 - 滥用
FINAL→ReplacingMergeTree的FINAL会实时合并,巨慢;优先用GROUP BY取最新。 - ORDER BY 放 UUID/随机列 → 数据完全无序,稀疏索引失效。
- 在 ClickHouse 里做复杂多表大 Join → 它能 join,但不擅长;大表 join 尽量用
PREWHERE、先过滤、或把维度表走Dictionary。 - 把 ClickHouse 当 OLTP 用 → 频繁 UPDATE/DELETE 是它弱项,别用它做业务主库。
六、总结与展望
6.1 ClickHouse 适合什么
- 日志 / 埋点 / 时序的实时分析与监控;
- 用户行为 / 广告 / 风控的多维即席查询;
- 需要 PB 级扫描、亚秒聚合、廉价存储的数据仓库;
- 替代 ES 做"带全文检索的分析"。
6.2 它与兄弟们的边界
- DuckDB:进程内、单机、嵌入应用的轻量 OLAP。适合分析师本地跑 Parquet。ClickHouse 是服务端、分布式、高并发的。两者互补,不少架构让 DuckDB 做边缘、ClickHouse 做中心。
- StarRocks / Doris:也做实时 OLAP,StarRocks 在"高并发点查 + 多表 Join"上更激进。ClickHouse 胜在写入吞吐、生态成熟度、单表聚合极限性能。选型看你是"写多查杂"还是"查多 JOIN 多"。
- Elasticsearch:全文检索王者,但聚合贵。ClickHouse +
inverted索引能蚕食其分析场景。 - 云数仓(BigQuery/Snowflake):托管省心但按量计费贵、延迟高。ClickHouse 自建成本更低、延迟更低,代价是运维。
6.3 趋势:实时 OLAP 正在"吞噬"一切
- ClickStack 把 logs/metrics/traces 统一进一个引擎;
- Lakehouse 集成:ClickHouse 可直接查 S3 上的 Parquet/Delta,成为"开放表格式"上的查询加速层;
- 原生 JSON 与动态子列让半结构化数据不再需要 ETL 预定义;
- Keeper 成熟 + 云原生部署让运维门槛持续下降;
- 向量检索(experimental)让它开始触碰 AI Embedding 场景。
6.4 给工程师的三条建议
- 先用对引擎,再谈调优:选错 ORDER BY、滥写单行,再怎么调参也救不回来。
- 把聚合下沉到写入侧:物化视图 / 预聚合表是 ClickHouse 的"免费午餐",能提前算的别留到查询时。
- 用系统表做循证优化:
system.query_log比任何"经验"都诚实。性能问题先看数据,再下结论。
结语
ClickHouse 不是"又一个数据库",它是一种关于数据如何落盘、如何被 CPU 吃掉、如何被网络搬运的极端工程态度。它用列存换 I/O、用向量化换吞吐、用稀疏索引换内存、用后台 merge 换写入自由。当你真正理解它的取舍,你会发现自己不再"调数据库",而是在设计数据在磁盘与内存之间的旅行路线。
如果说 DuckDB 让"在自己的笔记本上分析一个 G 的文件"变得理所当然,那么 ClickHouse 让"用一堆廉价机器撑起全公司的实时分析"变得理所当然。这,就是 2026 年工程师手里最锋利的那把 OLAP 刀。
参考资料:ClickHouse 官方文档 Changelog 2026、ClickHouse v26.2 发布说明、ClickStack 官方博客、《大数据核心框架更新周报》(2026-07)、ClickHouse GitHub 仓库。本文代码均基于 ClickHouse 25.x/26.x 语法,低版本部分特性(如原生 JSON、inverted 索引)需开启对应 experimental 设置。