编程 ClickHouse 2026 深度实战:当列式存储遇见 AI 时代——从 MergeTree 引擎到 PB 级实时分析,构建下一代数据基础设施的完全指南

2026-06-21 21:58:21 +0800 CST views 11

ClickHouse 2026 深度实战:当列式存储遇见 AI 时代——从 MergeTree 引擎到 PB 级实时分析,构建下一代数据基础设施的完全指南

2026 年,数据基础设施的战场已经从“能不能存得下”转移到“能不能查得快、用得省”。ClickHouse 这个源自 Yandex _metrica 的列式数据库,在实时分析、可观测性、AI 特征工程等场景里已经不再是“可选项”,而是很多团队的核心依赖。这篇文章不会只讲概念,而是带着你从存储引擎、查询执行、分布式架构到生产调优,完整走一遍 ClickHouse 的实战落地。

一、背景:为什么 2026 年我们还要认真聊 ClickHouse?

数据量还在涨。一个中等规模的互联网产品,每天产生的日志、事件、指标数据轻松就能到 TB 级别。传统关系型数据库在这种场景下早就力不从心,Hadoop 生态太重、实时性不够,而各种“新一代数仓”又往往在易用性和性能之间做取舍。

ClickHouse 的定位非常清晰:单表查询性能极致、SQL 原生、部署简单、生态开放。它不是万能数据库,但在 OLAP(联机分析处理)场景下,几乎是你能拿到的性价比最高的方案之一。

2026 年的几个趋势让 ClickHouse 更值得关注:

  1. AI 驱动的可观测性:LLM 应用、Agent 系统的调用链、成本、延迟指标爆炸式增长,需要极低的查询延迟和极高的压缩比。
  2. 实时特征工程:推荐、风控、广告系统越来越多地把特征计算下沉到 ClickHouse,用物化视图和投影做流式聚合。
  3. 云原生与存算分离:ClickHouse Cloud、S3 backed MergeTree 让 PB 级数据成本大幅下降。
  4. 开源生态爆发:围绕 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_idsipHash64(order_id) 保证数据分布均匀;
  • 分布式查询默认会广播到所有分片,结果在查询节点合并。

分布式架构的写入有两种模式:

  1. 同步写入(insert_distributed_sync=1):写入分布式表时,等所有分片 ack 再返回。可靠性高,但延迟大。
  2. 异步写入(默认):数据先写到分布式表的本地队列,后台再批量转发到各分片。吞吐高,但有小概率丢失。

3.3 查询生命周期:一条 SQL 是怎么执行的?

理解 ClickHouse 的查询生命周期,对调优至关重要:

  1. 解析与优化:SQL 解析成 AST,经过类型检查、常量折叠、谓词下推等优化;
  2. 索引剪枝:根据 WHERE 条件和稀疏索引,确定需要读取的 part 和 granule;
  3. 读取列数据:从磁盘读取目标列的压缩块,解压缩成列块;
  4. 过滤与计算:应用过滤条件、表达式计算、聚合;
  5. 合并结果:如果是分布式查询,各分片返回部分结果,由查询节点二次聚合;
  6. 返回客户端

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 S3SharedMergeTree)。

核心思路:热数据放在本地 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 表设计三板斧

  1. 排序键尽量贴近查询过滤条件:如果你的查询总是 WHERE user_id = ? AND event_date >= ?,那排序键就应该是 (user_id, event_date, ...)
  2. 分区粒度要适中:按月分区适合数据量大、按时间查询多的场景;按天分区适合数据量小、需要精确 TTL 的场景。分区太多会拖慢写入和元数据管理。
  3. 类型要用到极致:能用 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 可能会在以下几个方向继续发力:

  1. 更强的存算分离:SharedMergeTree 和 ClickHouse Cloud 会成为主流部署形态;
  2. AI 原生能力:向量检索、Embedding 存储、与 LLM 的更好集成;
  3. 更轻量的边缘部署:在端侧、IoT 网关里做本地分析;
  4. 与数据湖更深融合:Iceberg / Delta Lake 表函数、开放表格式支持。

如果你正在做数据平台、实时数仓、可观测性或 AI 基础设施,ClickHouse 值得你花一周时间深入实践。从建表、写入、查询到物化视图、分布式、性能调优,每一个环节都有明确的最佳实践,也有明确的取舍逻辑。

希望这篇长文能帮你少踩几个坑,多榨一点性能。有问题欢迎交流。

推荐文章

windon安装beego框架记录
2024-11-19 09:55:33 +0800 CST
php指定版本安装php扩展
2024-11-19 04:10:55 +0800 CST
Grid布局的简洁性和高效性
2024-11-18 03:48:02 +0800 CST
前端如何一次性渲染十万条数据?
2024-11-19 05:08:27 +0800 CST
JavaScript设计模式:组合模式
2024-11-18 11:14:46 +0800 CST
前端如何优化资源加载
2024-11-18 13:35:45 +0800 CST
程序员茄子在线接单