Home/Learn/SQL/Composite Index & Column Order

Composite Index & Column Order

Intermediate
Indexing & Performance

A 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.

SQL — leftmost prefix rule with EXPLAIN
-- 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 common

Column 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.

SQL — equality before range column ordering
-- 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.

SQL — low-cardinality leading column pitfall
-- 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 quickly

Key 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 Aria
1

Explain the leftmost prefix rule for composite indexes with an example.

MediumAmazon
2

For a query with WHERE department_id = 5 AND salary > 60000, should the composite index be (department_id, salary) or (salary, department_id)? Why?

HardGoogle
3

What is a covering index and how does it improve query performance?

MediumMicrosoft
4

Why is a boolean column a poor choice for the leading column of a composite index?

MediumUber

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.

Loading discussion…