What is an index nested loop join, and how does its cost model differ from a nested loop join over an inner table with no usable index?
answer
- probe an index per outer row, no inner scan
- cost = n_outer * (descent + fetches)
- inner table size only inside a log
- row fetches are random I/O
- covering index removes the fetch
basics
~20 sInstead of scanning the inner input, it performs an index lookup on the inner table for each outer row's join key. Cost becomes outer rows times the cost of one lookup (an index descent plus row fetches) rather than outer rows times the whole inner table, so it scales with the outer side, not the product.
solid answer
~60 sThe outer input is scanned once, and for each outer row the join key value is pushed into an index probe on the inner table — the join predicate becomes an access condition instead of a filter. Cost is roughly n_outer * (index descent + matching entries + row fetches), versus n_outer * size_of_inner for the unindexed version. That turns a multiplicative cost into one linear in the outer row count, with a small constant when the index is selective. The constant matters, though: each probe walks the index from the root and, unless the index covers every column the query needs, fetches the base row, and those fetches are effectively random I/O. So the operator is excellent when the outer input is small or well filtered and the inner index is selective, and it degrades badly as the outer row count grows — a million probes of a few random reads each is far worse than two sequential scans and a hash join. It also pipelines and needs almost no memory, giving the fastest first row.
code
text · 5 linesNested Loop
-> Index Scan on orders (filter: created_at >= ...) rows=120
-> Index Scan on customers using customers_pkey
Index Cond: customers.id = orders.customer_id
(executed 120 times)go deeper
Say each outer row does an index lookup on the inner table instead of scanning it, so the join is fast when the outer side is small.
Give the cost formula, note the inner size only appears logarithmically, and mention covering indexes removing base-row fetches.
Discuss random-I/O constants, cache behaviour, index usability pitfalls, and the crossover point where hash or merge join wins.
Frame index design and the OLTP-versus-scan regime split, and how estimate quality decides which side of the crossover the optimizer lands on.
## The variant An index nested loop join keeps the outer loop and deletes the inner one. For each outer row, the engine takes the join key value and uses it as a lookup key into an index on the inner table's join column. Only the matching inner entries are visited. The join predicate stops being a filter applied to every inner row and becomes an access condition that navigates directly to the matches. ## Cost model Unindexed nested loop: n_outer * P_inner page reads, growing with the size of both inputs. Index nested loop: one scan of the outer input, plus n_outer probes. Each probe costs a descent through the index (a handful of page accesses, logarithmic in inner table size and usually shallow because index fan-out is high), plus reading the matching index entries, plus one base-table fetch per matching row when the index does not contain all required columns. Total is roughly n_outer * (descent + matches * fetch). The size of the inner table appears only inside a logarithm and in cache behaviour — that is the whole point: the join now scales with the outer side. ## Why the constant is not small Each base-row fetch follows index order, not physical order, so it behaves like random I/O. Upper index levels are usually cached, but leaf pages and heap pages for a large inner table often are not. This is why the operator's advantage collapses as the outer count rises: at a million outer rows, even a few random reads each dwarfs the cost of scanning the inner table once sequentially and hashing it. Engines mitigate this with batched or prefetching lookup strategies that gather keys and fetch in a more sequential pattern, but the shape of the tradeoff stands. ## Requirements The inner index must be usable for the join predicate: the join column must be a leading column of the index, and the comparison must be one the index can navigate. Type mismatches or an expression wrapped around the inner column can silently prevent index use, turning the plan back into a full scan per outer row — a classic cause of a query that is suddenly thousands of times slower. A covering index that includes the other columns the query needs removes the base-row fetch and makes the probe dramatically cheaper. ## Where it shines Highly selective queries: fetch a handful of orders, then look up their customers by primary key. Point queries and small result sets in transactional workloads are almost always index nested loops, and that is usually right there — a hash join would read whole tables to answer a question about ten rows. It also pipelines, returning the first joined row after one probe, so queries that stop early benefit enormously. ## Where it fails When the outer input is large, when the inner index is not selective so each probe matches many rows, or when the inner table is far bigger than cache so every probe is a physical read. Those are the conditions under which hash join or merge join over sequential scans wins, and the optimizer's job here is estimating the outer row count well enough to tell the two regimes apart. ## Practical reading In a plan, this operator appears as a loop node whose inner child is an index access parameterised by a value from the outer side, executed once per outer row. The two numbers to look at are how many times the inner side executed and how many rows each execution returned; multiplied, they tell you what the join really cost.
- What makes a covering index especially valuable for the inner side of an index nested loop join?If the index contains every column the query needs from the inner table, the probe ends at the index leaf and no base-table row fetch is required. That removes the random I/O that usually dominates per-probe cost, so the join can stay cheap even at higher outer row counts.
- Why does an index nested loop join stop being a good choice as the outer input grows?The cost is linear in outer rows but the per-probe constant includes random access to index leaves and base rows. Once the number of probes is large enough that the join touches much of the inner table anyway, one sequential scan plus a hash or merge join is cheaper, because sequential I/O and a single pass beat millions of scattered lookups.
Instead of reading the phone book cover to cover for each name on your list, you look each name up alphabetically — fast for a short list, still slow if your list has a million names.
saying these in an interview costs you the question
- Saying the inner table is scanned once per outer row — that is the unindexed form, not the index variant
- Treating the per-probe cost as free and concluding index nested loop is always best
- Forgetting that a non-covering index adds a random base-row fetch per matching entry
- Assuming any index on the inner table helps — the join column must be a leading column and usable for the predicate