In a JPQL query with a LEFT JOIN, what is the difference between putting an extra restriction in the JOIN ... ON clause and putting it in the WHERE clause?
answer
- ON = during join, WHERE = after join
- LEFT + WHERE on right alias = inner join
- Nulls fail WHERE comparisons
- Inner join: ON and WHERE equivalent
- HQL 'with' = JPQL 'on'
basics
~20 sON restricts which rows may join, so unmatched parents survive with nulls. WHERE filters the joined result, so a condition on the outer side removes those parents and turns the outer join into an inner one. For inner joins the two are equivalent.
solid answer
~50 sWith `LEFT JOIN a.books b ON b.price > 30`, the ON condition is applied **while** joining: books cheaper than 30 simply do not match, but the author row is still emitted with `b` null. With `LEFT JOIN a.books b WHERE b.price > 30`, the join happens first, then the filter runs over the joined rows; every row where `b` is null fails the predicate, so authors with no expensive book — and authors with no books at all — are dropped. The query has become an inner join with extra steps. For an **inner** join the distinction disappears logically: rows that fail either way are removed, and the optimizer is free to push the predicate either direction. JPQL gained the explicit `ON` clause in JPA 2.1 (Hibernate has supported it longer, originally spelled `WITH` in HQL). Practical rule: conditions that describe *which related rows count* belong in ON; conditions that describe *which result rows you want* belong in WHERE.
code
java · 5 lines// keeps every author, expensive books attached when present
em.createQuery("select a, b from Author a left join a.books b on b.price > 30", Object[].class);
// silently equivalent to an inner join
em.createQuery("select a, b from Author a left join a.books b where b.price > 30", Object[].class);go deeper
Recall that ON filters during the join and WHERE filters afterwards, and that WHERE on an outer alias cancels the outer join.
Show both queries, describe the differing result sets, and note that the distinction vanishes for inner joins.
Recognise it as a common production defect in reporting queries, and require a test with a childless parent to prove intent.
Set the convention (relationship predicates in ON, result predicates in WHERE) so join-type changes stay safe, and treat outer-alias predicates in WHERE as review triggers.
## Two different moments in query evaluation A join builds a row set; the WHERE clause then filters it. Conceptually the order is: 1. FROM/JOIN — produce combined rows, honouring the join type. 2. ON — decides which pairs match. For an outer join, unmatched left rows are still preserved, padded with nulls. 3. WHERE — filters the rows that came out of step 1–2. Nulls fail comparisons. Because the outer join's row-preservation happens in step 2 and the WHERE runs in step 3, a predicate on the optional side behaves completely differently depending on where you put it. ## The canonical example ``` -- A: restriction inside the join select a, b from Author a left join a.books b on b.price > 30 -- B: restriction after the join select a, b from Author a left join a.books b where b.price > 30 ``` Query A returns **every** author. Authors with no books at all, and authors whose books are all cheap, come back with `b = null`. Query B returns only authors who have at least one book over 30, each repeated once per such book — identical to an inner join. This is exactly the difference between "list all authors and show their expensive books, if any" and "list authors who have an expensive book". Both are legitimate requirements; the bug is writing one and meaning the other. ## The third variant: null-tolerant WHERE ``` select a, b from Author a left join a.books b where b is null or b.price > 30 ``` This keeps bookless authors, but it does **not** keep an author who has only cheap books — because for that author `b` is not null and the price fails. So it sits between A and B, and is almost never what you want. When in doubt, use ON. ## Syntax and history - JPQL: `LEFT JOIN o.items i ON i.status = :status` — standardised in JPA 2.1. - HQL before that used `WITH`: `left join o.items i with i.status = :status`. Hibernate still accepts `with`, and treats `on` as the modern spelling. - The Criteria API expresses the same thing with `join.on(predicate)`. A related restriction worth knowing: in strict JPQL an ON clause on an *association* join may only reference the joined alias and already-visible aliases; you cannot use it to re-root the query. Hibernate is more permissive, especially for entity joins with an arbitrary ON condition. ## Inner joins: no semantic difference For `INNER JOIN`, `on b.price > 30` and `where b.price > 30` produce the same rows. Databases push predicates around freely, so there is normally no performance difference either. Style-wise, many teams still prefer ON for conditions that are *about the relationship* ("only active memberships") and WHERE for conditions about the result ("only customers in Germany"), because it reads better and survives a later change of the join type to LEFT. ## Where it bites in real code The most common production bug is a dashboard or export that is supposed to show every parent with an optional aggregate — every account with its last failed payment, every product with its current discount — written as a LEFT JOIN and then filtered in WHERE. It looks right in test data where every parent happens to have a child, and quietly drops rows in production. Because the JPQL is one line and the join type is present and correct, code review often misses it; the reliable check is a test case with a parent that has no matching child. ## Rule of thumb - Condition selects *which related rows participate* → ON. - Condition selects *which result rows survive* → WHERE. - Any predicate on the alias of a LEFT JOIN that appears in WHERE is a defect candidate until proven intentional.
- Is there any difference between ON and WHERE for an INNER JOIN?Not semantically: rows failing the predicate are removed either way, and query planners routinely move such predicates across the join. The choice is stylistic — putting relationship conditions in ON keeps the meaning stable if someone later changes the join to LEFT, which is when the placement suddenly starts to matter.
- How do you write the same ON restriction with the Criteria API?Create the join and call on() with a predicate built from the join alias, for example root.join("books", JoinType.LEFT).on(cb.gt(join.get("price"), 30)). Predicates added via where() on the CriteriaQuery apply after the join, reproducing the inner-join behaviour instead.
ON is the guest list for who may sit at the table; WHERE is the bouncer who empties tables afterwards — and an empty chair (null) never survives the bouncer.
saying these in an interview costs you the question
- Claiming ON and WHERE are always interchangeable
- Filtering a LEFT JOIN alias in WHERE and still expecting childless parents
- Thinking 'where b is null or b.price > 30' is equivalent to the ON version
- Believing JPQL has no ON clause and you must fall back to native SQL
- Assuming ON is only for unmapped/entity joins