一千次文本修订,原始体积20.4MB,压缩之后只剩80.3KB——这是Simon Willison最新公开的一个SQLite原型给出的headline数字。故事很轻:他遛狗时想到这个点子,先用ChatGPT的语音模式聊了个思路,回家后甩给GPT-5.6 Sol Pro写代码,模型跑了38分钟,交出两套原型:WholeBlobHistoryStore和ChunkedHistoryStore。Willison原文的评价是"效果很好"。
但完整基准数据讲了另一半故事:那个压缩比最惊艳的方案,写一千次修订要花26.8秒。这个数字原文没提。
被省略的代价
问题出在架构本身。WholeBlobHistoryStore把所有历史版本塞进一个JSON数组再整体压缩,这意味着每加一次新修订,程序都得把整个数组解压、追加、再重新压缩一遍。历史越长,这个循环越贵——写入耗时不是线性增长,而是接近指数级堆积。
对比之下,简单粗暴的"每行存一份"方案反而写得更快:
| 方案 | 压缩后大小 | 写入总耗时 | 末次编辑延迟 |
|---|---|---|---|
| 逐行不压缩 | 20.4MB | 70.3ms | 0.061ms |
| 逐行Zstd压缩 | 7.5MB | 218.2ms | 0.241ms |
| 整体BLOB压缩 | 80.3KB | 26.80s | 49.904ms |
| 分块64条 | 154.9KB | 0.926s | 0.957ms |
| 分块128条 | 109.9KB | 1.528s | 2.196ms |
逐行压缩牺牲了体积,换来的是写入速度快出上百倍。整体BLOB方案赌的是相反的方向——体积赢了,延迟输得一干二净。
压缩比赢了,写入延迟输得一干二净
- 风险.任何高频编辑场景——笔记应用、Wiki、协作文档——一旦复用整体BLOB思路,每次保存都可能卡出几十秒的等待,这在真实产品里是不可接受的。
为了缓解这个问题,Sol给出了折中方案:把历史切成多个分块,每块最多128条修订或3MB未压缩JSON,超过就密封开新块。这就是ChunkedHistoryStore。
64还是128,原型没有说清楚
分块阈值该怎么定,基准数据其实给出了一个耐人寻味的答案:64条一块比128条一块写得更快——0.926秒对1.528秒,快了将近40%。代价只是多占用约45KB存储,几乎可以忽略。
原型最终推荐的却是128。这个选择依据在原文和代码里都没有明确交代,更像是工程直觉,而不是经过严谨评估的最优解。对于一个由AI在38分钟内自动生成、连人类都没来得及细看的原型,这种"参数看起来合理但缺乏论证"的空白,恰恰是最容易被后来者直接照搬、却踩坑的地方。
原型和生产级方案之间差着几道坎
第三方技术评审给出的判断更保守:方向可信,但远未成熟。压缩比从来不该是唯一指标,写入放大、并发行为、数据损坏后的恢复能力、长期格式兼容性,这几项在原型里全部缺席。
SQLite本身的并发模型是单写者加WAL多读者,官方实验性的BEGIN CONCURRENT功能允许写者乐观执行,但提交依然是串行的,冲突时会被直接拒绝——不能当作可移植的并发基线来用。数据损坏方面,SQLite的.recover工具能在文件损坏时抢救内容,但完美恢复是例外不是常态,更不能替代备份。这些都是这个原型完全没有测过的场景。
评审建议的生产架构和原型的"两列BLOB"设计几乎是两条路:
推荐路径——写事务保持短小,压缩计算放到事务之外完成,历史切成不可变、内容寻址的分块,再用一份清单(manifest)记录哪个版本对应哪些分块。
这套思路在SQLite生态里并非空想。官方的ZIPVFS扩展早就证明页级压缩数据库可行,但它需要单独授权、要管理压缩空闲槽和碎片,不适合作为开源项目的基础;Session扩展把表变更记录为changeset/patchset,支持应用和反转,是变更表示和冲突处理的有用先例,但也不是完整的文本历史存储方案。原型选的"BLOB列+压缩JSON数组"是最简单的一条路,简单到几乎绕开了这些既有工具想解决的所有麻烦——也因此绕开了它们本该解决的所有问题。
- 结论.想复用这个思路的开发者,先问清楚自己的编辑频率和并发写者数量,再决定要不要为80.3KB的体积优势,承担写入延迟和恢复能力上的未知代价。
这份研究报告和代码本身也是AI生成的产物,仓库明确标注它应被当作探索性材料,而不是可发布的生产方案。压缩比数字好看,不代表工程严谨,这中间的距离,恰恰是原文的乐观叙事没有交代清楚的部分。
