ClickHouse 深度实战:把千亿行数据跑进亚秒级——从 MergeTree 列式内核到生产级 OLAP 架构全链路拆解
程序员视角,实用主义。本文不堆术语,只讲清楚一件事:为什么 ClickHouse 能在单机把千亿行数据聚合压进亚秒级,以及你上线前必须踩对的那些坑。全部带可运行代码。
一、背景介绍:当「数」变成洪水
先讲一个真实的起点。2016 年之前,Yandex 内部有一个叫 Metrica 的产品——可以理解为「俄罗斯版 Google Analytics」,每天要处理数百亿条网页事件,还要让运营在网页上点一下就能看到「过去 1 小时哪个页面掉量了」「昨天的新用户留存曲线」。
这种需求有一个共同特征:写多、读少、但读的时候要扫海量行、算一个聚合。这正是 OLAP(联机分析处理)的典型画像,和 OLTP(订单、转账、库存)是两套完全不同的物理世界。
很多团队的第一反应是「我 MySQL 很熟,加几个索引不就行了?」——然后被现实教做人:
- 行式存储里,一行里塞了 id、name、status、amount、remark……你只想
SELECT SUM(amount),却必须把整行从磁盘搬进内存再丢弃 90% 的字段。数据量一大,I/O 直接爆炸。 - B+Tree 索引擅长「点查一条 / 一小段」,但
GROUP BY扫全表时,索引基本帮不上忙,优化器往往选择全表扫描。 - 即使上大内存,单机行存的分析查询在十亿行量级就已经开始以「分钟」计。
ClickHouse 就是为这种场景生的:一个用 C++ 写的列式存储 OLAP DBMS,2016 年由 Yandex 开源,把 Metrica 那套内部引擎公开了出来。它的设计哲学非常极端且一致:
为了查询快,可以牺牲写入灵活性与一致性;为了扫得多,宁可后台慢慢整理数据。
这套哲学落到工程上,就是后面要拆的:列存、稀疏主键、后台 merge、向量化执行、极致并行。它不是「又一个数据库」,而是一种为分析而重新发明的数据布局。
本文基于 ClickHouse 26.7(2026-08 稳定版,26.3 为 LTS 线)展开,所有示例均可直接复制运行。
二、核心概念:MergeTree 这台「列式引擎」到底在想什么
ClickHouse 有 20 多种表引擎,但 99% 的生产表都建立在 MergeTree 家族之上。理解 MergeHouse,先理解 MergeTree。
2.1 列存:不是「把列分开存」那么简单
行存(MySQL/Postgres)在磁盘上是「一行挨一行」:
| id | name | amount | ts |
| 1 | 张三 | 100 | 10:01 |
| 2 | 李四 | 200 | 10:02 |
列存(ClickHouse)是「一列一个文件」:
id : [1, 2, 3, 4, ...]
name : [张三, 李四, 王五, ...]
amount: [100, 200, 150, ...]
ts : [10:01, 10:02, ...]
好处有两层,而且都和「分析」强相关:
- I/O 局部性:
SELECT SUM(amount)只读 amount 那个文件,其他列完全不碰。在宽表(几十上百列)上,这直接把磁盘读取量砍掉一个数量级。 - 压缩率:同一列的数据类型一致、取值范围相近(比如 amount 都在 0
10000 之间、ts 是单调递增的时间戳),压缩算法能吃得很饱。ClickHouse 默认用 LZ4,列存下常见压缩比 310x。
2.2 Parts 与 Merge:写时 append,读前整理
ClickHouse 写入不是「原地改一行」,而是:
- 每次
INSERT(或一大批)会形成一个 data part(数据片段),整体顺序写入磁盘,part 内部不可变。 - 后台有一个 merge 线程,像 LSM-Tree 的 compaction 一样,把多个小 part 按 ORDER BY 排序后合并成更大的 part。
这个模型带来两个关键结论:
- 写入极快:全部是顺序 append,HDD 上也能轻松吃到 50MB~200MB/s 的吞吐。
- 主键不是唯一的:因为 part 是各自独立的,同一个主键可能出现在多个 part 里。ClickHouse 不保证「主键唯一」,去重要靠 ReplacingMergeTree 或查询时
FINAL/argMax。
2.3 ORDER BY 就是一切:稀疏主键与 granule
这是 ClickHouse 最容易劝退新人的点,也是性能的核心开关。
CREATE TABLE events
(
event_date Date,
user_id UInt64,
event_type LowCardinality(String),
amount Float64,
timestamp DateTime64(3)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id, event_type)
要点:
ORDER BY是排序键:数据在 part 内按它排序存储。它默认同时就是主键(primary key)。- 你可以单独写
PRIMARY KEY (event_date, user_id),但它必须是 ORDER BY 的前缀。主键负责建索引,ORDER BY 负责物理排序,通常让主键 = ORDER BY 的前几列即可。 - ClickHouse 的主键是稀疏索引(sparse index):每
index_granularity(默认 8192)行抽一个标记(mark),记录这一「颗粒(granule)」的第一行 ORDER BY 值。它不指向单行,而是指向「这一整个颗粒」。
查询时发生了什么?假设 WHERE event_date = '2026-08-16' AND user_id = 123:
- 先用
PARTITION BY砍掉无关月份的分区(分区裁剪)。 - 在存活分区里,用稀疏主键二分查找,找出
event_date=2026-08-16 AND user_id=123覆盖的 mark 区间。 - 只读取落在这些 mark 区间里的 granule,其余整个跳过。
这就是 ClickHouse 所谓的「数据跳过(data skipping)」——它不靠逐行索引,而是靠「主键前缀 + 稀疏 mark」在毫秒级定位到要扫的那一小撮颗粒。所以:ORDER BY 设计得越贴合你的 WHERE,扫描的行就越少,查询越快。
2.4 压缩codec:在 LZ4 之前先「变形」
ClickHouse 允许给每个列单独指定压缩链。一条 codec 规则是「专用 codec 在前、通用 codec 在后」:
CREATE TABLE metrics
(
ts DateTime64(3) CODEC(Delta, ZSTD(1)), -- 时间戳:先存差值,再 ZSTD
value Float64 CODEC(Gorilla, ZSTD(1)),-- 浮点:Gorilla 对相近浮点极省
status UInt8 CODEC(ZSTD(3)),
payload String
)
ENGINE = MergeTree
ORDER BY ts
语义:
Delta:对单调递增/近似单调的整数、时间戳存「相邻差值」,压缩率惊人。Gorilla:Facebook 提出的浮点压缩,对变化平缓的时序指标(CPU、QPS)能压到几比特/值。DoubleDelta、T64、FPC、GCD:针对时序、整数、小数的专用变体。- 专用 codec 处理完,再用
LZ4/ZSTD(n)做通用压缩(n 越大压得越狠、越慢)。
经验法则:时间戳、单调 ID 用 Delta;指标用 Gorilla;日志类长文本用 ZSTD(3)。在监控场景,这套组合常把存储成本压到行存的 1/10。
2.5 数据跳过索引:给非主键列也装上「跳跃键」
主键只能前缀,那 WHERE city = 'Beijing' 这种非排序前缀的列怎么办?用 skipping index:
ALTER TABLE events
ADD INDEX idx_city city TYPE bloom_filter(0.01) GRANULARITY 4;
ALTER TABLE events
ADD INDEX idx_amount amount TYPE minmax GRANULARITY 8;
类型一览:
minmax:记录每个 granule 的 min/max,范围查询直接跳过。set:记录 granule 内去重值集合,等值查询用。bloom_filter/ngrambf_v1/tokenbf_v1:模糊/包含匹配(如LIKE '%error%')。
注意它叫「跳过索引」而非「加速索引」:它不能精确定位到行,只能帮优化器多跳过一些肯定不匹配的 granule。配合主键,效果叠加。
2.6 MergeTree 家族速查
| 引擎 | 一句话用途 |
|---|---|
MergeTree | 通用分析表,基础 |
ReplacingMergeTree | 按版本去重(最终一致,合并时才生效) |
AggregatingMergeTree | 物化视图里做「预聚合」,配 -State/-Merge |
SummingMergeTree | 自动按主键汇总数值列 |
CollapsingMergeTree | 用 sign 行「抵消」旧值,做行级更新近似 |
VersionedCollapsingMergeTree | 带版本的 Collapsing |
Distributed | 跨分片代理,本身不存数据 |
Kafka / RabbitMQ | 直接消费消息队列写入 |
三、架构分析:一条 SELECT 是怎么被「肢解」并并行执行的
理解内核,最好的方式是跟一条查询走完全程。
3.1 单机查询生命周期
SQL 文本
│
▼
Parser → AST
│
▼
Analyzer(类型检查、函数解析)
│
▼
Planner(生成 Pipeline)
│
▼
① 稀疏主键裁剪 mark 区间
② 按列、按 part 切分读取任务
③ 每个线程读一个 (part, 列, granule 区间)
④ 向量化:一次处理 8192 行的一个向量(column block)
⑤ 流式聚合 / 排序 / JOIN
│
▼
合并各线程结果 → 返回客户端
关键设计:
- 向量化执行(Vectorized):不是「一行一行」算,而是「一个 8192 行的 block 一起算」。CPU 分支预测、SIMD 都能吃满,函数是针对整列实现的(比如
sum直接对一个ColumnUInt64做累加)。这是它比「逐行解释执行」的分析型引擎快的根本原因。 - 线程级并行:一条查询默认会用满
max_threads个核(默认等于 CPU 核数)。每个线程认领一部分 granule,互不干扰地扫、算,最后合并。所以 ClickHouse 是「一条慢查询吃光整台机器」的野兽——这对并发 service 是双刃剑,后面优化章节会讲怎么治。 - 流水线(Pipeline):读、过滤、聚合是流式的,不需要把全表读进内存再算。这正是它能扫千亿行的内存基础。
3.2 分布式:Distributed 引擎 + Keeper
生产环境一定是集群。ClickHouse 的分布式很「轻」:
- 每个分片(shard)上放一张本地表(
ENGINE = MergeTree)。 - 再建一张
ENGINE = Distributed(cluster, db, local_table, sharding_key)的代理表。它不存数据,只负责把查询转发到各分片、汇总结果。
<!-- config.d/clusters.xml -->
<clickhouse>
<remote_servers>
<ch_cluster>
<shard>
<replica><host>ch-1</host><port>9000</port></replica>
<replica><host>ch-2</host><port>9000</port></replica>
</shard>
<shard>
<replica><host>ch-3</host><port>9000</port></replica>
<replica><host>ch-4</host><port>9000</port></replica>
</shard>
</ch_cluster>
</remote_servers>
</clickhouse>
CREATE TABLE events_local ON CLUSTER ch_cluster
AS events
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
ORDER BY (event_date, user_id, event_type);
CREATE TABLE events_dist ON CLUSTER ch_cluster
AS events_local
ENGINE = Distributed(ch_cluster, default, events_local, rand());
- **副本(replica)**靠
ReplicatedMergeTree+ ClickHouse Keeper(自研的 Raft 共识服务,替代 ZooKeeper)同步 part 元数据与数据,保证高可用。 sharding_key(上例rand())决定一行落到哪个分片,常用xxHash64(user_id)让同一用户的事件集中、且分片均衡。- 查询
events_dist时,各分片本地算完,再由初始节点做「二次聚合」(GLOBAL IN/DISTINCT等场景要小心跨分片广播,详见优化章)。
3.3 物化视图:把「计算」提前到写入时
ClickHouse 的物化视图(Materialized View)不是「视图」,而是一个 INSERT 触发器:源表插入数据时,MV 按定义的 SELECT 把结果写入自己的目标表。查询 MV 时读的是那份已经算好的目标表,不是现场重算。
这是实现「实时大盘」的标准姿势(见代码实战)。
四、代码实战:从零搭一个可观测性分析平台
下面所有命令基于 Docker 单机,照抄即可跑。
4.1 起服务 & 连上去
docker run -d --name ch \
--ulimit nofile=262144:262144 \
-p 8123:8123 -p 9000:9000 \
clickhouse/clickhouse-server:latest
# 进客户端
docker exec -it ch clickhouse-client
# 或者用 HTTP 接口(生产常用,方便走 LB/代理)
curl 'http://localhost:8123/?query=SELECT+version()'
4.2 建表:把 ORDER BY 和 codec 用对
CREATE TABLE events
(
event_date Date,
user_id UInt64,
event_type LowCardinality(String), -- 低基数字典编码,省空间又快
city LowCardinality(String),
amount Float64 CODEC(Gorilla, ZSTD(1)),
duration_ms UInt32 CODEC(Delta, ZSTD(1)),
timestamp DateTime64(3) CODEC(Delta, ZSTD(1))
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, city, user_id, event_type) -- 主键 = 排序键前缀
TTL toDateTime(timestamp) + INTERVAL 180 DAY; -- 过期数据自动清,省成本
注意 event_type/city 用了 LowCardinality(String):当字符串基数很低(如几十种事件类型),ClickHouse 会建全局字典,存储和比较都接近整数速度。
4.3 灌数据:用 Python 批量写(生产路径)
clickhouse-connect 是官方推荐的 Python 客户端,走 HTTP,天然适配 LB/防火墙。
import clickhouse_connect
import random
from datetime import datetime, timedelta
client = clickhouse_connect.get_client(
host='localhost', port=8123, username='default', password=''
)
EVENTS = ['page_view', 'click', 'add_cart', 'checkout', 'pay']
CITY = ['Beijing', 'Shanghai', 'Shenzhen', 'Hangzhou', 'Chengdu']
def gen(n: int):
now = datetime.now()
rows = []
for _ in range(n):
ts = now - timedelta(seconds=random.randint(0, 86400 * 30))
rows.append((
ts.date(),
random.randint(1, 5_000_000), # 500 万用户
EVENTS[random.randint(0, 4)],
CITY[random.randint(0, 4)],
round(random.uniform(0, 1000), 2),
random.randint(10, 5000),
ts,
))
return rows
# 一次插 100 万行,column-oriented 写入极快
BATCH = 1_000_000
data = gen(BATCH)
client.insert(
'events',
data,
column_names=['event_date','user_id','event_type','city','amount','duration_ms','timestamp']
)
print(f'inserted {BATCH} rows')
实战经验:单条
INSERT建议攒成 10 万~100 万行的大 block(由max_insert_block_size控制,默认 1048576)。ClickHouse 的 part 越少越大,merge 压力越小、查询越快。千万别「一行一插」。
想快速造数测试也可以纯 SQL:
INSERT INTO events
SELECT
toDate(now() - rand() % 2592000),
rand() % 5000000,
['page_view','click','add_cart','checkout','pay'][rand() % 5 + 1],
['Beijing','Shanghai','Shenzhen','Hangzhou','Chengdu'][rand() % 5 + 1],
rand() / 100000.0,
rand() % 5000,
now() - rand() % 2592000
FROM numbers(1000000);
4.4 查询实战 1:漏斗分析(Funnel)
「当天从 page_view 走到 pay 的用户占比」是增长团队的命根子。ClickHouse 窗口函数直接干:
SELECT
user_id,
groupArray((event_type, ts)) AS path
FROM (
SELECT user_id, event_type, timestamp AS ts
FROM events
WHERE event_date = '2026-08-16'
AND event_type IN ('page_view','add_cart','checkout','pay')
ORDER BY user_id, ts
)
GROUP BY user_id;
-- 更工程化的漏斗:用条件计数
SELECT
countIf(has_step) AS reached_pay,
uniqHLL++(user_id) AS total_users,
round(100 * countIf(has_step) / uniqHLL++(user_id), 2) AS pay_rate
FROM (
SELECT user_id,
max(event_type = 'pay') AS has_step
FROM events
WHERE event_date = '2026-08-16'
GROUP BY user_id
);
uniqHLL++ 是基于 HyperLogLog++ 的近似去重,误差 < 1%,但比 uniqExact(精确)快几个数量级、省内存几个数量级。海量 UV 场景闭眼用 uniqHLL++。
4.5 查询实战 2:留存(Retention)
次日留存 = 第 0 天活跃、且第 1 天也活跃的用户比例:
SELECT
base_day,
cohort_size,
arrayMap(
i -> round(100 * retained[i] / cohort_size, 2),
range(1, 8)
) AS retention_rate_pct
FROM (
SELECT
toDate('2026-08-01') AS base_day,
uniqExact(uid_0) AS cohort_size,
[ uniqExactIf(uid_1, day_diff = 1),
uniqExactIf(uid_1, day_diff = 2),
uniqExactIf(uid_1, day_diff = 3),
uniqExactIf(uid_1, day_diff = 4),
uniqExactIf(uid_1, day_diff = 5),
uniqExactIf(uid_1, day_diff = 6),
uniqExactIf(uid_1, day_diff = 7) ] AS retained
FROM (
SELECT
user_id AS uid_0, toDate(timestamp) AS d0, 0 AS day_diff
FROM events WHERE timestamp BETWEEN '2026-08-01' AND '2026-08-01'
UNION ALL
SELECT e.user_id AS uid_1,
toDate(e.timestamp) AS d1,
dateDiff('day', toDate('2026-08-01'), toDate(e.timestamp)) AS day_diff
FROM events e
WHERE e.timestamp BETWEEN '2026-08-01' AND '2026-08-08'
)
);
(真实留存常用 array + has 更紧凑,这里展开是为了讲清「用日期差分组 + 精确去重」的逻辑。)
4.6 查询实战 3:Top-N 与近似去重
-- 各城市支付总额 Top 10
SELECT city, sum(amount) AS gmv
FROM events
WHERE event_type = 'pay'
GROUP BY city
ORDER BY gmv DESC
LIMIT 10;
-- 某事件下独立用户数(近似)
SELECT event_type, uniqHLL++(user_id) AS uv
FROM events
WHERE event_date = '2026-08-16'
GROUP BY event_type;
-- 人均时长(窗口函数做累计)
SELECT user_id,
sum(duration_ms) OVER (PARTITION BY user_id) AS total_ms
FROM events
WHERE event_date = '2026-08-16'
LIMIT 10;
4.7 物化视图:把实时大盘「预聚合」掉
热点查询(比如「每分钟 GMV 大盘」)如果每次都扫原始千亿行,机器会哭。用 AggregatingMergeTree 在写入时就把聚合算好:
CREATE TABLE events_1m
(
ts_min DateTime,
event_type LowCardinality(String),
users AggregateFunction(uniqHLL++, UInt64),
gmv AggregateFunction(sum, Float64),
cnt AggregateFunction(count)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(ts_min)
ORDER BY (ts_min, event_type);
CREATE MATERIALIZED VIEW mv_events_1m
TO events_1m
AS SELECT
toStartOfMinute(timestamp) AS ts_min,
event_type,
uniqHLL++State(user_id) AS users, -- -State:只存中间态
sumState(amount) AS gmv,
countState() AS cnt
FROM events
GROUP BY ts_min, event_type;
-- 查询时再用 -Merge 把中间态合并
SELECT
ts_min,
event_type,
uniqHLL++Merge(users) AS uv,
sumMerge(gmv) AS gmv,
countMerge(cnt) AS cnt
FROM events_1m
WHERE ts_min >= now() - INTERVAL 1 HOUR
GROUP BY ts_min, event_type
ORDER BY ts_min;
注意 uniqHLL++State / sumState 存的是聚合中间态而非最终结果,Merge 时才算出答案。MV 写入一次、后续查询只读 _1m 这张小表——千亿行原始数据被压成了「每分钟一行」的汇总,查询从「分钟级」变「毫秒级」。
坑提醒:MV 只对它创建之后插入的数据生效(老数据不会自动回填)。历史数据迁移要手动
INSERT INTO events_1m SELECT ... FROM events。
4.8 去重:ReplacingMergeTree
「同一笔订单可能被重复投递,以最新版本为准」:
CREATE TABLE orders
(
order_id UInt64,
status UInt8,
version UInt32, -- 版本号 / 时间戳
updated DateTime
)
ENGINE = ReplacingMergeTree(version) -- 合并时保留 version 最大的一行
ORDER BY order_id;
-- 查询时拿最新:要么等后台合并后加 FINAL,要么直接用 argMax 规避
SELECT order_id, argMax(status, version) AS status
FROM orders
GROUP BY order_id;
FINAL 会在查询时强制做一次合并,简单但慢;生产更推荐 argMax/max 这类「查询期去重」,不依赖合并时机。
五、性能优化:让 26.7 跑出 26.7 倍
ClickHouse 快是快,但用错就是灾难。按优先级排序:
5.1 ORDER BY 设计 > 一切
这是 80% 性能的源头。原则:
- 把最常用做等值过滤的列放最前(分区裁剪 + 主键前缀命中)。
- 把最常用做范围过滤的列紧跟其后(时间戳类)。
user_id这类「高基数列」放后面——它适合做 hash 分片,不适合做需要二分跳过的前缀。- 主键列顺序必须和你的 TOP 查询的
WHERE完美对齐。没有银弹,按业务查询画像定。
反例:ORDER BY (user_id, timestamp) 但查询全是 WHERE city='X' AND ts>...——主键完全用不上,等于全扫。
5.2 列级 codec + LowCardinality
前文已讲。再补一句:Nullable 很贵。每列 Nullable 会额外存一个「是否为 null」的位图,且无法高效向量化。能用 0/空串代替就别用 Nullable。
5.3 Projection:给同一份数据多个「排法」
不想为不同查询建一堆 MV 复制数据?用 Projection(26.x 已稳定):在表上挂一个「按别的键排序的隐藏副本」,优化器自动选是否用它。
ALTER TABLE events
ADD PROJECTION p_by_city
(
SELECT * ORDER BY (city, timestamp)
);
之后 WHERE city='Beijing' 的查询,优化器会自动走 p_by_city 这份投影,无需改 SQL、无需建 MV。
5.4 物化视图预聚合:热点查询的终极大招
第 4.7 节已示范。黄金法则:让写入时多算一点,让读取时少扫亿点。
5.5 设置项:把并行与有序读打开
很多性能开关默认开,但你该知道它们存在:
-- 按 ORDER BY 顺序读,避免排序(大查询省一次巨量排序)
SET optimize_read_in_order = 1;
-- 按主键顺序做聚合,减少内存与排序
SET optimize_aggregation_in_order = 1;
-- 控制单查询吃多少核(防止一条慢查询拖垮整库)
SET max_threads = 8;
-- 强制走压缩缓存(热数据反复扫时显著提速)
SET use_uncompressed_cache = 1;
5.6 用 EXPLAIN 看它到底扫了多少
优化不是玄学,先看执行计划:
-- 看它用了哪些索引、跳过了多少 granule
EXPLAIN indexes = 1
SELECT count() FROM events
WHERE event_date = '2026-08-16' AND city = 'Beijing';
-- 看执行流水线(读、聚合、合并各阶段)
EXPLAIN PIPELINE
SELECT city, sum(amount) FROM events GROUP BY city;
-- 跟踪一次查询的详细日志
clickhouse-client --send_logs_level=trace --query "SELECT ..."
system.query_log / system.query_thread_log 是线上定位慢查询的金矿:哪个查询扫了多少行、用了多少内存、卡在哪一步,一目了然。
5.7 TTL 与生命周期
数据不是越久越好。给表或列设 TTL,让老数据自动删 / 自动下沉到冷盘(TTL MOVE TO DISK),成本和性能都可控(见 4.2 表的 TTL 示例)。
5.8 26.x 新能力速览(值得上车)
- 轻量级 DELETE / UPDATE(patch-part 机制):2025 引入的新 mutation,不再「整 part 重写」,实测比传统
ALTER ... UPDATE快到 近 1000 倍,行级修正终于可用。DELETE FROM events WHERE ...这类标准语法已就绪。 - JSON 对象类型:原生
JSON列支持动态 schema,半结构化日志不再只能塞进String后JSONExtract慢解析。 - Projection 稳定化:多排序视图零复制成本。
- LTS 节奏:26.3 为 LTS 线,追求稳定选 LTS,追新特性上 26.7+。
六、总结展望:ClickHouse 不是银弹,但它是 OLAP 的「瑞士军刀」
把话讲清楚:
什么时候用它?
- 写多读少、读必扫大量行的分析场景:日志/埋点/监控/BI/实时大盘/风控特征。
- 需要 SQL、又要亚秒级扫十亿行的团队。
- 想用物化视图把「实时聚合」做成产品能力的业务。
什么时候别用?
- 高并发点查、强事务、要「读己之所写」一致性——那是 Postgres/MySQL 的活,别难为 ClickHouse。
- 单行频繁更新删除(轻量 UPDATE 虽快,但本质仍非 OLTP)。
- 小数据量(< 千万行)硬上 ClickHouse,运维成本不划算,DuckDB/SQLite 更香。
和「已发布的那些」怎么摆? 本站前面拆过 DuckDB(进程内 OLAP)、MySQL 9 / Redis 8 / Postgres 18 的向量与内核。它们的边界其实很清楚:
- DuckDB:单机、进程内、嵌入式,分析「文件」和「中等数据」,零运维。
- ClickHouse:服务端、分布式、为「持续写入的流式海量数据」而生,要运维但上限极高。
- 关系型三巨头(MySQL/Postgres/Redis)是 OLTP 与缓存主场,硬做 OLAP 是扬短避长。
面向 2026,ClickHouse 的方向很明确:把「实时」和「简单」往前推——轻量更新让它能碰一点更新场景,JSON 类型吃掉半结构化日志,Projection 让多视图零成本,云版把运维进一步托管。它正在从「极致的 OLAP 引擎」变成「能扛实时数据平台的默认底座」。
对你而言,最实在的建议只有一句:先想清楚你的查询画像,再设计 ORDER BY 和 codec,最后才谈集群。 ClickHouse 的快,是设计出来的,不是开箱就有的。把本文第 4、5 节的代码跑一遍,你就已经超过了 80% 只会在 SELECT 上加索引的「ClickHouse 使用者」。
本文示例代码基于 ClickHouse 26.7,客户端用 clickhouse-connect;生产部署务必结合官方文档打磨 codec、分区粒度与集群拓扑。