PostgreSQLX · Query Planning, Indexes and Performance MethodPlanner
EXPLAIN ANALYZE and its safety boundary
What you'll learn
- State exactly what EXPLAIN ANALYZE executes and what it does not
- Analyse a write statement safely on a production system
- Recognise the side effects that a rollback does not undo
- Choose between EXPLAIN, EXPLAIN ANALYZE and auto_explain for a given question
Prerequisites
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
The name suggests inspection. The behaviour is execution.
The measurement
$ psql -U postgres -c "SELECT count(*) FROM safety" -c "EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) DELETE FROM safety WHERE id <= 500" -c "SELECT count(*) FROM safety" before_explain_analyze
------------------------
1000
Delete on safety (actual rows=0.00 loops=1)
Buffers: shared hit=505
-> Seq Scan on safety (actual rows=500.00 loops=1)
Filter: (id <= 500)
Rows Removed by Filter: 500
after_explain_analyze
-----------------------
500Five hundred rows gone. EXPLAIN ANALYZE is the statement plus
instrumentation, not a simulation of it.
EXPLAIN without ANALYZE is safe: it plans and prints, and
executes nothing.
The safe form
$ psql -U postgres -c "SELECT count(*) FROM safety" -c "BEGIN" -c "EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF) DELETE FROM safety WHERE id > 900" -c "ROLLBACK" -c "SELECT count(*) FROM safety" before
--------
500
... plan output ...
after_rollback
----------------
500Unchanged. This is the pattern to use for any write statement you need real numbers for:
BEGIN;
EXPLAIN (ANALYZE, BUFFERS) UPDATE orders SET status = 'archived' WHERE created_at < '2024-01-01';
ROLLBACK;
What a rollback does not undo
The transaction is reverted. Four things are not.
Sequence advances. nextval() is deliberately non-transactional, so
that concurrent sessions never receive the same value. An EXPLAIN ANALYZE of an INSERT consumes sequence values permanently.
SELECT last_value FROM orders_id_seq;
BEGIN;
EXPLAIN (ANALYZE) INSERT INTO orders (…) SELECT … FROM staging;
ROLLBACK;
SELECT last_value FROM orders_id_seq; -- advanced by the row count
Harmless in most schemas. Not harmless where the sequence feeds
externally-visible identifiers such as invoice numbers, and impossible
to explain afterwards if nobody knows an EXPLAIN was run.
External side effects. A trigger that writes to a foreign data wrapper, calls an extension that sends a notification, or writes to a file has already done so. Rollback governs this database, not the world.
Resource consumption. The statement ran. It read the pages, filled
shared buffers, wrote WAL for the changes it made, dirtied pages that a
checkpoint will flush, and generated dead tuples that vacuum must
reclaim. On a large UPDATE this is real load, and the rollback does not
give it back.
Locks, until the rollback happens. Between the EXPLAIN ANALYZE and
the ROLLBACK, every row the statement touched is locked. That window is
as long as you leave it.
EXPLAIN on a parameterised query
A query the application runs with parameters may not plan the same way
when you paste literals into psql. The planner sees the literal and
uses the exact value against the statistics; with a parameter it may
produce a generic plan instead.
PREPARE q (int) AS SELECT * FROM orders WHERE customer_id = $1;
EXPLAIN (ANALYZE, BUFFERS) EXECUTE q (42);
Executing a prepared statement several times is what makes PostgreSQL consider switching to a generic plan, so run it more than once if you are investigating a plan that only misbehaves in production.
What to take from this
EXPLAIN ANALYZEexecutes the statement. Measured: it deleted 500 rows.BEGIN…ROLLBACKis the safe form. Type theROLLBACKfirst.SET default_transaction_read_only = onmakes an investigation session incapable of writing.- Rollback does not undo sequence advances, external side effects, resource consumption or the locks held meanwhile.
- On production prefer plain
EXPLAIN, a restored copy, orauto_explain. - A query fast in
psqland slow from the application is often a generic plan.
Cross-course references
- Git, CI/CD & GitOps — Part LVII (Approval gates) covers why
running
EXPLAIN ANALYZEon a writing statement in production is a change, not an investigation, and belongs behind the same gate. - Observability for Production Sysadmins — Part CIX (Incident investigation workflows) covers capturing a plan as evidence with the timestamp and the settings that produced it.
Quiz
Knowledge check · 6 questions
Q1. An engineer runs EXPLAIN ANALYZE on a DELETE against production to see how long it would take. What happens?
Q2. A team wraps EXPLAIN ANALYZE of an INSERT in BEGIN and ROLLBACK. Days later the invoice numbering has gaps nobody can account for. What happened?
Q3. A query is consistently fast when tested in psql with literal values and slow when the application runs it. Data and server are identical. What is the most likely explanation?
Q4. Which effects of an EXPLAIN ANALYZE on a write statement survive a ROLLBACK? Select all that apply.
Q5. Setting default_transaction_read_only to on makes an investigation session unable to execute a write statement even under EXPLAIN ANALYZE.
Q6. You need real timings for a large UPDATE on production. What options would you consider, and in what order?
Passing score: 75%. Answers are checked in this browser.