PostgreSQL Indexing Basics: How Indexes Speed Up Queries
Learn what an index is in PostgreSQL, how to create one with CREATE INDEX, and how to prove it's being used with EXPLAIN — the foundation of query performance.
PostgreSQL Indexing Basics
Without an index, PostgreSQL answers a query with a sequential scan — reading every single row in the table to check if it matches. An index lets it jump straight to matching rows instead, the same trade-off MongoDB makes with its own indexes.
Creating a Basic Index
CREATE INDEX idx_employees_department ON employees (department);
Every table also gets an automatic index on its PRIMARY KEY, and PostgreSQL creates one automatically for UNIQUE constraints too.
Seeing the Difference: EXPLAIN
EXPLAIN SELECT * FROM employees WHERE department = 'Engineering';
Before an index exists:
Seq Scan on employees (cost=0.00..25.88 rows=6 width=64)
Filter: (department = 'Engineering'::text)
After creating the index:
Index Scan using idx_employees_department on employees (cost=0.28..8.45 rows=6 width=64)
Index Cond: (department = 'Engineering'::text)
Seq Scan → Index Scan is exactly the improvement an index provides (see the EXPLAIN lesson for a deeper dive into reading this output).
Composite (Multi-Column) Indexes
CREATE INDEX idx_dept_salary ON employees (department, salary);
Speeds up queries that filter on department alone, or department and salary together — but not salary alone, since column order in the index matters.
Dropping an Index
DROP INDEX idx_employees_department;
The Trade-Off: Indexes Aren't Free
- Indexes speed up reads (
SELECT), but slightly slow down writes (INSERT/UPDATE/DELETE), since every index also has to be updated. - Indexes take up disk space of their own.
- Only index columns you actually filter, join, or sort by often — indexing everything "just in case" hurts write performance for no benefit.
Common Errors & Troubleshooting
- Creating an index but
EXPLAINstill showsSeq Scan— the planner sometimes decides a sequential scan is genuinely faster on a small table, or when the query would match most of the rows anyway — this isn't always a bug; try filtering to fewer rows to see the index kick in, or trust the planner on small tables. - Statistics out of date after a big bulk load — run
ANALYZE tablename;after loading a large amount of new data so the planner's row estimates are accurate (see the Query Optimization lesson). - Adding an index and write performance getting noticeably worse — every index has to be updated on every insert/update/delete; if a table is write-heavy and rarely queried on a given column, that index may be costing more than it's worth.
Key Takeaways
- Without an index, Postgres falls back to scanning every row —
EXPLAINis how you confirm whether an index is actually being used. - A composite index's column order matters: it helps the leading column(s) most.
- Indexes trade faster reads for slightly slower writes — index deliberately, not everything.