Hook: A missing index is like a library with no shelf labels: every search has to walk the whole room.
Question: What is a missing index scenario in SQL, and how do you handle it?
Answer: It is when a query filters, joins, or sorts on columns that do not have a useful index, so the database scans far more rows than necessary. That usually increases CPU, memory use, and disk I/O, which makes the query slow under load. The right fix is not simply “add an index”; you first confirm the access pattern, then choose the smallest useful index that matches the query shape.
Interview-Ready Answer: In a missing index scenario, my query is paying for a scan because the columns in WHERE, JOIN, or ORDER BY are not supported by a useful index. I would verify that with EXPLAIN or, in SQL Server, the missing-index recommendation, then create the smallest index that matches the real workload, often a composite or covering index. I would also check the write cost, because every extra index makes inserts, updates, and deletes slower.
A missing index scenario is not magic; it is a mismatch between the query and the storage access path. The database wants to find rows with the least work. If it cannot jump directly to the needed rows, it reads a lot of pages, filters them one by one, and maybe sorts the result afterward.
sys.dm_db_missing_index_details; in PostgreSQL/MySQL, you usually infer the same need from EXPLAIN, slow-query logs, and runtime metrics.Indexes are usually B-tree structures, which means they are sorted trees. That lets the engine find a value in roughly O(log n) time, then read nearby leaf pages efficiently. A scan is closer to O(n), because the engine must inspect many rows or pages. On a table with 10 million rows, that difference is huge: a good index may touch only a few pages, while a scan may touch thousands of pages and generate a lot of logical reads.
customer_id or email.ORDER BY queries where the sort column can be aligned with the filter columns.Not every missing-index hint should become a real index. If a column has very low selectivity, like a boolean or a status value with only a few common states, the index may not help much because too many rows still match. Also, every extra index adds maintenance work on writes. On a busy OLTP system, a single table can have 3 to 8 useful indexes; adding 20 indexes often hurts more than it helps.
| Case | What happens | Typical result |
|---|---|---|
| Full scan | Reads most rows | Good for tiny tables or low-selectivity filters |
| Single-column index | Narrows by one predicate | Good for simple filters |
| Composite / covering index | Matches filter + sort + join | Often best for real workloads |
customer_id first, that column usually belongs first in the index.WHERE UPPER(name) = 'ALICE' may stop a normal index from being used unless you add an expression index or rewrite the query.Memory model: Seek is a shortcut; scan is a room search. If the shortcut exists, use it. If not, the engine has to walk the whole room.
Imagine a checkout service for an e-commerce app. Users open their order history page and expect the latest 20 orders for one customer to load instantly. The table has tens of millions of rows, but no index supports customer_id plus created_at. During a sale, the page starts timing out because every request scans a huge chunk of the orders table, then sorts the result.
What goes wrong: CPU climbs, disk reads spike, and the app server threads wait longer for the database. In SQL Server you might see rising logical reads and waits like PAGEIOLATCH or CXPACKET; in other systems you will still see the same shape: slow query logs fill up, p95 latency jumps, and customers report that order history never loads. The fix is usually a targeted composite index, not a random index on every column.
-- PostgreSQL-style demo: the idea is the same in other SQL databases.
-- We create a small orders table, show a query that benefits from an index,
-- then add the right index and show the better access path.
DROP TABLE IF EXISTS orders_demo;
CREATE TABLE orders_demo (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
status TEXT NOT NULL,
amount NUMERIC(10,2) NOT NULL
);
-- Enough rows to make the access pattern meaningful.
-- generate_series is PostgreSQL built-in, so this runs as-is there.
INSERT INTO orders_demo (customer_id, order_date, status, amount)
SELECT
(gs % 100) + 1,
DATE '2025-01-01' + (gs % 120),
CASE
WHEN gs % 10 = 0 THEN 'CANCELLED'
WHEN gs % 3 = 0 THEN 'PAID'
ELSE 'PENDING'
END,
((gs % 500) + 1)::numeric / 10
FROM generate_series(1, 5000) AS gs;
-- Before the index: the engine has fewer shortcuts, so it may scan more data.
EXPLAIN ANALYZE
SELECT order_id, customer_id, order_date, amount
FROM orders_demo
WHERE customer_id = 42
AND order_date >= DATE '2025-03-01'
ORDER BY order_date DESC
LIMIT 10;
-- This composite index matches the real access pattern:
-- 1) filter by customer_id
-- 2) range on order_date
-- 3) return rows already ordered by date descending
CREATE INDEX idx_orders_demo_customer_date
ON orders_demo (customer_id, order_date DESC);
-- After the index: the planner has a much better path.
EXPLAIN ANALYZE
SELECT order_id, customer_id, order_date, amount
FROM orders_demo
WHERE customer_id = 42
AND order_date >= DATE '2025-03-01'
ORDER BY order_date DESC
LIMIT 10;
-- Edge case: an index on a low-selectivity column may not help much.
-- If most rows share the same value, the planner can still prefer a scan.
CREATE INDEX idx_orders_demo_status
ON orders_demo (status);
EXPLAIN ANALYZE
SELECT COUNT(*)
FROM orders_demo
WHERE status = 'PENDING';
-- Cleanup is optional in a scratch session, but this keeps the script idempotent.
-- DROP TABLE orders_demo;Follow-up & Tricky Questions:
EXPLAIN output, logical reads, and latency under realistic data volume. A query that is faster in a toy dataset may not stay faster at production scale.WHERE UPPER(name) = 'ALICE' use a normal index on name? Usually no, because the function changes the searched value. You either rewrite the predicate or create an expression index if your database supports it.Common Mistakes:
Memory Hook: “Labels beat wandering.” An index is the shelf label that lets the database go straight to the right rows instead of wandering through the whole table.
Cheat Sheet:
WHERE / JOIN / ORDER BY pattern with a small index.EXPLAIN and real workload metrics.Practice Tasks:
EXPLAIN.WHERE plus ORDER BY, then design a composite index that supports both.status and observe why it may not be the best candidate.