ClickHouse是面向列式存储的OLAP数据库,在实时分析、用户行为画像、日志聚合等场景下展现出远超传统行式数据库的查询性能。单表聚合查询在亿级数据量下可达到毫秒到秒级响应,这得益于列式存储的压缩效率、向量化执行引擎和稀疏索引设计。本文从ClickHouse的架构原理、表引擎选型到查询优化策略,拆解实时OLAP分析的工程实践。
列式存储与向量化执行引擎
行式存储按行组织数据,读取一列需要加载整行数据到内存;列式存储按列组织,查询只读取涉及的列,磁盘I/O大幅减少。ClickHouse在列式存储基础上实现向量化执行——将数据按batch(通常8192行)批量处理,利用CPU SIMD指令并行计算,减少分支预测开销和函数调用频率。
-- 对比:行式数据库需要全行扫描
-- SELECT sum(price) FROM orders WHERE status = 'paid'
-- 行式存储:读取所有列数据再过滤
-- 列式存储:只读取status和price两列
-- ClickHouse的列式压缩效率极高
-- 10亿行订单数据,行式存储约120GB
-- ClickHouse列式存储约12GB(LZ4压缩)
ClickHouse支持多种压缩算法:LZ4(默认,速度快)、ZSTD(压缩率更高,速度略慢)和Delta编码。时间序列数据配合Delta编码可以达到10倍以上压缩率。
CREATE TABLE orders
(
order_id UInt64,
user_id UInt32,
product_id UInt32,
price Decimal(10,2),
status LowCardinality(String),
created_at DateTime,
-- 列级压缩配置
attrs String CODEC(ZSTD(3)),
tags Array(String) CODEC(LZ4)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (created_at, user_id)
SETTINGS index_granularity = 8192;
MergeTree表引擎与分区策略
MergeTree是ClickHouse最常用的表引擎,数据按ORDER BY键排序存储,后台自动合并小的data part。分区(PARTITION BY)将数据按时间维度物理隔离,查询时通过分区裁剪跳过无关数据。
-- 分区策略:按月分区,适合时序数据
CREATE TABLE events
(
event_id UInt64,
event_type LowCardinality(String),
user_id UInt32,
device_id String,
properties Map(String, String),
created_at DateTime64(3)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (event_type, created_at, user_id)
SETTINGS index_granularity = 8192;
-- TTL自动清理过期数据
ALTER TABLE events
MODIFY TTL created_at + INTERVAL 90 DAY DELETE;
-- 查看分区
SELECT partition, rows, bytes, formatReadableSize(bytes) AS size
FROM system.parts
WHERE table = 'events' AND active = 1
ORDER BY partition;
-- 分区操作:移动冷数据到S3存储
ALTER TABLE events
MOVE PARTITION 202501 TO DISK 's3_cold';
LowCardinality(String)对枚举值较少的列使用字典编码,存储和查询效率显著提升。ORDER BY键的选择直接影响查询性能——高频过滤条件放在前面,范围查询字段放在后面。
稀疏索引与跳数索引优化
ClickHouse的稀疏索引(primary index)每8192行记录一个索引标记(index_granularity),相比B+Tree的密集索引,内存占用极小但精度较低。跳数索引(skipping index)是二级索引,在稀疏索引基础上提供额外的过滤能力。
CREATE TABLE user_events
(
user_id UInt32,
event_name LowCardinality(String),
event_data String,
ip_address IPv4,
created_at DateTime,
-- minmax跳数索引:适合范围查询
INDEX idx_time created_at TYPE minmax GRANULARITY 4,
-- set跳数索引:适合等值查询
INDEX idx_event event_name TYPE set(1000) GRANULARITY 4,
-- bloom filter:适合高基数等值查询
INDEX idx_ip ip_address TYPE bloom_filter(0.01) GRANULARITY 4,
-- ngrambf:适合LIKE模糊查询
INDEX idx_data event_data TYPE ngrambf_v1(3, 256, 2, 0) GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(created_at)
ORDER BY (created_at, user_id)
SETTINGS index_granularity = 8192;
跳数索引的GRANULARITY参数控制索引粒度——值为N表示每N个granule(8192*N行)生成一个索引标记。值越小索引越精确但内存占用越大,通常设为1-4。
实时聚合方案:物化视图与AggregatingMergeTree
ClickHouse的物化视图在数据写入时自动触发预聚合,将原始数据按指定维度聚合后写入目标表。结合AggregatingMergeTree引擎,可以在写入时完成聚合计算,查询时直接读取预计算结果。
-- 原始事件表
CREATE TABLE events_raw
(
user_id UInt32,
event_type LowCardinality(String),
price Decimal(10,2),
created_at DateTime
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (created_at, user_id);
-- 聚合目标表
CREATE TABLE events_daily_agg
(
dt Date,
user_id UInt32,
event_type LowCardinality(String),
total_price AggregateFunction(sum, Decimal(10,2)),
event_count AggregateFunction(count, UInt32),
uniq_users AggregateFunction(uniq, UInt32)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(dt)
ORDER BY (dt, user_id, event_type);
-- 物化视图:写入时自动聚合
CREATE MATERIALIZED VIEW events_daily_mv
TO events_daily_agg
AS
SELECT
toDate(created_at) AS dt,
user_id,
event_type,
sumState(price) AS total_price,
countState() AS event_count,
uniqState(user_id) AS uniq_users
FROM events_raw
GROUP BY dt, user_id, event_type;
-- 查询预聚合结果
SELECT
dt,
event_type,
sumMerge(total_price) AS total_price,
countMerge(event_count) AS event_count,
uniqMerge(uniq_users) AS uniq_users
FROM events_daily_agg
WHERE dt BETWEEN '2026-08-01' AND '2026-08-21'
GROUP BY dt, event_type
ORDER BY dt, event_type;
查询优化实战技巧
ClickHouse的性能高度依赖查询写法。以下是实际项目中常见的优化策略。
-- 1. 避免SELECT *,只查需要的列
-- ❌ 慢:读取所有列
SELECT * FROM events WHERE created_at > '2026-08-01';
-- ✅ 快:只读取3列
SELECT user_id, event_type, created_at
FROM events WHERE created_at > '2026-08-01';
-- 2. 利用分区裁剪减少扫描量
-- ✅ 带分区过滤条件
SELECT count(), sum(price)
FROM orders
WHERE created_at >= '2026-08-01' AND created_at < '2026-09-01'
AND status = 'paid';
-- 3. 使用PREWHERE提前过滤(比WHERE更早执行)
SELECT user_id, count()
FROM events
PREWHERE event_type = 'click'
WHERE created_at > '2026-08-01'
GROUP BY user_id
ORDER BY count() DESC
LIMIT 100;
-- 4. 大IN查询替换为JOIN
-- ❌ 慢:IN列表过大
SELECT * FROM orders WHERE user_id IN (SELECT user_id FROM vip_users);
-- ✅ 快:使用JOIN
SELECT o.*
FROM orders o
INNER JOIN vip_users v ON o.user_id = v.user_id;
-- 5. 使用字典加速维度查询
CREATE DICTIONARY product_dict
(
product_id UInt32,
product_name String,
category LowCardinality(String)
)
PRIMARY KEY product_id
SOURCE(CLICKHOUSE(TABLE 'products' DB 'default'))
LAYOUT(FLAT())
LIFETIME(MIN 300 MAX 600);
SELECT
dictGet('product_dict', 'product_name', product_id) AS name,
dictGet('product_dict', 'category', product_id) AS category,
sum(price)
FROM orders
GROUP BY name, category;
ClickHouse的列式存储和向量化执行引擎为OLAP场景提供了极致的查询性能,MergeTree引擎的分区和排序设计适配时间序列数据的典型访问模式。物化视图实现写入时预聚合,将实时分析从查询时计算转变为写入时计算,查询延迟从分钟级降低到毫秒级。跳数索引和字典表为特定查询模式提供了针对性的优化手段,PREWHERE和分区裁剪是最基本也最有效的查询优化手段。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/clickhouse-lie-shi-cun-chu-yin-qing-jia-gou-yu-shi-shi-olap/