1 · The two arrow forms
One construct, written either direction
The arrow encodes the foreign key. Flip it and the two roles swap places — but the shape of the statement stays the same. Hover or tap any highlighted token to see what it does.
The query built so far — table, view, CTE, subquery, or join tree. The proof runs against this intermediate result, not the base tables.
[INNER] / LEFT / RIGHT / FULL — orthogonal to FOR KEY.
Outer types only change null-extension of unmatched rows. CROSS and NATURAL: excluded.
Declares that this equijoin follows a FOREIGN KEY; the compiler must prove it or the statement fails (see the errors). The list is always columns of the newly joined relation — never alias-qualified. Both lists: equal arity, pairwise comparable types.
The newly joined relation is the referencing side: descend to detail — the only join where rows can multiply (result shape).
The newly joined relation is the referenced side: a pure lookup — the running row count is unchanged (result shape).
An alias already visible in the left operand, and its columns. Pairing is positional — 1st with 1st, …, nth with nth; order need not match the constraint declaration.
Optional, join-local: ANDed into the match condition. Applied after the proof — never proof evidence for this join.
2 · What both forms mean
An ordinary equijoin — plus a proof obligation
Either arrow desugars to the same plain equijoin and the same
output columns as ON. The one thing it adds is a compile-time
proof that ON can never give you.
… JOIN … ON f.f1 = p.p1 AND … AND f.fn = p.pn [AND pred]
Output columns are exactly as with ON —
no USING-style column merging.
FOREIGN KEY (f1, …, fn) REFERENCES p (p1, …, pn)
On f's base table, ENFORCED and NOT DEFERRABLE — plus the proof facts still alive at the join point.
The desugared join is what runs. The FOR KEY wrapper adds
nothing at runtime — its entire job is the compile-time proof that this
equijoin really follows the declared key and preserves the referencing
rows.
3 · The contract
A key join holds iff three conditions do
State it over two multisets: the referencing multiset (the referencing side projected onto its key columns) and the referenced multiset (the referenced side projected onto its key columns, all-non-NULL rows only).
1 Uniqueness
Every tuple of the referenced multiset occurs exactly once — so no referencing row can match more than one, and none is duplicated.
2 Containment
Every all-non-NULL tuple of the referencing multiset appears among the referenced tuples — so no referencing row is dropped for want of a match.
3 Null safety
A referencing tuple with a NULL component matches nothing. Either (3a) every f-column is proven NOT NULL, or (3b) the join type preserves the referencing side.
Every referencing row appears exactly once in the result; each all-non-NULL key is enriched with its unique referenced row.
4 · Result shape
The arrow direction decides which side sets the rows
The diagrams follow the same visual convention as the contract: colored cells mark key values, grey cells represent other columns, and arrows point from referencing keys to referenced keys.
<- f
(…)-> p
(…)5 · Cardinality-preserving forms
Four forms keep the result 1 : 1 with the referencing side
Result rows = referencing rows, each exactly once, nothing else. These are exactly the join types that never null-extend the referenced side.
… [INNER] JOIN f FOR KEY (…) -> p (…) … RIGHT [OUTER] JOIN f FOR KEY (…) -> p (…) … [INNER] JOIN p FOR KEY (…) <- f (…) … LEFT [OUTER] JOIN p FOR KEY (…) <- f (…)
The rule behind the list: the outer-preserved side, if any,
must be the referencing side. Null-extending the
referenced side (a LEFT … ->,
RIGHT … <-, or any FULL) would
add its unmatched rows and break the 1 : 1.
Visual scan rule: in a FROM clause
made only of Key Joins, if every FOR KEY arrow points
<- and every join is INNER or
LEFT, the first FROM relation is preserved
1 : 1.
6 · The compile-time proof
Declarations grant facts; query shape revokes them
Each condition becomes a fact the compiler must still hold at the join point. Schema declarations grant the facts; upstream query operations can take them away.
| Fact needed at the join point | Granted by ✓ | Revoked by ✗ |
|---|---|---|
1UNIQUE on
(p1, …, pn) of the referenced side |
PRIMARY KEY or UNIQUE — ENFORCED, NOT DEFERRABLE; or, mid-query, GROUP BY / DISTINCT on exactly the key columns | a preceding join that can duplicate the referenced side |
2COVERAGE —
every all-non-NULL (f1, …, fn)
tuple has its referenced row |
FOREIGN KEY — ENFORCED, NOT DEFERRABLE | filtering the referenced side (WHERE, HAVING, sampling, set ops, OFFSET / FETCH) — unless each direct key-equality filter is matched on the referencing columns |
3NOT NULL on
f1 … fn of the referencing side |
(3a) a NOT NULL constraint — or (3b) a join type that preserves the referencing side | upstream outer-join null-extension |
Propagation through derived tables
declarations grant the facts: PRIMARY KEY / UNIQUE · FOREIGN KEY · NOT NULL
facts pass through while every named column traces to exactly one base column
set ops · recursive CTEs · LATERAL · expressions over keys
Stored key joins (views, SQL-body routines) depend on every proof source. Dropping or weakening one is RESTRICTed, or CASCADE drops the dependent definition.
7 · When it won't compile
Four ways the proof can fail
If no proof can be formed, the statement is rejected before it runs. Each error maps back to the condition it couldn't establish.
no matching foreign key constraint for f
(f1, …, fn) referencing
p (p1, …, pn)
The named column pairing must match a declared referential constraint.
1 referenced columns p
(p1, …, pn) are not proven unique at the join point
A preceding join may duplicate the referenced side, so it is no longer unique at this point.
2 not every f
(f1, …, fn) value can be proven to have a
matching p row
The referenced side is filtered before this key join — row coverage is lost.
3 this inner join could drop referencing rows:
f (f1, …, fn) can be NULL
Declared nullable, or null-extended by a preceding outer join — preserve it with an outer join type.