19、SQLite的VACUUM与空间回收:VACUUM命令、自动VACUUM、空间碎片整理
做嵌入式数据库开发的朋友,迟早会遇到一个问题:数据库文件越来越大,明明删了很多数据,文件大小却纹丝不动。嗯,这其实就是SQLite的“空间碎片”在作祟。今天我们就来聊聊怎么收拾这些碎片,把磁盘空间真正还回来。
为什么删了数据,文件不缩小?
SQLite的存储机制,说白了就是“只增不减”。当你执行DELETE时,它只是在数据页上打了个“已删除”的标记,并不会把物理空间还给操作系统。这些被标记的页,就成了所谓的“空闲列表”,留给后续INSERT复用。
我个人习惯把这种机制比作“占座”。你离开座位了,但椅子还在那儿,下一个人来了可以直接坐。但如果一直没人来坐,这些空椅子就白白占着地方。数据库文件也一样,空闲页多了,文件就虚胖。
核心问题:SQLite默认不会主动收缩文件。空闲页越多,文件越臃肿,查询时遍历的页也越多,性能会悄悄下降。
VACUUM命令:手动瘦身
VACUUM是SQLite提供的最直接的空间回收手段。它的工作原理很简单:
- 创建一个新的临时数据库文件
- 将原数据库中的有效数据逐页复制到新文件
- 删除原文件,将新文件重命名为原文件名
这样一来,所有空闲页都被丢弃了,数据页紧密排列,文件大小就是实际数据量的大小。
用法也极其简单:
-- 对整个数据库执行VACUUM
VACUUM;
-- 也可以只对某个表执行(SQLite 3.27.0+)
VACUUM my_table;
我的经验:VACUUM执行期间,数据库文件大小会翻倍(因为要创建临时副本)。如果你的存储空间本来就紧张,记得先检查剩余空间是否足够。我曾经在一个只有2MB剩余空间的设备上跑VACUUM,结果直接报错“disk I/O error”,差点把数据搞丢。
自动VACUUM:省心但有限
SQLite支持两种自动VACUUM模式:
| 模式 | 设置值 | 行为 |
|---|---|---|
| NONE | 0 | 默认模式,不自动回收空间 |
| FULL | 1 | 每次事务提交时,自动整理空闲页并收缩文件 |
| INCREMENTAL | 2 | 只整理空闲页,但不收缩文件,需配合PRAGMA incremental_vacuum使用 |
设置方法:
-- 开启自动VACUUM(必须在创建数据库后、建表前设置)
PRAGMA auto_vacuum = 1;
-- 查看当前模式
PRAGMA auto_vacuum;
注意:auto_vacuum一旦设置,就不能轻易改回。从FULL改回NONE,需要执行一次完整的VACUUM才能生效。而且,auto_vacuum = FULL会略微降低写入性能,因为每次提交都要做额外工作。
我个人不太推荐在生产环境开启FULL模式。原因很简单:它太频繁了。每次事务提交都做整理,对于写入频繁的场景,性能损耗很明显。我更喜欢用INCREMENTAL模式,自己控制回收时机。
增量VACUUM:灵活控制
INCREMENTAL模式给了你一把“手术刀”,而不是“大砍刀”。它的工作流程是:
- 设置PRAGMA auto_vacuum = 2
- 定期执行PRAGMA incremental_vacuum(N),其中N是要回收的页数
-- 开启增量模式
PRAGMA auto_vacuum = 2;
-- 每次回收100个空闲页
PRAGMA incremental_vacuum(100);
这样做的好处是,你可以选择在系统空闲时(比如深夜、用户不操作时)慢慢回收空间,避免影响正常业务。
避坑指南:我曾经在一个日志采集设备上用过FULL模式,结果发现写入速度从每秒500条掉到了200条。后来改成INCREMENTAL模式,每小时执行一次incremental_vacuum(500),写入性能恢复了,文件大小也控制得很好。
空间碎片整理:不只是VACUUM
VACUUM能解决文件虚胖问题,但碎片整理还有更细的粒度。SQLite的数据页内部也可能有碎片——比如一条记录被更新后,新数据比旧数据短,页内就会留下空洞。
这些页内碎片,VACUUM是管不了的。怎么办?
嗯,这里有个小技巧:重建表。说白了就是:
-- 方法1:使用CREATE TABLE AS
CREATE TABLE new_table AS SELECT * FROM old_table;
DROP TABLE old_table;
ALTER TABLE new_table RENAME TO old_table;
-- 方法2:使用VACUUM INTO(SQLite 3.27.0+)
VACUUM INTO 'compacted.db';
VACUUM INTO的好处是,它不会修改原文件,而是生成一个整理后的副本。你可以先检查副本大小,确认没问题再替换原文件。这在嵌入式设备上特别有用——万一空间不够,至少原文件还在。
我的建议:对于频繁更新的表,可以定期(比如每周)执行一次VACUUM。如果数据量不大,直接用VACUUM就行。如果数据量大且存储紧张,用VACUUM INTO先做检查,再决定是否替换。
知识体系一览
下面这张图,把本章的核心逻辑串起来了:
实战建议总结
- 小数据量(<100MB):直接用VACUUM,简单粗暴,效果最好。
- 大数据量且写入频繁:用INCREMENTAL模式,配合定时任务执行incremental_vacuum。
- 存储空间极度紧张:用VACUUM INTO先生成副本,确认没问题再替换。
- 只读数据库:不需要任何VACUUM,因为不会有碎片产生。
最后提醒一句:VACUUM会重建所有索引,如果你的表有大量索引,VACUUM的时间会比想象中长很多。我在一个500万行、8个索引的表上跑VACUUM,花了将近3分钟。所以,生产环境执行前,一定先评估时间窗口。
好了,关于SQLite的空间回收,核心就是这些。记住一句话:VACUUM不是银弹,但不用VACUUM,你的数据库迟早会变成“虚胖”的胖子。