- Learn
- PostgreSQL
- PostgreSQL CREATE INDEX
PostgreSQL CREATE INDEX
Speed up your queries by teaching PostgreSQL where to look.
What is an Index?
Imagine you're in a library with millions of books, and you need to find every book written by Stephen King. Without any system, you'd have to walk through every single aisle, checking every single book. That could take days.
But libraries have a card catalog (or these days, a computer system) that lets you look up "Stephen King" and instantly see exactly which shelves contain his books. You go straight there. Done in seconds.
A database index works the same way. Without an index, PostgreSQL performs a "sequential scan"—it reads every single row in the table to find matches. With an index, it jumps directly to the matching rows.
Basic Syntax
Creating an index in PostgreSQL is straightforward:
For example, if you have a users table and frequently search by email:
Now queries like this become much faster:
Index Types
PostgreSQL offers several index types, each optimized for different use cases:
B-tree (Default)
The workhorse of indexes. Great for equality and range queries. This is what you get if you don't specify a type.
Hash
Optimized for simple equality comparisons. Slightly faster than B-tree for = operations, but can't handle ranges.
GIN (Generalized Inverted Index)
Perfect for columns containing multiple values—arrays, JSONB, and full-text search.
GiST (Generalized Search Tree)
Used for geometric data, ranges, and full-text search. Essential for PostGIS spatial queries.
Common Patterns
Unique Index
Enforces uniqueness while also providing fast lookups:
Composite Index (Multiple Columns)
When you frequently filter or sort by multiple columns together:
Column order matters! The index above helps queries filtering by user_id, but won't help queries that only filter by created_at.
Partial Index
Index only a subset of rows. Smaller, faster, and more efficient:
Expression Index
Index the result of an expression:
Concurrent Index Creation
By default, CREATE INDEX locks the table for writes. On a production database with millions of rows, this could mean minutes of downtime.
Use CONCURRENTLY to build the index without blocking:
Caveat: Concurrent index creation takes longer and cannot run inside a transaction. If it fails partway through, you'll have an invalid index that needs to be dropped.
When NOT to Use Indexes
Indexes aren't free. They come with tradeoffs:
- Write overhead: Every INSERT, UPDATE, and DELETE must also update the index
- Storage space: Indexes consume disk space (sometimes significant)
- Maintenance: Indexes can become bloated and need periodic maintenance
Skip indexes when:
- The table is small (under ~10,000 rows)—sequential scan is often faster
- The column has very low cardinality (e.g., a boolean or status with 3 values)
- The table is write-heavy and rarely queried
- You're selecting most of the table anyway
Checking Index Usage
Use EXPLAIN to see if your query uses an index:
Check which indexes exist and their sizes:
Quick Reference
| Command | Description |
|---------|-------------|
| CREATE INDEX name ON table (col) | Create a B-tree index |
| CREATE UNIQUE INDEX ... | Create index that enforces uniqueness |
| CREATE INDEX ... USING gin | Create GIN index for arrays/JSONB |
| CREATE INDEX ... WHERE condition | Create partial index |
| CREATE INDEX CONCURRENTLY ... | Create without locking table |
| DROP INDEX name | Remove an index |
| REINDEX INDEX name | Rebuild a corrupted/bloated index |
Summary
Indexes are one of the most powerful tools for query optimization. Start with B-tree indexes on columns you frequently filter or sort by, use EXPLAIN to verify they're being used, and remember that the best index is one that matches your actual query patterns.
When in doubt, measure. A query that takes 500ms without an index might take 2ms with one—but an unnecessary index just wastes space and slows down writes.