ClickHouse列式存储原理与MergeTree家族
ClickHouse是面向OLAP场景的列式数据库,核心优势在于按列存储数据减少IO扫描量,配合向量化执行和MPP并行计算实现亚秒级分析查询。ClickHouse的存储引擎以MergeTree家族为核心,所有表引擎都基于MergeTree派生。数据按主键排序后写入不可变的Data Part,后台通过Merge操作不断合并小Part为大Part,这是ClickHouse高写入吞吐的关键设计——写入不修改已有数据,只追加新Part。
MergeTree表引擎与排序键设计
CREATE TABLE events (
event_date Date,
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
platform LowCardinality(String),
region LowCardinality(String),
duration_ms UInt32,
payload String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_type, user_id)
PRIMARY KEY (event_date, event_type)
TTL event_date + INTERVAL 90 DAY
SETTINGS
index_granularity = 8192,
min_bytes_for_wide_part = '10M',
min_rows_for_wide_part = 100000;
ORDER BY是ClickHouse性能优化的核心——查询条件匹配排序键前缀时,可以通过稀疏索引跳过大量不相关的Data Part。上例中(event_date, event_type, user_id)的排序键使得按日期+事件类型过滤的查询几乎只扫描目标数据。LowCardinality类型对低基数字符串使用字典编码,存储空间减少5-10倍,查询速度提升2-3倍。
分区策略与数据生命周期管理
分区决定了数据的物理隔离和TTL管理粒度:
-- 按月分区适合日志类数据
PARTITION BY toYYYYMM(event_date)
-- 按日分区适合高频写入、需要按日删除的场景
PARTITION BY toYYYYMMDD(event_date)
-- 按地区+月份分区适合多租户场景
PARTITION BY (region, toYYYYMM(event_date))
分区不宜过多——每个分区对应独立的Data Part目录,过多分区会导致文件系统inode压力和Merge开销增大。经验值是总分区数不超过1000个。TTL自动过期删除过期分区,比手动DELETE高效得多——ClickHouse的DELETE是Mutation操作,会重写整个Part。
稀疏索引与跳数索引优化
ClickHouse默认的稀疏索引每8192行(index_granularity)记录一个索引标记,对排序键前缀过滤极为高效,但对非排序键列的过滤无效。跳数索引(Skip Index)弥补了这一空白:
ALTER TABLE events ADD INDEX idx_user_bloom user_id TYPE bloom_filter(0.01) GRANULARITY 4;
ALTER TABLE events ADD INDEX idx_duration_minmax duration_ms TYPE minmax GRANULARITY 2;
ALTER TABLE events ADD INDEX idx_payload_token payload TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4;
ALTER TABLE events ADD INDEX idx_type_set event_type TYPE set(0) GRANULARITY 1;
ALTER TABLE events MATERIALIZE INDEX idx_user_bloom;
-- bloom_filter:适合等值查询 WHERE user_id = 12345
-- minmax:适合范围查询 WHERE duration_ms BETWEEN 100 AND 500
-- tokenbf_v1:适合全文搜索 WHERE payload LIKE '%keyword%'
-- set:适合低基数精确匹配 WHERE event_type = 'click'
跳数索引在查询时用于跳过不包含目标值的Granule,减少扫描行数。bloom_filter的误判率参数越小索引体积越大,需要根据查询频率和空间预算权衡。索引只对新写入的数据和MATERIALIZE后的数据生效。
查询优化实战技巧
-- 1. 利用排序键前缀加速过滤
SELECT count() FROM events
WHERE event_date = '2026-08-18' AND event_type = 'click';
-- 2. 使用PREWHERE优化列读取顺序
SELECT count() FROM events
PREWHERE (event_date = '2026-08-18') AND (event_type = 'click')
WHERE duration_ms > 1000;
-- 3. 避免SELECT *,只查需要的列
SELECT event_date, event_type, count() FROM events GROUP BY event_date, event_type;
-- 4. 聚合查询优化:AggregatingMergeTree + 物化视图
CREATE TABLE events_hourly (
hour DateTime,
event_type LowCardinality(String),
uv AggregateFunction(uniq, UInt64),
pv AggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree()
ORDER BY (hour, event_type);
CREATE MATERIALIZED VIEW events_hourly_mv
TO events_hourly AS
SELECT
toStartOfHour(event_time) AS hour,
event_type,
uniqState(user_id) AS uv,
sumState(duration_ms) AS pv
FROM events
GROUP BY hour, event_type;
-- 查询预聚合结果
SELECT
hour,
event_type,
uniqMerge(uv) AS unique_users,
sumMerge(pv) AS total_duration
FROM events_hourly
GROUP BY hour, event_type;
AggregatingMergeTree + 物化视图是ClickHouse最强大的预聚合方案,将实时写入的明细数据自动聚合为小时/天级汇总表,查询延迟从秒级降到毫秒级。
分布式表与集群部署
-- 本地表
CREATE TABLE events_local ON CLUSTER my_cluster (
-- 列定义同上
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/events/{shard}',
'{replica}'
)
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_type, user_id);
-- 分布式表(查询入口)
CREATE TABLE events_dist ON CLUSTER my_cluster AS events_local
ENGINE = Distributed(
my_cluster,
currentDatabase(),
events_local,
rand()
);
-- 查询走分布式表自动路由到各Shard
SELECT event_type, count() FROM events_dist
WHERE event_date = '2026-08-18'
GROUP BY event_type;
分布式表是查询路由层,不存储数据。写入时可以通过分布式表自动分片,也可以直接写本地表避免分布式写入的性能开销。生产环境推荐直接写本地表,用分布式表做跨Shard查询。ClickHouse的分布式Join能力较弱,大表Join应通过预聚合和宽表设计规避。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/clickhouse-lie-shi-cun-chu-yin-qing-jia-gou-yu-olap-cha-xun/