ClickHouse 2026 深度实战:从列式存储到向量检索——OLAP 之王的全栈工程化完全指南
2026年,ClickHouse 已经从一个"俄罗斯公司的玩具数据库"成长为全球 OLAP 领域无可争议的性能之王。30 亿行数据 1 秒出结果不是噱头,而是日常。本文从底层存储引擎到最新向量检索能力,从集群架构到生产级调优,手把手带你把 ClickHouse 用到极致。
一、为什么 2026 年你该认真对待 ClickHouse?
1.1 数据爆炸时代的残酷现实
先说一个真实场景:
你的系统每天产生 5 亿条日志,每条日志平均 1.2KB,一天就是 600GB 原始数据。用 MySQL 做一张宽表存下来,一个月 18TB。这时候产品经理跑过来:"帮我拉一下上个月每个渠道的用户行为漏斗,按小时粒度,拆分 50 个维度。"
MySQL:30 分钟,超时。
PostgreSQL:15 分钟,勉强出来。
ClickHouse:3 秒。
这不是夸张,这是列式存储的物理优势。行存数据库查一列要读整行,列存数据库只读那一列。当你只需要 20 个字段中的 3 个做聚合时,IO 量直接降为原来的 15%。再配合 LZ4 压缩(通常 5-10 倍压缩比),实际磁盘读取量可能只有原始数据的 1-2%。
1.2 2026 年 ClickHouse 生态全景
ClickHouse 在 2026 年已经不只是一个数据库了,它是一个完整的数据平台:
- ClickHouse Core:核心查询引擎,单节点即可处理数十亿行
- ClickHouse Cloud:官方云服务,按查询计费,Serverless 模式
- ClickPipes:官方数据管道,原生支持 Kafka、PostgreSQL CDC、S3 等
- clickhousectl:CLI 管理工具(v0.2.0 已支持 PostgreSQL 集成)
- ClickStack:完整的监控、告警、备份栈
- 原生向量检索:2025-2026 年引入,支持 ANN 向量搜索,可直接用 SQL 做语义检索
同时,2026 年 5 月 ClickHouse 官方宣布集成了 Rust Delta Kernel,原生支持读取 Delta Lake 表。这意味着 ClickHouse 可以直接查询 Lakehouse 架构中的数据,无需额外 ETL。
1.3 与竞品的真实对比
| 维度 | ClickHouse | Apache Druid | StarRocks | Doris |
|---|---|---|---|---|
| 单表查询性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 实时写入 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 运维复杂度 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| SQL 兼容性 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 压缩比 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 向量检索 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 生态丰富度 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
关键判断:如果你的场景是 OLAP 分析查询为主、写入频率适中、需要高压缩比,ClickHouse 是 2026 年的最佳选择。如果你的场景更偏实时数仓(亚秒级延迟),StarRocks/Doris 可能更合适。
二、核心架构深度拆解
2.1 列式存储的物理本质
理解 ClickHouse,先理解列存。假设有一张用户行为表:
| user_id | event | timestamp | page_url | duration | device |
|---------|-------|-----------|----------|----------|--------|
| 1001 | click | 1716xxx | /home | 5.2 | iOS |
| 1002 | view | 1716xxx | /product | 12.8 | Android|
行存(MySQL/PostgreSQL):数据按行连续存储在磁盘上。
[1001][click][1716xxx][/home][5.2][iOS][1002][view][1716xxx][/product][12.8][Android]...
列存(ClickHouse):每列独立存储。
user_id: [1001][1002]...
event: [click][view]...
timestamp:[1716xxx][1716xxx]...
page_url: [/home][/product]...
duration: [5.2][12.8]...
device: [iOS][Android]...
当你执行 SELECT event, count() FROM table GROUP BY event 时:
- 行存:读取每行的 user_id、timestamp、page_url、duration、device 全部跳过,只提取 event。IO 浪费 80%+
- 列存:只读 event 列,直接操作连续的内存块
关键结论:列存的优势在列数多、查询列少的场景下指数级放大。你的表越宽、查询越精简,ClickHouse 的优势越明显。
2.2 MergeTree 引擎家族
MergeTree 是 ClickHouse 的灵魂。所有表引擎都围绕它展开。
MergeTree 基础原理
写入数据 → 写入内存 Buffer → Buffer 满了 → 生成 Part 文件(LSM-Tree 风格)
↓
后台线程自动合并 Parts
↓
大文件更少的 Parts
每个 Part 是一组有序的数据文件:
.bin:压缩后的列数据.mrk:标记文件(索引粒度标记).mrk2:自适应粒度标记.prim.idx:主键索引
CREATE TABLE events
(
user_id UInt64,
event_type LowCardinality(String),
timestamp DateTime,
page String,
duration Float32,
device LowCardinality(String),
-- 核心参数:排序键决定数据物理分布
)
ENGINE = MergeTree()
ORDER BY (event_type, timestamp)
-- 排序键是最重要的参数!决定了查询是否高效
ORDER BY 的设计原则:
- 查询条件优先:WHERE 子句中最常出现的列放前面
- 基数从大到小:高基数的列(如 timestamp、user_id)在低基数列(如 event_type)后面
- 前缀原则:ClickHouse 的主键索引只支持排序键的前缀匹配
错误示例:
-- ❌ 把 user_id 放第一个,event_type 放第二个
-- 查询 WHERE event_type = 'click' 时,索引无法命中
ENGINE = MergeTree() ORDER BY (user_id, event_type)
正确示例:
-- ✅ event_type 在前,查询 WHERE event_type = 'click' 能用上索引
ENGINE = MergeTree() ORDER BY (event_type, timestamp, user_id)
ReplacingMergeTree:去重利器
CREATE TABLE events_dedup
(
user_id UInt64,
event_type String,
timestamp DateTime,
version UInt64
)
ENGINE = ReplacingMergeTree(version)
ORDER BY (user_id, timestamp);
-- 相同 (user_id, timestamp) 的行,保留 version 最大的
注意:ReplacingMergeTree 的去重发生在后台合并时,不是实时去重。查询时用 FINAL 关键字强制去重,但性能很差。生产环境建议通过分区+查询时 GROUP BY 去重。
SummingMergeTree:预聚合
CREATE TABLE metrics
(
metric_name String,
dimension String,
timestamp DateTime,
value UInt64
)
ENGINE = SummingMergeTree()
ORDER BY (metric_name, dimension, timestamp);
-- 相同 key 的行会自动合并 value
CollapsingMergeTree:取消对
CREATE TABLE inventory
(
sku String,
warehouse String,
quantity Int64,
sign Int8 -- 1 = 新增, -1 = 取消
)
ENGINE = CollapsingMergeTree(sign)
ORDER BY (sku, warehouse);
-- 新增库存
INSERT INTO inventory VALUES ('SKU001', 'WH-A', 100, 1);
-- 取消上面的新增
INSERT INTO inventory VALUES ('SKU001', 'WH-A', 100, -1);
AggregatingMergeTree:物化聚合
CREATE TABLE daily_stats
(
date Date,
event_type String,
stats AggregateFunction(sum, UInt64),
unique_users AggregateFunction(uniq, UInt64)
)
ENGINE = AggregatingMergeTree()
ORDER BY (date, event_type);
-- 写入预聚合数据
INSERT INTO daily_stats
SELECT
toDate(timestamp) AS date,
event_type,
sumState(count()) AS stats,
uniqState(user_id) AS unique_users
FROM events
GROUP BY date, event_type;
-- 查询时合并
SELECT
date,
event_type,
sumMerge(stats) AS total_events,
uniqMerge(unique_users) AS unique_users
FROM daily_stats
GROUP BY date, event_type;
2.3 数据类型深度选择
ClickHouse 的类型系统比 MySQL 丰富得多,选对类型直接影响存储和查询性能。
CREATE TABLE optimized_events
(
-- 数值类型:精确选大小
user_id UInt32, -- 4 字节,最大 42 亿,够用
quantity UInt16, -- 2 字节,0-65535
-- 枚举:比 String 省空间,比 Enum8/16 省内存
status Enum8('active'=1, 'inactive'=0, 'banned'=2),
-- LowCardinality:高重复字符串的杀手
country LowCardinality(String), -- 国家码总共不到 200 个
device LowCardinality(String), -- 设备类型更少
browser LowCardinality(String),
-- 普通字符串:低基数字段用 String
session_id String,
page_url String,
-- 时间类型
created_at DateTime, -- 秒级
updated_at DateTime64(3), -- 毫秒级
created_at_date Date, -- 只要日期,3 字节
-- Nullable 的代价:每个元素额外 1 字节标志位
-- ❌ 能不用就不用
middle_name Nullable(String),
-- Array 和 Map
tags Array(String),
properties Map(String, String),
-- IPv4/IPv6 原生类型
ip IPv4,
ipv6_addr IPv6,
-- UUID
request_id UUID
)
ENGINE = MergeTree()
ORDER BY (created_at, status);
LowCardinality 的魔法:
LowCardinality(String) 会自动为不同值创建字典编码。假设 country 列有 200 个唯一值,5000 万行数据:
- String 存储:平均 12 字节 × 5000 万 = 600MB
- LowCardinality(String) 存储:字典 200 × 12 = 2.4KB + 索引 5000 万 × 1 字节 ≈ 47MB
压缩比提升 12 倍以上,而且字典编码后的 GROUP BY 性能大幅提升。
2.4 索引系统
ClickHouse 的索引和 MySQL 的 B+Tree 完全不同:
主键索引(primary.idx):稀疏索引,每个 Granule(默认 8192 行)一条记录。不是指向具体行,而是指向 Granule 的范围。
Granule 0: event_type = 'click', min_ts = 2026-01-01
Granule 1: event_type = 'click', min_ts = 2026-01-02
Granule 2: event_type = 'view', min_ts = 2026-01-01
Granule 3: event_type = 'view', min_ts = 2026-01-02
查询 WHERE event_type = 'view' 时:
- 读取 primary.idx
- 用二分查找找到 Granule 2 的起始位置
- 跳过 Granule 0 和 1,只读 Granule 2 和 3
跳表索引(Skip Index):可以理解为二级索引,支持多种类型:
-- Bloom Filter 索引:适合等值查询
ALTER TABLE events ADD INDEX idx_status bloom_filter(0.01) GRANULARITY 4;
-- MinMax 索引:适合范围查询
ALTER TABLE events ADD INDEX idx_ts_minmax minmax GRANULARITY 4;
-- NgramBF 索引:适合 LIKE 查询
ALTER TABLE events ADD INDEX idx_url_ngram ngrambf_v1(3, 256, 2, 0) GRANULARITY 4;
-- TokenBF 索引:适合全文搜索
ALTER TABLE events ADD INDEX idx_tags tokenbf_v1(512, 3, 0) GRANULARITY 4;
-- 查询时启用索引
SELECT * FROM events WHERE status = 'active' SETTINGS index_usage=1;
三、向量化执行引擎
3.1 为什么 ClickHouse 能这么快?
ClickHouse 的速度秘密不只是列存,更重要的是向量化执行(Vectorized Execution)。
传统数据库的执行模式是"一次一行":
for each row:
if row.status == 'active':
sum += row.value
ClickHouse 的执行模式是"一次一批":
// SIMD 指令同时处理 8 个/16 个/32 个值
__m256i mask = _mm256_cmpeq_epi8(status_vec, "active");
__m256i values = _mm256_and_si256(value_vec, mask);
sum = _mm256_add_epi64(sum, values);
这利用了 CPU 的 SIMD(Single Instruction Multiple Data)指令集(AVX2/AVX-512),一条指令同时处理多个数据。配合列存的数据连续排列,CPU 缓存命中率极高。
3.2 JIT 编译
ClickHouse 还会动态编译查询计划为机器码,消除解释执行的开销:
-- 查看编译缓存
SELECT * FROM system.compiled_expression_cache;
-- 每个 JIT 编译的表达式都会缓存
-- 首次执行较慢(编译),后续执行直接复用
四、集群架构设计
4.1 分片(Sharding)策略
-- 定义集群
CREATE CLUSTER analytics_cluster ON ANCHOR 'ch-node1'
(
'ch-node1' WITH REPLICATION 'replica_1',
'ch-node2' WITH REPLICATION 'replica_2',
'ch-node3' WITH REPLICATION 'replica_3'
);
-- 分布式表
CREATE TABLE events_distributed ON CLUSTER analytics_cluster
(
user_id UInt64,
event_type LowCardinality(String),
timestamp DateTime,
page String
)
ENGINE = Distributed(analytics_cluster, default, events_local, cityHash64(user_id));
-- cityHash64(user_id) 决定了每条数据落到哪个分片
分片键的选择:
| 分片键 | 适用场景 | 注意事项 |
|---|---|---|
cityHash64(user_id) | 按用户维度查询 | 同一用户数据在同一分片 |
cityHash64(toYYYYMM(timestamp)) | 按时间维度查询 | 方便按月删除旧数据 |
cityHash64(rand()) | 纯均匀分布 | 任何维度查询都需要扫描所有分片 |
cityHash64(toString((user_id, toStartOfHour(timestamp)))) | 用户+时间复合 | 数据更均匀,但查询灵活度稍低 |
4.2 副本(Replication)
-- ReplicatedMergeTree:自动多副本同步
CREATE TABLE events_local ON CLUSTER analytics_cluster
(
user_id UInt64,
event_type LowCardinality(String),
timestamp DateTime,
page String
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
ORDER BY (event_type, timestamp);
关键配置:
<!-- config.xml -->
<zookeeper>
<node>
<host>zk1</host>
<port>2181</port>
</node>
</zookeeper>
<macros>
<shard>1</shard>
<replica>replica_1</replica>
</macros>
4.3 ClickHouse Keeper(替代 ZooKeeper)
2026 年,ClickHouse 官方推荐使用 ClickHouse Keeper 替代 ZooKeeper:
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id>
<log_storage_path>/var/lib/clickhouse/keeper/logs</log_storage_path>
<snapshot_storage_path>/var/lib/clickhouse/keeper/snapshots</snapshot_storage_path>
<coordination_settings>
<operation_timeout_ms>10000</operation_timeout_ms>
<session_timeout_ms>30000</session_timeout_ms>
<raft_logs_level>warning</raft_logs_level>
</coordination_settings>
<raft_configuration>
<server>
<id>1</id>
<hostname>ch-node1</hostname>
<port>9234</port>
</server>
<server>
<id>2</id>
<hostname>ch-node2</hostname>
<port>9234</port>
</server>
<server>
<id>3</id>
<hostname>ch-node3</hostname>
<port>9234</port>
</server>
</raft_configuration>
</keeper_server>
优势:少一个外部依赖、运维更简单、性能更好。
五、数据摄入实战
5.1 从 Kafka 实时写入
-- Kafka 引擎表(缓冲层)
CREATE TABLE events_queue
(
user_id UInt64,
event_type String,
timestamp DateTime,
page String,
device String,
duration Float32
)
ENGINE = Kafka
SETTINGS
kafka_broker_list = 'kafka1:9092,kafka2:9092',
kafka_topic_list = 'user_events',
kafka_group_name = 'clickhouse_consumer',
kafka_format = 'JSONEachRow',
kafka_max_block_size = 1048576, -- 1MB 批量写入
kafka_num_consumers = 4, -- 并行消费
kafka_skip_broken_messages = 100; -- 容忍 100 条格式错误
-- 物化视图自动转换
CREATE MATERIALIZED VIEW events_mv TO events
AS SELECT
user_id,
event_type,
timestamp,
page,
device,
duration
FROM events_queue
SETTINGS max_insert_threads = 4;
5.2 使用 ClickPipes(2026 新方式)
ClickPipes 是 ClickHouse 官方的数据管道服务,支持可视化配置:
# clickhousectl 配置 PostgreSQL CDC
clickhousectl pipes create \
--source postgres \
--source-config 'host=pg.local port=5432 database=analytics user=replicator' \
--target clickhouse \
--target-config 'host=ch.local port=8443 database=analytics' \
--tables 'public.events,public.users' \
--mode cdc
5.3 从 S3/MinIO 批量导入
-- 直接查询 S3 上的文件
SELECT *
FROM s3(
'https://bucket.s3.amazonaws.com/logs/*.parquet',
'Parquet',
'user_id UInt64, event_type String, timestamp DateTime'
)
WHERE timestamp >= '2026-05-01';
-- 导入到本地表
INSERT INTO events
SELECT *
FROM s3(
'https://bucket.s3.amazonaws.com/logs/2026-05-*.parquet',
'Parquet',
'user_id UInt64, event_type String, timestamp DateTime, page String'
);
-- Delta Lake 原生支持(2026 年 Rust Delta Kernel 集成)
SELECT *
FROM deltaLake('s3://lakehouse/events/')
WHERE date >= '2026-05-01';
5.4 INSERT 优化技巧
-- ❌ 单条插入:性能极差
INSERT INTO events VALUES (1, 'click', now(), '/home');
-- ✅ 批量插入:推荐方式
INSERT INTO events VALUES
(1, 'click', now(), '/home'),
(2, 'view', now(), '/product'),
(3, 'click', now(), '/home');
-- ✅✅ 从文件批量导入
cat events.jsonl | clickhouse-client \
--query="INSERT INTO events FORMAT JSONEachRow"
-- ✅✅✅ 批量设置
SET max_insert_block_size = 1048576; -- 1MB 块大小
SET min_insert_block_size_rows = 10000; -- 最少 1 万行一批
六、查询优化深度指南
6.1 SAMPLE BY:原生随机采样(2026 新特性)
这是 2026 年 5 月 ClickHouse 官方博客重点介绍的特性。当你不需要精确结果,只需要大致趋势时,采样查询能提升性能数十倍:
-- 创建支持采样的表
CREATE TABLE uk_price_paid
(
price UInt32,
date Date,
postcode1 LowCardinality(String),
postcode2 LowCardinality(String),
type Enum8('terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4, 'other' = 0),
is_new UInt8,
duration Enum8('freehold' = 1, 'leasehold' = 2, 'unknown' = 0),
addr1 String,
addr2 String,
street LowCardinality(String),
locality LowCardinality(String),
town LowCardinality(String),
district LowCardinality(String),
county LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY (postcode1, postcode2, addr1, addr2)
SAMPLE BY intHash32(postcode1); -- 采样键
-- 0.1% 采样查询
SELECT
avg(price) AS avg_price,
count() AS total
FROM uk_price_paid
SAMPLE 0.1;
-- 1/1000 采样
SELECT
town,
avg(price) AS avg_price
FROM uk_price_paid
SAMPLE 1/1000
GROUP BY town
ORDER BY avg_price DESC
LIMIT 10;
采样键的选择很关键:必须是排序键的前缀表达式,通常用 intHash32() 或 cityHash64() 对某个维度做哈希。
6.2 查询 Profile 分析
-- 开启查询日志
SET send_logs_level = 'trace';
-- 查看查询执行详情
SELECT
query_id,
event_time,
query_duration_ms,
read_rows,
read_bytes,
memory_usage,
profile_events['SelectedParts'] AS parts_scanned
FROM system.query_log
WHERE query_id = currentQueryId()
ORDER BY event_time;
-- 查看最慢查询
SELECT
query,
query_duration_ms / 1000 AS duration_sec,
read_rows,
memory_usage / 1024 / 1024 AS memory_mb
FROM system.query_log
WHERE type = 'QueryFinish'
AND query NOT LIKE '%system.query_log%'
ORDER BY query_duration_ms DESC
LIMIT 10;
6.3 高级聚合技巧
-- RolapCube:多维预聚合
SELECT
event_type,
device,
toDate(timestamp) AS day,
count() AS cnt,
uniq(user_id) AS unique_users,
sum(duration) AS total_duration
FROM events
WHERE timestamp >= now() - INTERVAL 30 DAY
GROUP BY ROLLUP(event_type, device, day)
ORDER BY cnt DESC;
-- Window Functions 窗口函数
SELECT
user_id,
timestamp,
event_type,
sum(duration) OVER (PARTITION BY user_id ORDER BY timestamp RANGE BETWEEN INTERVAL 1 HOUR PRECEDING AND CURRENT ROW) AS last_hour_duration,
row_number() OVER (PARTITION BY user_id ORDER BY timestamp DESC) AS event_rank
FROM events
WHERE timestamp >= now() - INTERVAL 1 DAY
LIMIT 1000;
-- ARRAY JOIN:展平嵌套数据
SELECT
tags.name AS tag_name,
count() AS cnt
FROM events
ARRAY JOIN tags AS tags
WHERE timestamp >= now() - INTERVAL 7 DAY
GROUP BY tag_name
ORDER BY cnt DESC
LIMIT 20;
-- materialize 视图预计算
SELECT
materialize(toStartOfHour(timestamp)) AS hour,
materialize(event_type) AS event,
count() AS cnt
FROM events
GROUP BY hour, event;
6.4 常见查询陷阱
-- ❌ 陷阱1:SELECT *
-- 列存数据库每多读一列就多一次 IO
SELECT * FROM events WHERE timestamp > now() - INTERVAL 1 DAY;
-- ✅ 只查需要的列
SELECT event_type, count() FROM events WHERE timestamp > now() - INTERVAL 1 DAY GROUP BY event_type;
-- ❌ 陷阱2:不在 WHERE 中使用排序键
-- ORDER BY (event_type, timestamp),但 WHERE 只有 page_url
-- 索引无法命中,需要全表扫描
SELECT count() FROM events WHERE page_url LIKE '%product%';
-- ✅ 加跳表索引或者换排序键
ALTER TABLE events ADD INDEX idx_page ngrambf_v1(3, 256, 2, 0) GRANULARITY 4;
SELECT count() FROM events WHERE page_url LIKE '%product%' SETTINGS index_usage=1;
-- ❌ 陷阱3:用 JOIN 代替 GROUP BY
-- ClickHouse 的 JOIN 性能不如 GROUP BY
SELECT u.name, count() FROM events e JOIN users u ON e.user_id = u.id GROUP BY u.name;
-- ✅ 能用 GROUP BY 就不用 JOIN
SELECT user_id, count() FROM events GROUP BY user_id;
-- ❌ 陷阱4:UNION ALL 滥用
-- 每个子查询独立执行,互不共享中间结果
SELECT event_type, count() FROM events WHERE device = 'iOS' GROUP BY event_type
UNION ALL
SELECT event_type, count() FROM events WHERE device = 'Android' GROUP BY event_type;
-- ✅ 一条 GROUP BY 搞定
SELECT device, event_type, count() FROM events WHERE device IN ('iOS', 'Android') GROUP BY device, event_type;
-- ❌ 陷阱5:FINAL 关键字
-- ReplacingMergeTree + FINAL 强制去重,性能极差
SELECT * FROM events FINAL WHERE user_id = 1001;
-- ✅ 用 argMax 或 GROUP BY 去重
SELECT user_id, argMax(event_type, version) AS latest_event FROM events WHERE user_id = 1001 GROUP BY user_id;
七、向量检索:ClickHouse 的 AI 时代新能力
7.1 为什么在 OLAP 数据库中做向量搜索?
传统架构中,向量搜索用专门的向量数据库(Milvus、Pinecone、Weaviate),业务数据在 OLAP 数据库中。这意味着:
- 两套系统,数据同步成本高
- 向量检索结果无法直接与业务数据关联查询
- 运维复杂度翻倍
ClickHouse 的向量检索让你用 SQL 一步完成「语义搜索 + 业务过滤 + 聚合分析」。
7.2 向量索引与查询
-- 创建带向量列的表
CREATE TABLE documents
(
doc_id UInt64,
title String,
content String,
category LowCardinality(String),
created_at DateTime,
-- 768 维向量(支持 OpenAI text-embedding-ada-002 等)
embedding Array(Float32),
-- 使用 usearch 索引(ClickHouse 内置的近似最近邻搜索)
INDEX vec_idx usearch(embedding)
)
ENGINE = MergeTree()
ORDER BY (category, created_at);
-- 插入带向量的数据
INSERT INTO documents VALUES
(1, 'Introduction to ClickHouse', 'ClickHouse is a fast OLAP database...', 'database', now(), [0.1, 0.2, ...]),
(2, 'Redis Performance Guide', 'Redis achieves low latency through...', 'cache', now(), [0.3, 0.1, ...]);
-- 向量相似度搜索:找最相似的文档
SELECT
doc_id,
title,
cosineDistance(embedding, [0.15, 0.18, ...]) AS distance
FROM documents
ORDER BY distance ASC
LIMIT 10;
-- 向量搜索 + 业务过滤组合
SELECT
doc_id,
title,
category,
cosineDistance(embedding, [0.15, 0.18, ...]) AS distance
FROM documents
WHERE category = 'database'
AND created_at > now() - INTERVAL 30 DAY
ORDER BY distance ASC
LIMIT 10;
-- 向量搜索 + 聚合分析
SELECT
category,
count() AS doc_count,
avg(cosineDistance(embedding, [0.15, 0.18, ...])) AS avg_distance
FROM documents
GROUP BY category
ORDER BY avg_distance ASC;
7.3 向量索引参数调优
-- usearch 索引配置
ALTER TABLE documents DROP INDEX vec_idx;
ALTER TABLE documents ADD INDEX vec_idx usearch(embedding)
WITH M = 32, -- 连接数,越大越精确但越慢
ef_construction = 128, -- 构建时搜索宽度
ef_runtime = 64, -- 查询时搜索宽度
quantization = 'f32' -- 量化精度
GRANULARITY 1;
关键参数说明:
M(连接数):每个节点的最大连接数。M=32 是精度和性能的良好平衡ef_construction:构建索引时的搜索宽度。越大索引质量越高,构建越慢ef_runtime:查询时的搜索宽度。越大精度越高,查询越慢quantization:f32(全精度)、f16(半精度,内存减半)、i8(8位整数,内存减4倍)
八、物化视图实战
物化视图是 ClickHouse 最强大的特性之一,本质上是一个自动触发的 INSERT-SELECT。
8.1 实时预聚合
-- 原始表:存储所有事件
CREATE TABLE raw_events
(
user_id UInt64,
event_type LowCardinality(String),
timestamp DateTime,
page String,
duration Float32,
revenue Decimal64(2)
)
ENGINE = MergeTree()
ORDER BY (event_type, timestamp);
-- 预聚合表:按小时聚合
CREATE TABLE hourly_stats
(
event_type LowCardinality(String),
hour DateTime,
event_count AggregateFunction(sum, UInt64),
unique_users AggregateFunction(uniq, UInt64),
total_duration AggregateFunction(sum, Float64),
total_revenue AggregateFunction(sum, Decimal64(2))
)
ENGINE = AggregatingMergeTree()
ORDER BY (event_type, hour);
-- 物化视图:自动预聚合
CREATE MATERIALIZED VIEW hourly_stats_mv
TO hourly_stats
AS SELECT
event_type,
toStartOfHour(timestamp) AS hour,
sumState(toUInt64(1)) AS event_count,
uniqState(user_id) AS unique_users,
sumState(duration) AS total_duration,
sumState(revenue) AS total_revenue
FROM raw_events
GROUP BY event_type, hour;
-- 查询预聚合结果
SELECT
event_type,
hour,
sumMerge(event_count) AS events,
uniqMerge(unique_users) AS users,
sumMerge(total_duration) / sumMerge(event_count) AS avg_duration,
sumMerge(total_revenue) AS revenue
FROM hourly_stats
WHERE hour >= now() - INTERVAL 7 DAY
GROUP BY event_type, hour
ORDER BY hour;
8.2 多级物化视图
-- 第1级:每分钟聚合
CREATE TABLE minute_stats
(
event_type LowCardinality(String),
minute DateTime,
cnt AggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree()
ORDER BY (event_type, minute);
-- 第2级:每小时聚合(从第1级聚合)
CREATE TABLE hour_stats
(
event_type LowCardinality(String),
hour DateTime,
cnt AggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree()
ORDER BY (event_type, hour);
CREATE MATERIALIZED VIEW minute_mv TO minute_stats AS
SELECT event_type, toStartOfMinute(timestamp) AS minute, sumState(1) AS cnt
FROM raw_events GROUP BY event_type, minute;
CREATE MATERIALIZED VIEW hour_mv TO hour_stats AS
SELECT event_type, toStartOfHour(minute) AS hour, sumMergeState(cnt) AS cnt
FROM minute_stats GROUP BY event_type, hour;
九、性能调优实战
9.1 内存管理
-- 全局内存限制
SET max_memory_usage = 10000000000; -- 10GB
-- 单查询内存限制
SET max_bytes_before_external_group_by = 2000000000; -- 2GB 后溢出到磁盘
-- 排序溢出
SET max_bytes_before_external_sort = 1000000000; -- 1GB
-- Join 内存
SET join_use_nulls = 1;
SET max_bytes_in_join = 2000000000; -- 2GB
9.2 并发控制
-- 限制并发查询数
SET max_concurrent_queries = 20;
-- 优先级队列
SET priority = 0; -- 0=最高, 1=高, 2=正常, 3=低
-- 查看当前查询
SELECT query_id, query, elapsed, read_rows, memory_usage
FROM system.processes
ORDER BY memory_usage DESC;
-- Kill 慢查询
KILL QUERY WHERE query_id = 'xxx';
9.3 分区与 TTL
-- 按月分区
CREATE TABLE events_partitioned
(
-- columns...
timestamp DateTime
)
ENGINE = MergeTree()\nPARTITION BY toYYYYMM(timestamp)
ORDER BY (event_type, timestamp)
TTL timestamp + INTERVAL 6 MONTH; -- 6个月后自动删除
-- 查看分区
SELECT partition, name, rows, bytes_on_disk
FROM system.parts
WHERE table = 'events_partitioned' AND active
ORDER BY partition DESC;
-- 手动删除旧分区
ALTER TABLE events_partitioned DROP PARTITION '202501';
-- 移动冷数据到 S3
ALTER TABLE events_partitioned
MOVE PARTITION '202501' TO DISK 's3_cold';
-- 配置冷热存储
-- config.xml
<!--
<storage_configuration>
<disks>
<default>
<path>/var/lib/clickhouse/</path>
</default>
<s3_cold>
<type>s3</type>
<endpoint>https://s3.amazonaws.com/clickhouse-cold/</endpoint>
</s3_cold>
</disks>
<policies>
<hot_and_cold>
<volumes>
<hot>
<disk>default</disk>
<max_data_part_size_bytes>10737418240</max_data_part_size_bytes> <!-- 10GB -->
</hot>
<cold>
<disk>s3_cold</disk>
</cold>
</volumes>
<move_factor>0.8</move_factor> <!-- 80% 满时移到冷存储 -->
</hot_and_cold>
</policies>
</storage_configuration>
-->
9.4 服务端配置调优
<!-- config.xml 关键配置 -->
<clickhouse>
<!-- 内存 -->
<max_memory_usage>10000000000</max_memory_usage> <!-- 10GB -->
<max_memory_usage_for_user>8000000000</max_memory_usage_for_user> <!-- 单用户 8GB -->
<max_memory_usage_for_all_queries>8000000000</max_memory_usage_for_all_queries>
<!-- 后台合并 -->
<background_pool_size>16</background_pool_size> <!-- 合并线程数 -->
<background_merges_mutations_concurrency_ratio>2</background_merges_mutations_concurrency_ratio>
<background_schedule_pool_size>8</background_schedule_pool_size>
<!-- 网络 -->
<max_connections>4096</max_connections>
<keep_alive_timeout>300</keep_alive_timeout>
<!-- 查询 -->
<max_execution_time>300</max_execution_time> <!-- 5分钟超时 -->
<max_insert_block_size>1048576</max_insert_block_size>
<max_threads>8</max_threads> <!-- 查询并行线程 -->
<!-- 缓存 -->
<mark_cache_size>5368709120</mark_cache_size> <!-- 标记缓存 5GB -->
<uncompressed_cache_size>8589934592</uncompressed_cache_size> <!-- 未压缩缓存 8GB -->
</clickhouse>
十、监控与运维
10.1 关键指标监控
-- 查询性能
SELECT
count() AS total_queries,
avg(query_duration_ms) AS avg_duration,
quantile(0.95)(query_duration_ms) AS p95_duration,
quantile(0.99)(query_duration_ms) AS p99_duration,
max(query_duration_ms) AS max_duration
FROM system.query_log
WHERE event_date = today()
AND type = 'QueryFinish';
-- 存储使用
SELECT
table,
sum(rows) AS total_rows,
formatReadableSize(sum(bytes_on_disk)) AS disk_size,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
sum(bytes_on_disk) / sum(data_uncompressed_bytes) AS compression_ratio
FROM system.parts
WHERE active
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;
-- 合并堆积
SELECT
table,
count() AS parts_count,
formatReadableSize(sum(bytes_on_disk)) AS total_size,
avg(rows) AS avg_rows_per_part
FROM system.parts
WHERE active
GROUP BY table
HAVING parts_count > 100 -- Parts 过多说明合并跟不上写入
ORDER BY parts_count DESC;
-- 副本延迟
SELECT
database,
table,
absolute_delay,
queue_size,
inserts_in_queue,
log_pointer
FROM system.replication_queue
ORDER BY absolute_delay DESC;
10.2 Prometheus + Grafana 监控
<!-- 开启 Prometheus 指标暴露 -->
<prometheus>
<endpoint>/metrics</endpoint>
<port>9363</port>
<metrics>true</metrics>
</prometheus>
关键监控指标:
clickhouse_queries_total:查询总数clickhouse_query_duration_seconds:查询耗时分布clickhouse_merge_tree_parts_count:Parts 数量clickhouse_replication_queue_size:复制队列大小clickhouse_inserted_rows_total:写入行数clickhouse_read_rows_total:读取行数
10.3 备份策略
# clickhouse-backup 工具
# 安装
clickhouse-backup create --config /etc/clickhouse-backup/config.yml
# 创建备份
clickhouse-backup create backup_20260524
# 上传到 S3
clickhouse-backup upload backup_20260524 s3
# 从 S3 恢复
clickhouse-backup download backup_20260524 s3
clickhouse-backup restore backup_20260524
# 定时备份(crontab)
# 0 2 * * * clickhouse-backup create daily_$(date +\%Y\%m\%d) && clickhouse-backup upload daily_$(date +\%Y\%m\%d) s3
十一、与其他系统集成
11.1 与 PostgreSQL 联邦查询
-- ClickHouse 查询 PostgreSQL
SELECT *
FROM postgresql(
'host=pg.local port=5432 dbname=analytics user=readonly',
'users',
'id UInt32, name String, email String'
)
WHERE created_at > '2026-01-01';
-- 与本地表 JOIN
SELECT
e.event_type,
count() AS cnt,
p.name AS user_name
FROM events e
JOIN postgresql('host=pg.local port=5432 dbname=analytics', 'users', 'id UInt32, name String') p
ON e.user_id = p.id
GROUP BY e.event_type, p.name;
11.2 与 Grafana 集成
ClickHouse 官方提供了 Grafana 数据源插件,支持直接在 Grafana 中查询 ClickHouse 并构建 Dashboard。
# grafana datasources.yml
apiVersion: 1
datasources:
- name: ClickHouse
type: grafana-clickhouse-datasource
access: proxy
url: http://ch-node1:8123
jsonData:
defaultDatabase: analytics
port: 8443
protocol: http
十二、生产级最佳实践清单
部署阶段
- 单节点测试通过后再上集群,避免过度设计
- 使用 ClickHouse Keeper 替代 ZooKeeper
- 冷热存储分层,热数据 NVMe,冷数据 S3
- 配置 TTL 自动清理过期数据
- 部署 Prometheus + Grafana 监控
建表阶段
- ORDER BY 字段按「查询高频 → 低频」排列
- 字符串字段优先用 LowCardinality(String)
- 数值类型精确选大小(UInt8/16/32/64)
- 避免使用 Nullable,用默认值替代
- 合理选择分区键(按时间分区最常见)
写入阶段
- 批量写入,单次至少 1000 行
- 使用 Kafka 引擎 + 物化视图 实现流式摄入
- 避免 schema 频繁变更(ALTER TABLE代价大)
查询阶段
- 禁止 SELECT *,只查需要的列
- WHERE 条件使用排序键前缀
- 使用 SAMPLE 做近似查询
- 大 JOIN 拆分为多个小查询
- 使用 EXPLAIN 查看执行计划
运维阶段
- 监控 Parts 数量,防止合并堆积
- 定期备份(clickhouse-backup)
- 关注磁盘使用率,ClickHouse 压缩后仍需要足够空间
- 关注 ZooKeeper/Keeper 负载
总结
ClickHouse 在 2026 年已经是一个非常成熟的数据基础设施。它的列式存储 + 向量化执行 + LSM-Tree 架构形成了强大的性能三角,让它在 OLAP 场景中几乎无可匹敌。2026 年新增的 SAMPLE BY 原生采样、Rust Delta Kernel 集成、向量检索增强等特性,进一步拓宽了它的应用边界。
从技术选型的角度,如果你面临以下场景,ClickHouse 应该是你的首选:
- 日志分析:TB/PB 级日志的实时查询
- 用户行为分析:漏斗、留存、路径分析
- 业务指标看板:实时聚合的 KPI Dashboard
- 时序数据存储:监控指标、IoT 数据
- A/B 测试分析:实验数据的统计分析
- 向量检索 + 业务分析:语义搜索与业务数据联动
记住 ClickHouse 的黄金法则:列存的优势在列数多、查询列少时指数级放大。你的表设计要服务于查询模式,而不是反过来。
最后送一句:数据量不够大的时候,什么数据库都行。数据量大了以后,选对了数据库,你就赢了。