Hook: Interviewers love this topic because it separates 'I know indexes exist' from 'I know how the database actually finds rows'.
Question: What is a composite index, and why does the column order matter?
Answer: A composite index is one index built on two or more columns. The database stores the key values in order, so it can quickly search by the first column, then the second, and so on. This is useful when your common queries filter or sort on the same column combination, such as customer_id plus order_date.
Interview-Ready Answer: I think of a composite index as a sorted list on multiple columns, not separate indexes glued together. The order matters because most databases can use the leftmost part of the index most effectively, so a query like WHERE customer_id = ? AND order_date >= ? can be very fast, while a query on only order_date may not benefit much. A good composite index can also help with ORDER BY and can even become a covering index if it contains all the columns the query needs.
Detailed Explanation: A composite index, sometimes called a multi-column index, is an index whose key is made from several columns in a fixed order. The index is usually stored as a balanced tree, most commonly a B-tree (a tree that stays shallow so lookups are fast). The important idea is that the database does not store the columns as separate piles; it stores one ordered sequence like (a, b, c).
(tenant_id, created_at).tenant_id = 42 AND created_at = '2026-01-01' is a tighter match than just tenant_id = 42.created_at >= '2026-01-01', the engine often cannot keep using later columns for direct positioning in the same way. This is the classic gotcha.Think of the index like a phone book sorted by city, then street, then house number. If you know the city, it is easy to jump into the right section. If you only know the house number, the book is much less helpful. That is why the leftmost column is usually the most important one for search and sort patterns.
In practice, the best order is driven by your most common query shape. A common rule is: put columns used in equality filters first, put the range column later, and put sort columns where they match your ORDER BY. For many workloads, the best index for WHERE a = ? AND b = ? AND c > ? ORDER BY d is not random; it is chosen to match how the query narrows rows.
| Option | Best for | Weakness |
|---|---|---|
| Composite index | Frequent multi-column filters or sorts | Column order matters a lot |
| Single-column index | Queries on one column only | Less help for combined predicates |
| Two separate indexes | Different independent queries | Not the same as one combined path |
Separate indexes are not a magic replacement. Some databases can combine them in special ways, such as bitmap or index-merge plans, but that is usually not as efficient as one well-chosen composite index for a tight combined lookup.
O(log n + k), where n is rows in the index and k is the number of matching rows returned.O(n log n) because the engine must organize all keys.ORDER BY. Some engines can scan an index backwards, but you should still design for the query pattern you actually need.Memory check: a composite index is not just more columns; it is one sorted path with a left-to-right rule.
Real-World Example: Imagine a checkout service for a SaaS platform with millions of payment rows. The team shows merchants recent payments with a query like WHERE tenant_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT 50. A composite index on (tenant_id, created_at DESC) lets the database jump directly to one tenant’s recent rows and stop after 50 matches, which keeps the page fast even as the table grows.
Now the bug story: an engineer accidentally created (created_at, tenant_id) because the screen was sorted by date first. The app worked in testing, but in production the database had to scan huge date ranges across many tenants before it could filter by tenant. Symptoms showed up as p95 latency jumping from under 100 ms to several seconds, slow query logs filling with the same statement, and CPU spiking on the primary database. Users saw the payment history page spinner forever, and support tickets started saying, 'My dashboard loads, but my recent transactions are missing or delayed.'
The fix was simple but important: redesign the index to match the actual filter order used by the query. Once the leftmost column matched tenant_id, the index became a narrow, efficient path instead of a nearly useless sorted list.
-- SQLite-compatible demo of a composite index and its leftmost-prefix behavior.
-- This script is self-contained: create data, build the index, and inspect query plans.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date TEXT NOT NULL, -- ISO-8601 text sorts correctly as dates in SQLite
status TEXT NOT NULL,
total_cents INTEGER NOT NULL
);
INSERT INTO orders (order_id, customer_id, order_date, status, total_cents) VALUES
(1, 101, '2026-01-01', 'paid', 2500),
(2, 101, '2026-01-03', 'paid', 4800),
(3, 101, '2026-01-05', 'refunded',2500),
(4, 102, '2026-01-02', 'paid', 1200),
(5, 102, '2026-01-04', 'paid', 3100),
(6, 103, '2026-01-03', 'pending', 900),
(7, 104, '2026-01-06', 'paid', 7700),
(8, 104, '2026-01-07', 'paid', 1800),
(9, 105, '2026-01-08', 'paid', 6400),
(10, 105, '2026-01-09', 'failed', 6400);
-- Composite index: first customer_id, then order_date.
-- This matches the common access pattern: one customer's orders over time.
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);
-- Good path: equality on the leftmost column, then a range on the second.
-- The planner can use the index to jump into customer 101's slice.
EXPLAIN QUERY PLAN
SELECT order_id, order_date, total_cents
FROM orders
WHERE customer_id = 101
AND order_date >= '2026-01-03'
ORDER BY order_date;
-- Still useful: filtering on just the leftmost column can use the prefix.
EXPLAIN QUERY PLAN
SELECT order_id
FROM orders
WHERE customer_id = 102;
-- Edge case: filtering only on the second column does NOT match the leftmost prefix.
-- In many engines this becomes a scan or a much weaker plan.
EXPLAIN QUERY PLAN
SELECT order_id
FROM orders
WHERE order_date >= '2026-01-03';
-- Another edge case: the index helps search, but it may not cover the query.
-- Because status is not in the index, the engine must read the table row for that column.
SELECT order_id, status
FROM orders
WHERE customer_id = 104
ORDER BY order_date;
-- If you want a true covering index for this exact query shape, you might add status too:
-- CREATE INDEX idx_orders_customer_date_status ON orders(customer_id, order_date, status);
-- Trade-off: faster reads for this query, but more storage and slower writes.
Follow-up & Tricky Questions:
ORDER BY? Yes, if the sort order matches the index order and the filter uses the leftmost prefix. Then the database may avoid a separate sort step.Tricky / gotchas:
Common Mistakes:
ORDER BY and only thinking about WHERE. Correction: the same composite index can often satisfy both filtering and sorting if the order is chosen well.Memory Hook: Think of a composite index like a filing cabinet sorted by department, then team, then name. If you know the department, the search is fast; if you start with the name only, you are digging through the wrong drawers.
Cheat Sheet:
WHERE, sometimes for ORDER BY, and sometimes for covering reads.Practice Tasks:
tenant_id, created_at, and status, then add a composite index and check the plan for a query with both filters.