Composite Index & Column Order
IntermediateA composite index covers multiple columns, but only queries that reference the leftmost prefix of the index columns can use it — column order is a critical design decision.
Overview
A composite (multi-column) index on (col_a, col_b, col_c) is sorted first by col_a, then by col_b within each col_a value, then by col_c. The optimizer can use the index for queries filtering on col_a alone, (col_a, col_b), or (col_a, col_b, col_c) — but not on col_b alone (the leading column is skipped). This is the leftmost prefix rule. For range conditions (BETWEEN, >, <), put equality columns before range columns in the index. A covering index includes all columns the query needs so no heap fetch is required. Low-cardinality leading columns (booleans, small enums) waste the tree depth and hurt performance.
Leftmost Prefix Rule with EXPLAIN Proof
Only queries that reference the leading column(s) of a composite index can use it. Queries that skip the leading column fall back to a sequential scan.
-- Index: (department_id, salary)
CREATE INDEX idx_emp_dept_salary ON employees (department_id, salary);
-- Query 1: uses index (leading column present)
EXPLAIN SELECT name, salary FROM employees
WHERE department_id = 3;
-- ✓ Index Scan using idx_emp_dept_salary
-- Query 2: uses index (both columns)
EXPLAIN SELECT name FROM employees
WHERE department_id = 3 AND salary > 70000;
-- ✓ Index Scan — range on second column is fine after equality on first
-- Query 3: CANNOT use index (leading column absent)
EXPLAIN SELECT name, salary FROM employees
WHERE salary > 70000;
-- ✗ Seq Scan — salary is not the leading column
-- Fix: create a separate index on (salary) if this query is commonColumn Ordering: Equality Before Range
Place equality filter columns first in the index definition and range filter columns last. This maximises the portion of the index usable for the range scan.
-- Wrong column order for this query pattern:
-- WHERE status = 'pending' AND created_at > '2024-01-01'
CREATE INDEX idx_wrong ON orders (created_at, status);
-- Optimizer can use this only for range on created_at,
-- then must filter status in memory.
-- Correct order: equality column first
CREATE INDEX idx_correct ON orders (status, created_at DESC);
-- Optimizer binary-searches to status = 'pending' leaf,
-- then scans forward on the sorted created_at range.
-- Proof: compare EXPLAIN output
EXPLAIN SELECT id, amount FROM orders
WHERE status = 'pending' AND created_at > '2024-01-01'
ORDER BY created_at DESC;When Composite Index Hurts: Low-Cardinality Leading Column
If the leading column has very few distinct values (e.g. a boolean is_active with true/false), the index provides little branching benefit and the optimizer often prefers a seq scan. Flip the column order or use a partial index instead.
-- Bad: is_active has only 2 values — poor leading column
CREATE INDEX idx_bad_leading ON users (status, name);
-- If status has values: 'active', 'inactive', 'banned' (3 values),
-- 33% of rows match 'active' — planner may prefer seq scan.
-- Better option 1: partial index (no low-cardinality column needed)
CREATE INDEX idx_active_users_name ON users (name) WHERE status = 'active';
-- Better option 2: put high-cardinality column first
-- If searching by city + status:
CREATE INDEX idx_city_status ON users (city, status);
-- city has high cardinality → narrows result set quicklyKey Points to Remember
- 1The leftmost prefix rule: a composite index on (a, b, c) can be used by queries filtering a, (a,b), or (a,b,c) — never b or c alone.
- 2Place equality-filter columns before range-filter columns in composite index definitions.
- 3A covering index includes all columns referenced in SELECT, WHERE, and ORDER BY — eliminates heap fetches entirely.
- 4Low-cardinality leading columns (booleans, small enums) reduce index selectivity; use a partial index instead.
- 5Index skip scan (MySQL 8+, PostgreSQL 13+ loose index scan) can sometimes use an index without the leading column, but it is not universally available.
- 6Always verify index usage with EXPLAIN before and after adding composite indexes in production.
Interview Questions
Sign in to ask AriaExplain the leftmost prefix rule for composite indexes with an example.
For a query with WHERE department_id = 5 AND salary > 60000, should the composite index be (department_id, salary) or (salary, department_id)? Why?
What is a covering index and how does it improve query performance?
Why is a boolean column a poor choice for the leading column of a composite index?
Ask Aria about Composite Index & Column Order
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.