Chapter 11 · Database Files, Pages, B-Trees, Freelist, and Storage Internals
Freelist, Deleted Space, auto_vacuum, VACUUM, and File Shrinkage
Explain why DELETE normally creates reusable free pages instead of shrinking the file, compare auto_vacuum modes, and safely measure VACUUM, incremental_vacuum, and VACUUM INTO on disposable databases.
Learning outcomes
Deleting 100,000 rows can leave a database file almost the same size. That is not evidence that DELETE failed. In the default auto-vacuum mode, SQLite keeps freed pages inside the file on a freelist so later inserts can reuse them without asking the filesystem to grow the file again. File shrinkage is a separate operation and has its own costs.
Explain live pages versus freelist pages and why DELETE normally does not shrink the file under auto_vacuum=NONE.
Observe freelist_count before and after large deletes.
Compare NONE, FULL, and INCREMENTAL auto_vacuum behavior and the pointer-map requirement.
Explain VACUUM as a database rebuild/compaction with disk-space, locking, and rowid implications.
Distinguish VACUUM, PRAGMA incremental_vacuum, and VACUUM INTO.
Run safe create/delete/measure/vacuum experiments only on disposable databases and avoid routine VACUUM folklore.
Freelist: free inside the file is not free outside the file
When a page is no longer needed by a table/index, SQLite can place it on the database freelist. The page still occupies bytes in the file but is available for reuse by future allocations. This avoids unnecessary filesystem shrink/grow churn.
PRAGMA auto_vacuum;PRAGMA page_size;PRAGMA page_count;PRAGMA freelist_count;-- After a large DELETE:PRAGMA page_count;PRAGMA freelist_count;With the common auto_vacuum=NONE setting, expect freelist_count to rise while page_count and file size may remain almost unchanged.
auto_vacuum modes
| Mode | What happens to free pages | Tradeoff / setup rule |
|---|---|---|
NONE (0) | Freed pages stay on freelist for reuse; file does not automatically shrink. | Default unless compile-time overridden. Full VACUUM can reclaim external file space. |
FULL (1) | At each commit, freelist pages are moved toward file end and file is truncated. | Requires pointer-map metadata; can increase fragmentation and does not repack partially filled pages like VACUUM. |
INCREMENTAL (2) | Tracks pointer maps like FULL, but reclaim happens only when incremental_vacuum is invoked. | Lets the application choose when/how much freelist truncation to request. |
SQLite must know how pages refer to one another to move free pages safely for auto-vacuum, so enabling FULL/INCREMENTAL requires database-format support established before normal table population or by setting the mode and rebuilding with VACUUM. Switching between FULL and INCREMENTAL is easier because both already have the pointer-map infrastructure.
Disposable experiment A: NONE + VACUUM
from pathlib import Pathimport sqlite3p = Path("vacuum-none.db")p.unlink(missing_ok=True)con = sqlite3.connect(p)con.execute("PRAGMA auto_vacuum=NONE")con.execute("CREATE TABLE payload(id INTEGER PRIMARY KEY, body BLOB NOT NULL)")con.executemany("INSERT INTO payload(body) VALUES(?)", [(b"x"*4096,) for _ in range(6000)])con.commit()def snap(label): pages = con.execute("PRAGMA page_count").fetchone()[0] free = con.execute("PRAGMA freelist_count").fetchone()[0] print(label, "pages=", pages, "free=", free, "bytes=", p.stat().st_size)snap("after insert")con.execute("DELETE FROM payload WHERE id <= 5500")con.commit()snap("after delete")con.execute("VACUUM")snap("after VACUUM")con.close()Expect the delete to create many free pages without proportionally shrinking the file. VACUUM rebuilds a compact database and normally reduces both page_count and file length. Your exact counts are local.
What VACUUM actually means operationally
VACUUM rebuilds the database into a new compact image and replaces the original through SQLite’s transactional machinery. It can reclaim freelist space and reduce fragmentation/partially filled pages. It is therefore much heavier than “remove free pages.”
| Operational consideration | Why it matters |
|---|---|
| Free disk space | SQLite needs temporary space to build the compact copy; plan headroom. |
| Locks / active statements | VACUUM cannot run while its connection has an open transaction and requires exclusive write access to complete. |
| Time / I/O | Rewriting the database can be substantial for large files. |
| ROWIDs | VACUUM may change rowids for tables that do not declare an explicit INTEGER PRIMARY KEY. Application code must never treat hidden rowids as durable external IDs. |
| Page size / auto-vacuum format changes | VACUUM is also the mechanism used for some persistent file-format changes. |
Run VACUUM for a reason: reclaiming substantial unused disk space, changing supported file-format settings, or deliberately compacting a fragmented database after measurement. Rebuilding every database on a fixed schedule can add unnecessary I/O and downtime.
VACUUM INTO: compact copy instead of in-place rebuild
VACUUM INTO 'file' produces a compact database copy at a target filename rather than replacing the source. It can be useful for creating a cleaned snapshot/export. The destination must be handled as a new artifact; it is not a substitute for the online backup API’s concurrency semantics in every scenario.
-- Run from the source database connection.VACUUM INTO 'fieldnotes-compact-copy.db';-- Then open the output separately and verify:-- PRAGMA integrity_check;-- PRAGMA foreign_key_check;SQLite 3.53.x also has newer URI options for VACUUM INTO targets, but this chapter intentionally uses the simple portable form.
Disposable experiment B: INCREMENTAL auto-vacuum
Incremental auto-vacuum must be enabled before the database is populated (or enabled through a VACUUM rebuild). This fresh-file lab sets it before table creation.
-- On a NEW disposable database:PRAGMA auto_vacuum=INCREMENTAL;VACUUM; -- establishes the format if needed before normal usePRAGMA auto_vacuum;CREATE TABLE payload(id INTEGER PRIMARY KEY, body BLOB NOT NULL);-- Populate with your local seeder, then delete most rows.PRAGMA page_count;PRAGMA freelist_count;PRAGMA incremental_vacuum(100);PRAGMA page_count;PRAGMA freelist_count;-- Run again only if your maintenance policy intentionally wants more reclamation.PRAGMA incremental_vacuum(100);incremental_vacuum(N) requests removal of up to N pages from the freelist and truncates the file by the pages it can reclaim. It does nothing when the database is not in INCREMENTAL mode.
FULL auto-vacuum is not “VACUUM every commit”
FULL auto-vacuum moves freelist pages toward the file end and truncates at commit. It does not perform the complete repacking/defragmentation of a VACUUM rebuild and can itself increase fragmentation by moving pages. Choose it for a workload that values automatic file truncation enough to justify the pointer-map and layout tradeoffs.
Failure cases
| Mistake | Observed problem | Correction |
|---|---|---|
| Assume DELETE shrinks a NONE-mode file. | File remains large, freelist increases. | Reuse the space or plan a justified VACUUM. |
| Run incremental_vacuum on a NONE database. | No effect. | Use it only on a database prepared for INCREMENTAL auto-vacuum. |
| Enable auto_vacuum after tables exist and expect immediate format change. | Setting alone cannot add required pointer-map support to populated layout. | Follow documented set-mode + VACUUM process. |
| VACUUM a live production DB without capacity planning. | Contention, long I/O, insufficient disk risk. | Measure size, free disk, maintenance window, backups and workload first. |
| Expose hidden rowid as a permanent document ID. | VACUUM may change it for tables without INTEGER PRIMARY KEY. | Use an explicit stable key. |
Integrity and verification after maintenance
PRAGMA page_count;PRAGMA freelist_count;PRAGMA auto_vacuum;PRAGMA integrity_check;PRAGMA foreign_key_check;SELECT COUNT(*) FROM payload;File-size maintenance must not replace semantic verification. integrity_check validates structural/index/constraint consistency but, as Chapter 5 taught, foreign keys require the separate foreign_key_check.
Checkpoint and bridge
Space-management checkpoint
Choose the least disruptive tool that meets the requirement.
- Why does DELETE often leave the file size unchanged?
- What does freelist_count measure?
- What extra database-format support do FULL/INCREMENTAL auto-vacuum need?
- How does FULL auto-vacuum differ from VACUUM?
- When does incremental_vacuum have an effect?
- Why can hidden rowid be unsafe as an external identity across VACUUM?
Review the answers
DELETE commonly returns pages to an internal freelist instead of truncating the file. freelist_count is the number of unused pages in that file. FULL/INCREMENTAL require pointer-map information. FULL truncates freelist pages at commit but does not fully repack like VACUUM. incremental_vacuum acts only in INCREMENTAL mode. A hidden rowid is not guaranteed stable across a VACUUM when there is no explicit INTEGER PRIMARY KEY.
Production judgment and next step
Page allocation explains file size, but page access is mediated through memory. Lesson 5 completes the storage model with the pager, page cache, memory mapping, temporary B-trees/files, the OS cache, and the VFS boundary.