PostgreSQL · lesson 6 of 7
Intermediate
6- 1 EXPLAIN vs EXPLAIN ANALYZE
- 2 Indexing Strategy: When an Index Actually Helps
- 3 JSONB Containment Queries Need a GIN Index, Not a B-tree
- 4 VACUUM Doesn't Shrink Tables — VACUUM FULL Does, at a Cost
- 5 A Real Deadlock, and Why Lock Order Fixed It
- 6 The WAL Durability Tradeoff: 0.30s vs 0.02s for the Same 200 Inserts
Also part of PostgreSQL
7The WAL Durability Tradeoff: 0.30s vs 0.02s for the Same 200 Inserts
Every committed write has to hit durable storage before COMMIT returns — turning that guarantee off doesn't change what gets written, only when Postgres waits for it, and the difference is measured, not asserted.
By SkillByExample · Published
COMMIT isn’t just a bookkeeping signal — with default settings, it doesn’t return until the
transaction’s record is durably on disk. That guarantee has a real, measurable cost.
Every write goes to the WAL first
SELECT pg_current_wal_lsn();
SELECT wal_records, wal_fpi, wal_bytes FROM pg_stat_wal;
pg_current_wal_lsn
--------------------
0/90B5150
(1 row)
wal_records | wal_fpi | wal_bytes
-------------+---------+-----------
646971 | 8947 | 133386176
(1 row)
Every change this container has made so far — every INSERT, every VACUUM, every index
build across this whole track — is represented as a WAL record. pg_current_wal_lsn() is the
current position in that log; it only ever moves forward.
The cost of a durable commit, measured
200 individual INSERT statements, each its own autocommitted transaction, with the default
synchronous_commit = on:
time psql -U postgres -f many_inserts.sql > /dev/null
real 0m0.30s
Same 200 inserts, with synchronous_commit = off:
ALTER SYSTEM SET synchronous_commit = off;
SELECT pg_reload_conf();
time psql -U postgres -f many_inserts2.sql > /dev/null
real 0m0.02s
0.30 seconds down to 0.02 — roughly 15x, for the identical 200 rows. The rows are written to
the WAL either way; the only difference is whether COMMIT waits for that write to be flushed
to durable storage before returning. Turned off, the client gets control back immediately, and
a crash in the small window before the async flush completes could lose those commits.
ALTER SYSTEM SET synchronous_commit = on;
SELECT pg_reload_conf();
Reset back to the safe default before anything in this track depends on durability again.
Practice
1. Confirm synchronous_commit is back to on after resetting it.
SHOW synchronous_commit;
synchronous_commit
--------------------
on
(1 row)
Worth checking explicitly rather than assuming — ALTER SYSTEM writes to
postgresql.auto.conf and needs pg_reload_conf() (or a restart) to actually take effect, so
a forgotten reload leaves the setting silently unchanged.
2. Check how many WAL bytes a single large transaction generates versus many small ones.
SELECT wal_bytes FROM pg_stat_wal;
INSERT INTO books (title, author, genre, published_year)
SELECT 'Bulk ' || i, 'A', 'scifi', 2000 FROM generate_series(1, 5000) i;
SELECT wal_bytes FROM pg_stat_wal;
wal_bytes
-----------
133386176
(1 row)
INSERT 0 5000
wal_bytes
-----------
145688634
Roughly 12.3MB of WAL for 5,000 rows in one transaction — the WAL volume scales with the
amount of data changed, not with the number of separate commits, which is the other half of
why batching writes into fewer transactions helps throughput independent of the
synchronous_commit setting.
3. Look up the current checkpoint statistics.
SELECT num_timed, num_requested FROM pg_stat_checkpointer;
num_timed | num_requested
-----------+---------------
3 | 1
A checkpoint flushes all dirty data pages to disk and lets Postgres reuse older WAL segments —
num_timed counts checkpoints triggered by the regular checkpoint_timeout schedule (three
have already elapsed during this track’s testing session), while num_requested counts the
one triggered early, by VACUUM FULL in an earlier lesson.
Next: backup and point-in-time recovery — restoring a database to the exact moment before a mistake.