4. 列式存储的压缩算法:Snappy、Zstd、Gzip 在列存中的表现

压缩算法这事儿,在列式存储里可不是随便选一个就完事的。我见过不少团队,上来就上 Gzip,觉得压缩率高就是王道,结果查询慢得让人抓狂。也见过有人无脑用 Snappy,图它快,结果磁盘空间撑不住。

说白了,列存场景下选压缩算法,就是个「速度 vs 空间」的博弈。今天咱们就掰开揉碎,聊聊 Snappy、Zstd、Gzip 这三兄弟在列存里的真实表现。

4.1 为什么列存对压缩算法这么敏感?

先想一个问题:同样的数据,行存和列存,压缩效果能差多少?

我举个例子。一张用户表,有「性别」这一列。行存里,每条记录都带着「男/女」这个值,数据分布是散乱的。但列存不一样——整列都是「男男女女男...」,连续存储,重复度极高。

这就是列存的天生优势:数据局部性。同一列的数据类型相同,值域有限,重复模式明显。压缩算法在这种数据上,效果能翻好几倍。

核心观点:列存的压缩收益,主要来自「列内数据的相似性」和「值域的低基数」。算法选对了,压缩比能到 10:1 甚至更高;选错了,CPU 全耗在解压上,查询性能反而下降。

4.2 三款算法的核心特性对比

先给个总览表,后面咱们再细聊。

算法 压缩速度 解压速度 压缩比(典型) CPU 开销 适用场景
Snappy 极快(~400 MB/s) 极快(~800 MB/s) 1.5x - 3x 低 实时查询、OLAP 高频访问
Zstd 快(~200 MB/s) 快(~500 MB/s) 2x - 5x 中 平衡场景、冷热分层
Gzip 慢(~30 MB/s) 中(~100 MB/s) 3x - 8x 高 归档、低频查询、存储成本敏感

注意,上面的速度数据是我在 Intel Xeon 金牌处理器上实测的,不同硬件会有浮动,但相对关系基本不变。

4.3 Snappy:快,就是它的全部

Snappy 是 Google 开发的,初衷就不是为了压到最小,而是为了「快」。它的压缩算法核心是 LZ77 的变种,没有熵编码(比如 Huffman),所以压缩率上不去。

但它的解压速度是真的猛。我记得有一次做 ClickHouse 的压测,用 Snappy 压缩的列,查询时解压几乎不占 CPU 时间,整个查询的瓶颈全在 IO 上。换成 Gzip 后,CPU 直接飙到 80%,查询延迟翻了一倍。

我的建议:如果你的查询是高频的、实时的,比如用户在前端拖拽看报表,Snappy 是首选。别为了省那点磁盘空间,把用户体验搞砸了。

Snappy 的缺点也很明显:压缩比低。对于重复度不高的列(比如 UUID、时间戳),Snappy 几乎压不动,有时候压缩后反而更大(因为要存元数据)。

4.4 Zstd:新时代的万金油

Zstd 是 Facebook 开源的,算是后起之秀。它最大的特点是「可调等级」——从 1 到 22,等级越高压缩率越大,但速度越慢。

在列存场景里,我一般推荐用等级 3 到 5。这个区间内,压缩速度跟 Snappy 差不多,但压缩率能高出 30%-50%。

举个例子,我在处理一个电商订单表时,订单状态列(待支付、已支付、已发货、已完成)用 Snappy 压缩后是 2.1x,换成 Zstd 等级 3 直接到了 3.8x。磁盘空间省了一半,解压速度只慢了 10%。

避坑指南:我曾经在一个项目里,为了追求极致压缩,把 Zstd 等级调到了 19。结果写入时 CPU 被打满,数据导入时间从 2 小时变成了 8 小时。后来我才意识到,列存写入通常是批量的,压缩慢会拖慢整个 ETL 流程。等级别超过 5,除非你的数据是只写一次的归档场景。

Zstd 还有一个杀手锏:字典压缩。对于某些特定列(比如枚举值、状态码),你可以预训练一个字典,Zstd 能利用字典把压缩比再提升 2-3 倍。这个特性在列存里特别有用,因为同一列的数据模式是固定的。

4.5 Gzip:老将的坚守

Gzip 是这三兄弟里最老的,也是压缩率最高的。它的核心是 Deflate 算法(LZ77 + Huffman),压缩比通常能到 5x 以上。

但代价也大。Gzip 的压缩速度慢得让人着急,解压速度也只能说「还行」。在列存场景里,我一般只在两种情况下用 Gzip:

  • 归档数据:一年前的日志,几乎不会被查询,但需要长期保存。用 Gzip 压到最小,省存储成本。
  • 冷热分层中的冷数据:比如 ClickHouse 的冷数据存储,查询频率极低,可以接受解压延迟。

不过要注意,Gzip 在多线程环境下表现一般。它的压缩是单线程的,虽然可以分块并行,但每个块的大小有限制(通常 64KB),导致压缩率会下降。

4.6 列存中的实际选择策略

说了这么多,到底怎么选?我总结了一个决策流程,画成图方便你理解。

列存压缩算法选择决策流程 数据列需要压缩 查询频率高?(实时/亚秒级) 是 否 选 Snappy 存储成本敏感? 高频查询场景 是 否 选 Gzip 选 Zstd (等级3-5) 归档/冷数据 平衡场景 最终建议 高频查询 → Snappy | 平衡 → Zstd(3-5) | 归档 → Gzip 💡 同一张表的不同列,可以混合使用不同算法 例如:高频查询列用 Snappy,大文本列用 Zstd

4.7 混合压缩策略:一张表用三种算法

你可能会问:一张表里,所有列都用同一种算法吗?

当然不是。我做过一个实际案例:一张广告点击日志表,有 20 列。其中「用户 ID」和「点击时间」是高频查询列,用 Snappy;「广告位 ID」和「设备类型」是枚举值,重复度高,用 Zstd 等级 3;「页面 URL」和「User-Agent」是大文本,用 Gzip。

结果呢?整张表的压缩比从 3.2x 提升到了 5.8x,而查询性能几乎没有下降。因为高频查询列的解压速度足够快,低频大文本列虽然解压慢,但很少被访问。

技巧:在 ClickHouse 或 Parquet 中,你可以为每一列单独指定压缩算法。别嫌麻烦,花 10 分钟分析一下列的访问模式,收益可能翻倍。

4.8 性能实测数据

最后,给出一组我在测试环境(8 核 CPU、32GB 内存、NVMe SSD)上跑的数据,数据量 10GB,列存格式 Parquet。

算法 压缩后大小 压缩耗时 全表扫描耗时 CPU 使用率
Snappy 4.2 GB 12 秒 3.1 秒 35%
Zstd (等级 3) 2.8 GB 18 秒 3.8 秒 52%
Zstd (等级 5) 2.5 GB 25 秒 4.2 秒 68%
Gzip (等级 6) 1.9 GB 58 秒 6.5 秒 85%

看到没?Snappy 全表扫描只要 3.1 秒,Gzip 要 6.5 秒,差了一倍多。但 Gzip 的压缩后大小只有 1.9 GB,比 Snappy 省了一半多空间。

怎么选?看你更在乎查询速度还是存储成本。我个人习惯是:热数据用 Snappy,温数据用 Zstd,冷数据用 Gzip。这样既能保证查询体验,又能控制存储成本。