Chapter 11 · Database Files, Pages, B-Trees, Freelist, and Storage Internals
Payload, Overflow Pages, Large Rows, and BLOB/Text Storage
Follow large row payloads from B-tree cells into overflow pages, measure the storage effect of wide TEXT/BLOB values, and choose between database BLOBs and external files using application and operational requirements rather than slogans.
Learning outcomes
A B-tree leaf page has finite usable space. SQLite therefore cannot promise that an arbitrarily large TEXT or BLOB value will fit entirely inside one cell on one page. Instead, a cell keeps some payload locally and links additional overflow pages when the record is too large. That mechanism is why a row can be much larger than the database page size—and why large payloads have locality and operational consequences.
Explain local cell payload versus overflow-page payload without memorizing file-format formulas.
Observe overflow pages with dbstat when the build supports it and provide a no-dbstat fallback.
Measure database/page growth for small rows versus large TEXT/BLOB rows.
Discuss when BLOBs belong inside SQLite and when external/object storage may be operationally preferable.
Explain incremental BLOB I/O as a driver/API capability and its important rowid/size constraints.
Design storage for images, documents, and telemetry from access, backup, atomicity, lifecycle, and portability requirements.
Why overflow exists
Suppose a database uses 4 KiB pages and you insert a 200 KiB BLOB. The table cell cannot consume dozens of neighboring bytes outside its page; page boundaries are structural. SQLite stores an amount of the record payload locally according to documented page-format rules, then stores the remainder in one or more overflow pages linked as a chain.
table leaf page
+--------------------------------------+
| cell: rowid + local payload + ptr ----+----> overflow page #1
+--------------------------------------+ |
v
overflow page #2
|
v
...The exact local/overflow split depends on page type, usable page size, and payload length. Practitioners normally need the consequence, not the formula: a large row may require multiple page reads and additional page writes.
Measure small versus large payloads
Use separate tables so the contrast is easy to inspect. The BLOB values are generated locally with zeroblob(); there is no download and no real document content.
DROP TABLE IF EXISTS small_payload;DROP TABLE IF EXISTS large_payload;CREATE TABLE small_payload( id INTEGER PRIMARY KEY, body TEXT NOT NULL);CREATE TABLE large_payload( id INTEGER PRIMARY KEY, body BLOB NOT NULL);PRAGMA page_size;PRAGMA page_count;WITH RECURSIVE seq(x) AS ( VALUES(1) UNION ALL SELECT x+1 FROM seq WHERE x<2000)INSERT INTO small_payload(body)SELECT printf('small-%05d-%0200d',x,x) FROM seq;PRAGMA page_count;WITH RECURSIVE seq(x) AS ( VALUES(1) UNION ALL SELECT x+1 FROM seq WHERE x<20)INSERT INTO large_payload(body)SELECT zeroblob(262144) FROM seq; -- 256 KiB × 20 rowsPRAGMA page_count;SELECT COUNT(*), sum(length(body)) FROM large_payload;The 20 large BLOB rows contain about 5 MiB of logical payload, so the file should grow by many pages. Exact overhead depends on page size and packing. This is a storage observation—not a speed benchmark.
Observe overflow with dbstat when available
SELECT sqlite_compileoption_used('ENABLE_DBSTAT_VTAB') AS has_dbstat;-- Only if has_dbstat=1:SELECT name, pagetype, COUNT(*) AS pages, SUM(payload) AS payload_bytes, SUM(unused) AS unused_bytes, MAX(mx_payload) AS largest_cell_payloadFROM dbstatWHERE name IN ('small_payload','large_payload')GROUP BY name, pagetypeORDER BY name, pagetype;SELECT pageno, path, pagetype, payload, unused, mx_payloadFROM dbstatWHERE name='large_payload' AND pagetype='overflow'LIMIT 12;Expect large_payload to show overflow pages in a dbstat-enabled build. If dbstat is unavailable, use the page_count/file-size experiment plus the official file-format diagram; the overflow mechanism is not optional even though this inspection interface is.
Large rows change locality
A narrow row often lets many logical records share one leaf page. Large fields reduce that packing density and can create overflow chains. Therefore, fetching one logical row may touch multiple database pages, and a scan that selects wide payload columns can transfer far more data through the pager and application boundary than a metadata-only query.
| Query pattern | Storage implication |
|---|---|
| List 100 document titles/IDs | Can often read comparatively narrow index/table records. |
| List 100 documents including multi-megabyte bodies | Must retrieve and copy far more payload/overflow content. |
| Covering index for metadata columns | Can avoid reading the large table payload for metadata-only queries. |
| UPDATE a large BLOB | May dirty many pages; journaling/WAL must protect changed pages. |
Chapter 10 warned against SELECT * for access-path reasons. Large payloads add a storage reason: selecting a BLOB you do not need can force payload pages through SQLite and your driver.
BLOB inside SQLite or external file/object?
There is no universal threshold at which BLOBs “must” leave SQLite. The decision is operational. SQLite can store binary data transactionally with related metadata, while a filesystem/object store can offer independent streaming, CDN/object lifecycle tools, and avoid copying huge blobs during some database operations.
| Requirement | SQLite BLOB may be attractive | External/object storage may be attractive |
|---|---|---|
| Atomic metadata + binary update | One database transaction can update both. | Requires an application protocol/outbox/compensation across systems. |
| Single-file portability | Data travels with the database file. | Requires copying database + external object set consistently. |
| Very large media streaming | Possible, including incremental BLOB APIs. | Often integrates naturally with HTTP range/object delivery. |
| Backups | Database backup includes BLOB payload. | Database backups stay smaller but object backups are separate. |
| Access control | Application controls DB access. | Object store can provide separate policies/URLs. |
| Dedup/lifecycle/CDN | Application must implement policy. | Object platforms commonly provide specialized lifecycle features. |
Measure with your real payload sizes, backup method, write pattern, and restore requirements. “Filesystem is always faster” and “SQLite is always simpler” are both too broad.
Incremental BLOB I/O: avoid materializing the entire value at once
SQLite’s C API exposes sqlite3_blob_open(), sqlite3_blob_read(), and sqlite3_blob_write(). Many language drivers expose corresponding functionality. An incremental BLOB handle can read or overwrite subsections of an existing TEXT/BLOB value without first copying the entire value into one application buffer.
Important limits matter: the handle targets a rowid-table row; it does not work on WITHOUT ROWID tables; writes cannot change the BLOB size; and the row/column has additional restrictions when used by certain indexes/constraints. A common pattern is to create a fixed-size value with zeroblob(N), then fill it incrementally.
import sqlite3con = sqlite3.connect("fieldnotes-storage.db")con.execute("DROP TABLE IF EXISTS blob_stream")con.execute("CREATE TABLE blob_stream(id INTEGER PRIMARY KEY, data BLOB NOT NULL)")rowid = con.execute( "INSERT INTO blob_stream(data) VALUES(zeroblob(?)) RETURNING id", (1024*1024,)).fetchone()[0]# Python 3.11+ sqlite3 exposes Connection.blobopen().with con.blobopen("blob_stream", "data", rowid, readonly=False) as blob: blob.seek(0) blob.write(b"FIELDNOTES") blob.seek(0) print(blob.read(10)) # b'FIELDNOTES' print(len(blob)) # 1048576con.commit()con.close()If your driver lacks an incremental-BLOB API, bind BLOB parameters normally and design chunking at the application layer. Do not concatenate binary data into SQL text.
Failure cases and diagnosis
| Symptom | Likely lesson | Diagnosis / correction |
|---|---|---|
| Database grows dramatically after adding images. | Logical payload now needs many pages/overflow. | Measure length, page_count, dbstat if available; decide whether DB storage still meets operations needs. |
| Metadata list query feels heavier after adding BLOBs. | Query may be selecting payload unnecessarily. | Project only metadata; inspect plan and selected columns. |
| Incremental BLOB open fails on WITHOUT ROWID table. | API requires a rowid target. | Use normal bound-value reads/writes or redesign if incremental I/O is essential. |
| Incremental write cannot extend the BLOB. | Blob API overwrites existing size only. | UPDATE to a new-sized value or preallocate with zeroblob. |
| Backups suddenly become huge. | Embedded payload is part of database state. | Include payload volume in backup/restore design before choosing storage. |
Design exercise: three payload classes
Payload design review
Choose a representation and explain the operational reason.
- A 40 KiB signed PDF must remain transactionally bound to its record and travel in one offline project file. Where would you start?
- A 4 GiB training video is streamed over HTTP to thousands of clients while SQLite stores catalogue metadata. What is the likely split?
- A sensor emits 200-byte binary packets at high frequency. What matters more than the fact that the values are BLOBs?
- Why should a metadata query avoid selecting a 5 MiB payload column when it only needs title/status?
- What does an overflow page solve?
- What must be true about BLOB size for incremental writes?
Review the answers
The small transactionally bound PDF is a reasonable SQLite-BLOB candidate. The multi-gigabyte streamed video is usually better as external/object content with SQLite metadata, subject to system requirements. For telemetry, write volume, batching, retention, indexing and query shape matter more than the BLOB label. Narrow projection avoids moving unnecessary overflow payload. Overflow pages extend a cell beyond its leaf page. Incremental blob writes can overwrite only within the existing allocated value size.
Bridge
Large payloads explain one way files grow. Deletions explain another surprise: removing data often does not make the operating-system file smaller. Lesson 4 follows freed pages onto the freelist and compares auto-vacuum modes, full VACUUM rebuilds, incremental vacuum, and VACUUM INTO.