Module P-2·21 min read

The B-tree index for practitioners, EXPLAIN basics, composite and partial indexes, and the write cost trade-off.

P-2 — Indexes: When and How to Add Them

Who this module is for: You have built schemas and written queries. Now a query that was fast with 100 rows is slow with 100,000. This module explains what indexes actually do (without diving into the B-tree internals — that is Phase 3), how to tell if you need one, how to create and use them correctly, and the cost that most tutorials never mention: every index makes writes slower.


What an Index Actually Does

When you run SELECT * FROM products WHERE price = 49.99, PostgreSQL has two options:

Sequential scan — read every single row from the table and check whether price = 49.99. If you have 1,000,000 rows, it reads all 1,000,000.

Index scan — jump directly to the rows where price = 49.99 using a pre-built lookup structure. If 3 rows match, it reads roughly 3 rows plus the index overhead.

An index is a separate data structure maintained by PostgreSQL that maps column values to the physical locations of rows. Think of it like a book's index: instead of reading every page to find "PostgreSQL", you look it up in the index and go directly to the pages listed.

The core tradeoff: an index makes SELECT faster but makes INSERT, UPDATE, and DELETE slower — because every write must update both the table and the index. Adding an index to every column is not a good strategy.


Reading EXPLAIN — Your Diagnostic Tool

Before adding an index, measure. EXPLAIN shows the query execution plan PostgreSQL would use.

sql

Reading the output:

text
FieldMeaning
Seq ScanSequential scan — reading every row
cost=0.00..35.50Estimated cost (startup..total, arbitrary units)
rows=234Estimated row count
actual time=0.012..0.487Real time in milliseconds (start..end)
Rows Removed by Filter: 316How many rows were read but discarded
Planning TimeTime to generate the plan
Execution TimeTotal actual execution time

After adding an index:

text

The Seq Scan became an Index Scan. Execution time dropped from 0.532ms to 0.112ms — about 5× faster on a small table. On a million-row table, the difference would be far more dramatic.


Creating Indexes

Basic Index

sql

CREATE INDEX CONCURRENTLY — No Table Lock

Regular CREATE INDEX locks the table from writes for the duration of the build. On a large production table, this can take minutes and block your application.

sql

Unique Index

sql

Dropping an Index

sql

Composite Indexes — Column Order Matters

A composite index on (a, b) can be used for queries filtering on a alone, or a AND b together. It cannot efficiently answer queries on b alone.

sql

Rule: put the equality filter column first, the range filter column second.

sql

Partial Indexes — Index Only the Rows You Query

A partial index only includes rows matching a WHERE condition. It is smaller, faster to build, and uses less memory than a full index.

sql

For a table where 95% of rows are in terminal states (done, archived), a partial index on the active 5% is dramatically smaller and faster.


Expression Indexes — Index a Computed Value

sql

Foreign Key Indexes — The Most Forgotten Optimization

PostgreSQL does not automatically create indexes on foreign key columns. This surprises many engineers. When you delete a row from the parent table, PostgreSQL must scan the child table to check for references — without an index, this is a full sequential scan.

sql

Rule: every foreign key column should have an index. Without it, joins and deletions that reference that column are slow.


The Write Cost of Indexes

Every index you add slows down every INSERT, UPDATE, and DELETE on that table.

text

For a table that has 100,000 reads per second but only 10 writes per second, many indexes are fine. For a table that has 50,000 writes per second (like an event log or metrics table), every index is expensive.

This is why you should not add an index "just in case." Measure first, add indexes for proven slow queries.


Common Index Mistakes

Mistake 1: Indexing a low-cardinality column

sql

Mistake 2: Over-indexing a write-heavy table

sql

Mistake 3: Indexing a column used in a function

sql

Mistake 4: Not running ANALYZE after bulk loads

sql

Monitoring Index Usage

sql

Practical Exercise: Indexing the Task Manager

Starting with the task manager schema from F-7 with 50,000 rows seeded:

sql

Summary

ConceptKey Takeaway
Sequential scanReads every row — fine for small tables or when most rows match
Index scanJumps directly to matching rows — fast for selective queries
EXPLAIN ANALYZEAlways measure before and after adding an index
CREATE INDEX CONCURRENTLYBuild index without blocking writes — use in production
Composite indexColumn order matters: equality columns first, range columns last
Partial indexOnly indexes rows matching a condition — smaller and faster
Expression indexIndex a computed value (LOWER(email))
Foreign key indexesNot automatic — add manually for every FK column
Write costEvery index slows down INSERT/UPDATE/DELETE
Low cardinalityLow-cardinality columns (status with 4 values) rarely benefit from full indexes
Index monitoringpg_stat_user_indexes.idx_scan = 0 means the index is never used — drop it

Module P-3 covers transactions and ACID in practice — what BEGIN, COMMIT, ROLLBACK actually do, isolation levels, and writing safe atomic operations for scenarios like bank transfers and inventory deduction.

Next: P-3 — Transactions and ACID in Practice →


Knowledge Check

A software engineer observes a critical production query, SELECT id, name FROM orders WHERE customer_id = 123 AND status = 'pending' ORDER BY created_at DESC;, performing a Seq Scan on a 10 million row orders table. EXPLAIN ANALYZE shows actual time in the hundreds of milliseconds. The status column has low cardinality (e.g., 'pending', 'shipped', 'cancelled', 'returned'), but 'pending' orders represent only 5% of the total rows. Which index strategy is most appropriate to optimize this query for production, minimizing write impact and maximizing read performance?


A high-traffic microservice manages user activity logs in a user_events table, experiencing 50,000 INSERT operations per second. Initially, the table had no indexes. To support analytical queries, a junior engineer proposes adding five new indexes on various columns (user_id, event_type, timestamp, session_id, ip_address) using CREATE INDEX. As a Senior Principal Software Engineer, what is the most critical concern and the recommended immediate action?


A database administrator reports that DELETE FROM projects WHERE id = 1; queries are taking an unusually long time, sometimes several seconds, even for projects with few associated tasks. The projects table is small, but the tasks table has millions of rows. The tasks table has a foreign key constraint tasks_project_fk referencing projects(id) on its project_id column. Upon running EXPLAIN ANALYZE DELETE FROM projects WHERE id = 1;, the DBA observes a Seq Scan on the tasks table. What is the most likely root cause of this performance issue and the recommended solution?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.