Course topics

By WebNest Studio Engineering Team

PostgreSQL Tutorial

Indexes in PostgreSQL: B-tree, GIN, and GiST

An index is a separate data structure that lets PostgreSQL find rows matching a condition without scanning the entire table. Choosing the right index type for the right kind of query and data is one of the highest-leverage skills in database work — the wrong index (or no index) can turn a millisecond query into one that takes seconds.

PostgreSQL supports several index types, each optimized for a different access pattern. The three you will use most often are B-tree (the default, for equality and ranges), GIN (for values containing multiple elements, like JSONB and arrays), and GiST (for geometric, range, and full-text search scenarios).

Why it matters

PostgreSQL has several kinds of index, each built for a different sort of question. A B-tree is useless for searching inside a JSON document, and a GIN index is useless for sorting by date. Picking the right type is the difference between an index that helps and one that is never used.

B-tree: The Default, General-Purpose Index

CREATE INDEX idx_name ON table(column); creates a B-tree index by default. B-tree indexes excel at equality (=), range comparisons (<, >, BETWEEN), and sorting (ORDER BY), which covers the vast majority of everyday query filtering. Primary keys and unique constraints are automatically backed by a B-tree index.

GIN: Indexing "Contains Multiple Values" Data

A Generalized Inverted Index (GIN) is built for columns where each value effectively contains many searchable "elements" — JSONB documents, arrays, and full-text search vectors (tsvector). GIN builds an index entry per element rather than per row, which is exactly what makes containment queries like @> on JSONB or && (overlap) on arrays fast. GIN indexes are slower to update than B-tree but much faster for these containment-style lookups.

GiST: Indexing Geometric and Range-Like Data

A Generalized Search Tree (GiST) index supports more exotic notions of "nearest" or "overlapping" that a strict ordering (like B-tree) cannot express well — geometric data (points, boxes), range types (tsrange, int4range), and full-text search are common use cases. GiST is also the index type behind PostgreSQL's exclusion constraints, which can enforce, for example, that no two date ranges in a bookings table overlap.

Choosing the Right Index

As a practical rule: use B-tree by default for ordinary WHERE/ORDER BY columns; reach for GIN when the column is JSONB, an array, or a full-text tsvector and you filter with containment operators; reach for GiST for geometric/range data or exclusion constraints. Always verify an index is actually being used with EXPLAIN — an unused index still costs write performance and disk space.

Examples

B-tree index for a common equality/range filter

SQL
CREATE INDEX idx_products_price ON products(price);

EXPLAIN SELECT * FROM products WHERE price < 500;
Output
                        QUERY PLAN
------------------------------------------------------------
 Index Scan using idx_products_price on products
   Index Cond: (price < '500'::numeric)

GIN index enabling a fast JSONB containment query

SQL
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);

EXPLAIN SELECT * FROM products WHERE attributes @> '{"ram_gb": 16}';
Output
                          QUERY PLAN
---------------------------------------------------------------
 Bitmap Heap Scan on products
   Recheck Cond: (attributes @> '{"ram_gb": 16}'::jsonb)
   ->  Bitmap Index Scan on idx_products_attrs

Common Mistakes

  • Creating a plain B-tree index on a JSONB or array column and expecting containment queries (@>, &&) to use it — those need GIN.
  • Adding indexes to every column "just in case," which slows down every INSERT/UPDATE without necessarily speeding up reads that never filter on those columns.
  • Forgetting to run EXPLAIN to confirm an index is actually chosen by the planner — a low-selectivity index (e.g. on a boolean column) is often ignored in favor of a sequential scan.
  • Indexing a column used only with functions or expressions (like LOWER(email)) with a plain index instead of a matching expression index, so the index is silently never used.

Exercise

This exercise uses PostgreSQL index types, so it needs a PostgreSQL server. For the events table, create three indexes: a B-tree index for filtering and sorting by created_at; a GIN index so that containment queries on the JSONB column payload are fast; and a partial index on user_id that covers only unprocessed events.

Expected result: the query planner can use the first for date ranges, the second for payload @> queries and the third for finding a user's unprocessed events.

SQL · starter code
CREATE TABLE events (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id BIGINT NOT NULL,
  payload JSONB NOT NULL,
  processed BOOLEAN NOT NULL DEFAULT false,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- TODO: B-tree index on created_at

-- TODO: GIN index on payload

-- TODO: partial index on user_id for rows where processed is false
Show hints
  • B-tree is the default, so no type needs to be named. A GIN index is written USING GIN (column).
  • A partial index ends with a WHERE clause.
Show solution
SQL · solution
CREATE TABLE events (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id BIGINT NOT NULL,
  payload JSONB NOT NULL,
  processed BOOLEAN NOT NULL DEFAULT false,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_events_created_at ON events (created_at);

CREATE INDEX idx_events_payload ON events USING GIN (payload);

CREATE INDEX idx_events_unprocessed ON events (user_id) WHERE processed = false;

Quick quiz

1. Which index type does CREATE INDEX build when none is specified?

  • A. GIN
  • B. B-tree
  • C. GiST
  • D. Hash
Show answer

B. B-tree handles equality, ranges and sorting, which covers most queries.

2. Which index type suits JSONB documents, arrays and full-text search?

  • A. GIN
  • B. B-tree
  • C. BRIN
  • D. Hash
Show answer

A. GIN indexes the individual elements inside a composite value.

3. What does CREATE INDEX CONCURRENTLY do?

  • A. Builds several indexes at once
  • B. Builds the index faster
  • C. Makes the index unique
  • D. Builds the index without blocking writes to the table
Show answer

D. An ordinary CREATE INDEX blocks inserts, updates and deletes until it finishes.

Interview questions

What index types does PostgreSQL offer and when is each used?

Show answer

B-tree, the default, for equality, range comparisons and sorting on ordinary values. GIN for values that contain many elements: JSONB, arrays and full-text search. GiST for geometric data, ranges and nearest-neighbour searches. BRIN for very large tables whose values follow the physical order of the rows, such as timestamps in an append-only log. Hash for equality only.

What is a partial index and when is it useful?

Show answer

A partial index covers only the rows that satisfy a WHERE condition given when it is created. It is smaller and cheaper to maintain than a full index and suits queries that always target a subset, such as unprocessed jobs or active users. It can also enforce uniqueness within a subset, for example a unique email among rows that are not soft-deleted. A query can use it only if its own condition implies the index condition.

Key Points to Remember

  • B-tree is the default index type, ideal for equality, ranges, and ORDER BY.
  • GIN indexes containment-style data: JSONB, arrays, and full-text search vectors.
  • GiST supports geometric data, range types, and exclusion constraints.
  • Always confirm index usage with EXPLAIN rather than assuming an index will be picked up.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering