搜索

Esc

按标签浏览

最近文章

软件工程# 软件工程

SQLite 删掉九成数据,数据库文件为什么几乎没变小?

SQLite 删掉九成数据,数据库文件为什么几乎没变小?
文章摘要通过AI生成文章摘要
DyuGPT
SQLite 删掉九成数据,数据库文件为什么几乎没变小?

一组实际运行的 SQLite 实验展示了删除、补写与 VACUUM 后的文件大小和空闲页变化。文章解释 page_count、freelist_count 与 WAL 的关系,并提供可复现 SQL。

一个日志表删掉了九成记录,查询行数已经下降,数据库文件却几乎一样大。对使用 SQLite 保存本地日志、缓存或离线数据的应用来说,行数与文件大小之间的关系,取决于数据库页如何回收。

SQLite 在 2026 年 10 月发布的 3.54.0 中,为命令行加入了 .diskused 磁盘分析命令。空间分析也可以先从三个长期支持的指标入手,观察总页数、空闲页数和每页大小。下面用一组实际运行的数据解释它们的关系。 SQLite 3.54.0 发布记录

删除之后,空闲页增加了

本次实验使用 Windows 上 Node.js v24.19.0 的内置 SQLite 3.53.3,页大小设为 4096 字节,关闭自动收缩,并使用 DELETE 日志模式。建表后插入 10,000 条记录,每条记录的 BLOB 字段固定占 3000 字节,然后依次删除、补写,再执行 VACUUM。

每一步都在事务提交之后读取主数据库文件的字节数。结果如下。

操作完成后 记录数 文件字节数 总页数 空闲页数
插入 10,000 条 10,000 41,058,304 10,024 0
删除前 9,000 条 1,000 41,058,304 10,024 9,019
再插入 9,000 条 10,000 41,062,400 10,025 0
删除刚补写的 9,000 条 1,000 41,062,400 10,025 9,020
执行 VACUUM 1,000 4,112,384 1,004 0

第一次删除让记录数降到了原来的十分之一,文件大小保持在 41,058,304 字节,同时出现了 9,019 个空闲页。再次插入同等数量的数据后,文件仅增加了一个 4096 字节的页,空闲页归零。这组结果显示,数据库已经把大量原有空间用于后续写入。

SQLite 在 auto_vacuum=NONE 模式下,会把删除数据释放出的整页空间保留在文件内部,供后续分配复用。文件仍然占着磁盘空间,但其中部分页已经可再次使用。 VACUUM 通过重建数据库,重新组织有效内容并回收多余空间。 SQLite VACUUM 文档

用三个数区分“文件大”和“空闲多”

在数据库连接中运行以下查询即可取得对应指标。

sql
PRAGMA page_size;
PRAGMA page_count;
PRAGMA freelist_count;

page_count 是数据库总页数,freelist_count 是空闲页数。二者乘以 page_size,可以分别估算数据库页的总字节数与空闲整页的字节数。 SQLite PRAGMA 文档

第一次删除后的计算是 9019 × 4096 = 36,941,824 字节。约 35.23 MiB 的整页空间已进入空闲页列表,约占数据库总页数的 90%。

还需把部分填充的数据页考虑在内。一个页可能仍然保存着有效记录,即使内部有剩余空间,也不能把整页算进空闲列表。因此,空闲页比例适合观察可复用的整页空间,完整评估碎片与表、索引的占用则需要进一步的磁盘分析。

本次保留的记录、表结构和索引结构共同占用了剩余页面。数据行数变化与文件大小变化因而具有不同的粒度。

在新建数据库中复现

以下 SQL 用于新建的空实验库,生成相同数量和负载大小的数据。先确认运行库版本与模式。

sql
SELECT sqlite_version();

PRAGMA page_size=4096;
PRAGMA auto_vacuum=NONE;
PRAGMA journal_mode=DELETE;

CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  payload BLOB NOT NULL
);

BEGIN;
WITH RECURSIVE seq(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM seq WHERE n < 10000
)
INSERT INTO events(payload)
SELECT zeroblob(3000) FROM seq;
COMMIT;

DELETE FROM events WHERE id <= 9000;

SELECT count(*) FROM events;
PRAGMA page_count;
PRAGMA freelist_count;

此时可以记录文件大小,再单独执行下面的操作并重复查询。

sql
VACUUM;

SELECT count(*) FROM events;
PRAGMA page_count;
PRAGMA freelist_count;

具体页数可能随 SQLite 版本和数据布局改变。本次实验的结果只对应上述负载和配置,核心观察项是删除前后的空闲页变化,以及重建之后的文件大小。

对持续滚动写入的日志库,保留下来的空闲页可以承接下一批记录。对完成大量清理后将长期保持较小数据量的库,重建操作则可能明显减少磁盘占用。执行时需要预留工作空间并安排维护时段,官方文档指出普通 VACUUM 可能需要额外达到原数据库大小两倍的可用磁盘空间,且执行连接必须结束已有事务。 VACUUM 执行条件

开启 WAL 后,还要把日志文件算进去

上述实验刻意使用 DELETE 日志模式,使记录的文件大小对应主数据库。WAL 模式会把新修改先追加到 -wal 文件,再通过 checkpoint 将内容写回主数据库。因此,排查应用的实际磁盘占用时,需要同时观察主文件与 WAL 文件,以及 checkpoint 的进展。 SQLite WAL 文档

这也对应两项不同的操作。checkpoint 推进 WAL 中的修改落入主库,VACUUM 重建数据库并压缩布局。对一个文件持续变大的实例,先确认增长来自主库还是 WAL,再检查空闲页,才能确定需要处理的是空间布局还是日志积累。


资料与实验记录日期为 2026 年 11 月 5 日。本文表格来自本地 SQLite 实验,未进行业务吞吐量测试。

参考资料

评论

0 条讨论
还没有评论来做第一个留言的人。