SQLite Cheatsheet

Pragmas and WAL

Use this SQLite reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Pragmas are per connection

Almost every pragma applies to one connection and is forgotten when it closes. Only a handful are stored in the database file itself.

PRAGMA foreign_keys;         -- read the current value
PRAGMA foreign_keys = ON;    -- set it, on this connection only
PRAGMA main.cache_size;      -- scope a pragma to one attached database
PRAGMA temp.temp_store;
Persisted in the filePer connection, must be re-set every time
journal_mode (once WAL, always WAL)foreign_keys
page_size (fixed at creation, or via VACUUM)busy_timeout
auto_vacuumcache_size
encoding (before the first write)synchronous
application_id, user_versionmmap_size, temp_store, recursive_triggers
wal_autocheckpoint is a file-level default with a per-connection overridequery_only, defer_foreign_keys

Because of that split, set the per-connection ones in your pool's on-connect hook, not in a migration. A connection that skips PRAGMA foreign_keys = ON writes orphan rows the other connections cannot prevent.

Every pragma that returns a value also exists as a table-valued function, which is how you read one from application code without a special API.

SELECT * FROM pragma_table_info('users');
SELECT * FROM pragma_index_list('users');
SELECT * FROM pragma_foreign_key_list('posts');
SELECT * FROM pragma_table_list;

Also worth knowing: an unrecognized pragma name is silently ignored rather than raising, so a typo like PRAGMA foriegn_keys = ON does nothing at all and reports success. Read the value back to confirm it took.

journal_mode = WAL

The default journal is a rollback journal, where a writer copies original pages into file.db-journal and every reader is blocked for the duration. WAL inverts that: a writer appends new pages to file.db-wal and readers keep reading the old ones.

PRAGMA journal_mode;         -- delete, on a fresh database
PRAGMA journal_mode = WAL;   -- returns 'wal' on success
PRAGMA journal_mode;         -- wal, and it persists in the file
ModeBehavior
DELETERollback journal, deleted on commit. The default
TRUNCATESame, truncated to zero instead of deleted. Slightly faster
PERSISTSame, header zeroed instead. Avoids directory writes
MEMORYJournal in RAM. A crash mid-transaction corrupts the database
WALWrite-ahead log. Readers and one writer proceed concurrently
OFFNo journal, no rollback, no atomicity. Bulk import into a throwaway file only

What WAL buys, and what it costs:

- Readers never block the writer and the writer never blocks readers. Writers still serialize against each other, one at a time. - Commits are usually faster, because they are a sequential append. - It needs three files, file.db, file.db-wal, and file.db-shm, and the -shm is a shared-memory coordination file. - It requires real shared memory, so WAL does not work over NFS or SMB, and it does not work on a read-only filesystem unless the -wal is already checkpointed away. - PRAGMA journal_mode = WAL is a persistent property, so every later connection uses it. - It cannot be set inside a transaction.

-wal and -shm are not optional extras. The -wal file holds committed data that is not yet in the main file, so copying only file.db from a live WAL database loses recent commits and can produce a corrupt copy. Delete the -wal of a cleanly closed database and you lose whatever it held.

ls -la app.db*
# app.db  app.db-shm  app.db-wal

PRAGMA locking_mode = EXCLUSIVE combined with WAL lets a single-process application drop the -shm file entirely, which is how WAL works on a filesystem without shared memory.

synchronous, and the durability tradeoff

synchronous controls how often SQLite calls fsync, which is the only thing standing between a power cut and lost data.

ValueNameGuarantees
3EXTRAAlso syncs the directory after a rollback-journal commit
2FULLDurable against a power loss. The upstream default
1NORMALDurable against an application crash. A power loss can lose recent commits
0OFFNo syncing. A power loss can corrupt the database
PRAGMA synchronous;           -- 2 on a stock build
PRAGMA synchronous = NORMAL;  -- 1

With WAL, synchronous = NORMAL is the recommended setting and it cannot corrupt the database, only lose transactions committed since the last checkpoint. With a rollback journal, NORMAL can corrupt on power loss. So the pairing matters: WAL plus NORMAL is safe and fast, DELETE plus NORMAL is not.

synchronous = OFF is only for a database you can recreate from scratch, for example a build cache or an import scratch file.

Note that some distributions ship altered defaults. The sqlite3 that comes with macOS, for instance, reports synchronous = 1, secure_delete = 2, and legacy_alter_table = 1, none of which are the upstream defaults. Always read a pragma back rather than assuming.

