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

压缩过的 JSON 与 Protobuf:谁更小,什么时候该换格式

大家常说“换 Protobuf 就小了”。压缩之后还是这样吗?这篇逐字段拆开看谁赢了哪一列,再把数据模型的改动和格式的改动分开量,看收益到底来自哪里。

855401 对 970854
逐列压缩后合计:JSON 与 Protobuf(2 万条 products,zstd-3)
25.1% 对 11.7%
改数据模型让新 Protobuf 压缩后小的比例,与新模型下 JSON 换成 Protobuf 的收益(zstd-3)
703321 字节
改了数据模型再按列排的 JSON 用 zstd-19 压缩后的大小(原始 JSON Lines 的 9.7%)

Protobuf 赢在哪几列

实测 · 本机执行输出
  字段              JSON 原    →zstd  MsgPack 原    →zstd  Protobuf 原    →zstd   谁压缩后最小
  id              160001     8886     100003    62871       60004    44014   JSON
  url            1135001    11989    1115003    12386     1115000    12248   JSON
  title          1223717   413803    1203160   436587     1203716   436512   JSON
  price           118460    49891     180003    55116      160004    63401   JSON
  currency        120001     6301      80003     5942      100000     6125   MsgPack
  stock            75643    31332      44645    29661       34860    26799   Protobuf
  rating           80001    20284     180003    31267      160004    30526   JSON
  tags            772005    99219     652007   127291      712004   128992   JSON
  category        451749    35091     411751    35938      431748    36111   JSON
  updated_at      460001   100143     420003    99373      440000    99867   MsgPack
  seller_id        77314    33137      50477    32279       36844    29016   Protobuf
  seller_name     280281    45325     240283    56549      260280    57243   JSON
  各列压缩后合计:JSON 855401、MessagePack 985260、Protobuf 970854

逐列比较,Protobuf 只在 stock、seller_id 这类小整数列上压缩后最小;id 这类连续整数,十进制文本反而被压得很干净(JSON 8886 字节,Protobuf 44014 字节);字符串列 JSON 略优(第 3 篇讲过长度前缀的原因)。

先改数据模型

实测 · 本机执行输出
  一条记录:原模型 JSON 358 字节、新模型 JSON 240 字节;原模型 Protobuf 241 字节、新模型 Protobuf 93 字节
  新模型示例:{"id":1000000,"page":0,"title":"wico biwa wasu kosekefo julomesa zanisa wegiraka","price_cents":11340,"currency":0,"stock":249,"rating10":11,"tags":[53,24,27],"category":19,"updated":1790893697,"seller_id":739,"seller_name":"shop-kujimuge"}

价格存整数分、评分存整数、分类和标签存编号、时间存 Unix 秒、去掉可以由别的字段推出的 url:单条 JSON 从 358 字节降到 240 字节,Protobuf 从 241 降到 93。这些改动与格式无关,JSON 同样受益。

收益来自哪里

实测 · 本机执行输出
  组合                            原始    zstd-3   zstd-19    gzip-6
  原模型 JSON Lines           7214161   1331194   1047098   1484671
  原模型 Protobuf             4914350   1378051   1081117   1449113
  新模型 JSON Lines           4955536   1169428    927076   1258389
  新模型 Protobuf             2106013   1032452    918732   1042267
  换数据模型的收益(zstd-3):JSON 12.2%、Protobuf 25.1%;换格式的收益(新模型,JSON → Protobuf):原始 57.5%、zstd-3 +11.7%

zstd-3 压缩后,改数据模型让 JSON 小 12.2%、Protobuf 小 25.1%;在新模型下 JSON 换成 Protobuf 只小 11.7%(zstd-19 下只小 0.9%)。换格式的收益比数据模型小。

下限

实测 · 本机执行输出
  2 万个 title 用换行拼起来 1183715 字节,zstd-3 409620、zstd-19 364005;占新模型 JSON zstd-3 总量 1169428 的 35.0%
  去掉 title 以后:新模型 JSON 634906、新模型 Protobuf 526241(zstd-3 字节),title 单独压缩 409620

title 是随机拼的词,单独压缩要 409620 字节(zstd-3),占新模型 JSON 压缩后总量的 35%。内容本身的熵决定下限,换格式降不下来。

单条

实测 · 本机执行输出
  组合                      不压缩   zstd-3   zstd-3+字典
  原模型 JSON              359.6    288.6       109.1
  原模型 Protobuf          242.6    232.7       106.5
  新模型 JSON              246.7    199.9        95.3
  新模型 Protobuf          103.1    112.1        79.3

新模型 Protobuf 单条 103.1 字节,无字典 zstd-3 反而是 112.1;加字典 79.3 字节。数据模型优化和字典叠加后,单条从 359.6 字节降到 79.3 字节。

综合

实测 · 本机执行输出
  新模型 列式 JSON            原始   2555549  zstd-3    782551  zstd-19    703321  gzip-6    802876
  原模型 列式 JSON            原始   4954174  zstd-3    864712  zstd-19    745186  gzip-6    919905
  新模型 Protobuf           原始   2106013  zstd-3   1032452  zstd-19    918732  gzip-6   1042267
  新模型 JSON Lines         原始   4955536  zstd-3   1169428  zstd-19    927076  gzip-6   1258389
  原模型 JSON Lines         原始   7214161  zstd-3   1331194  zstd-19   1047098  gzip-6   1484671
  原模型 Protobuf           原始   4914350  zstd-3   1378051  zstd-19   1081117  gzip-6   1449113

把数据模型改好、按列排布、再用 zstd-19 压缩的 JSON(703321 字节)比任何 Protobuf 变体都小。格式不是体积的决定因素,数据本身的表示、排列和压缩级别才是。如果你要的是类型安全、版本演进、跨语言,Protobuf 仍有它的价值(第 5、6 篇),只是不要指望它自己带来多大的压缩收益。

分步演示

数据模型
本步的对象

    本文用到的工具与边界

    • 本机 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,取多次中位数,数字每次运行会有小幅抖动。protobuf 7.36.2、msgpack 1.2.3,zstd 级别 3 和 19;新数据模型是我手工设计的(每一步转换是否可逆做了校验,url 由 id 和 page 还原这一条逐条断言);“列式 JSON”每列一个 JSON 数组按列拼接;单条测试的字典沿用第 17 篇的方法;所有数字都是确定性的字节数
    • 没有验证:对 Protobuf 使用 sint、fixed、packed、枚举等更多编码特性的进一步优化(新模型已经用了整数和 repeated 整数);其它数据集上的结果(timeseries 和 nested 在第 19 篇);Parquet 等真正的列存格式;数据模型优化后的可读性与演进成本
    京ICP备2026067502号-1京公网安备11010502063487号