Skip to main content

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples in this guide work on VillageSQL. Install Now →
Subqueries and JOINs often produce the same result. The choice between them matters for readability, correctness with NULLs, and — in some cases — performance.

Types of Subqueries

An uncorrelated subquery runs once and its result is used by the outer query:
A correlated subquery references a column from the outer query, so it runs once per row:
Correlated subqueries can be slow on large tables — the subquery executes for every row in the outer query. Check EXPLAIN to see how MySQL executes them.

IN vs EXISTS vs JOIN

These three patterns often solve the same problem:
The MySQL optimizer often rewrites IN (subquery) as a semi-join internally, so the performance difference is frequently negligible. For large subquery result sets, EXISTS or a JOIN is more reliable.

The NULL Problem with NOT IN

NOT IN behaves unexpectedly when the subquery returns any NULL:
If orders.customer_id has even one NULL row, the entire NOT IN returns empty. This is correct SQL behavior — NULL is unknown, so NOT IN (NULL, 1, 2) is UNKNOWN for every comparison. Use NOT EXISTS instead:
For more on MySQL’s NULL behavior, see NULL in MySQL.

Subqueries in FROM (Derived Tables)

A subquery in the FROM clause creates a derived table:
A CTE is usually clearer for this pattern — the logic is named and defined at the top rather than nested inline. See CTEs in MySQL.

Performance Considerations

The MySQL optimizer rewrites many subquery patterns. Before optimizing manually:
  1. Run EXPLAIN on both the subquery and JOIN versions to see what MySQL actually executes
  2. Ensure indexes exist on the join/correlation columns — that’s usually the bottleneck
  3. Only rewrite if EXPLAIN shows a significant difference in execution plan

Frequently Asked Questions

Is JOIN always faster than a subquery?

Not necessarily. MySQL 8.0’s optimizer rewrites IN (subquery) to semi-joins in many cases, producing the same execution plan as an explicit JOIN. Measure with EXPLAIN rather than assuming one form is always faster.

When should I use EXISTS instead of IN?

Use EXISTS when you only care whether a match exists (not what the matched data is), when the subquery could return NULLs, or when the subquery returns a large result set. EXISTS short-circuits on the first match; IN must build the full list.

What’s a semi-join?

A semi-join returns rows from the left table that have at least one matching row on the right — without producing duplicate left-table rows for multiple matches. MySQL’s optimizer uses semi-joins internally to execute IN (subquery) and EXISTS efficiently. You can’t write a semi-join directly in SQL, but EXISTS and IN (subquery) both trigger the optimizer to consider it.

Troubleshooting

See also