为什么选 ClickHouse
OLAP(联机分析处理)场景下,行存数据库(MySQL/PostgreSQL)在亿级数据聚合查询时力不从心。ClickHouse 用列存 + 向量化执行做到了极致的查询性能:
| 维度 | MySQL/PG(行存) | ClickHouse(列存) |
|---|---|---|
| 10 亿行 COUNT/SUM | 30-60s | 0.1-0.5s |
| 10 亿行 GROUP BY | 60-120s | 0.5-2s |
| 写入吞吐(单机) | 1-5 万行/秒 | 10-50 万行/秒 |
| 数据压缩率 | 1-3× | 5-15× |
| 点查(按主键查单行) | 快 | 慢(不擅长) |
| 事务支持 | ACID | 无(仅批量原子写入) |
| 并发查询 | 千级 | 百级(不适合高并发点查) |
核心适用场景:
- 日志/事件分析(千万级日志实时聚合)
- 监控指标存储与查询(类似 InfluxDB 但更强)
- 用户行为分析(漏斗/留存/路径)
- 广告/推荐系统特征存储
不适合的场景:OLTP 事务、高并发点查、频繁 UPDATE/DELETE。
安装部署
单机安装
# CentOS/RHEL
cat > /etc/yum.repos.d/clickhouse.repo << 'EOF'
[clickhouse-stable]
name=ClickHouse Stable Repository
baseurl=https://packages.clickhouse.com/rpm/stable
gpgcheck=1
gpgkey=https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key
enabled=1
EOF
yum install -y clickhouse-server clickhouse-client
systemctl enable --now clickhouse-server
# Ubuntu/Debian
apt install -y apt-transport-https ca-certificates
wget -qO- https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key | apt-key add -
echo "deb https://packages.clickhouse.com/deb stable main" > /etc/apt/sources.list.d/clickhouse.list
apt update && apt install -y clickhouse-server clickhouse-client
systemctl enable --now clickhouse-server
核心目录
| 路径 | 用途 |
|---|---|
/etc/clickhouse-server/config.xml |
全局配置 |
/etc/clickhouse-server/users.xml |
用户与配额配置 |
/var/lib/clickhouse/ |
数据存储目录 |
/var/log/clickhouse-server/ |
日志目录 |
/var/lib/clickhouse/tmp/ |
临时文件(大查询的中间结果) |
基本配置调优
<!-- /etc/clickhouse-server/config.xml -->
<clickhouse>
<listen_host>0.0.0.0</listen_host>
<http_port>8123</http_port>
<tcp_port>9000</tcp_port>
<!-- 内存限制 -->
<max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
<max_thread_pool_size>10000</max_thread_pool_size>
<!-- 存储策略 -->
<storage_configuration>
<disks>
<default>
<path>/var/lib/clickhouse/</path>
</default>
<ssd_disk>
<path>/data/ssd/clickhouse/</path>
</ssd_disk>
</disks>
<policies>
<hot_cold>
<volumes>
<hot>
<disk>ssd_disk</disk>
</hot>
<cold>
<disk>default</disk>
</cold>
<move_factor>0.2</move_factor>
</volumes>
</hot_cold>
</policies>
</storage_configuration>
</clickhouse>
# 连接
clickhouse-client --host localhost --port 9000
# 设置管理员密码
clickhouse-client
:) SET PASSWORD FOR default = 'StrongPassword123!';
表引擎:MergeTree 家族
ClickHouse 的核心是 MergeTree 引擎族——所有高性能场景都用它。
MergeTree 基础
CREATE TABLE events (
event_time DateTime,
event_date Date DEFAULT toDate(event_time),
user_id UInt64,
event_type LowCardinality(String),
event_data String,
ip IPv4
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date) -- 按月分区
ORDER BY (event_type, user_id, event_time) -- 排序键(也是主键)
SETTINGS index_granularity = 8192; -- 索引粒度(每 8192 行一个索引标记)
关键概念:
| 概念 | 说明 |
|---|---|
| PARTITION | 物理分区,按分区独立存储和管理,旧分区可独立删除 |
| ORDER BY | 排序键 = 主键索引,数据按此排序存储,查询时跳过不匹配的 granule |
| index_granularity | 索引粒度,每 N 行一个索引标记,默认 8192 |
| Data part | 每次 INSERT 产生一个 part,后台 merge 合并 |
分区设计原则
-- 日志类:按月分区(每月一个分区,方便 TTL 清理旧数据)
PARTITION BY toYYYYMM(event_date)
-- 时序监控:按天分区(每天数据量不大,天级分区管理灵活)
PARTITION BY toDate(timestamp)
-- 超大表:按周分区(平衡分区数和单分区大小)
PARTITION BY toYYYYWW(event_date)
-- 不要按过细粒度分区(如按小时),分区数过多会拖慢 merge
⚠️ 避坑 #1:单表分区数建议 < 1000。分区过多 = 后台 merge 线程忙不过来 = 查询性能下降。按月分区 10 年才 120 个分区,安全。
ORDER BY 排序键设计
排序键决定了查询跳过能力(类似聚簇索引):
-- 好:查询条件在前,范围在后
ORDER BY (event_type, user_id, event_time)
-- SELECT * FROM events WHERE event_type='click' AND user_id=123
-- → 高效跳过不匹配的 granule
-- 坏:高基数列在前,低基数在后
ORDER BY (user_id, event_type, event_time)
-- SELECT * WHERE event_type='click'
-- → 无法跳过(user_id 不在条件里),全表扫描
排序键选择原则:
- 查询过滤条件中最常用的列放前面
- 低基数列(如 type/status)优先放前面
- 时间列通常放最后(范围查询用)
- 排序键列数建议 3-5 个,不宜过多
ReplacingMergeTree(去重引擎)
-- 自动去重相同主键的记录(后台 merge 时生效)
CREATE TABLE users (
user_id UInt64,
updated_at DateTime,
name String,
email String
) ENGINE = ReplacingMergeTree(updated_at) -- 保留 updated_at 最大的版本
ORDER BY (user_id)
PARTITION BY toYYYYMM(updated_at);
⚠️ 避坑 #2:ReplacingMergeTree 的去重是最终一致的——merge 完成前仍有重复。查询时需加
FINAL关键字强制去重:SELECT * FROM users FINAL。但FINAL性能差,大数据量时避免使用。
SummingMergeTree(预聚合引擎)
-- 自动对相同主键的数值列求和
CREATE TABLE page_views_daily (
view_date Date,
page_id UInt64,
visits UInt64,
duration UInt32
) ENGINE = SummingMergeTree()
ORDER BY (view_date, page_id)
PARTITION BY toYYYYMM(view_date);
TTL 自动过期
-- 30 天后自动删除数据
ALTER TABLE events MODIFY TTL event_date + INTERVAL 30 DAY;
-- 30 天后移动到冷存储,90 天后删除
ALTER TABLE events MODIFY TTL
event_date + INTERVAL 30 DAY TO VOLUME 'cold',
event_date + INTERVAL 90 DAY DELETE;
数据写入
批量 INSERT(推荐)
-- 单次大批量写入(最优)
INSERT INTO events VALUES
('2026-07-21 10:00:00', '2026-07-21', 1001, 'click', '{"btn":"buy"}', '1.2.3.4'),
('2026-07-21 10:01:00', '2026-07-21', 1002, 'view', '{"page":"home"}', '1.2.3.5'),
...;
-- 从文件导入
clickhouse-client --query "INSERT INTO events FORMAT CSV" < events.csv
clickhouse-client --query "INSERT INTO events FORMAT JSONEachRow" < events.json
写入吞吐关键:
- 每次插入 1-10 万行(太少产生过多 part,太多锁表久)
- 每秒不超过 1 次 INSERT(合并 part 的速度跟不上高频小批量)
- 用 Buffer 表缓冲小批量写入
Buffer 表引擎(缓冲小写入)
-- 底层表
CREATE TABLE events_raw (...) ENGINE = MergeTree() ...;
-- 缓冲表
CREATE TABLE events_buffer AS events_raw
ENGINE = Buffer(currentDatabase, events_raw,
16, -- num_layers(并发缓冲层数)
600, -- min_time(秒,缓冲最短时间)
3600, -- max_time(秒,缓冲最长时间后强制刷盘)
10000, -- min_rows(最少行数才刷盘)
1000000, -- max_rows(最大行数后强制刷盘)
10000000, -- min_bytes
100000000 -- max_bytes(100MB 后强制刷盘)
);
-- 写入 buffer 表,自动合并后写入 raw 表
INSERT INTO events_buffer VALUES (...);
流式写入(Kafka)
-- Kafka 引擎消费消息
CREATE TABLE events_kafka (
event_time DateTime,
user_id UInt64,
event_type String,
event_data String
) ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'kafka1:9092,kafka2:9092',
kafka_topic_list = 'events',
kafka_group_name = 'clickhouse_consumer',
kafka_format = 'JSONEachRow';
-- 物化视图自动消费 Kafka 写入 MergeTree
CREATE MATERIALIZED VIEW events_consumer TO events_raw AS
SELECT * FROM events_kafka;
⚠️ 避坑 #3:ClickHouse 不适合频繁 UPDATE/DELETE。
ALTER TABLE ... UPDATE是异步重写整个分区,极慢。需要更新的场景用 ReplacingMergeTree + 版本号。
查询优化
跳过索引(Data Skipping Index)
CREATE TABLE events (
...
INDEX idx_user user_id TYPE minmax GRANULARITY 4, -- minmax:数值范围
INDEX idx_data event_data TYPE tokenbf_v1(4096, 3, 0) GRANULARITY 4, -- 布隆过滤器:文本包含
INDEX idx_ip ip TYPE set(100) GRANULARITY 4 -- set:去重值集合
) ENGINE = MergeTree() ...;
| 索引类型 | 适用 | 说明 |
|---|---|---|
| minmax | 数值/日期 | 记录每 N 个 granule 的 min/max,范围查询跳过 |
| set(N) | 低基数字符串 | 记录去重值集合,等值查询跳过 |
| bloom_filter | 任意 | 布隆过滤器,等值/IN 查询跳过 |
| tokenbf_v1 | 文本 | 分词布隆过滤器,LIKE 查询 |
| ngrambf_v1 | 文本 | N-gram 布隆过滤器,子串查询 |
查询性能技巧
-- 1. 分区裁剪:只在需要的分区查询
SELECT count() FROM events
WHERE event_date = '2026-07-21'; -- ✓ 只扫一个分区
SELECT count() FROM events
WHERE event_time >= '2026-07-21 00:00:00'; -- ✗ 扫所有分区(没有用分区列过滤)
-- 2. PREWHERE 优化(先过滤再读取其他列)
SELECT user_id, event_data FROM events
PREWHERE event_type = 'click' -- 先只读 event_type 列过滤
WHERE event_type = 'click' AND user_id > 1000;
-- 3. 避免高基数 GROUP BY
-- ✓ 好:GROUP BY 维度少
SELECT event_type, count() FROM events GROUP BY event_type;
-- ✗ 坏:GROUP BY user_id(百万级分组,内存爆炸)
SELECT user_id, count() FROM events GROUP BY user_id;
-- 替代方案:先采样或用近似函数
SELECT user_id, count() FROM events GROUP BY user_id LIMIT 100;
-- 或用近似去重
SELECT uniqExact(user_id) FROM events;
-- 4. 近似函数(大数据量时接受精度换速度)
SELECT uniq(user_id) FROM events; -- 近似去重(HyperLogLog)
SELECT uniqExact(user_id) FROM events; -- 精确去重(慢)
SELECT quantile(0.99)(latency) FROM events; -- 近似分位数
⚠️ 避坑 #4:
SELECT *是 ClickHouse 的性能杀手。列存数据库读取每一列都要单独 I/O,查 100 列比查 5 列慢 20 倍。永远只 SELECT 需要的列。
集群架构
分片 + 副本
┌──────────────────┐
应用 ──写入──→ │ ClickHouse 节点 │ (分片1 副本1)
└────────┬─────────┘
│ ReplicatedMergeTree 复制
┌────────▼─────────┐
│ ClickHouse 节点 │ (分片1 副本2)
└──────────────────┘
分片1 [副本1, 副本2] 分片2 [副本1, 副本2]
数据范围: user_id % 2=0 数据范围: user_id % 2=1
- 分片(Shard):数据水平切分到多节点,提升写入吞吐和存储容量
- 副本(Replica):同一分片的数据冗余,高可用 + 读分散
ZooKeeper 配置(副本必需)
<!-- /etc/clickhouse-server/config.xml -->
<zookeeper>
<node>
<host>zk1.internal</host>
<port>2181</port>
</node>
<node>
<host>zk2.internal</host>
<port>2181</port>
</node>
<node>
<host>zk3.internal</host>
<port>2181</port>
</node>
</zookeeper>
<!-- 集群定义 -->
<remote_servers>
<analytics_cluster>
<shard>
<replica>
<host>ch1.internal</host>
<port>9000</port>
</replica>
<replica>
<host>ch2.internal</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>ch3.internal</host>
<port>9000</port>
</replica>
<replica>
<host>ch4.internal</host>
<port>9000</port>
</replica>
</shard>
</analytics_cluster>
</remote_servers>
ReplicatedMergeTree 建表
-- 在所有节点上执行(相同语句)
CREATE TABLE events_replicated (
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String)
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/events_replicated', -- ZK 路径({shard} 宏自动替换)
'{replica}' -- 副本名({replica} 宏自动替换)
)
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, user_id, event_time);
<!-- 每个节点的 config.xml 定义宏 -->
<macros>
<shard>01</shard> <!-- ch1/ch2: 01, ch3/ch4: 02 -->
<replica>ch1</replica> <!-- 每节点不同 -->
</macros>
Distributed 表(查询路由)
-- 本地表(每个分片上各自存一部分数据)
CREATE TABLE events_local ON CLUSTER analytics_cluster (
...
) ENGINE = ReplicatedMergeTree(...) ...;
-- 分布式表(查询入口,自动路由到各分片)
CREATE TABLE events_all ON CLUSTER analytics_cluster AS events_local
ENGINE = Distributed(
analytics_cluster, -- 集群名
currentDatabase(), -- 数据库
events_local, -- 本地表名
rand() -- 分片键(写入时用,查询时不影响路由)
);
-- 写入分布式表 → 自动分散到各分片
INSERT INTO events_all VALUES (...);
-- 查询分布式表 → 自动并行查所有分片聚合
SELECT event_type, count() FROM events_all GROUP BY event_type;
⚠️ 避坑 #5:写入分布式表有一个性能陷阱——数据先写入接收节点内存,再异步转发到各分片。如果接收节点宕机,内存中的未转发数据丢失。生产环境推荐直写本地表(应用层按分片键路由),分布式表只用于查询。
备份与恢复
clickhouse-backup 工具(推荐)
# 安装
wget https://github.com/Altinity/clickhouse-backup/releases/download/v2.0.0/clickhouse-backup-linux-amd64.tar.gz
tar xzf clickhouse-backup-linux-amd64.tar.gz
mv clickhouse-backup /usr/local/bin/
# 配置
cat > /etc/clickhouse-backup/config.yml << 'EOF'
general:
remote_storage: s3
disable_progress_bar: false
clickhouse:
host: localhost
port: 9000
username: default
password: 'StrongPassword123!'
s3:
access_key: AKIAxxx
secret_key: xxx
bucket: clickhouse-backup
region: cn-north-1
EOF
# 创建备份
clickhouse-backup create backup-20260721
# 上传到 S3
clickhouse-backup upload backup-20260721
# 恢复
clickhouse-backup download backup-20260721
clickhouse-backup restore backup-20260721
文件级备份(替代方案)
# 1. 冻结表(创建硬链接快照)
clickhouse-client --query "ALTER TABLE events FREEZE PARTITION '202607'"
# 2. 备份快照目录
rsync -av /var/lib/clickhouse/shadow/ /backup/clickhouse-$(date +%Y%m%d)/
# 3. 清理快照
clickhouse-client --query "ALTER TABLE events UNFREEZE PARTITION '202607'"
监控与运维
-- 关键运维查询
-- 1. 磁盘使用
SELECT
database, table,
formatReadableSize(sum(bytes_on_disk)) AS size,
sum(rows) AS rows,
count() AS parts
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY size DESC;
-- 2. part 合并进度
SELECT database, table, elapsed, progress, num_parts
FROM system.merges;
-- 3. 慢查询
SELECT query, elapsed, formatReadableSize(memory_usage) AS memory, read_rows
FROM system.processes
WHERE elapsed > 10
ORDER BY elapsed DESC;
-- 4. 复制延迟
SELECT database, table, queue_size, log_pointer, last_queue_update
FROM system.replicas
WHERE is_readonly = 0;
-- 5. ZooKeeper 连接状态
SELECT name, host, port, is_connected
FROM system.zookeeper_connection;
关键告警阈值:
| 指标 | 警告 | 严重 | 说明 |
|---|---|---|---|
| 磁盘使用 | >80% | >90% | 需扩容或清理旧分区 |
| part 数量 | >500/表 | >2000/表 | merge 跟不上,需优化写入频率 |
| 复制队列 | >100 | >1000 | 副本同步滞后 |
| 慢查询 | >30s | >120s | 需优化 SQL 或加索引 |
| ZK 延迟 | >100ms | >1s | ZooKeeper 性能瓶颈 |
| 内存使用 | >85% | >95% | 需调 max_memory_usage |
常见故障排查
| 故障 | 诊断 | 解决 |
|---|---|---|
| Too many parts | system.parts count 高 |
减少写入频率;增大 batch size |
| 内存不足 | Memory limit exceeded |
调大 max_memory_usage;优化查询减少聚合维度 |
| 复制停止 | system.replicas is_readonly=1 |
检查 ZooKeeper 连接;重启复制 SYSTEM RESTART REPLICA |
| 查询超时 | system.processes 找慢查询 |
KILL QUERY;加 PREWHERE/分区裁减 |
| ZK 超时 | system.zookeeper_connection |
ZK 集群扩容;清理 ZK 旧日志 |
| 磁盘满 | df -h |
删旧分区 ALTER TABLE ... DROP PARTITION |
| part 不合并 | system.merges 为空 |
手动触发 OPTIMIZE TABLE events FINAL |
⚠️ 避坑 #6:
OPTIMIZE TABLE ... FINAL会重写整个表的所有 part,极慢且消耗大量 I/O。只在 part 数量异常时用,不要当日常维护操作。
十条避坑清单
- 别用 ClickHouse 做 OLTP:没有事务、不支持高频点查、UPDATE 极慢——它是 OLAP 专用
- 分区别太细:按月分区是黄金标准,按天/小时分区会导致 part 数量爆炸
- 排序键决定性能:最常用的过滤条件放前面,低基数优先——选错了全表扫描
- SELECT * 是禁忌:列存数据库读 100 列比读 5 列慢 20 倍,只查需要的列
- 写入要批量:单次 1-10 万行,每秒不超 1 次 INSERT——太多小写入 = Too many parts
- ReplacingMergeTree 去重不实时:查询加 FINAL 性能差,设计时避免依赖实时去重
- 别直写 Distributed 表:数据先入内存再转发,宕机会丢——直写 Local 表更安全
- ZooKeeper 是命脉:副本依赖 ZK,ZK 挂了复制停摆——ZK 至少 3 节点,独立部署
- TTL 自动清理:日志类数据必设 TTL,否则磁盘迟早被撑爆
- 近似函数优先:大数据量用
uniq()代替count(DISTINCT),精度够用速度快 10 倍
总结
ClickHouse 运维核心要点:
- MergeTree 是根基——PARTITION BY 管分区裁剪,ORDER BY 管查询跳过,两者选对了查询就快
- 写入要大不要频——大批量少频率,用 Buffer/Kafka 缓冲小写入
- 集群 = 分片 + 副本——ReplicatedMergeTree + Distributed 是标配,ZooKeeper 是命脉
- 查询只读必要列——列存 + PREWHERE + 分区裁剪 + 近似函数,四板斧覆盖 90% 优化
- 监控 part 数量和复制队列——这两个指标最能反映集群健康度
掌握表引擎选择、排序键设计、集群部署、查询优化这四块,ClickHouse 生产运维就能覆盖日志分析、监控指标、用户行为分析等主流 OLAP 场景。