Answers · The systems around the model
What's the difference between a nested loop join and a hash join?
In shortOne looks each outer row up in the inner table, fast only when the outer side is tiny and indexed; the other builds a lookup table in memory and reads each side once, which suits two large tables.
A nested loop join takes one row from the outer table, hunts through the inner table for matches, emits them, and moves to the next row. A hash join reads one table to the end, arranges it in memory so any value can be found immediately, then reads the other table once, probing that structure as it goes. Both return the same rows. The difference is entirely in how the cost grows, and it is not a small difference: on one pair of tables the first method can be three hundred times slower than the second, and on another pair it is forty times faster.
Here is the number of rows on the outer side, the one being walked, is the cost of one descent into the inner side's index, is the matches found per outer row, and is the cost of fetching each match. Put numbers in it. Twelve outer rows at four page reads per descent, with two matches each at one page read per fetch, is page reads. Nothing about that is slow. Raise the outer side to two million rows and keep everything else identical: page reads, each one a scattered seek rather than a sequential run. If the inner side has no usable index, stops being four pages and becomes a full scan of the whole inner table, which is the single worst thing you can ask a database to do.
Now the same join done the other way, at a scale where it matters: one million rows joined to two hundred thousand, with an index on the inner side and a hundred rows to a page. The nested loop costs page reads, scattered. The hash join reads pages to build its structure and pages to probe it, so page reads, sequential. The ratio is . Same query, same answer, and no amount of tuning closes a gap of that shape.
| nested loop | hash join |
|---|---|
| outer side read once, inner side searched once per outer row | each side read exactly once |
| grows in proportion to the rows on the outer side | grows in proportion to the two sides added together |
| no working memory beyond one index lookup at a time | the build side's used columns must fit the memory allowance |
| outer side is tens or hundreds of rows and the inner side is indexed | both sides are large and the smaller one fits in memory |
| any predicate an index can answer, including ranges | equality on the joining column only |
Why the plan is not a coin toss
The usual assumption is that the database picks one at random, or that the hash join is the modern method and the nested loop is the naive one kept for backwards compatibility. Run the arithmetic at the small end and it inverts. Twelve outer rows against an indexed inner side cost page reads. A hash join cannot beat that, because before it matches anything it must read the entire build side: if the inner table is 200,000 rows at a hundred to a page, that is pages. The ratio is , and it favours the loop. Reading two thousand pages to match twelve rows is pure waste, which is exactly why the database keeps both methods and decides from the sizes.
For a hash join the consequential decision is which side gets built, and it must be the smaller one. At 120 bytes per row of working memory, 900,000 build rows need bytes, about 108 MB, which sits inside a 128 MB allowance. At 1,600,000 rows the same arithmetic gives 192 MB, which does not. That is not a marginal miss. Past the allowance the join does not fail, it splits both sides into groups by the joining value, writes them to disk, and joins one pair of groups at a time: correct, and several times slower, with the slowdown arriving as a step rather than a slope. A join that ran in 1.6 seconds at 900,000 build rows can take 9.1 seconds at 1.6 million, with nothing in the query changed. This is why an estimate saying four thousand rows when the truth is two million is so damaging: it does not merely misprice the plan, it puts the wrong table in memory.
Where each of them stops
A hash join needs an equality on the joining column, because it works by turning a value into an address. A predicate like a range or a not-equal cannot be probed, so for those the nested loop is the only one of the two available, and it is excellent if an index can answer the predicate. The nested loop's limit is on the other axis: it is proportional to the outer side, so it stops being viable the moment the outer side reaches the thousands, which is why it is so often the wrong choice in a plan produced by a bad estimate of how small the small side really is. And if both sides happen to arrive already sorted on the joining column, from an index or from an earlier step, walking them together in one pass beats both of these, since it needs no memory and no per-row lookup at all.
Worth keeping
- A nested loop join's cost is proportional to the number of rows on the outer side, so it is either the cheapest method available or by far the worst, with little in between.
- A hash join reads each side exactly once, but only while the build side's used columns fit in the memory it was given; past that it splits both sides to disk and the time jumps in a step.
- Neither method is an upgrade on the other: the choice turns on how many rows are on each side and whether the inner side has an index.
Where this goes next
This question is one step of Why a Query Is Slow, a course that works through its subject from the beginning. The page that teaches this part of it is Putting Two Tables Together, and it reads the whole way through whether or not you have an account.
What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.
Read that pageThe whole courseOther questions people ask about this
- What's the difference between a CDN cache and caching inside my own app?A CDN cache sits near the user and can only hold answers that are identical for everyone, while a cache inside your app can be personal but exists once per process.
- What's the difference between LRU and LFU, and which one should I use?LRU drops the item unused for longest; LFU drops the item requested least often. Both guess at future use, and the cheap win is refusing one-off arrivals rather than counting requests.
- Why can't a system just stay consistent and available when the network splits?During a split, a cut-off machine can only answer from the data it already holds, so it must either risk being wrong or refuse to answer.
- How can I tell if my traffic is bursty or just high volume?Compare the variance of the gaps between arrivals with the square of their average gap: high volume alone does not make traffic bursty.
- Why does average latency hide the slow requests users actually feel?Waiting times are lopsided, so the average sits between the common case and the bad case and describes neither, and it is the bad case that users keep meeting.
- Should the gradients be summed or averaged across GPUs?Sum the gradients when each device already divides its loss by the global batch size; average them when each device uses its own local mean. Doing both makes the gradient too small by the number of devices.