Episode 04 · 11 min · 26 August 2026

Your query got slow and nothing about it changed

A query runs in eighty milliseconds. You change one word in it and the same query takes nine seconds. Same table, same index, same columns. The database decided to answer it a completely different way, and it did not tell you.

Chapters

You run a query and it comes back in eighty milliseconds. You run the same query again with one word changed, and this time it takes nine seconds.

It is the same table. The same index is there, on the right column. Nothing about the query changed.

The database decided to answer it a completely different way, and it did not tell you. This is why that happens, and why the index you added is not the switch you think it is.

The short version

  • Without an index the database reads every row in order. That is called a scan, and it is not stupid. Reading in order is what a disk does best.
  • An index is not a setting. It is a second copy of your data, sorted, kept in a tree that is four or five layers deep even over millions of rows.
  • Using it is two jobs, not one: find the matching values, then go and fetch each matching row. Once enough rows match, the fetching costs more than reading everything, so the database stops using the index. That is usually it being right.
  • Which means a rare word is fast and a common word is slow, with the same query and the same index. And the words people type are the common ones.
  • The copy has to be kept correct on every write, forever, whether anyone reads it or not.

What happens with no index at all

You ask the database for one row. It does the only thing it can: reads the first row and checks it, then the second, then the third, all the way down.

A table cylinder with an arrow travelling straight down through every row in order
That is a sequential scan, and it means exactly what it sounds like. Every row, in order, checked one at a time. With twelve rows that is instant. With ten million it is not.

Here is the part that usually gets left out. A scan is not stupid. Reading in order is what a disk does best. There is no jumping: it asks for a block of rows, gets a block of rows, and keeps going.

So the scan is not the enemy. It is the honest starting point. It is the price of telling the database nothing in advance.

An index is a second copy of your data

An index is how you tell it in advance, and it is not a setting you switch on.

Take one column. Copy every value out of the table. Sort them. Next to each value, write down where the real row lives.

One column of values copied out of the table and sorted into a separate list
One column, copied out and sorted, with a pointer back to where each real row lives.

So why is that fast? Not because sorted lists are quick to search. If it really were one long list of ten million values you would look in the middle, keep one side, and repeat, and every one of those steps lands somewhere else that the database has to go and read. Sorting reduced the jumping. It did not remove it.

So it is not a list. It is a tree. The bottom row is your values in order. Above them sits a layer of signposts, each saying everything under this one is between these two values, then another layer above that, until the top is a single node.

A tree of signposts above a row of values, with the arithmetic four per node, ten layers, 1,048,576 values
Each node does not carry two entries, it carries hundreds, so every layer multiplies what the tree can hold. Real nodes are far wider than this, which is why an index over millions of rows is only four or five layers deep. Finding one row among ten million takes four steps, not ten million checks.

The tree is only part of it. The bottom layer is also a chain, each value pointing to the next one and the one before.

The bottom layer of the tree drawn as a linked chain running left to right
Once the tree finds your starting point you do not go back up, you walk sideways. That is why asking for everything between March and June is fast, and why sorting your results is sometimes free. The data is already in that order.

The mistake almost everyone makes

An index can cover more than one column. Say you index category and colour, in that order. Sorted like that, all the shoes sit together, and inside the shoes the colours are in order. So the index can find shoes, and it can find red shoes.

Three groups labelled shoes, shirts and bags, with red items scattered through all three
But it cannot find everything red. Red shoes are in one place, red shirts in another, red bags somewhere else. Colour was sorted second, so red is everywhere and there is nowhere to start.

And nothing warns you. The index has colour in its name. It looks like it covers colour. It does not.

Why it ignores the index you built

There is a part of the database called the planner. Its job is to decide how to answer your question, and it does not know your data. It estimates. Before it runs anything, it prices both options: the scan, and the index.

The index is two jobs, not one.

An index tree finding matching values, then a fan of separate jumps into the table to fetch each row
First it finds which values match. Then it goes to the table and fetches those rows, one jump for every row that matched.

So everything depends on how many rows match. Ten rows is ten jumps and that is cheap. A million rows is a million jumps, and at that point reading every row in order is cheaper.

A bar showing the index winning at the left end and reading everything winning past a crossover point
Once more than about five or ten percent of the table matches what you asked for, the scan wins. So when you hear that the database is ignoring your index, that is usually the database being right.

