SQLite的VACUUM命令用来重建数据库文件、回收已删除数据占用的空间。这个命令在小库上跑起来毫无感知,但当一个库膨胀到几个GB,VACUUM就可能变成一个漫长的等待过程。SQLite 3.47针对这个问题做了实质性改进,让VACUUM的执行速度大幅提升,某些场景下提速接近一倍。这篇文章就来拆解这次改进的技术细节,看看速度到底从哪里省出来的。

VACUUM的工作原理与性能瓶颈
要理解3.47为什么变快,得先弄清楚VACUUM在做什么。VACUUM并不是原地整理旧文件,而是把整个数据库复制到一个新的临时文件里。具体过程是:SQLite先开启一个临时数据库,然后遍历原库中的每一个页面,把有效数据重新写入临时库,最后用临时库替换原库文件。这种方式的好处是彻底——重建后的文件排列紧凑、页内碎片完全消除、索引统计信息也会刷新。
但代价也很明显。整个VACUUM过程在一个事务里完成,期间所有页面都要经过一次读加写的完整流程,还会产生大量的WAL或回滚日志。对一个数GB的数据库来说,这意味着海量的磁盘IO。而且旧版本在写临时库时,某些页面会被反复读取多次,比如包含溢出页的长记录、内部索引页等,重复读放大了IO开销。
另一个瓶颈在于提交阶段。VACUUM结束后需要fsync临时文件确保数据落盘,再把文件重命名回原路径,随后还要同步目录项。在慢速存储设备上,这些同步操作本身就占用了可观的耗时。3.47之前的版本对临时文件采用了与其他文件相同的同步策略,而临时库在替换完成后就变成正式库,某些同步其实存在优化空间。
3.47版本具体优化了什么
SQLite 3.47的官方发布说明里明确提到VACUUM提速,核心改动集中在两点。第一点是对页面复制顺序的调整。新版本在构建新数据库时,改进了freelist的处理和B树页面的分配策略,让顺序读写更加连续,减少了磁头来回跳跃的随机IO。对机械硬盘这种对随机访问敏感的设备,这个改动收益尤其明显。
第二点是事务与日志策略的优化。VACUUM过程中临时库原来运行在完整的日志保护之下,3.47将部分中间阶段改为批量提交,减少了日志刷盘的次数。同时对新文件写入的时序做了调整,把多个小的同步操作合并为更少的大块同步。下面这段简单的测试代码可以验证效果:
# 分别用 3.46 和 3.47 的命令行工具对同一个库做 VACUUM 计时
# 生成一个约 2GB 的测试库
python3 -c "
import sqlite3
conn = sqlite3.connect('test.db')
conn.execute('CREATE TABLE t(a INTEGER PRIMARY KEY, b TEXT)')
conn.executemany('INSERT INTO t(b) VALUES (?)',
(('x' * 500,) for i in range(4_000_000)))
conn.execute('DELETE FROM t WHERE a % 4 != 0') # 制造大量空洞
conn.commit()
"
# 计时执行
time sqlite3 test.db 'VACUUM;'官方基准测试显示,在一个含有大量碎片的大库上,3.47的VACUUM耗时相比3.46降低了大约40%到50%,库越大、碎片越多,提升越明显。对于本身就是顺序紧凑的小库,提升幅度则相对有限,因为这类库本来就没有多少可优化的空间。
如何利用好这次改进
升级本身很简单,SQLite是嵌入式库,只要把依赖的版本号更新即可。用Python的话确认sqlite3.sqlite_version输出为3.47以上;用系统自带命令行工具的话,检查sqlite3 --version的输出。需要提醒的是,应用依赖的SQLite版本往往由编译环境决定,别只看代码里的期望版本,要实际验证运行时链接到的库版本。
除了升级,一些实践建议依然适用。VACUUM会锁定整个数据库,执行前应安排在低峰期,或者配合PRAGMA wal_checkpoint(FULL)先把WAL日志合并。如果只是想控制文件体积而不追求极致紧凑,PRAGMA auto_vacuum = INCREMENTAL配合PRAGMA incremental_vacuum是更平滑的替代方案,它把整理工作拆成小步,避免长时间阻塞。
-- 查看当前版本 SELECT sqlite_version(); -- 大库整理前先合并WAL,减少VACUUM需要处理的日志量 PRAGMA wal_checkpoint(FULL); -- 执行碎片整理 VACUUM; -- 整理后查看文件页数与空闲页 PRAGMA page_count; PRAGMA freelist_count;
最后说明一点,VACUUM提速并不意味着可以频繁执行。它本质上仍是全库重建,对闪存的写入寿命也有消耗。合理的做法是根据freelist_count与page_count的比例判断是否需要整理,一般空闲页占比超过两三成时再考虑。3.47让这件事从“能忍”变成了“轻松”,但什么时候做,还是要靠业务判断。