编程 ClickHouse 深度实战:把千亿行数据跑进亚秒级——从 MergeTree 列式内核到生产级 OLAP 架构全链路拆解

2026-08-16 10:13:11 +0800 CST views 8

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, ...]

好处有两层,而且都和「分析」强相关:

  1. I/O 局部性SELECT SUM(amount) 只读 amount 那个文件,其他列完全不碰。在宽表(几十上百列)上,这直接把磁盘读取量砍掉一个数量级。
  2. 压缩率:同一列的数据类型一致、取值范围相近(比如 amount 都在 010000 之间、ts 是单调递增的时间戳),压缩算法能吃得很饱。ClickHouse 默认用 LZ4,列存下常见压缩比 310x。

2.2 Parts 与 Merge:写时 append,读前整理

ClickHouse 写入不是「原地改一行」,而是:

  1. 每次 INSERT(或一大批)会形成一个 data part(数据片段),整体顺序写入磁盘,part 内部不可变。
  2. 后台有一个 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

  1. 先用 PARTITION BY 砍掉无关月份的分区(分区裁剪)。
  2. 在存活分区里,用稀疏主键二分查找,找出 event_date=2026-08-16 AND user_id=123 覆盖的 mark 区间
  3. 只读取落在这些 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)能压到几比特/值。
  • DoubleDeltaT64FPCGCD:针对时序、整数、小数的专用变体。
  • 专用 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,半结构化日志不再只能塞进 StringJSONExtract 慢解析。
  • 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、分区粒度与集群拓扑。

推荐文章

CSS 中的 `scrollbar-width` 属性
2024-11-19 01:32:55 +0800 CST
html夫妻约定
2024-11-19 01:24:21 +0800 CST
Golang Sync.Once 使用与原理
2024-11-17 03:53:42 +0800 CST
MySQL设置和开启慢查询
2024-11-19 03:09:43 +0800 CST
批量导入scv数据库
2024-11-17 05:07:51 +0800 CST
基于Flask实现后台权限管理系统
2024-11-19 09:53:09 +0800 CST
程序员茄子在线接单