Warming up the neural circuits...
Without indexes, Postgres scans every row to find what you need. Indexes are like a book's — they let you jump directly to the data. By the end of this module you will:
What it is: An index is a data structure (typically a B-tree) that maintains a sorted copy of specific columns, allowing the database to find rows without scanning the entire table. It's like a book's index — instead of reading every page, you jump directly to the relevant section.
Why we use it: Without indexes, PostgreSQL performs a sequential scan — reading every row to find matches. On a table with millions of rows, this is slow. Indexes reduce query time from seconds to milliseconds.
When we use it: On columns used in WHERE clauses, JOIN conditions, ORDER BY, and GROUP BY. Don't index small tables or columns with very few unique values.
-- Create an index on the email column
CREATE INDEX idx_users_email ON users(email);
-- Now this query is fast:
SELECT * FROM users WHERE email = 'alice@example.com';What it is: PostgreSQL supports several index types optimized for different data patterns. B-tree is the default and works for most cases. Hash is for exact equality, GIN for arrays/JSONB/full-text, and GiST for geometric/range data.
Why we use it: Different data patterns benefit from different index structures. Using the right index type can significantly improve performance for specific query patterns.
When we use it: B-tree for 90% of cases. GIN when indexing JSONB columns or arrays. GiST for geometric data or range types. Hash rarely (B-tree is usually better).
| Type | Best For | Example |
|---|---|---|
| B-tree (default) | Equality and range queries | =, <, >, BETWEEN |
| Hash | Equality only (=) | Exact match lookups |
| GIN | Arrays, JSONB, full-text search | @>, ?, @@ |
| GiST | Geometric, range types, full-text | <<, >>, @> |
-- B-tree (default, most common)
CREATE INDEX idx_users_email ON users(email);
-- Hash (equality only, smaller)
CREATE INDEX idx_users_email_hash ON users(email) USING hash;
-- GIN (for JSONB)
CREATE INDEX idx_data_metadata ON events USING gin(metadata);
-- GiST (for geometric data)
CREATE INDEX idx_locations_coords ONWhat it is: Not every column needs an index. Indexes speed up reads but slow down writes (every INSERT/UPDATE/DELETE must also update the index). The key is to index columns that are frequently used in queries.
Why we use it: Over-indexing wastes storage and slows writes. Under-indexing causes slow queries. Finding the right balance is a core DBA skill.
When we use it: Index columns in WHERE clauses, JOIN conditions (foreign keys), ORDER BY, and GROUP BY. Don't index tiny tables, low-cardinality columns (booleans), or columns rarely used in queries.
| Column Type | Should Index? | Why |
|---|---|---|
| WHERE clauses | ✅ Yes | Speeds up filtering |
| JOIN conditions (FKs) | ✅ Yes | Speeds up joins |
| ORDER BY | ✅ Yes | Speeds up sorting |
| GROUP BY | ✅ Yes | Speeds up aggregation |
| Tiny tables (< 1000 rows) | ❌ No | Full scan is faster |
| Low cardinality (boolean) | ❌ No | Index not selective enough |
What it is: A partial index only indexes rows that match a WHERE condition. For example, indexing only active users or only published posts. The index is smaller and faster to maintain.
Why we use it: When you only query a subset of rows — "active users", "published posts", "pending orders" — a partial index is more efficient than indexing the entire table.
When we use it: When queries consistently filter by the same condition, when the table has a large number of rows that are rarely queried, or when you want to reduce index size.
-- Only index active users
CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
-- Only index published posts
CREATE INDEX idx_published_posts ON posts(created_at) WHERE published = true;What it is: EXPLAIN ANALYZE shows the query execution plan and actual runtime statistics. It tells you which scan type was used (Seq Scan vs Index Scan), how many rows were processed, and how long it took.
Why we use it: Without EXPLAIN, you're guessing about performance. It shows you exactly how PostgreSQL executes your query — whether it uses an index, how many rows it scans, and where the bottleneck is.
When we use it: Every time a query is slow, when verifying that an index is being used, when comparing query plans, or when optimizing database performance.
-- See the query plan
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';
-- See the query plan AND actual execution time
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';Seq Scan on users (cost=0.00..1.04 rows=1 width=...)
Filter: (email = 'alice@example.com'::text)If you see Seq Scan on a large table, consider adding an index on the filtered column.
What it is: A composite index covers multiple columns in a single index. The order of columns matters — the index can be used for queries that filter by the leftmost column(s), but not for queries that skip to later columns.
Why we use it: When queries frequently filter by multiple columns together — WHERE category = 'Electronics' AND price > 1000. A composite index on (category, price) is faster than two separate indexes.
When we use it: When queries consistently filter by the same combination of columns, when building covering indexes, or when optimizing multi-column WHERE clauses.
-- Index on category + price (for queries that filter by both)
CREATE INDEX idx_products_category_price ON products(category, price);Column order matters. A composite index on (category, price) helps queries filtering by category alone, but NOT queries filtering by price alone.
title column(instructor, price)Next up: Indexes speed up filtering. In the next module, you'll learn Window Functions — powerful analytics that don't collapse rows like GROUP BY.