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.

Beginner120–140 minutesFreelist + VACUUM labSQLite 3.53.4 baselineDisposable databases onlyLast reviewed: August 2026

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.

01

Explain live pages versus freelist pages and why DELETE normally does not shrink the file under auto_vacuum=NONE.

02

Observe freelist_count before and after large deletes.

03

Compare NONE, FULL, and INCREMENTAL auto_vacuum behavior and the pointer-map requirement.

04

Explain VACUUM as a database rebuild/compaction with disk-space, locking, and rowid implications.

05

Distinguish VACUUM, PRAGMA incremental_vacuum, and VACUUM INTO.

06

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.

sql · observe free pages
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

ModeWhat happens to free pagesTradeoff / 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

python · vacuum-none.py
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 considerationWhy it matters
Free disk spaceSQLite needs temporary space to build the compact copy; plan headroom.
Locks / active statementsVACUUM cannot run while its connection has an open transaction and requires exclusive write access to complete.
Time / I/ORewriting the database can be substantial for large files.
ROWIDsVACUUM 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 changesVACUUM is also the mechanism used for some persistent file-format changes.
No weekly-VACUUM rule

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.

sql · create a compact copy
-- 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.

sql · incremental-vacuum.sql
-- 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

MistakeObserved problemCorrection
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

sql · post-maintenance checks
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.

  1. Why does DELETE often leave the file size unchanged?
  2. What does freelist_count measure?
  3. What extra database-format support do FULL/INCREMENTAL auto-vacuum need?
  4. How does FULL auto-vacuum differ from VACUUM?
  5. When does incremental_vacuum have an effect?
  6. 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.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.