编程 ClickHouse 2026 深度实战:从列式存储到向量检索——OLAP 之王的全栈工程化完全指南

2026-05-24 10:34:46 +0800 CST views 10

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 与竞品的真实对比

维度ClickHouseApache DruidStarRocksDoris
单表查询性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
实时写入⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
运维复杂度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
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 的设计原则

  1. 查询条件优先:WHERE 子句中最常出现的列放前面
  2. 基数从大到小:高基数的列(如 timestamp、user_id)在低基数列(如 event_type)后面
  3. 前缀原则: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' 时:

  1. 读取 primary.idx
  2. 用二分查找找到 Granule 2 的起始位置
  3. 跳过 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 数据库中。这意味着:

  1. 两套系统,数据同步成本高
  2. 向量检索结果无法直接与业务数据关联查询
  3. 运维复杂度翻倍

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:查询时的搜索宽度。越大精度越高,查询越慢
  • quantizationf32(全精度)、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 应该是你的首选:

  1. 日志分析:TB/PB 级日志的实时查询
  2. 用户行为分析:漏斗、留存、路径分析
  3. 业务指标看板:实时聚合的 KPI Dashboard
  4. 时序数据存储:监控指标、IoT 数据
  5. A/B 测试分析:实验数据的统计分析
  6. 向量检索 + 业务分析:语义搜索与业务数据联动

记住 ClickHouse 的黄金法则:列存的优势在列数多、查询列少时指数级放大。你的表设计要服务于查询模式,而不是反过来。

最后送一句:数据量不够大的时候,什么数据库都行。数据量大了以后,选对了数据库,你就赢了。

推荐文章

Vue中如何使用API发送异步请求?
2024-11-19 10:04:27 +0800 CST
XSS攻击是什么?
2024-11-19 02:10:07 +0800 CST
Nginx 如何防止 DDoS 攻击
2024-11-18 21:51:48 +0800 CST
一些实用的前端开发工具网站
2024-11-18 14:30:55 +0800 CST
支付轮询打赏系统介绍
2024-11-18 16:40:31 +0800 CST
18个实用的 JavaScript 函数
2024-11-17 18:10:35 +0800 CST
免费常用API接口分享
2024-11-19 09:25:07 +0800 CST
聚合支付管理系统
2025-07-23 13:33:30 +0800 CST
用 Rust 构建一个 WebSocket 服务器
2024-11-19 10:08:22 +0800 CST
任务管理工具的HTML
2025-01-20 22:36:11 +0800 CST
HTML5的 input:file上传类型控制
2024-11-19 07:29:28 +0800 CST
什么是Vue实例(Vue Instance)?
2024-11-19 06:04:20 +0800 CST
Rust 并发执行异步操作
2024-11-18 13:32:18 +0800 CST
程序员茄子在线接单