Now watch what that does to the same query.

Two identical queries differing by one word, one returning in 80 ms and the other matching most of the table
Search for a rare word and few rows match, so the index is worth using and it is fast. Search for a common word and a lot of rows match, so the index is not worth using and it is not used. Same query, same index, completely different plan. And the words people actually type are the common ones.

That estimate comes from a summary of your data that the database keeps and refreshes in the background. If it is out of date, every decision after it is made using last month’s shape of your data. It does not warn you about that either.

There is a slower version of this. Nobody changes the query, the table just grows. A word that used to match a thousand rows now matches most of the table, the estimate crosses the line, the plan flips, and a query that worked for two years is suddenly slow. Nothing about it changed. The data did.

Joins, where the guessing gets worse

Real queries use several tables. When a query uses two tables together the database matches the rows in one against the rows in the other. That is a join, and the planner has to guess how many rows the first join gives back. It uses that guess for the next one, and that one for the next.

A bar chart showing guesses wrong by ten times or more, at 16 percent for one join, 32 for two and 52 for three
Measured on Postgres in the Join Order Benchmark. With one join, one guess in six is wrong by ten times or more. With three joins it is more than half, so once your query touches three tables the guess is wrong more often than it is right.

Postgres also has a limit. Past eight tables it stops looking for a better order to combine them. Past twelve it stops searching for the best order at all, and instead makes plans at random, keeps the better ones and mixes them. That is a genetic algorithm. So your database has a number where guessing becomes cheaper than thinking, and it is twelve.

No index fixes this. An index finds rows. This is about which tables to combine first, and that is a different problem.

The half nobody asks about

Everything so far has been about whether the database uses your index. There is another question.

When you added that index you did not make the query faster. You made a second copy of your data, and something has to keep that copy correct on every write.

One row being inserted into a table with five indexes, counting up to six separate writes
Insert one row. That is one write to the table, then one more for every index you have. Five indexes and putting one row in your database is six separate writes.

Change one column and every index that mentions that column has to be rewritten too. Delete the row and all of them have to forget it.

I used to think an index could only help. Worst case, I thought, it just sits there and nobody uses it. That is wrong. An index nobody reads still costs you on every single write, forever, and nothing ever tells you.

Building one is not free either. By default, creating an index blocks all writes to that table until it finishes.

So this is what I think people get wrong. It is not that they add an index too early, because adding one is often the right answer. It is that they think it is free.

A table of index usage counts, with one index showing zero uses
Something to do this week: your database counts how many times it has used each index. Look at that list and find the zeros. An index nobody uses should be deleted. That is not a clever trick, it is a bill you did not know you were paying.

The real one

I want to be honest about this first: I did not watch it happen, I arrived after.

An online store with an old system underneath it and a search page that was already slow when I got there. The person who built it was not careless. It worked when they built it. The data grew for years and nobody was watching, and by the time it reached me it was just the slow search page.

The first thing I did was look for missing indexes. I added them. The page time barely moved. So I blamed the application code, too many trips to the database and loading more than it needed. That was not it either.

What was actually happening is the thing you already know. Some searches were fine. The popular words, the ones people actually typed, were the slow ones. And the data was spread across a lot of tables, so one search had to reach into a lot of them. I do not remember how many, so I am not going to tell you it was past twelve.

A search box above a set of tables reshaped into two, kept up to date with the originals
At some point I stopped trying to make the query faster and changed the data instead. About a fifth of it moved into new tables, shaped for the way the search actually read it. The answer no longer had to be put back together every time.

And I do not have before and after numbers for you. Nobody measured it properly, and I am not going to invent a figure to make this sound better. Every article about this ends with two seconds becoming three milliseconds. Mine ends with a page that stopped being the thing people complained about.

Three things I left out on purpose: there are other kinds of index, not just the tree. There is putting a cache in front of all this, which is a real fix. And there is what happens when two people write at the same time.

So, nothing about it changed

The database is guessing how much work each option will be. That guess comes from the shape of your data. Your data changes shape, and the plan follows it.

And an index is not a switch you turn on. It is a second copy of your data, kept correct on every write, and read only when the database thinks it is worth it.

If this is the first one you have read, there are three more that go with it: containers, load balancers and what a database is actually for. Together they are how something you wrote on your laptop ends up serving strangers.