SELECT sqlite_version(), sqlite_source_id();

Checkpointing

A checkpoint copies pages from the -wal back into the main database file. It happens automatically when the WAL passes wal_autocheckpoint pages, by default 1000 pages, which at a 4096-byte page size is about 4MB.

PRAGMA wal_autocheckpoint;         -- 1000 pages
PRAGMA wal_autocheckpoint = 4000;  -- less frequent, larger WAL
PRAGMA wal_autocheckpoint = 0;     -- disable, you must checkpoint manually
PRAGMA journal_size_limit = 67108864;   -- cap the WAL at 64MB after a checkpoint
PRAGMA wal_checkpoint(PASSIVE);    -- default. Never blocks, may do nothing
PRAGMA wal_checkpoint(FULL);       -- wait for readers, copy everything
PRAGMA wal_checkpoint(RESTART);    -- FULL, then force the next writer to restart the WAL
PRAGMA wal_checkpoint(TRUNCATE);   -- RESTART, then shrink the WAL file to zero
-- returns three integers: busy | wal_pages | pages_checkpointed

The three-column result is busy (1 if it could not finish), the WAL size in pages, and how many pages were moved.

The failure mode to know: a long-running read transaction prevents checkpointing, so the -wal grows without bound. An analytics query that holds a read snapshot for an hour can leave a multi-gigabyte WAL, and disk fills up. Keep read transactions short, and if a long one is unavoidable, run it against a VACUUM INTO copy.

-- What to run before a backup or on a clean shutdown
PRAGMA wal_checkpoint(TRUNCATE);

busy_timeout and SQLITE_BUSY

Only one connection may write at a time. A second writer gets SQLITE_BUSY immediately unless you tell SQLite to wait.

PRAGMA busy_timeout;          -- 0, no waiting at all
PRAGMA busy_timeout = 5000;   -- wait up to 5000 ms for a lock
Error: database is locked

That message is SQLITE_BUSY, and a default-configured connection produces it under the lightest concurrency. Set busy_timeout on every connection, 3000 to 10000 ms is typical.

Two cases busy_timeout does not save you from:

- A deferred transaction upgrading to a write. BEGIN takes a read lock, and if another writer commits before your first UPDATE, the upgrade fails with SQLITE_BUSY_SNAPSHOT and no amount of waiting helps, because waiting would deadlock. The fix is BEGIN IMMEDIATE for any transaction that will write, which takes the write lock up front where the timeout does apply. - A checkpoint blocked by a reader, which returns busy rather than waiting forever.

Error: database is locked (SQLITE_BUSY_SNAPSHOT)
-- Correct shape for a writing transaction
PRAGMA busy_timeout = 5000;
BEGIN IMMEDIATE;
  UPDATE accounts SET balance = balance - 50 WHERE id = 1;
  UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;

Also retry on busy at the application level, with a small backoff, because a timeout can still expire under real contention.

Performance and memory pragmas

PRAGMA cache_size = -64000;   -- NEGATIVE means KiB, so 64 MiB. Positive means PAGES
PRAGMA mmap_size = 268435456; -- 256 MiB memory-mapped I/O, 0 disables
PRAGMA temp_store = MEMORY;   -- temp tables and sort files in RAM (2)
PRAGMA page_size = 8192;      -- before the first write, or with a VACUUM
PRAGMA auto_vacuum = INCREMENTAL;
PRAGMA threads = 4;           -- worker threads for a large sort
PRAGMA analysis_limit = 400;  -- cap the work PRAGMA optimize will do
PragmaDefaultNotes
cache_size-2000, so 2 MiBNegative is KiB, positive is a page count. Per connection
mmap_size0Reads bypass the page cache. Helps read-heavy loads
temp_store0 (DEFAULT)1 FILE, 2 MEMORY
page_size4096Only settable before the first table, or via VACUUM
auto_vacuum0 (NONE)1 FULL, 2 INCREMENTAL. Changing it needs a VACUUM
secure_delete01 overwrites deleted content, slower. 2 is FAST
journal_size_limit-1, unlimitedBytes to shrink the WAL or journal back to
locking_modeNORMALEXCLUSIVE keeps the file lock held between transactions
query_only01 makes the connection reject every write

auto_vacuum reclaims free pages as rows are deleted, at the cost of extra bookkeeping on every write. INCREMENTAL lets you reclaim on your own schedule.

