-- Speeds up queries filtering or joining on orders.user_id
Note Indexes speed up reads but slow down writes (INSERT, UPDATE, DELETE) because the index must be maintained. Only index columns that appear frequently in WHERE, JOIN, and ORDER BY clauses.
create indexadd indexspeed up queryindex column
Unique Index
Syntax
CREATEUNIQUEINDEX index_name ONtable(column);
Example
CREATEUNIQUEINDEX idx_users_email ONusers(email);
Output
-- Enforces uniqueness and speeds up lookups by email
Note A unique index enforces a constraint and provides index performance in one operation. It is functionally similar to a UNIQUE constraint. In PostgreSQL, UNIQUE constraints are implemented as unique indexes internally.
-- Optimizes queries filtering by user_id AND order_date
Note Column order matters enormously. The index is useful for queries on (user_id), (user_id, order_date), but NOT for (order_date) alone. Put the most selective or most-filtered column first. This is the leftmost prefix rule.
composite indexmulti column indexcompound indexindex order
CREATEINDEX idx_orders_pending
ONorders(created_at)WHERE status ='pending';
Output
-- Smaller, faster index that only covers pending orders
Note Partial indexes are smaller and faster because they only include rows matching the WHERE condition. PostgreSQL supports them natively. MySQL does not - use a generated column with an index as a workaround.
partial indexfiltered indexconditional indexindex where
-- Index-only scan possible for queries selecting total_amount and status filtered by user_id
Note A covering index includes all columns a query needs, enabling an index-only scan without touching the table. INCLUDE columns are stored in the index but not used for searching or sorting.
covering indexinclude columnsindex only scanavoid table lookup
EXPLAINANALYZESELECT u.first_name,COUNT(*)AS order_count
FROM users u
JOIN orders o ON o.user_id= u.idWHERE u.is_active= true
GROUPBY u.first_name;
Output
-- Shows execution plan with actual timing and row counts
-- Look for: Seq Scan (bad on big tables), Index Scan (good), Hash Join vs Nested Loop
Note EXPLAIN shows the plan; EXPLAIN ANALYZE actually runs the query and shows real timings. Use EXPLAIN (FORMAT JSON) in PostgreSQL for machine-parseable output. Never run EXPLAIN ANALYZE on destructive queries (DELETE/UPDATE) without wrapping in a transaction and rolling back.
explain queryquery plananalyze performanceslow queryexecution plan
When to Add Indexes
Syntax
-- Index when:-- 1. Column is in WHERE frequently-- 2. Column is in JOIN conditions-- 3. Column is in ORDER BY-- 4. Column has high cardinality (many unique values)-- Skip indexing when:-- 1. Table is small (< few thousand rows)-- 2. Column has low cardinality (e.g., boolean)-- 3. Table has heavy write load-- 4. Column is rarely queried
Example
-- Good: frequently filtered, high cardinalityCREATEINDEX idx_orders_user_id ONorders(user_id);-- Questionable: boolean column, low cardinality-- CREATE INDEX idx_users_active ON users (is_active);-- Better as a partial index:CREATEINDEX idx_users_active ONusers(id)WHERE is_active = true;
Output
-- Indexing strategy depends on query patterns and data distribution
Note Use EXPLAIN to verify indexes are being used. The optimizer may ignore an index if it estimates a sequential scan is faster (e.g., when selecting most of the table). Regularly check for unused indexes and remove them.
indexing strategywhen to indexindex best practicesshould I index
DROP INDEX
Syntax
-- PostgreSQLDROPINDEX index_name;DROPINDEX CONCURRENTLY index_name;-- MySQLDROPINDEX index_name ON table_name;
-- Index removed; writes become slightly faster, reads may slow down
Note In PostgreSQL, DROP INDEX CONCURRENTLY avoids locking the table during removal but cannot run inside a transaction. MySQL always requires specifying the table name. Remove unused indexes to save storage and speed up writes.
drop indexremove indexdelete index
Expression (Functional) Index
Syntax
CREATEINDEX index_name ONtable(expression);
Example
CREATEINDEX idx_users_email_lower
ONusers(LOWER(email));-- Query that benefits:-- SELECT * FROM users WHERE LOWER(email) = '[email protected]';
Output
-- Speeds up case-insensitive email lookups
Note The query WHERE clause must use the exact same expression as the index. WHERE LOWER(email) = 'x' uses the index; WHERE email = 'x' does not. PostgreSQL and MySQL 8.0+ support expression indexes.
expression indexfunctional indexindex on functionlower index