SQLite作为嵌入式数据库,整个数据库就是一个单独的文件。随着数据的不断插入、更新和删除,这个文件会逐渐膨胀,即使删掉了大量数据,文件大小也常常不见缩小,查询性能还可能随之下降。这就是典型的碎片问题。理解碎片产生的原因并掌握正确的整理方法,对维护一个长期运行的SQLite应用非常关键。

碎片是怎么产生的
SQLite将数据库文件划分为固定大小的页(默认通常是4096字节),每一页存放B树节点、索引节点或者溢出数据。当你执行DELETE语句时,被删除的数据并不会从文件中物理移除,而是把所在页标记为空闲页,加入到空闲页链表中,供后续写入复用。也就是说,文件占用的磁盘空间不会因为删除而自动归还给操作系统。
空闲页本身还不是问题的全部。更麻烦的是页内部碎片:更新一条记录时,如果新数据比原来长,页内放不下,SQLite会在页内寻找空闲槽位或分配新页,导致原本连续的逻辑数据被拆散到多个页中。久而久之,一次范围查询可能需要读取大量分散的页,磁盘IO次数增加,性能自然下滑。
另外,删除大量数据后文件中会留下大量空闲页,这些页虽然不存有效数据,但依然占据磁盘空间,并且新写入的数据可能复用这些页,导致热数据和冷数据交错分布,页缓存命中率下降。可以用下面的命令查看数据库的碎片状况:
-- 查看空闲页数量(未使用的页) PRAGMA freelist_count; -- 查看总页数 PRAGMA page_count; -- 查看页大小 PRAGMA page_size;
如果freelist_count占page_count的比例超过百分之二十到三十,就值得做一次碎片整理了。
VACUUM命令:完整的碎片整理方案
VACUUM是SQLite官方提供的碎片整理命令,它的原理非常直接:把当前数据库的内容完整复制到一个临时数据库文件中,在这个过程中重新组织所有页,去除空闲页和页内碎片,然后用整理后的临时文件替换原数据库文件。执行完成后,文件体积会收缩到实际数据所需的最小大小,页分布也变得紧凑连续。
基本用法很简单:
-- 整理整个数据库文件 VACUUM; -- 使用指定的SQL语句接口 sqlite3_exec(db, "VACUUM", 0, 0, &errMsg);
不过VACUUM有几个必须了解的特点。第一,它需要大约两倍于原数据库大小的临时磁盘空间,因为整理过程是先复制到新文件再替换。第二,执行期间数据库会被锁定,其他连接无法写入,大数据库整理一次可能耗时较长,不适合在业务高峰期执行。第三,执行VACUUM之后,rowid没有WITHOUT ROWID的普通表其行ID不会改变,但统计信息会被重置,建议在整理后执行一次ANALYZE,让查询计划器拿到最新的统计数据:
VACUUM; ANALYZE;
如果数据库启用了auto_vacuum模式,VACUUM还有一个作用:在启用PRAGMA auto_vacuum=FULL的数据库上执行VACUUM可以将模式在FULL和INCREMENTAL之间切换。需要注意auto_vacuum必须在数据库创建时就设置好,事后修改只能通过VACUUM重建来生效。
增量整理与日常维护策略
对于不能接受长时间锁库的场景,可以考虑增量式自动整理。首先在创建数据库时设置:
PRAGMA auto_vacuum = INCREMENTAL;
之后可以在任意时刻执行增量整理,每次只归还一部分空闲页,把长暂停拆分成多个短操作:
-- 设置本次要回收的空闲页数量上限 PRAGMA incremental_vacuum(100);
这种方式不会重建整个数据库,锁持有时间短,但整理效果不如完整VACUUM彻底,页内碎片无法消除,只回收空闲页。因此它适合写入频繁的在线系统,配合定期低峰期做完整VACUUM效果最好。
日常维护上还有几点建议。一是控制整理频率,碎片整理本身有IO开销,一般在线应用每天或每周低峰期执行一次即可,具体可以依据freelist_count的比例来触发。二是VACUUM之前做好备份,虽然VACUUM过程本身是原子的,但如果整理中途磁盘空间不足或进程被强制杀死,处理起来仍然麻烦。三是大文件场景可以考虑用sqlite3命令行工具的.backup命令在线备份到新文件,效果等同于一次碎片整理且不阻塞读操作。
# 在线备份并整理,生成紧凑的新数据库文件 sqlite3 old.db ".backup new.db" # 替换原文件 mv new.db old.db
把碎片监控、整理时机和备份流程纳入应用的运维脚本中,SQLite文件就能长期保持在一个健康的体积和性能水平上,不会随着运行时间无限膨胀。
修改时间:2026-09-11 21:18:32