PRAGMA auto_vacuum = INCREMENTAL;   -- then VACUUM once to apply
PRAGMA incremental_vacuum(100);     -- release 100 free pages now
PRAGMA freelist_count;              -- pages currently free
PRAGMA page_count;                  -- total pages, times page_size is the file size

Statistics, integrity, and introspection

ANALYZE gathers table and index statistics into sqlite_stat1, and the planner uses them to choose between indexes. Without it, the planner guesses.

ANALYZE;                 -- the whole database, can be slow on a large one
ANALYZE users;           -- one table and its indexes
PRAGMA optimize;         -- run the analyses that are actually worth running
PRAGMA optimize(0x02);   -- see the recommended commands without running them
PRAGMA analysis_limit = 400;   -- bound the work, then optimize is cheap

Run PRAGMA optimize before closing every long-lived connection, with analysis_limit set to a few hundred. It is the documented recommendation, it is nearly free, and it keeps sqlite_stat1 fresh without the full cost of ANALYZE.

PRAGMA integrity_check;        -- full check, reports 'ok' or a list of problems
PRAGMA integrity_check(10);    -- stop after 10 errors
PRAGMA quick_check;            -- skips the slow index-content comparison
PRAGMA foreign_key_check;      -- every violating row in the database

integrity_check reads every page, so it is O(database size). quick_check is much faster and catches most real corruption. Neither repairs anything, and the recovery path for a damaged file is .recover.

PRAGMA table_info(users);      -- cid, name, type, notnull, dflt_value, pk
PRAGMA table_xinfo(users);     -- the same, plus hidden and generated columns
PRAGMA table_list;             -- every table in every attached database
PRAGMA index_list(users);      -- seq, name, unique, origin, partial
PRAGMA index_info(idx_name);   -- the indexed columns
PRAGMA index_xinfo(idx_name);  -- plus direction, collation, and key/value role
PRAGMA foreign_key_list(posts);
PRAGMA database_list;          -- seq, name, file, for main and every attachment
PRAGMA compile_options;        -- how this binary was built
PRAGMA function_list;          -- every SQL function available
PRAGMA module_list;            -- every virtual table module, fts5 included

table_xinfo is the one to use when generated columns are involved, since table_info hides them. Its extra hidden column is 0 for a normal column, 2 for a VIRTUAL generated column, and 3 for a STORED one.

user_version for migrations

SQLite has no migration table built in, but it has a 32-bit integer in the file header that is perfect for a schema version.

PRAGMA user_version;        -- 0 on a fresh database
PRAGMA user_version = 3;    -- write it, persists in the file
PRAGMA application_id;      -- another header integer, for a magic number
PRAGMA application_id = 0x48554442;

A complete migration runner is about ten lines.

-- Read the version, then apply only what is missing, bumping as you go
PRAGMA foreign_keys = OFF;
BEGIN IMMEDIATE;
  -- if user_version < 1
  CREATE TABLE users(id INTEGER PRIMARY KEY, email TEXT UNIQUE NOT NULL) STRICT;
  PRAGMA user_version = 1;
  -- if user_version < 2
  ALTER TABLE users ADD COLUMN name TEXT;
  PRAGMA user_version = 2;
COMMIT;
PRAGMA foreign_keys = ON;

PRAGMA user_version is transactional, so a rolled-back migration rolls back its version bump too. Keep PRAGMA foreign_keys outside the transaction, since it is a no-op inside one.

application_id is a separate header field for identifying the file type, so file app.db and other tools can recognize your schema. Pick a four-byte value and register it in the SQLite magic-number list if the format is public.

VACUUM

VACUUM rebuilds the database file from scratch, defragmenting it and returning free pages to the filesystem.

VACUUM;                          -- rebuild main in place
VACUUM INTO 'compact.db';        -- 3.27+, write a fresh compacted copy
PRAGMA page_size = 8192; VACUUM; -- the only way to change page_size after creation
PRAGMA auto_vacuum = FULL; VACUUM;   -- the only way to change auto_vacuum

Costs and constraints: it needs free disk space of roughly twice the database size, it takes a write lock for the whole operation, it cannot run inside a transaction, and it invalidates every open statement. It also changes rowids in a table that has no INTEGER PRIMARY KEY, which is one more reason to always declare one.

VACUUM INTO is the version to reach for. It leaves the original untouched, needs no exclusive lock, and produces a compacted, checkpointed, single-file copy, which makes it both a defragmenter and the modern online backup.