Indexing Strategies
An index trades write cost and storage for read speed — and only for the specific access patterns it was built for.
Intermediate
INSERT/UPDATE/DELETE on the indexed columns, since the index has to be kept in sync.| Good candidate | Why |
|---|---|
Columns in WHERE clauses, especially on large tables | turns a full table scan into a targeted lookup |
| Foreign key columns | speeds up joins, and speeds up checking the constraint itself on delete |
Columns in ORDER BY | can let the database skip a sort step entirely |
CREATE INDEX idx_orders_customer_status ON orders (customer_id, status);
-- this index is used by:
SELECT * FROM orders WHERE customer_id = 5; -- yes, leading column
SELECT * FROM orders WHERE customer_id = 5 AND status = 'paid'; -- yes, both columns
-- this index is NOT used by:
SELECT * FROM orders WHERE status = 'paid'; -- no — status isn't the leading column
-- partial: index only the rows that matter, smaller and cheaper to maintain
CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'pending';
-- expression: index the result of a function, not the raw column
CREATE INDEX idx_users_lower_email ON users (LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com'; -- uses the index; a plain WHERE email = ... would not
UNIQUE constraints and primary keys are implemented as unique indexes under the hood — there's no separate mechanism. CREATE UNIQUE INDEX directly is equivalent, and is how you'd add a partial-unique constraint (e.g. "email must be unique among active users"), which the plain constraint syntax can't express.pg_stat_user_indexes view shows how often each index is actually used — worth checking periodically on a mature schema rather than assuming every index someone added years ago is still earning its keep. See EXPLAIN ANALYZE for confirming whether a specific query is actually using the index you expect.