19、SQLite的VACUUM与空间回收:VACUUM命令、自动VACUUM、空间碎片整理

做嵌入式数据库开发的朋友,迟早会遇到一个问题:数据库文件越来越大,明明删了很多数据,文件大小却纹丝不动。嗯,这其实就是SQLite的“空间碎片”在作祟。今天我们就来聊聊怎么收拾这些碎片,把磁盘空间真正还回来。

为什么删了数据,文件不缩小?

SQLite的存储机制,说白了就是“只增不减”。当你执行DELETE时,它只是在数据页上打了个“已删除”的标记,并不会把物理空间还给操作系统。这些被标记的页,就成了所谓的“空闲列表”,留给后续INSERT复用。

我个人习惯把这种机制比作“占座”。你离开座位了,但椅子还在那儿,下一个人来了可以直接坐。但如果一直没人来坐,这些空椅子就白白占着地方。数据库文件也一样,空闲页多了,文件就虚胖。

核心问题:SQLite默认不会主动收缩文件。空闲页越多,文件越臃肿,查询时遍历的页也越多,性能会悄悄下降。

VACUUM命令:手动瘦身

VACUUM是SQLite提供的最直接的空间回收手段。它的工作原理很简单:

  1. 创建一个新的临时数据库文件
  2. 将原数据库中的有效数据逐页复制到新文件
  3. 删除原文件,将新文件重命名为原文件名

这样一来,所有空闲页都被丢弃了,数据页紧密排列,文件大小就是实际数据量的大小。

用法也极其简单:

-- 对整个数据库执行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模式给了你一把“手术刀”,而不是“大砍刀”。它的工作流程是:

  1. 设置PRAGMA auto_vacuum = 2
  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先做检查,再决定是否替换。

知识体系一览

下面这张图,把本章的核心逻辑串起来了:

SQLite空间回收知识体系 文件虚胖问题 手动VACUUM 自动VACUUM 增量VACUUM • 重建整个数据库 • 需要双倍空间 • 彻底回收空间 • FULL/INCREMENTAL • 自动触发 • 影响写入性能 • 手动控制页数 • 灵活调度 • 适合嵌入式设备 选择建议:写入频繁 → 增量VACUUM 空间紧张 → 手动VACUUM INTO

实战建议总结

  • 小数据量(<100MB):直接用VACUUM,简单粗暴,效果最好。
  • 大数据量且写入频繁:用INCREMENTAL模式,配合定时任务执行incremental_vacuum。
  • 存储空间极度紧张:用VACUUM INTO先生成副本,确认没问题再替换。
  • 只读数据库:不需要任何VACUUM,因为不会有碎片产生。

最后提醒一句:VACUUM会重建所有索引,如果你的表有大量索引,VACUUM的时间会比想象中长很多。我在一个500万行、8个索引的表上跑VACUUM,花了将近3分钟。所以,生产环境执行前,一定先评估时间窗口。

好了,关于SQLite的空间回收,核心就是这些。记住一句话:VACUUM不是银弹,但不用VACUUM,你的数据库迟早会变成“虚胖”的胖子。


公众号:蓝海资料掘金营,微信deep3321