OPTIONAL and left-join semantics

Keep rows even when part of the pattern is missing — and avoid the classic traps.

0/3 done

Theory

A plain Basic Graph Pattern is an inner join: if any triple pattern fails, the whole row disappears. OPTIONAL turns a sub-pattern into a left outer join — the left-hand bindings survive even if the optional part doesn't match; the optional variables are simply left unbound.

SELECT ?ninja ?master WHERE {
  ?ninja a :Ninja .
  OPTIONAL { ?master :teaches ?ninja . }
}

Every ninja is returned; ?master is bound where a teacher exists and unbound otherwise. This is how you express 'list all X, plus their Y if they have one' — the single most common real-world query shape.

Three traps that bite professionals

  1. bound() / COALESCE for the missing case. To detect or default an absent value, use FILTER(!bound(?master)) ('ninjas with no teacher') or COALESCE(?master, :Unknown). A normal FILTER(?master = ...) drops the unbound rows you were trying to keep.
  2. FILTER inside vs outside OPTIONAL. A FILTER inside the OPTIONAL { } decides whether the optional part matches; the same filter outside runs after the left join and can delete rows you wanted to keep. These are different queries.
  3. Nested / multiple OPTIONALs are order-sensitive. Unlike a BGP, consecutive OPTIONALs are evaluated left to right and a later one can see variables an earlier one bound. Reordering them can change results, not just speed.

Analogy

OPTIONAL is the 'if available' line on a form. 'Full name (required); middle name if you have one.' Everyone still submits the form; the middle-name box is just blank for those without one. An inner join, by contrast, would throw away every form that left the box empty.

Worked example — OPTIONAL and the anti-join

Worked example — 'every ninja, and their teacher if known'.

SELECT ?ninja ?master WHERE {
  ?ninja a :Ninja .
  OPTIONAL { ?master :teaches ?ninja . }
}
ORDER BY ?ninja

To instead find ninjas with no teacher, add a bound-check after the OPTIONAL:

SELECT ?ninja WHERE {
  ?ninja a :Ninja .
  OPTIONAL { ?master :teaches ?ninja . }
  FILTER(!bound(?master))
}

(The second query is the 'anti-join' idiom — the next lesson shows the cleaner FILTER NOT EXISTS form.)

Reading in progress · 0 of 3 activities done