bigywater.tech
数据编码与压缩 · 17

zstd 字典:几百字节的小记录为什么能压到原来的零头

几百字节的小记录怎么压?单独压缩几乎没效果,攒批又要延迟。zstd 字典把“记录之间共有的内容”提前交给压缩器。这篇量字典大小、样本数、记录大小、泛化、过期和使用代价。

1.25× 对 3.30×
单条 360 字节的 products 记录:zstd-3 无字典与有 16 KiB 字典的压缩比
0.86× 对 1.75×
54 字节的 timeseries 一行:无字典(反而变大)与有字典
115.6 对 148.8
数据漂移后每条字节:重新训练的字典与旧字典

为什么小对象压不动

实测 · 本机执行输出
  方式                              2000 条共     平均每条     压缩比
  不压缩(紧凑 JSON)                     719257    359.6   1.00×
  gzip-6                           548649    274.3   1.31×
  zstd-3,无字典                       577103    288.6   1.25×
  zstd-3,16 KiB 字典                 218147    109.1   3.30×
  zstd-19,16 KiB 字典                213615    106.8   3.37×
  对照:每 100 条攒成一批、无字典 zstd-3:174706 字节(平均每条 87.4,4.12×)
  解压校验(200 条):全部一致

压缩器靠“前面出现过什么”。单条记录压缩时,前面什么都没有,记录自己的键名和公共前缀白白占字节(360 字节只压到 288.6 字节)。54 字节的行甚至压缩后更大(62.9 字节)。

字典做了什么

实测 · 本机执行输出
      字典大小      样本 100      样本 500     样本 2000     样本 5000
       1KiB       132.6       131.2       130.3       130.4   (平均每条字节)
       4KiB       122.6       120.7       120.6       120.6   (平均每条字节)
      16KiB       113.1       109.7       109.7       109.1   (平均每条字节)
      64KiB       114.6       100.9        99.6        98.8   (平均每条字节)
     110KiB       114.6       100.6        97.4        97.6   (平均每条字节)

字典是一段训练出来的样本片段(以及预先算好的熵编码表),压缩和解压时当作“已经见过的内容”。单条 products 记录从 288.6 字节降到 109.1 字节(3.30×),接近攒 100 条一批的 87.4 字节/条。

字典要多大

实测 · 本机执行输出
  记录类型                           平均大小      无字典      有字典      无字典压缩比      有字典压缩比
  timeseries 一行                    54     62.9     30.7       0.86×       1.75×
  products 一条                     361    289.6    110.8       1.25×       3.26×
  nested 一个文档                    1852    429.3    265.0       4.31×       6.99×
  pages 一页(13 KiB)              13904   4012.2   3182.0       3.47×       4.37×

1 KiB 的字典就能降到约 130 字节/条;16 KiB 109.1;64 KiB 98.8;收益递减。样本太少时大字典没有用:100 条样本训练 64 KiB 字典是 114.6,500 条样本是 100.9。

适合什么样的数据

实测 · 本机执行输出
  字典共 16384 字节,字典 ID 1516849344;开头 4 字节魔数 37a430ec;后面先是熵编码的表(Huffman/FSE 表),最后是一段样本里常见的原始内容
  字典最后 300 字节(原始内容部分):'guwa"],"category":"fecuraka/gisezamu/hajifudu","updated_at":"2026-10-09T02:51:16Z","seller":{"id":644,"name":"shop-pola"}}{"id":1000024,"url":"https://shop.example.com/item/1000024?ref=list&page=24","title":"rebohofa teci jiwaji neze bekufu geda saru munu ritabina","price":122.58,"currency":"USD","s'
  用字典压缩的帧头里记录了字典 ID:1516849344(与字典自己的 ID 1516849344 一致);无字典压缩的帧头 dict_id = 0

记录越小收益越大(54 字节:0.86× 到 1.75×;361 字节:1.25× 到 3.26×;13904 字节:3.47× 到 4.37×)。字典只对训练样本相似的数据有效:用 products 的字典压 timeseries 几乎没效果(59.8 对 30.0)。

过期

实测 · 本机执行输出
  products 字典压 timeseries 一行:无字典 62.9 字节/条,products 字典 59.8 字节/条,自己的字典 30.0 字节/条
  数据漂移(换了词表、新增 brand 和 warehouse 字段)的 products:无字典 313.0 字节/条,旧字典 148.8 字节/条;而同样字段不漂移的测试集:无字典 288.4,旧字典 109.0
  用漂移后的数据重新训练的字典(新数据 500 条 + 少量旧数据):漂移数据 115.6 字节/条

数据的形态变了(词表变、字段增加),旧字典就变差(148.8 对未漂移的 109.0)。要定期用新数据重新训练,新字典带新的 ID,旧数据要用旧字典解。

代价

实测 · 本机执行输出
  无字典:压缩 500 条单条记录 中位 1.8 ms(平均每条 4 微秒);解压 500 条 中位 0.7 ms(平均每条 1 微秒)
  16 KiB 字典:压缩 500 条单条记录 中位 0.6 ms(平均每条 1 微秒);解压 500 条 中位 0.3 ms(平均每条 1 微秒)
  不带字典的解压器解压带字典的帧:失败:ZstdError: decompression error: Dictionary mismatch
  带另一份字典的解压器解压带字典的帧:失败:ZstdError: decompression error: Dictionary mismatch

解压端必须有同一份字典,帧头里的字典 ID 让它知道要哪一份;不匹配就报 Dictionary mismatch。字典要和数据一起保管、版本化。

分步演示

zstd 字典
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8(系统 Python,自带 compression.zstd)/ zstandard 0.25.0 / lz4 4.4.5 / zstd、lz4、brotli、protoc 命令行(Homebrew);msgpack、protobuf、snappy 等第三方库临时装在临时目录里,用完删除;所有数据都是本机脚本生成的合成数据(固定随机种子),没有使用任何真实网站或用户的数据;计时用 time.perf_counter,取多次中位数,数字每次运行会有小幅抖动。zstandard 0.25.0 的 train_dictionary(训练集和测试集不重叠:训练用前 5000 条或前 1000–1500 条,测试用后面的记录);“漂移数据”是我构造的(换词表、加字段);时间是 5 次中位数;字典内容的说明(熵编码表在前)是推测
    • 没有验证:字典内容部分的格式(没有解析字典文件);字典训练算法(COVER、fastCover)的参数调优;在不同 zstd 级别下字典的收益变化(只测了 3 和 19);大量不同字典同时使用的内存占用;字典的预加载(ZstdCompressionDict.precompute_compress)对速度的影响;带字典后每条记录的压缩速度比无字典更快的原因
    京ICP备2026067502号-1京公网安备11010502063487号