Resolve names through nested scopes, so that a subquery may name the outer query's columns while the outer query never sees the subquery's, and run such a correlated subquery once per row that reaches it, reporting the runs as loops=N.
"Users who have at least one order" is exists (select 1 from orders o where o.user_id = u.id). The subquery mentions u.id, a column of the outer query, so its answer is different for every user. It cannot run once. It must run again for each user, with that user's id.
That is a correlated subquery, and it needs two things this part has not built yet: a binder that can see outwards, and a subquery that runs per row.
The qualified-names stage resolved every name against one FROM. Now there are nested ones: the subquery's FROM inside the outer query's. Names are looked up inside first, then outwards:
o.user_id = u.id, o is found in the subquery's FROM and u one level out;id that both have means the inner one: in exists (select 1 from orders o where id = 10), id is o.id, and the subquery is not correlated at all;o, which is 01701, nor a column only the subquery's tables have, written bare, such as total, which is 00402.A column found in an outer scope is an outer reference. For one run of the subquery it is just a value, as a literal would be. So o.user_id = u.id can even use an index on orders (user_id), printed key=u.id, exactly like the lookup of an index nested loop.
If that looks like a nested loop join, it is one: users outside, orders inside, a condition between them. Real planners notice, and rewrite EXISTS into a semi-join and NOT EXISTS into an anti-join, so that the hash and merge joins of this part can run them. Running the subquery per row is the plain version every rewrite must agree with.
With an order whose user_id is NULL, the last stage's id not in (select user_id from orders) returned nothing. not exists (select 1 from orders o where o.user_id = u.id) asks a different question for each user: is there an order whose user_id equals this id? For cy there is none (the NULL order equals nobody), so NOT EXISTS keeps cy. That is the answer people expect, and why NOT EXISTS is usually the safer way to write "has no ...".
Inside first, then outwards.
Once per evaluation.
These are the exact lines the tests check. The full definition is on the output contract page, section 78.
| Input | Output |
|---|---|
select o.total from users u where exists (select 1 from orders o where o.user_id = u.id); | [ERROR:01701] ... |
explain of the EXISTS query | Filter (EXISTS (subquery 1)) over the users scan and Subquery (1, per row) |
explain analyze of it, 3 users | Subquery (1, per row) (actual rows=2 pages=0 loops=3) |
$droid --db corr.dbWelcome 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);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, 'cy');Insert statement executed successfully (1 rows)>insert into orders (id, user_id) values (10, 1);Insert statement executed successfully (1 rows)>select u.name, (select count(*) from orders o where o.user_id = u.id) as n from users u;name | nada | 1cy | 0(2 rows)[EXEC] rows_examined=4>.exitexiting.. good bye!
01701 there.loops for the whole statement. It is the number of runs: the rows that reached the expression.The page above is the same for every language. These are the only language-specific hints.
Scope* parent chain in the binder; at run time, each subquery evaluation gets a pointer to the outer row.&[&Row], indexed by scope depth.open.