The library / Relational databases

Read a PostgreSQL query plan without getting lost

Follow the work through a query plan, compare estimates with actual rows, and learn what to investigate before adding another index.

The short answer

A query plan is a description of how PostgreSQL intends to produce your result. Start with the scan nodes, then follow joins, sorts, and aggregates toward the final output. Look for the part of the plan that does unexpectedly large amounts of work.

A sequential scan is not automatically a problem. Reading a small table, or most of a large table, may be cheaper than visiting rows through an index.

Set up a disposable example

Use a scratch database where creating a table will not affect an application. The following names belong only to this exercise.

CREATE TABLE demo_orders AS
SELECT
  n AS id,
  n % 1000 AS customer_id,
  DATE '2026-01-01' + (n % 180) AS ordered_on,
  (n % 200 + 1)::numeric AS total
FROM generate_series(1, 100000) AS n;

ANALYZE demo_orders;

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total
FROM demo_orders
WHERE customer_id = 42;

The query should return 100 rows. Record the scan type, estimated rows, actual rows, execution time, and buffer information. Exact timings and buffer counts depend on your environment.

Change one thing

CREATE INDEX demo_orders_customer_idx
ON demo_orders (customer_id);

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total
FROM demo_orders
WHERE customer_id = 42;

Compare the two plans. PostgreSQL may choose a bitmap or another index-based access path. The useful observation is whether less work is required for the same result, not whether a particular node name appears.

Ask better questions

When estimates and actual row counts diverge, investigate statistics and data distribution. When a node runs many times, account for its loop count. When a query sorts or hashes a large intermediate result, inspect the upstream join and filtering behavior.

Planner cost is not milliseconds. Parent-node work includes child work, so adding every displayed time together can mislead you.

Keep the measurement honest

EXPLAIN ANALYZE executes the statement. For data-changing statements, its effects are real. This example uses a SELECT in a disposable table.

Run comparisons under similar conditions, inspect more than one execution, and record your PostgreSQL version. An index also consumes space and increases write work; a faster read is only part of the tradeoff.

Your next experiment

Change the predicate to return half the table, then inspect the plan again. Notice how a useful index for a selective lookup may stop being the cheapest option for a broad query. Remove the demo table when you are finished with the exercise.

Keep exploring

Go deeper with the original documentation.

Official documentation
D
DBMinutes Editorial

Practical explanations of database systems, cloud services, and the engineering decisions between them.

AI-assisted content · Our editorial process

Keep the curiosity going.

Back to the library
A little learning goes a long way

Make room for a few good minutes.

Join the list for practical guides, thoughtful comparisons,
and ideas worth bringing to your next project.

Find your next answer

Search concepts, tools, and practical guides.