Skip to main content
RunBook Academy

PostgreSQLX · Query Planning, Indexes and Performance MethodPlanner

EXPLAIN ANALYZE and its safety boundary

Intermediate⏱ ~25 minpsql

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

Not yet marked complete on this device.

The name suggests inspection. The behaviour is execution.

The measurement

Data-loss riskEXPLAIN ANALYZE on a DELETE
$ 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
-----------------------
                  500

Five 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

Read-only / Safethe same analysis with the write discarded
$ 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
----------------
          500

Unchanged. 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 (…) SELECTFROM 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 ANALYZE executes the statement. Measured: it deleted 500 rows.
  • BEGINROLLBACK is the safe form. Type the ROLLBACK first.
  • SET default_transaction_read_only = on makes 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, or auto_explain.
  • A query fast in psql and slow from the application is often a generic plan.

Cross-course references

  • Git, CI/CD & GitOps — Part LVII (Approval gates) covers why running EXPLAIN ANALYZE on 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

  1. Q1. An engineer runs EXPLAIN ANALYZE on a DELETE against production to see how long it would take. What happens?

  2. 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?

  3. 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?

  4. Q4. Which effects of an EXPLAIN ANALYZE on a write statement survive a ROLLBACK? Select all that apply.

  5. Q5. Setting default_transaction_read_only to on makes an investigation session unable to execute a write statement even under EXPLAIN ANALYZE.

  6. 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.