--- name: postgres-explain-analyze description: Reading an EXPLAIN ANALYZE plan to find why a query is slow, rather than adding indexes hopefully. when_to_use: A query is slow, or you want to confirm an index is actually used. tags: [backend, postgres] --- # Read the plan; do not guess at indexes An index added without a plan is as likely to be unused as to help, and each one costs write throughput forever. ## Always ANALYZE, and usually BUFFERS ```sql EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...; ``` `EXPLAIN` alone shows the planner's *estimate*. `ANALYZE` executes and shows what happened, which is the only thing worth reading. `BUFFERS` shows whether the data came from cache or disk — a "slow" query that is entirely `shared read` is an I/O problem, not a plan problem. Note that `ANALYZE` actually runs the statement. Wrap a mutation in a transaction you roll back. ## Read it inside out, and look for two things Plans nest; the innermost node runs first. Scan for: 1. **The largest `actual time`,** not the largest estimate. That is where the time went. 2. **Estimate versus actual rows.** `rows=10` with `actual rows=48000` is the planner being wrong, and a wrong estimate is usually the *cause* of a bad plan — it picked a nested loop because it expected ten rows. Fix the statistics (`ANALYZE `, or raise the statistics target) before touching the query. ## What the node types tell you - **Seq Scan** on a large table with a selective filter → a missing index, or a filter the index cannot serve (a function on the column, a leading wildcard). - **Nested Loop** with a large outer side → usually the wrong-estimate problem above; correct rows would have produced a hash join. - **Sort** with `Sort Method: external merge Disk` → `work_mem` too small, or an index could provide the order for free. - **Bitmap Heap Scan** with high `Rows Removed by Filter` → the index found candidates the table had to reject; consider a composite or partial index. ## Confirm the index is used, not just present After adding one, re-run the plan. An index that does not appear is dead weight: it slows every write and helps nothing. Common causes are a type mismatch, a function on the column, or a column order that does not match the predicate — see `postgres-index-selection`. ## Test against realistic data volume Plans change with size. Every plan is a Seq Scan on a thousand rows, and the planner is right to choose it. Validate on production-shaped data or the exercise is theatre.