ClickHouse是面向OLAP场景的列式存储数据库,在亿级数据量的聚合查询中表现出远超传统行式数据库的性能。其核心MergeTree引擎通过稀疏索引、数据分区和后台合并机制实现高效写入与快速查询。掌握MergeTree的存储结构和调优方法,是ClickHouse生产部署的关键。
ClickHouse列式存储与MergeTree引擎存储结构
MergeTree将数据按分区(Partition)组织,每个分区内按主键排序存储。物理存储结构如下:
data/
├── table_name/
│ ├── 20230801_20230831_1/ # 分区目录
│ │ ├── primary.idx # 主键稀疏索引
│ │ ├── data.mrk3 # 标记文件
│ │ ├── [column].bin # 各列压缩数据文件
│ │ ├── [column].mrk3 # 各列标记文件
│ │ ├── count.txt # 该分区总行数
│ │ ├── columns.txt # 列信息
│ │ ├── checksums.txt # 校验和
│ │ └── partition.dat # 分区信息
│ ├── 20230901_20230930_2/
│ └── 20231001_20231031_3/
列式存储的核心优势:查询只读取需要的列,跳过无关列的IO。100列的表查询3列,IO量减少97%。每列独立压缩,相同类型的数据压缩率远高于行式存储的整行压缩。
稀疏索引(primary.idx)不索引每一行,而是每index_granularity行(默认8192)记录一个主键值。查询时通过二分查找快速定位到数据块范围,再扫描块内数据。这使得索引体积极小,可以完全加载到内存中。
MergeTree表创建与分区设计
CREATE TABLE events
(
event_time DateTime64(3),
event_date Date DEFAULT toDate(event_time),
event_type LowCardinality(String),
user_id UInt64,
device_id String,
properties Map(LowCardinality(String), String),
amount Decimal(18, 4),
created_at DateTime DEFAULT now()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_type, event_date, user_id)
PRIMARY KEY (event_type, event_date)
SETTINGS
index_granularity = 8192,
index_granularity_bytes = 10485760,
min_compress_block_size = 65536,
max_compress_block_size = 1048576,
min_bytes_for_wide_part = 10485760,
min_rows_for_wide_part = 10000;
PARTITION BY:按月分区。分区是数据管理的物理边界,查询过滤分区条件时可跳过整个分区的IO。分区粒度不宜过细,每个分区至少100万行数据为宜。过多分区会导致大量小文件,影响合并效率。
ORDER BY:主键排序字段。排序键决定了数据在磁盘上的物理排列顺序,直接影响查询性能。排序键的基数从低到高排列效果最好。将过滤条件最频繁的字段放在前面。
LowCardinality(String):对低基数字符串使用字典编码,将字符串映射为整数存储。事件类型字段如果有100种取值,LowCardinality可将存储从平均8字节/行降至1字节/行。
min_bytes_for_wide_part / min_rows_for_wide_part:控制Wide与Compact格式切换阈值。数据量小于阈值时使用Compact格式,大于阈值时使用Wide格式。Wide格式支持按列裁剪,Compact格式减少小文件数量。
批量写入策略与合并机制调优
ClickHouse不推荐高频小批量写入,每次写入会生成一个新的data part,后台合并需要消耗CPU和IO。推荐的写入模式:
# Python批量写入示例
from clickhouse_driver import Client
import time
client = Client(host='localhost', port=9000)
def batch_insert(events, batch_size=100000):
# 分批写入,每批10万行
for i in range(0, len(events), batch_size):
batch = events[i:i+batch_size]
client.execute(
'INSERT INTO events (event_time, event_type, user_id, device_id, properties, amount) VALUES',
batch
)
print(f"已写入 {i + len(batch)} / {len(events)} 行")
# 流式写入:每30秒或累积1万行提交一次
class BufferedWriter:
def __init__(self, table, flush_interval=30, max_buffer=10000):
self.table = table
self.flush_interval = flush_interval
self.max_buffer = max_buffer
self.buffer = []
self.last_flush = time.time()
def add(self, row):
self.buffer.append(row)
if (len(self.buffer) >= self.max_buffer or
time.time() - self.last_flush >= self.flush_interval):
self.flush()
def flush(self):
if not self.buffer:
return
client.execute(
f'INSERT INTO {self.table} VALUES',
self.buffer
)
self.buffer.clear()
self.last_flush = time.time()
合并相关配置:
-- 查看合并状态
SELECT
database, table,
count() as parts_count,
sum(rows) as total_rows,
sum(bytes_on_disk) as total_bytes,
avg(rows) as avg_part_rows
FROM system.parts
WHERE active AND table = 'events'
GROUP BY database, table
-- 手动触发合并
OPTIMIZE TABLE events FINAL;
-- 查看合并队列
SELECT * FROM system.merges WHERE table = 'events'
-- 调整合并参数
SET max_bytes_to_merge_at_max_space_in_pool = 161061273600
SET max_parts_to_merge = 100
parts_to_delay_insert(默认150):当分区内活跃parts数超过此值时,写入请求被延迟。parts_to_throw_insert(默认300):超过此值直接拒绝写入。这两个参数防止小批量写入导致parts数量失控。
生产环境写入频率建议:每批1万到10万行,提交间隔不少于1秒。使用Kafka作为缓冲层,消费者批量写入ClickHouse是常见架构。
聚合查询优化与物化视图加速
-- 低效查询:扫描全表做聚合
SELECT
event_type,
toDate(event_time) as day,
count() as cnt,
sum(amount) as total_amount,
uniqExact(user_id) as unique_users
FROM events
WHERE event_time >= '2026-08-01' AND event_time < '2026-09-01'
GROUP BY event_type, day
ORDER BY day, event_type
-- 优化1:利用分区裁剪和排序键
SELECT
event_type,
event_date as day,
count() as cnt,
sum(amount) as total_amount,
uniqExact(user_id) as unique_users
FROM events
WHERE event_date BETWEEN '2026-08-01' AND '2026-08-31'
AND event_type IN ('purchase', 'signup', 'refund')
GROUP BY event_type, day
ORDER BY day, event_type
-- 优化2:使用物化视图预聚合
CREATE MATERIALIZED VIEW events_daily_mv
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (event_type, day)
AS
SELECT
event_type,
toDate(event_time) as day,
count() as event_count,
sum(amount) as total_amount,
uniqExact(user_id) as unique_users
FROM events
GROUP BY event_type, day
-- 查询物化视图,秒级返回
SELECT
event_type,
day,
event_count,
total_amount,
unique_users
FROM events_daily_mv
WHERE day BETWEEN '2026-08-01' AND '2026-08-31'
ORDER BY day, event_type
-- 优化3:使用AggregateFunction类型存储预计算状态
CREATE TABLE events_daily_agg
(
day Date,
event_type LowCardinality(String),
count_state AggregateFunction(count),
amount_state AggregateFunction(sum, Decimal(18,4)),
users_state AggregateFunction(uniq, UInt64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (event_type, day);
CREATE MATERIALIZED VIEW events_daily_agg_mv TO events_daily_agg
AS
SELECT
toDate(event_time) as day,
event_type,
countState() as count_state,
sumState(amount) as amount_state,
uniqState(user_id) as users_state
FROM events
GROUP BY day, event_type;
-- 查询时合并状态
SELECT
event_type,
day,
merge(count_state) as event_count,
merge(amount_state) as total_amount,
merge(users_state) as unique_users
FROM events_daily_agg
WHERE day BETWEEN '2026-08-01' AND '2026-08-31'
GROUP BY event_type, day
ORDER BY day, event_type
物化视图方案的性能对比(10亿行数据,30天范围查询):
直接查询原始表:扫描约3亿行,耗时约12秒。
SummingMergeTree物化视图:扫描约9000行预聚合数据,耗时约0.03秒。加速约400倍。
AggregatingMergeTree + AggregateFunction:扫描约9000行状态数据,uniq精确去重耗时约0.05秒。比SummingMergeTree的uniqExact去重慢一点,但存储空间更小且支持增量合并。
选择建议:仅需要简单sum/count时用SummingMergeTree。需要精确去重、分位数等复杂聚合时用AggregatingMergeTree + AggregateFunction。物化视图会增加写入开销,但查询加速效果显著,OLAP场景中值得使用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/clickhouse-lie-shi-cun-chu-yin-qing-jia-gou-yu-mergetree/