把抓下来的数据存起来,有 JSON Lines、zstd 文件、sqlite 文本、sqlite 压缩 BLOB 等做法。这篇在同一份 2 万条商品数据上比较体积、写入、点查、扫描、更新和删除,看看各自适合什么。
方式 文件字节 写入耗时 相对 JSONL A JSON Lines 文本 7214162 76ms 100.0% B JSON Lines 整个文件一个 zstd-3 帧 1331194 87ms 18.5% C JSON Lines 每 1000 条一个 zstd 帧(带偏移索引) 1421758 17ms 19.7% D sqlite:id + JSON 文本 7790592 49ms 108.0% E sqlite:id + 每条单独 zstd-3 的 BLOB 6324224 115ms 87.7% F sqlite:id + 每条 zstd-3 + 16 KiB 字典 2449408 59ms 34.0% G sqlite:每个字段一列(关系表) 4886528 47ms 67.7% H sqlite:每 100 条一个 zstd-3 的 BLOB 1773568 25ms 24.6%
整个文件一个 zstd 帧最小(18.5%),但不能随机访问;每条单独压缩的 BLOB 只省 12%,加字典 34%;每 100 条一块 24.6%,每 1000 条一帧 19.7%。块越大压缩越好,点查越慢。
A JSONL + 行偏移索引 5.5 微秒 B 整个文件一个 zstd 帧(每次解压整个文件!) 7657.5 微秒 C 每 1000 条一个帧 + 偏移索引 369.5 微秒 D sqlite JSON 文本 4.1 微秒 E sqlite 每条 zstd BLOB 5.9 微秒 F sqlite 每条 zstd + 字典 4.3 微秒 G sqlite 列(取一整行) 6.9 微秒 H sqlite 每 100 条一个 BLOB 43.8 微秒
带偏移索引的 JSONL、sqlite 文本、每条压缩的 BLOB 都是几微秒;整个文件一个 zstd 帧每次点查要 7657.5 微秒;每 100 条一块 43.8 微秒,每 1000 条一块 369.5 微秒。
A JSONL 逐行 json.loads 58.5 ms 结果 884659.70 ✓ B zstd 帧解压后逐行 json.loads 61.2 ms 结果 884659.70 ✓ D sqlite JSON 文本,SQL 里 json_extract 14.2 ms 结果 884659.70 ✓ E sqlite zstd BLOB,逐行解压 + json.loads 89.6 ms 结果 884659.70 ✓ G sqlite 列,SQL 直接聚合 1.1 ms 结果 884659.70 ✓ 正确结果 884659.70
要对字段做过滤聚合,关系列最快(1.1 ms);sqlite 的 json_extract 14.2 ms;逐行 json.loads 58.5 ms;压缩 BLOB 最慢(89.6 ms),因为数据库没法帮你过滤压缩过的字节。
逻辑大小 20859437 字节;1500 个文件的磁盘占用(按块计)24576000 字节(117.8%,每个文件向上取整到文件系统的块大小) sqlite 原样存放 21520384 字节(103.2%) sqlite 每页 zstd-3 7405568 字节(35.5%) sqlite 每页 zstd-3 + 64 KiB 字典 6164480 字节(29.6%) 对照:全部页面拼成一个 zstd-3 帧 4151340 字节(19.9%);字典本身 65536 字节(要和数据一起保存)
1500 个页面当文件存,磁盘占用比逻辑大小多 17.8%(块对齐);放进 sqlite 原样是 103.2%;每页压缩 35.5%,加字典 29.6%,整体压缩 19.9%。
A JSONL:重写整个文件(7.2 MB) 7 ms D sqlite JSON 文本 15 ms E sqlite 每条 zstd BLOB 17 ms G sqlite 列 8 ms H sqlite 每 100 条一个 BLOB(每条更新都要重压 100 条) 127 ms
每条单独存放的方式更新 1000 条只要 8–17 ms;攒批压缩的 BLOB 每次更新要解压并重压整块,127 ms。追加不改的数据适合攒批压缩,经常更新的不适合。
隔行删除(id 为偶数的行):写入后 7790592 字节(1902 页 × 4096);删除后文件 7790592 字节,空闲页 1 个(0.1%);VACUUM 后 3907584 字节(用时 21 ms) 连续删除(前一半 id):写入后 7790592 字节(1902 页 × 4096);删除后文件 7790592 字节,空闲页 951 个(50.0%);VACUUM 后 3891200 字节(用时 24 ms)
sqlite 删除后文件不缩小:隔行删除几乎没有整页被释放,连续删除释放的页留在空闲列表。文件缩小要 VACUUM,会重写整个库。