Codex 日志数据库占满 C 盘:用 VACUUM INTO 找回 26GB
导语: 用了几个月 Codex 后,C 盘突然红了。SpaceSniffer 一扫,.codex 目录占了 40GB,其中 logs_2.sqlite 一个文件就接近 27GB。更离谱的是,库中约 97% 的页都在 freelist 里;压缩后,数据库只剩 742MB。本文记录从发现问题到用 Claude Code 一条 prompt 安全回收 26.1GB 的全过程,也解释 SQLite 为什么会出现这种现象。
这是一次发生在 Windows 上的真实故障处理,数据来自 2026 年 8 月 27 日的本机快照。
logs_2.sqlite属于 Codex 当前版本的内部文件,路径和结构以后可能变化。下面采用的是 SQLite 层面的备份压缩方案;操作前要退出 Codex、保留原库,并完成前后校验。
背景
我一直用 Codex(OpenAI 的 AI 编程助手)做日常开发。几个月下来,C 盘开始频繁告警。用 SpaceSniffer 扫描后,发现占用大头是:
1 | C:\Users\zcs\.codex\ → 40GB |

在此之前,我对 SQLite 文件膨胀几乎没有概念——不就是个日志数据库吗,能大到哪去?
发现过程
第一步,用 SpaceSniffer 找到大文件。第二步,用 SQLite 工具读取内部状态:
1 | PRAGMA quick_check; -- ok |
原库的实测结果如下:
1 | 日志行数 144,249 |
空闲页占比为:
1 | 6,833,529 / 7,041,670 ≈ 97.04% |
这比简单拿“文件大小 ÷ 日志条数”更能说明问题:27GB 主要是数据库保留的历史空间,不是当前 14 万行日志真实需要的体积。
还有一个更直接的证据:
1 | SELECT MIN(id), MAX(id), COUNT(*) FROM logs; |
1 | MIN(id) 988,031,822 |
id 是自增主键。最高 ID 已接近 10 亿,而当前只保留 14.4 万行,说明这个库经历过大量写入,旧记录也在持续淘汰。具体清理周期不影响这次判断:真正占住磁盘的是淘汰后没有及时归还给文件系统的空间。
问题原因
Codex 会删除超限或过期日志,SQLite 则把不再使用的整页放进 freelist,供后续写入复用;这并不等于把空间归还给 Windows。这个库启用了 auto_vacuum=INCREMENTAL,但只有应用主动执行 PRAGMA incremental_vacuum,空闲尾页才会逐步从数据库文件中移除。
我另外核对了 Codex 是否执行过这条命令。本机应用版本为 26.818.8289.0;公开源码固定在 commit 2c4a95736bea64256a50f7b8506bd33c181cc85a。源码会把可写连接设置为 INCREMENTAL,也会在日志超限及启动维护时执行删除,但仓库中没有 incremental_vacuum 的调用。本机保留的 SQL 执行日志同样记录了 PRAGMA auto_vacuum = INCREMENTAL,没有记录 PRAGMA incremental_vacuum。结合原库中 683 万个 freelist 页,可以判断:Codex 删除了旧日志,却没有主动把这些空闲页归还给文件系统。
处理方式(一句话版)
当时 C 盘只剩约 3GB,不能在原盘执行普通 VACUUM。我改用 VACUUM INTO 在 D 盘生成紧凑副本,校验完整性和行数后,再替换原库。
Prompt:让 Claude 帮你安全压缩
以下是经过实际验证的 prompt。可以直接复制,但要替换用户名、工具路径、日期和目标盘符:
1 | 请帮我安全压缩 Codex 本地日志数据库,目标是释放 C 盘空间。 |
执行结果:27GB → 742MB,释放 26.1GB,当时的 144,249 条日志一条不少。原库也完整保留在 D 盘,随时可以回滚。
深入原理:SQLite 为什么会占住这么多空间?
1. SQLite 的存储模型:页(Page)
SQLite 把数据库文件划分为固定大小的页。本例的 PRAGMA page_size 返回 4096,也就是每页 4KB。表记录、索引和元信息都存储在页中,数据库文件大小大致等于页数乘以页大小。
1 | ┌──────────────────────────────────────────────────────────────┐ |
第 1 页的开头包含数据库文件头,后续页面用于保存表、索引和其他结构。
2. Freelist:删除数据不等于缩小文件
数据被删除后,已经完全不用的页可以进入 freelist(空闲页链表)。这些页仍在数据库文件内部,但后续写入可以再次使用。
1 | 删除前: |
freelist_count 只统计完全空闲的页面。一个页面即使删掉了部分记录,只要还保存着有效内容,就不会计入 freelist。VACUUM 在重建数据库时还会整理这些未填满的页面,因此压缩结果通常比“总页数减去 freelist 页数”的估算更小。
auto_vacuum=NONE 会把空闲页留在文件中等待复用;INCREMENTAL 具备逐步回收文件尾部空闲页的能力,但要由应用主动触发。本例使用的是 INCREMENTAL,Codex 只启用了这种能力,没有执行实际回收所需的 incremental_vacuum。
3. 文件为什么没有跟着数据一起变小
page_count 对应数据库文件当前占用的页面总数。删除记录会增加 freelist_count,却不会直接减少 page_count。本例的变化过程可以概括为:
1 | 持续写入日志 |
三种 auto-vacuum 模式处理空闲页的方式不同:NONE 保留空闲页供复用,INCREMENTAL 等待显式回收,FULL 在事务提交时尝试移动并截断尾部空闲页。本例虽然使用 INCREMENTAL,但 Codex 没有执行显式回收,文件因此停留在约 27GB。
4. Codex 日志场景为什么特别明显
从自增 ID 和保留行数看,这个库的工作负载可以概括为:
1 | 高频写入日志 |
直接成本是磁盘空间,同时整库复制、备份和安全扫描也会处理这个 27GB 文件。普通 B-tree 查询只访问相关页面,不会机械遍历全部 freelist;这里最突出的故障仍然是 C 盘被占满。
5. VACUUM 是怎么工作的
VACUUM 会重建数据库,把有效内容重新组织到一个紧凑文件中。它不仅能丢掉 freelist 页,还能整理部分填充的页,所以最终文件可能比简单计算“总页数减 freelist 页数”更小。
1 | VACUUM INTO 的概念示意: |
VACUUM INTO 会把逻辑内容写入一个全新的紧凑数据库,源库保持不变,因此也可以用于生成在线数据库备份。这次操作还包含行数核对、WAL/SHM 文件处理和原库替换。提前退出 Codex,可以避免校验期间产生新日志,也能确保不会替换一个仍在使用的数据库。
6. 三种空间回收方式对比
| 方案 | 原理 | 优点 | 局限 |
|---|---|---|---|
VACUUM / VACUUM INTO | 重建数据库并压紧页面 | 回收彻底,也能整理部分填充页 | 大库耗时;原地 VACUUM 需要额外空间 |
auto_vacuum=FULL | 每次事务提交时把空闲页移到文件末尾并截断 | 能持续归还尾部空闲空间 | 可能增加碎片;不会像 VACUUM 那样压紧部分填充页 |
auto_vacuum=INCREMENTAL | 建立可移动页面所需的指针信息,再由 incremental_vacuum 分批回收 | 可控制单次回收量 | 不会自动执行;应用必须主动调用 |
auto_vacuum 模式通常在建表前确定;已有数据库也可以通过 VACUUM 重建后切换模式。Codex 管理着自己的内部数据库,手工改变模式可能与后续升级或应用逻辑冲突。对于这个已经使用 INCREMENTAL 的库,更合适的维护方式是:
- 保持 Codex 更新,观察新版本是否改善回收行为;
- 定期查看文件大小和
freelist_count / page_count,不要只看行数; - 只有当空闲页比例异常、磁盘压力明显时,才在退出 Codex 并完整备份后执行
VACUUM INTO; - 如果问题持续复现,向 OpenAI 反馈版本、文件大小和上述 PRAGMA 数据。
7. 为什么选 VACUUM INTO,而不是原地 VACUUM
普通 VACUUM 最坏可能需要相当于原数据库两倍的额外可用空间。对于一个 27GB 原库,保守估算可能还需要约 54GB 空闲空间;当时 C 盘只剩约 3GB,显然无法满足。
1 | C 盘原地 VACUUM: |
VACUUM INTO 会一直保留原库,直到新库通过校验。目标文件必须不存在或为空文件,因此每次操作都应使用新的备份文件名。
8. 总结
1 | ┌──────────────────────────────────────────────────────────────┐ |
这次留下了一套很实用的判断方法:SQLite 文件大小不等于当前有效数据量;先测 page count 和 freelist,再决定怎么处理。
下次 C 盘红了,别急着删文件——先看看是不是某个数据库在默默囤地。