Indexes Overview B-tree · Hash · GIN · GiST · BRIN

Indexes — Introduction

A side data structure that lets the database jump straight to the rows you want instead of scanning every page. Learn the index types PostgreSQL offers, when each fits, and the trade-offs before you sprinkle them everywhere.

An index is a side data structure that helps the database find rows quickly. The classic analogy is the index at the back of a book: instead of flipping through every page to find a topic, you look it up in a sorted list and jump straight there. Database indexes work the same way — without one, finding a row means scanning the whole table; with one, the database can navigate directly to the matching rows.

The trade-off: faster reads in exchange for slightly slower writes (every INSERT / UPDATE / DELETE has to keep the index in sync) and some extra disk space. For most tables, that's a great deal — but only if the index actually gets used.

WITHOUT INDEX SELECT … WHERE id = 42 scan EVERY row in the table O(n) — slow on large tables WITH INDEX SELECT … WHERE id = 42 walk down the B-tree O(log n) — fast even on millions of rows Same query, very different work.
CREATE INDEX index_name
    ON table_name
    [USING index_type]
    (column_name [, column_name, ...]);

Defaults to a B-tree index, which is the right choice for the vast majority of cases.

TypeBest forNotes
B-treeEquality, range, ORDER BY, anything sortableDefault. Use it unless you have a reason not to.
HashEquality only (=)Slightly faster than B-tree for pure equality, but rarely worth it.
GINArrays, JSONB, full-text searchMany keys per row. Slower writes, fast contains-style queries.
GiSTGeometric data, ranges, full-textGeneralized for "nearest neighbour" and overlap queries.
SP-GiSTNon-balanced data (phone numbers, IPs)Space-partitioned variant of GiST.
BRINVery large tables with naturally ordered data (timestamps)Tiny size, lossy. Great for "data lake" style logs.
SQL
CREATE INDEX idx_customer_id
    ON customers (customer_id);

-- Same thing explicitly using B-tree:
CREATE INDEX idx_customer_id
    ON customers USING BTREE (customer_id);

Equality (=), range (<, >, BETWEEN), IS NULL, sorting (ORDER BY), and even pattern matching anchored at the left (LIKE 'abc%') all use B-tree indexes.

SQL
CREATE INDEX idx_product_id
    ON customers USING HASH (product_id);

Hash indexes only help with =. They're rarely the best choice — B-tree handles equality nearly as fast and supports much more besides.

SQL
-- JSONB key/value lookups
CREATE INDEX idx_doc_data ON documents USING GIN (data);

-- "tag in array" lookups
CREATE INDEX idx_post_tags ON posts USING GIN (tags);

-- Full-text search
CREATE INDEX idx_post_search ON posts USING GIN (to_tsvector('english', body));

GIN ("Generalized Inverted iNdex") shines when one row contains many searchable values — like every tag in an array, every word in a body of text, or every key in a JSONB document.

SQL
-- Logs table where rows are inserted in time order
CREATE INDEX idx_logs_created ON logs USING BRIN (created_at);

BRIN ("Block Range INdex") stores summary information for ranges of disk blocks. The index itself is tiny, but it relies on the data being naturally ordered by the indexed column. For a time-series log table that's almost always the case.

WinCost
Faster SELECT, JOIN, ORDER BYDisk space — sometimes more than the table itself
Faster constraint checks (UNIQUE, FOREIGN KEY)Slower INSERT, UPDATE, DELETE
Quicker MIN/MAX/COUNT in many casesIndex may need REINDEX as it bloats over time
💡 Don't index everything. An index that never gets used is pure overhead. Use EXPLAIN to see which queries are slow, then index the columns those queries filter or join on. Look at pg_stat_user_indexes later to find indexes that aren't pulling their weight.

Some indexes are created automatically — you don't have to ask:

  • Primary keys get a unique B-tree index automatically.
  • UNIQUE constraints create a unique B-tree index to enforce themselves.
  • Foreign keys get an index on the parent side (the PK) but not on the child side — see the FK page for why you usually want to add the child index yourself.
  • Unique index — enforce no-duplicates plus get a fast lookup.
  • Multicolumn index — index more than one column; column order matters.
  • Partial index — index only the rows you actually query.
  • Index on expression — index a computed value (e.g. LOWER(email)).
  • REINDEX / DROP / list — maintenance and inspection.