SQLite是一个单文件嵌入式数据库,很多桌面软件、移动应用和小型服务端都把它作为本地存储方案。用久了之后不少人会发现一个现象:明明删掉了几十万条记录,数据库文件的体积却几乎没有变化,甚至只增不减。这并不是Bug,而是SQLite的空间管理策略决定的——被删除的数据页并不会立刻归还给操作系统,而是标记为空闲页,等待后续写入时复用。如果业务读多写少,这些空闲页可能长期闲置,文件就会一直保持臃肿状态。要真正把空间还给磁盘,需要借助VACUUM家族的命令,本文重点讨论VACUUM INTO以及与之相关的auto_vacuum增量回收机制。

为什么删除数据后文件不会变小
要理解空间回收,先要知道SQLite的存储结构。整个数据库由固定大小的页(page)组成,默认每页4096字节。增删改操作都以页为单位进行,删除一行数据只是把该页内的空间标记为可用,页本身仍然留在文件里,被记入空闲页链表。这种设计对写入性能是友好的,因为新数据可以直接写入空闲页,不需要频繁移动文件内容。
问题出在长期运行之后。空闲页可能散落在文件各个位置,形成碎片;有些页只残留少量数据,利用率很低。文件末尾即使全是空闲页,SQLite也不会主动截断文件(除非开启了auto_vacuum)。于是文件体积只能反映“历史最高水位”,而不是当前实际数据量。
可以用PRAGMA freelist_count查看空闲页数量,再乘以PRAGMA page_size就能估算出可回收的空间大小。如果空闲页占比很高,就值得做一次空间整理了。
-- 查看数据库的页大小和空闲页数量 PRAGMA page_size; PRAGMA freelist_count; PRAGMA page_count;
全量VACUUM的原理与代价
传统的VACUUM命令是全量整理:SQLite会新建一个临时数据库,把当前所有有效数据按顺序重新写入,然后删掉旧文件、把新文件改名顶替。整个过程结束后,空闲页被彻底挤出,数据排列紧凑,查询性能也会有一定改善,因为相关的数据页在物理上更连续了。
但全量VACUUM的代价不小。第一,它需要大约两倍于原数据库的磁盘空间来容纳临时文件,如果数据库有几个GB,而所在分区剩余空间不足,操作会直接失败。第二,VACUUM执行期间会持有独占锁,其他连接完全无法读写,对可用性要求高的场景很难接受。第三,整理过程涉及全部数据的搬移,大库可能耗时数分钟以上。因此生产环境中直接对大库执行VACUUM需要格外谨慎,通常要安排在维护窗口进行。
另外要注意,VACUUM会重建整个数据库,rowid表的rowid可能发生变化(除非显式指定了INTEGER PRIMARY KEY),触发器、外键约束在整理期间的暂时表现也值得留意。执行前务必确认有可用备份。
VACUUM INTO:生成整理后的副本
从SQLite 3.27版本开始引入了VACUUM INTO语句,它的思路和全量VACUUM类似,但有一个关键区别:不会替换原数据库,而是把整理后的结果写入一个指定的新文件。原库在执行过程中仍然可以正常读取(写入会被阻塞),这就大大降低了操作风险。
-- 把整理压缩后的数据库写入新文件 VACUUM INTO '/backup/app_compact.db'; -- 文件名包含时间戳的场景,可以在应用程序层拼接路径后执行 -- 执行成功后可以校验新文件,再择机替换原文件 PRAGMA integrity_check;
VACUUM INTO天然适合做备份。生成出来的副本是经过整理的、逻辑上完全一致的新库,体积通常只有原库的有效数据大小。一个常见的实践是:定期用VACUUM INTO产出紧凑副本,校验通过后原子性地替换线上文件,既完成了空间整理,又顺带做了一次备份。相比在原库上直接VACUUM,这种方式把“整理”和“切换”两个动作解耦了,即使中途出问题,原文件也不受影响。
需要注意目标文件不能已经存在,否则会报错,这是一个防止误覆盖的保护机制。此外目标路径所在分区要有足够空间,命令本身不会自动清理旧文件。
auto_vacuum:增量式的空间回收
如果希望数据库在日常运行中自动归还空闲页,就要用到auto_vacuum模式。它有两种级别:INCREMENTAL允许配合PRAGMA incremental_vacuum分批回收,FULL则在事务提交时自动截断空闲页。
-- auto_vacuum只能在空数据库上设置,或设置后执行VACUUM使其生效 PRAGMA auto_vacuum = INCREMENTAL; VACUUM; -- 之后可以分批回收,每次最多释放指定页数 PRAGMA incremental_vacuum(100); -- 不带参数则尽可能回收所有空闲页 PRAGMA incremental_vacuum;
auto_vacuum的工作原理是把空闲页从空闲链表转移到指针映射页(pointer-map page)管理的体系中,删除数据后可以逐步把文件末尾的空闲页归还。INCREMENTAL模式的优势在于可控:每次只回收一小部分,事务短暂,不会造成长时间锁库,特别适合7x24小时运行的服务。而FULL模式虽然全自动,但每次事务提交都可能触发页移动,写入放大比较明显。
auto_vacuum也有代价。指针映射页本身要占用额外空间(约每512页需要一个映射页),写入路径变复杂后整体性能会略有下降,通常有百分之一到百分之几的开销。它是必须在建库之初就规划好的特性,中途开启需要执行一次全量VACUUM来重建,这一步的代价和直接做全量VACUUM是一样的。
如何选择合适的方案
三个方案各有适用面。已有的大库、不方便停服、又想整理空间,用VACUUM INTO最稳妥,生成副本后择机切换。新项目、预计会有频繁删除操作的,建库时就开启PRAGMA auto_vacuum = INCREMENTAL,配合定时任务调用incremental_vacuum,空间可以持续保持紧凑。维护窗口充足的小型库,传统VACUUM依然简单直接。
还有一点值得强调:预防胜于治疗。如果业务里存在大量“删旧插新”的循环,考虑用PRAGMA journal_mode = WAL配合合理的批量删除节奏,避免同一批次内频繁搬动数据页,能从源头减缓碎片化速度。定期通过freelist_count监控空闲页比例,超过两三成时再安排整理,是比较健康的运维习惯。SQLite虽小,空间管理的门道并不少,理解这些机制后,数据库文件“只增不减”的烦恼基本就能避免了。
SQLiteVACUUM INTOauto_vacuum修改时间:2026-09-13 08:58:31