核心观点
- 用「把所有历史版本的全文拼成一个 JSON 字符串数组,再整体 zlib/zstd 压缩」的方案,SQLite 里存修订历史的体积可压缩 250 倍:1,000 次模拟修订的 20.4MB 原始文本压缩到 80.3KB。
- 两个实验原型并行验证:WholeBlobHistoryStore 每次编辑重写整个压缩 blob,ChunkedHistoryStore 把压缩块封存成多行以支撑长历史扩展。
- 设计细节明确:历史与时间戳分两列(时间戳数组不必压缩)、默认跳过未变化的替换、用 BEGIN IMMEDIATE 串行化写入保证原子性。
- 思路本身源于一次日常散步,原型由 ChatGPT 的 GPT-5.6 对话直接生成——AI 协作写数据库实验代码的现实样本。
内容精讲
关系数据库里怎么存文本的修订历史,是个老问题。最朴素的做法是每版一行,但长文档的每次编辑都要追加整份副本:一份 20KB 的文档,改一次就新增 20KB 数据,几百次编辑下来体积失控。新思路则完全换了个角度:把全部历史版本的全文放进一个大 JSON 字符串数组,然后对整体施加 zlib 或 zstd 压缩。理由很直觉——同一文档的相邻版本绝大部分文本是重复的,压缩算法对这种「满是重复字符串」的输入特别有效,能一口气抹掉海量冗余。
**方案的具体形状。** 表里只有一个 history 列,类型是 BLOB,存放「zlib 或 zstd 压缩的 JSON 文本数组」,数组里是文档从创建至今的每个历史版本全文。时间戳单独一列,是 JSON 整数数组(Unix 时间戳),不需要压缩——整数数组本身就很紧凑。整套机制就这两列,没有任何索引或辅助表。
**两个原型各管一摊。** WholeBlobHistoryStore 的模型最直白:每次编辑就把整个历史 blob 解压、追加新版本、重新压缩写回——读起来省事,但编辑成本随历史长度线性增长。ChunkedHistoryStore 则针对长历史的扩展性:压缩块写满后封存为独立一行,不再反复解压重写整个历史。两者都保留旧文本与时间戳、默认跳过未变化的替换、并用 BEGIN IMMEDIATE 串行化写入以保证原子更新。封存的块是只读的,未来的读取只需拼装各块。
**实测数据很有说服力。** 对一篇文档做 1,000 次模拟修订:原始修订文本累计 20.4MB,zstd 压缩成 JSON 数组后只剩 80.3KB——压缩率约 250 倍。这个数字直接验证了「相邻版本高度重复」的直觉。不过全量数组方案有一个现实负担:每次编辑都要解压-修改-重压整个数组,编辑成本随版本数增长。为此,原型引入的分块思路(每行最多 128 次修订或 3MB 未压缩 JSON)把「整体重写」变成「追加新块」,把编辑成本从 O(历史长度) 降到近似 O(1)。
**原型的诞生过程也值得一提。** 这个想法始于一次遛狗时的闲聊式头脑风暴,随后用 ChatGPT iPhone 应用的语音模式(GPT-Live)口述讨论,再让 GPT-5.6 Sol Pro 用 Python 实现原型——模型「吭哧吭哧」跑了 38 分钟后交付了完整代码和文件。这个流程本身是 2026 年「语音即 IDE、对话即实现」的典型写照:概念提出、技术选型讨论、原型实现与迭代,都可以在一条 AI 对话流里完成。
对需要在数据库里维护「每版全文 + 时间戳」的应用(文档协作、配置版本、内容审计),这套方案给出一个极低复杂度的备选:两列、一个压缩库调用、两种存储策略二选一。是否值得用到生产,取决于编辑频率与历史长度的具体比例。
阅读价值
对后端工程师,这是一个高性价比的存储方案实验:压缩数组 + 分块封存的组合可直接评估试用;对关注 AI 协作开发的读者,它展示了从「散步中的想法」到「可运行原型」如何由对话式 Agent 全流程加速。
内容与图片版权归原作者所有 · 原文: https://simonwillison.net/2026/Aug/9/sqlite-text-history-prototype/#atom-everything