PostgreSQLIndexes Overview

Indexes Overview

An index is a separate data structure that PostgreSQL maintains alongside a table, built specifically to make certain lookups fast. Without one, finding a row that matches a WHERE condition means scanning every row in the table in order — a sequential scan. With the right index, PostgreSQL can jump almost directly to the matching rows instead. This page covers what an index actually costs, how to create the simplest kind, and a concrete before/after look at the difference one makes.

The fundamental trade-off

An index is not free. Every INSERT, UPDATE, or DELETE that touches an indexed column has to also update the index, not just the table row — so more indexes means slower writes. Indexes also take up disk space, sometimes a substantial amount for a large table. The trade-off is: faster reads, slower writes, more storage. That is exactly why PostgreSQL does not simply index every column automatically — for a column that is rarely filtered or sorted on, an index would add write overhead for no real read benefit.

Without the right index

With the right index

Read (matching WHERE/JOIN/ORDER BY)

Sequential scan — checks every row

Index scan — jumps to matching rows

Write (INSERT/UPDATE/DELETE)

Only updates the table

Also updates every affected index

Storage

Just the table

Table plus index storage

Creating an index

A basic index

SQL
CREATE INDEX idx_orders_customer_id ON orders (customer_id);

This tells PostgreSQL to maintain a sorted structure over orders.customer_id, so any query filtering or joining on that column can use it instead of scanning the whole table.

Primary keys and UNIQUE constraints already have an index
PostgreSQL automatically creates a unique index behind every PRIMARY KEY and every UNIQUE constraint — that is in fact how it enforces the uniqueness rule efficiently. You never need to CREATE INDEX separately for a primary key column; it already has one from the moment the constraint is declared.
Seeing the difference: EXPLAIN before and after

EXPLAIN shows the query plan PostgreSQL intends to use, without actually running the query. Here is the same query against an orders table with a few hundred thousand rows, before and after adding an index on customer_id.

Before: no index on customer_id

SQL
EXPLAIN SELECT * FROM orders WHERE customer_id = 4821;
Seq Scan on orders  (cost=0.00..8934.00 rows=6 width=48)
  Filter: (customer_id = 4821)

After: adding the index

SQL
CREATE INDEX idx_orders_customer_id ON orders (customer_id);

EXPLAIN SELECT * FROM orders WHERE customer_id = 4821;
Index Scan using idx_orders_customer_id on orders  (cost=0.29..8.45 rows=6 width=48)
  Index Cond: (customer_id = 4821)

The Seq Scan is gone, replaced by an Index Scan, and the estimated cost drops from thousands down to single digits. On a small table PostgreSQL might reasonably choose a sequential scan anyway — reading a few hundred rows directly can be cheaper than the overhead of consulting an index — but as a table grows into the thousands or millions of rows, that gap keeps widening in the index's favor.

This is just the surface
EXPLAIN and its more detailed sibling EXPLAIN ANALYZE (which actually runs the query and reports real timings, not just estimates) deserve a page of their own — the Query Plans (EXPLAIN ANALYZE) page goes much deeper into reading plans, spotting the moments a query is not using the index you expect, and diagnosing why.
  • An index trades write speed and storage for faster reads on the indexed column(s).

  • CREATE INDEX ON table (column) is the basic form.

  • PRIMARY KEY and UNIQUE constraints already come with an automatic index — no separate CREATE INDEX needed.

  • EXPLAIN shows whether a query will use a Seq Scan or an Index Scan before you run it.