← All cheatsheets
Data Engineer · #016 · August 8, 2026 · 3 min read

Why is my SQL query slow even though the column is indexed?

The index exists and the engine ignores it. The sorted-copy mental model, the sargable rewrite that wakes it up, the leftmost-prefix rule, and when an index actually hurts.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

The column is indexed. The query is still slow. The EXPLAIN output says Seq Scan, twelve million rows, forty seconds. One wrapped function is usually responsible:

-- created_at is indexed
SELECT * FROM orders
WHERE YEAR(created_at) = 2026;

Why the index sat idle

An index is a sorted copy of a column with pointers back to the rows. Sorted is the whole trick: the engine can binary-search its way to the value in a handful of hops instead of walking the table.

YEAR(created_at) is not that column. It is a brand-new value computed from it, and nothing stores those computed values in sorted order. So the engine has no choice: compute the function for every row, twelve million times.

The sargable rewrite

Ask for a range of raw values instead of a computed one:

SELECT * FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at <  '2027-01-01';

Same rows, same meaning, but the predicate now compares the indexed column directly, so the engine jumps straight to the start of the range and reads until the end. This is what "sargable" means: written so the search argument can use the index. The pattern generalizes to prefixes (LIKE 'abc%' is sargable, LIKE '%abc' is not) and to any range.

What an index actually is

CREATE INDEX idx_orders_created
ON orders (created_at);

Think book index: a sorted list of terms, each pointing at a page. The table itself stays unsorted; the index is a side structure (almost always a B-tree) that the engine keeps in sync for you. Lookups cost a few tree hops instead of a full walk, which is the difference between 0.2 seconds and 40.

Trap 1: the leftmost prefix

A composite index only works from its first column:

CREATE INDEX idx ON orders
  (customer_id, created_at);

WHERE customer_id = 42            -- uses idx
WHERE created_at > '2026-01-01'   -- ignores idx

The index is sorted by customer_id first and by created_at only within each customer. Like a phone book sorted by last name: perfect for finding a Dupont, useless for finding every Marie. Order composite columns by how your queries actually filter.

Trap 2: indexes are not free

Every index is a promise the engine has to keep on every write. Each INSERT and UPDATE maintains every index on the table, so a table with eight speculative indexes pays eight times on every load. Low-cardinality columns (a 3-value status flag) barely benefit. And SELECT * on wide rows still has to visit the table even after the index finds the matches.

The discipline: index the columns your WHERE and JOIN clauses actually use, check with EXPLAIN that the plan says Index Scan and not Seq Scan, and drop the indexes no query ever touches.

The takeaway

An index is a sorted copy the engine can seek in. Keep your predicates sargable so the sort applies, respect the leftmost prefix on composite indexes, and treat every index as a write cost you must justify. The print-ready PDF above fits the whole checklist on one page.

Frequently asked questions

Why does my query not use the index?
The most common reason is a function wrapped around the indexed column, like YEAR(created_at) = 2026. The index stores raw column values in sorted order; a computed value is not in that order, so the engine falls back to scanning every row. Rewrite the condition as a range on the raw column.
What does sargable mean in SQL?
A predicate is sargable (Search ARGument able) when the engine can use an index to evaluate it. Comparing the raw column to a constant or a range is sargable; wrapping the column in a function, casting it, or matching with a leading wildcard is not.
When should you not add an index?
On columns with very few distinct values (an index on a 3-value status column barely narrows anything), on tables that take heavy writes (every INSERT and UPDATE must maintain every index), and on columns no WHERE or JOIN clause ever touches. Verify usage with EXPLAIN and drop the indexes no query hits.
What is the leftmost prefix rule for composite indexes?
A composite index on (customer_id, created_at) is sorted by customer_id first, then created_at within each customer. Queries filtering on customer_id (alone or with created_at) can use it; queries filtering on created_at alone cannot, like a phone book is useless for searching first names.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

More cheatsheets