Journal modes and WAL

SQLite · Advanced · 14 min

WAL mode is the single biggest improvement for SQLite concurrency and write speed in most apps.

SQLite needs a journal to make transactions atomic. The default rollback journal (journal_mode = DELETE) copies original pages to a -journal file before changing the database. While a writer commits, it needs an exclusive lock, so readers and the writer block each other.

WAL mode (PRAGMA journal_mode = WAL) inverts this: changes are appended to a separate -wal file, and the main database file is updated later. Benefits:

  • Readers don't block the writer and the writer doesn't block readers. Each reader sees a consistent snapshot as of when its read transaction began.
  • Writes are usually faster: sequential appends and fewer fsyncs, especially with PRAGMA synchronous = NORMAL, which in WAL mode can lose the most recent commits on power loss but never corrupts the database.

There is still only one writer at a time.

Periodically the WAL content is copied back into the database file by a checkpoint — automatically when the WAL reaches about 1000 pages (wal_autocheckpoint), or manually with PRAGMA wal_checkpoint(TRUNCATE). A reader that never finishes can prevent the checkpoint from completing, letting the WAL grow without bound.

Details to know: WAL mode is persistent (stored in the file), so set it once. It creates -wal and -shm files next to the database; copy them along with it, or use the backup API. All processes must be on the same machine: WAL relies on shared memory and doesn't work over network filesystems.

Example

import sqlite3, os, tempfile

path = os.path.join(tempfile.mkdtemp(), "app.db")
w = sqlite3.connect(path, isolation_level=None)
print(w.execute("PRAGMA journal_mode = WAL").fetchone())     # ('wal',)
w.execute("PRAGMA synchronous = NORMAL")
w.execute("CREATE TABLE log (id INTEGER PRIMARY KEY, msg TEXT)")
w.execute("INSERT INTO log (msg) VALUES ('first')")

r = sqlite3.connect(path, isolation_level=None)
r.execute("BEGIN")                                            # reader snapshot
print(r.execute("SELECT count(*) FROM log").fetchone())       # (1,)

w.execute("BEGIN IMMEDIATE")                                  # writer not blocked
w.execute("INSERT INTO log (msg) VALUES ('second')")
w.execute("COMMIT")
print(r.execute("SELECT count(*) FROM log").fetchone())       # (1,) same snapshot
r.execute("COMMIT")
print(r.execute("SELECT count(*) FROM log").fetchone())       # (2,)

print(w.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone())   # (0, 0, 0)
print(sorted(os.listdir(os.path.dirname(path))))   # ['app.db', 'app.db-shm', 'app.db-wal']

Remember

  • WAL: readers and the single writer don't block each other
  • synchronous=NORMAL with WAL is fast and corruption-safe
  • Checkpoints move WAL content into the database; long readers can stall them

Watch out for

  • Copying only app.db while the app runs in WAL mode, leaving recent commits behind in app.db-wal.

Check yourself

  1. In WAL mode, what happens to a reader while a write transaction commits?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: sqlite.org/wal.html