ClickHouse系列(四):压缩不是为了省磁盘,而是为了更快的查询

在 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 逐步生效。

总结

压缩策略的核心思路:

  1. 先理解数据特征------单调递增?低基数?随机?
  2. 选择合适的预处理编码------DoubleDelta、T64、Gorilla 针对不同模式
  3. 叠加通用压缩------热数据 LZ4,冷数据 ZSTD
  4. system.columns 验证效果------数字说话,不要猜

记住:在 ClickHouse 中,好的压缩 = 更少的 IO = 更快的查询。这不是存储优化,这是查询优化。

下一篇我们将探讨 ClickHouse 的写入机制与 MergeTree 的 merge 策略。

相关推荐
刘~浪地球2 小时前
Redis 从入门到精通(十四):内存管理与淘汰策略
数据库·redis·缓存
海边的Kurisu2 小时前
MySQL | 从SQL到数据的完整路径
数据库·mysql·架构
hINs IONN2 小时前
maven导入spring框架
数据库·spring·maven
CV艺术家2 小时前
mysql数据迁移到达梦数据库
java·数据库
枫叶林FYL2 小时前
【自然语言处理 NLP】工具学习与Agent架构:从函数调用到多智能体协作
数据库
不愿透露姓名的大鹏2 小时前
Oracle Undo空间爆满急救指南(含在线切换+更优方案+避坑指南)
linux·运维·数据库·oracle
做个文艺程序员2 小时前
MySQL 主从延迟根因诊断法
数据库·mysql
计算机安禾3 小时前
【数据结构与算法】第33篇:交换排序(二):快速排序
c语言·开发语言·数据结构·数据库·算法·矩阵·排序算法
l1t3 小时前
测试clickhouse 26.3的新功能
数据库·clickhouse