很多场景只要一条记录或一个字段,却要把整批数据都解开。这篇量一量各格式点查、扫一列的代价,JSON 字节级扫描的陷阱,以及压缩分块大小怎么在压缩比和随机访问之间取舍。
JSON 数组(整批 json.loads) 每次查询 42.8016 ms(每次要处理约 7045.1 KiB 的输入) JSON 数组(整批 orjson.loads) 每次查询 21.6310 ms(每次要处理约 7045.1 KiB 的输入) JSON Lines + 行偏移索引 每次查询 0.0030 ms(每次要处理约 0.4 KiB 的输入) MessagePack 数组(整批 unpackb) 每次查询 31.1409 ms(每次要处理约 6208.3 KiB 的输入) MessagePack 逐条 + 偏移索引 每次查询 0.0017 ms(每次要处理约 0.3 KiB 的输入) Protobuf Batch(整批解析) 每次查询 3.7981 ms(每次要处理约 4799.2 KiB 的输入) Protobuf 逐条 + 偏移索引 每次查询 0.0011 ms(每次要处理约 0.2 KiB 的输入)
如果每次都要把整个数组解开才能取一条,哪种格式都是几毫秒到几十毫秒;如果每条记录单独存放并记下偏移,取一条只处理几百字节,0.001–0.003 ms。差距来自存放方式,不是格式。
json.loads 后求和 中位 42.8 ms(42.2–43.7),求和 1105543.79 ✓ orjson.loads 后求和 中位 21.0 ms(20.5–21.5),求和 1105543.79 ✓ msgpack.unpackb 后求和 中位 32.1 ms(31.2–37.5),求和 1105543.79 ✓ Protobuf 完整 schema(11 个字段) 中位 6.3 ms(6.3–6.3),求和 1105543.79 ✓ Protobuf 只声明 price 一个字段的 schema 中位 3.9 ms(3.9–4.0),求和 1105543.79 ✓ JSON Lines 上用正则直接扫 "price": 中位 6.9 ms(6.8–6.9),求和 1105543.79 ✓ 正确的和是 1105543.79
Protobuf 访问单个字段不用把整条记录转成 Python 对象,求 price 的和 3.9–6.3 ms,JSON 要 21.0–42.8 ms。正则直接在文本上扫是 6.9 ms,但它依赖固定的写法。
同样的数据用 json.dumps 默认写法(冒号后面有空格):正则 "price":([0-9.]+) 扫出 0 个,和 0.00(真实和 1105543.79,共 20000 条) 每 100 条里有 1 条的 seller 对象里也有一个 price 键(值 1.0,共 200 条):正则的和 1105743.79,顶层 price 的真实和 1105543.79,多算 200.00(正好是嵌套的 price 之和) title 里含有 "price":999 这样的文本时,json.dumps 会把引号转义成 \",所以正则 没有被骗:找到 1 个 price(正确是 1 个)
正则 "price": 只对紧凑写法(冒号后没空格)有效,换成 json.dumps 默认写法一个都扫不到;对象里嵌套了同名的键就会多算。字符串里的引号会被转义,所以字符串内容本身骗不了它。
schema 里声明 1 个字段:中位 4.0 ms(4.0–4.1) schema 里声明 3 个字段:中位 4.3 ms(4.3–4.4) schema 里声明 6 个字段:中位 4.6 ms(4.6–4.9) schema 里声明 11 个字段:中位 6.4 ms(6.3–6.4) 投影 schema 解析后再序列化:4914350 字节(原 4914350 字节)——未声明的字段作为未知字段全部保留
Protobuf 解析时可以用只声明部分字段的 schema,未声明的字段作为未知字段跳过(并保留,重新序列化不丢)。声明越少越快,但解析本身的开销已经很小,差距有限。
每块条数 压缩后总字节 压缩比 点查中位耗时
1 5771649 1.25× 0.0044 ms
10 2472557 2.92× 0.0084 ms
100 1746783 4.13× 0.0425 ms
1000 1421724 5.07× 0.3583 ms
20000 1331194 5.42× 6.9427 ms压缩后想随机访问就得分块压缩:每块 1 条压缩比只有 1.25×,每块 1000 条 5.07×,整个文件一块 5.42×,点查从 0.0044 ms 升到 6.9427 ms。第 17 篇的 zstd 字典是解决“块小压缩比低”的另一种办法。