在 ClickHouse 中,压缩率和查询速度往往是正相关的。理解这一点,才能做出正确的 Codec 选择。
ClickHouse 为什么越压缩越快
传统认知:压缩 = 用 CPU 换磁盘空间。但在分析型数据库中,瓶颈通常是 磁盘 IO,而不是 CPU。
一次典型查询的数据流:
磁盘 → [读取压缩块] → 内存 → [解压] → CPU 计算
~~~~~~~~~~~ ~~~~
瓶颈在这里 这里很快
假设一列原始大小 1GB,压缩后 100MB:
- 不压缩:从磁盘读 1GB,耗时 ~3s(SSD 顺序读 ~400MB/s)
- LZ4 压缩:从磁盘读 100MB(~0.3s),解压耗时 ~0.1s(LZ4 解压速度 ~4GB/s)
总耗时从 3s 降到 0.4s。 压缩让需要从磁盘搬运的数据量减少了 10 倍,而解压的 CPU 开销远小于省下的 IO 时间。
这就是核心公式:
查询耗时 ≈ 压缩后数据量 / 磁盘带宽 + 原始数据量 / 解压速度
当 解压速度 >> 磁盘带宽 时,压缩率越高,查询越快。
LZ4 vs ZSTD 的本质差异
ClickHouse 默认使用 LZ4,同时支持 ZSTD。两者的定位完全不同:
| 维度 | LZ4 | ZSTD(level) |
|---|---|---|
| 设计目标 | 极致解压速度 | 高压缩率 |
| 解压速度 | ~4 GB/s | ~1.5 GB/s(level 1) |
| 压缩率(典型) | 2-4x | 3-8x |
| 压缩速度 | 快 | 中等(level 越高越慢) |
| CPU 开销 | 低 | 中 |
选择原则:
频繁查询的热数据 → LZ4(解压快,查询延迟低)
冷数据 / 归档数据 → ZSTD(3)(压缩率高,省存储)
写入密集场景 → LZ4(压缩速度快,不拖慢写入)
ZSTD 的 level 参数(1-22)控制压缩率与速度的权衡。生产环境建议 ZSTD(1) 到 ZSTD(3),更高的 level 压缩率提升有限但 CPU 开销显著增加。
不同字段类型的压缩策略
ClickHouse 支持在通用压缩(LZ4/ZSTD)之前叠加一层预处理编码,针对数据特征做变换,让后续压缩更高效。
时间戳 / 单调递增字段
sql
timestamp DateTime64(3) CODEC(DoubleDelta, LZ4)
DoubleDelta 对单调递增数据做二阶差分:
原始值: 1000, 1001, 1002, 1003, 1004
一阶差分: 1, 1, 1, 1 ← Delta
二阶差分: 0, 0, 0 ← DoubleDelta,几乎全是 0
全是 0 的数据,LZ4 可以压缩到接近于零。时间戳列使用 DoubleDelta 通常能达到 20-50 倍压缩率。
整数度量字段(duration、count、status_code)
sql
duration_us UInt64 CODEC(T64, LZ4)
status_code UInt16 CODEC(T64, LZ4)
T64 将 64 个值组成一个块,找到这些值的有效位宽,只存储有效位。例如 status_code 的值域是 200-599,只需要 10 bit 而不是 16 bit。
traceId / UUID / 随机字符串
sql
trace_id String CODEC(ZSTD(1))
request_id UUID CODEC(ZSTD(1))
随机数据没有规律可循,预处理编码(Delta、DoubleDelta、T64)对它们无效。直接用 ZSTD 获取最大压缩率是最优解。不要对随机字符串使用 LZ4------压缩率太低,浪费 IO。
IP 地址
sql
client_ip IPv4 CODEC(T64, LZ4)
IPv4 本质是 UInt32,同一服务的客户端 IP 通常集中在少数网段,T64 能有效压缩。
低基数字符串
sql
service_name LowCardinality(String) -- 字典编码本身就是压缩
LowCardinality 已经将字符串转为整数 ID,额外的 Codec 收益有限。使用默认 LZ4 即可。
压缩比、CPU、IO 三者的权衡
不存在万能的 Codec 组合。选择取决于你的硬件瓶颈:
高压缩率
▲
│
ZSTD(3) │ ZSTD(1)
│
─────────────┼──────────── → 低 CPU
│
LZ4 │
│
低压缩率
决策矩阵:
| 场景 | 磁盘类型 | 推荐 Codec | 理由 |
|---|---|---|---|
| 实时分析(P99 < 1s) | NVMe SSD | LZ4 | IO 不是瓶颈,优先解压速度 |
| 日志存储(海量数据) | HDD / S3 | ZSTD(1) | IO 是瓶颈,压缩率优先 |
| 混合负载 | SSD | 按列选择 | 热列 LZ4,冷列 ZSTD |
| 写入密集(>100万行/s) | 任意 | LZ4 | 压缩速度快,不拖慢写入 |
如何验证压缩效果
sql
SELECT
column,
type,
formatReadableSize(data_compressed_bytes) AS compressed,
formatReadableSize(data_uncompressed_bytes) AS uncompressed,
round(data_uncompressed_bytes / data_compressed_bytes, 2) AS ratio
FROM system.columns
WHERE database = 'default' AND table = 'traces'
ORDER BY data_compressed_bytes DESC;
示例输出:
| column | type | compressed | uncompressed | ratio |
|---|---|---|---|---|
| attributes | Map(String,String) | 2.31 GiB | 18.5 GiB | 8.01 |
| trace_id | String | 1.82 GiB | 4.66 GiB | 2.56 |
| timestamp | DateTime64(3) | 42.3 MiB | 3.73 GiB | 90.2 |
| duration_us | UInt64 | 89.1 MiB | 3.73 GiB | 42.8 |
| service_name | LowCardinality(String) | 12.4 MiB | 3.73 GiB | 307 |
| status_code | UInt16 | 8.71 MiB | 932 MiB | 107 |
timestamp 用 DoubleDelta 达到 90 倍压缩,service_name 用 LowCardinality 达到 307 倍------这些数字直接决定了查询时需要读取的数据量。
生产环境 Codec 推荐组合
基于实际生产经验,以下是按字段类型的推荐 Codec:
| 字段类型 | 示例 | 推荐 Codec | 预期压缩率 |
|---|---|---|---|
| 时间戳 | timestamp, created_at | DoubleDelta, LZ4 |
20-100x |
| 单调递增 ID | auto_increment_id | Delta, LZ4 |
10-50x |
| 小范围整数 | status_code, error_code | T64, LZ4 |
50-200x |
| 度量值 | duration, latency, count | T64, LZ4 |
10-50x |
| 随机字符串 | trace_id, uuid | ZSTD(1) |
2-4x |
| 低基数字符串 | service, region, env | LowCardinality + 默认 |
100-500x |
| IP 地址 | client_ip, server_ip | T64, LZ4 |
20-50x |
| JSON / Map | attributes, tags | ZSTD(1) |
5-10x |
| 浮点数 | temperature, score | Gorilla, LZ4 |
5-20x |
完整建表示例:
sql
CREATE TABLE traces
(
timestamp DateTime64(3) CODEC(DoubleDelta, LZ4),
service_name LowCardinality(String),
env LowCardinality(String),
status_code UInt16 CODEC(T64, LZ4),
trace_id String CODEC(ZSTD(1)),
span_id String CODEC(ZSTD(1)),
parent_span_id String CODEC(ZSTD(1)),
duration_us UInt64 CODEC(T64, LZ4),
http_method LowCardinality(String),
http_url String CODEC(ZSTD(1)),
client_ip IPv4 CODEC(T64, LZ4),
attributes Map(String, String) CODEC(ZSTD(1))
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (service_name, env, status_code, timestamp, trace_id)
TTL timestamp + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
修改已有表的 Codec
sql
-- 修改单列 Codec
ALTER TABLE traces MODIFY COLUMN duration_us CODEC(T64, LZ4);
-- 修改后需要触发数据重写才能生效
ALTER TABLE traces UPDATE duration_us = duration_us WHERE 1;
-- 或者等待自然 merge
OPTIMIZE TABLE traces FINAL;
注意:
OPTIMIZE TABLE FINAL会触发全表 merge,在大表上可能耗时数小时且占用大量 IO。生产环境建议在低峰期执行,或依赖后台自然 merge 逐步生效。
总结
压缩策略的核心思路:
- 先理解数据特征------单调递增?低基数?随机?
- 选择合适的预处理编码------DoubleDelta、T64、Gorilla 针对不同模式
- 叠加通用压缩------热数据 LZ4,冷数据 ZSTD
- 用
system.columns验证效果------数字说话,不要猜
记住:在 ClickHouse 中,好的压缩 = 更少的 IO = 更快的查询。这不是存储优化,这是查询优化。
下一篇我们将探讨 ClickHouse 的写入机制与 MergeTree 的 merge 策略。