With --join inl, replace a join's inner scan by a lookup through the inner table's key or an index on its join column, run once per outer row, and report it in explain, in the counters, and as loops=N in explain analyze.
A nested loop over 100 users and 200 orders examines 100 + 100 × 200 = 20100 rows to find 200 pairs. Almost all of that work is reading orders that belong to someone else. If orders has an index on user_id, each user's orders can be found in a few page reads, instead.
That is an index nested loop join: the same loop, with the inner scan replaced by a lookup whose key comes from the outer row. When the outer side is small, nothing beats it. It is how most OLTP queries join, and the reason almost every foreign key column gets an index.
Until now an IndexScan's key was a literal: key=5. Here it is a column of the other table:
For each user, the join reads the user's id, runs the right-hand scan as if it were user_id = <that id>, and pairs the user with what it finds. The rest of the condition, if any, is tested on each pair, as before.
Which column can be looked up:
u.id = o.user_id finds each user by key, IndexScan (table=users, alias=u, key=o.user_id);(user_id, total) works; one on (total, user_id) does not, for the same reason a WHERE on user_id alone cannot use it: its entries are in total order.When the index holds every column the statement reads of that entry, the lookup is an IndexOnlyScan, as for one table: it never visits the table, so it adds nothing to index_lookups.
Anything else, such as no index, < instead of =, or an expression, stays a nested loop, and explain shows it. This is the first time --join can name an algorithm that does not apply, and the rule is the same for every later one: fall back to the nested loop, visibly.
A NULL outer value looks nothing up: NULL = anything is unknown. Under a LEFT JOIN, an outer row whose lookup finds nothing is padded, as in the last stage.
rows_examined adds up what every lookup returned, and index_lookups the rows fetched through an index entry, as for a single-table index scan. A lookup by the table's own key goes through no index entry, so it adds to rows_examined only.
explain analyze gains one field. A node on the right side of a nested loop runs many times, and loops=<L> says how many: the lookup above ends with (actual rows=200 pages=<P> loops=100). Its rows and pages add up over all its runs. Every node on the right side of a nested loop prints loops, even loops=1 or loops=0; a node anywhere else prints none, so earlier output does not change.
Can the inner side be found instead of read?
One lookup per outer row.
These are the exact lines the tests check. The full definition is on the output contract page, section 69.
| Input | Output |
|---|---|
--join inl, users join orders, index on orders (user_id) | [EXEC] rows_examined=300 index_lookups=200 |
explain of it | IndexNestedLoopJoin (on: u.id = o.user_id) over SeqScan (table=users, alias=u) and IndexScan (table=orders, alias=o, index=by_user, key=u.id) |
| orders left join users | IndexNestedLoopJoin (left, on: u.id = o.user_id) with IndexScan (table=users, alias=u, key=o.user_id) |
explain analyze of ten users' orders | ... key=u.id) (actual rows=20 pages=<P> loops=10) |
$droid --db inl.db --join inlWelcome to droid!>create table users (id int primary key, name varchar(20));Create table statement executed successfully>create table orders (id int primary key, user_id int, total int);Create table statement executed successfully>insert into users (id, name) values (1, 'ada');Insert statement executed successfully (1 rows)>insert into users (id, name) values (2, 'bob');Insert statement executed successfully (1 rows)>insert into orders (id, user_id, total) values (10, 2, 5);Insert statement executed successfully (1 rows)>insert into orders (id, user_id, total) values (11, 1, 7);Insert statement executed successfully (1 rows)>create index by_user on orders (user_id);Create index statement executed successfully>select u.name, o.total from users u join orders o on u.id = o.user_id;name | totalada | 7bob | 5(2 rows)[EXEC] rows_examined=4 index_lookups=2>.exitexiting.. good bye!
index_lookups for a lookup by the table key. No index entry is read.loops on the outer side. It runs once.The page above is the same for every language. These are the only language-specific hints.