SQLite凭借单文件、零配置的优势,被大量应用在移动端、嵌入式设备和中小型后台服务中。但一个常见的问题经常困扰使用者:明明执行了DELETE删掉了大量数据,查看数据库文件却发现体积纹丝不动,甚至偶尔还会变大。这不是bug,而是SQLite存储引擎的设计使然。本文将深入分析空间不释放的原因,并给出几种可靠的回收方案。

为什么删除数据后文件体积不变
要理解这个问题,先要了解SQLite的文件组织方式。整个数据库就是一个文件,内部被划分成固定大小的页(page),默认每页4096字节。表数据、索引、系统元信息都存放在这些页中。当执行DELETE FROM log_table WHERE create_time < '2023-01-01'这样的语句时,SQLite并不是把相关页从文件中物理移除,而是将这些页标记为「空闲页」,挂到内部的空闲页链表(freelist)上。
这种设计的出发点是性能:下次插入新数据时,SQLite可以直接复用空闲页,避免文件膨胀。所以删除数据之后,文件大小不变是正常行为,空间处于「逻辑释放、物理保留」的状态。可以通过下面的命令查看空闲页的数量:
-- 查看页状态 PRAGMA page_count; -- 总页数 PRAGMA freelist_count; -- 空闲页数量 PRAGMA page_size; -- 每页字节数 -- 粗略估算可回收空间(字节) -- freelist_count * page_size
如果freelist_count乘以page_size的结果有几百MB,就说明有大量空间被空闲页占着,此时才值得考虑做空间回收。如果空闲页很少,盲目执行回收操作收益甚微,反而会增加系统负担。
使用VACUUM命令彻底回收空间
VACUUM是SQLite官方提供的空间回收命令,它的原理是:把当前数据库的内容完整复制到一个临时数据库文件中,重建所有页的紧凑布局,然后用新文件替换旧文件。执行完毕后,空闲页被彻底消除,索引也会被重新整理,碎片化程度显著降低,查询性能往往有一定提升。
-- 标准用法 VACUUM; -- 只对指定数据库( attached 数据库)执行 VACUUM main; -- 使用改进的增量方式,减少临时空间占用(较新版本支持) VACUUM INTO 'backup_compact.db';
不过VACUUM有几个必须注意的特性。第一,执行期间需要获取独占锁,其他连接的写入会被阻塞,读操作在大部分阶段也会受限,对于线上服务要安排在低峰期执行。第二,临时数据库会占用与原数据库相当的磁盘空间,如果磁盘剩余空间不足,VACUUM会中途失败。第三,执行过程中数据库内容被完整复制一遍,大库耗时可能从几分钟到几十分钟不等,需要提前评估。
另外,VACUUM会重置rowid相关的计数行为,并且使所有预编译语句的查询计划失效重编译。对于依赖rowid顺序做业务逻辑的应用,需要确认这一点不会带来副作用。执行后建议顺手执行一次PRAGMA optimize;,让查询统计信息得到更新。
auto_vacuum增量回收模式
如果不想每次手动执行VACUUM,可以在建库时启用自动回收模式。该模式将数据库分为固定大小的页块(默认约1024页一块),当某个块内的所有页都变为空闲时,系统会自动把该块截断回收,文件会逐步缩小。
-- 查看当前模式,0为关闭,1为FULL,2为INCREMENTAL PRAGMA auto_vacuum; -- 必须在空库或VACUUM之后才能修改 PRAGMA auto_vacuum = FULL; -- 完全自动模式 PRAGMA auto_vacuum = INCREMENTAL; -- 增量模式 -- 增量模式下需手动触发回收,可控制节奏 PRAGMA incremental_vacuum(100); -- 每次回收100页
需要注意,auto_vacuum无法在已有数据的库上直接开启,必须先设置pragma再执行一次VACUUM才能生效。INCREMENTAL模式的优势在于可以分批执行incremental_vacuum,把锁表时间打散成多个短小的事务,适合不能长时间停服的场景。代价是数据库需要维护指针映射页(pointer-map page),文件会略微增大(通常1%到2%),大事务场景下的写入性能也有小幅下降。
从工程经验来看,日志类、流水类表删除频繁的库更适合INCREMENTAL模式;而数据基本只增不删的配置型库,保持默认关闭反而更简单高效。
备份重建与方案选型建议
除了VACUUM,还有一种思路是备份重建:利用sqlite3命令行工具的.dump导出全部SQL语句,再导入到一个全新的数据库文件中。新库天然紧凑,还顺带完成了碎片整理和格式升级。
# 导出SQL脚本 sqlite3 old.db .dump > dump.sql # 重建新库 sqlite3 new.db < dump.sql # 校验无误后替换旧文件 mv old.db old.db.bak mv new.db old.db
这种方式适合配合版本升级或迁移做一次性整理,缺点是过程相对繁琐,且期间数据的写入会丢失,必须在维护窗口执行。相比之下,VACUUM INTO命令是更现代的替代方案,它在生成紧凑副本的同时不影响原库继续提供服务,生成完毕后原子性地替换文件即可。
综合来看,方案选择可以参考以下标准:临时清理一次性大删除后的空间,用标准VACUUM,安排在低峰期;业务要求长时间在线、不能接受大锁,启用INCREMENTAL的auto_vacuum并分批回收;恰逢版本升级窗口,用dump重建或VACUUM INTO。无论采用哪种方式,操作前务必做好完整备份,并确认磁盘剩余空间至少是数据库体积的1.5倍以上。养成定期检查freelist_count的习惯,把空间管理纳入数据库的日常运维节奏,才能让SQLite在项目中长期稳定地运行。