VillageSQL is a drop-in replacement for MySQL with extensions.
All examples on this page work on VillageSQL. Install Now →
The tables used in this guide
Index the join column
Without an index on the joining column, the server builds a hash table over one side and scans the other.EXPLAIN FORMAT=TREE names it:
join_buffer_size the server spills it to disk in batches.
Adding the index replaces all of that with a lookup:
EXPLAIN output the same 2 plans read as Extra: Using join buffer (hash join) and type: ref. That table also shows the order the server
chose to join in, which matters as soon as a query has 3 or more tables: the
server reads them top to bottom, and the row estimate on the first one
multiplies through everything below it.
Reading EXPLAIN output covers the rest of the plan.
An equality condition is what a hash join can look up. Join on a range or an
inequality instead and the server still builds the hash, but it has nothing to
look up with: the plan reads Inner hash join (no condition) with the test in a
Filter above it, so every pair of rows is formed and then thrown away. That is
the cost an indexed equality join avoids.
A WHERE clause turns a LEFT JOIN back into an INNER JOIN
The most common wrong-row-count bug, and nothing warns you.o.amount. WHERE
then tests each row, NULL > 50 is NULL rather than true, and those rows are
dropped. Moving the condition into ON makes it part of what counts as a
match instead.
The rule: on an outer join, a condition on the inner table belongs in ON, and
a condition on the preserved table belongs in WHERE. The exception is
WHERE <inner table key> IS NULL, where dropping the matched rows is the
point.
A NULL in the join column matches nothing
orders.customer_id is nullable, and order 4 has no customer. An inner join
drops it, because NULL is not equal to anything, including another NULL.
customer_id is NULL, which is
the half that surprises people: the row exists, its amount is real, and no join
condition can reach it. A revenue total built on that join is short by 15.00 and
nothing says so.
When a join column is nullable, decide what an orphan row should do before you
write the join. LEFT JOIN from orders keeps it with a NULL customer name.
NULL in MySQL covers the comparison rule behind it.
A one-to-many join multiplies your rows
A join returns 1 row per match, so joining to a table that holds several matching rows repeats the left row once per match. Totals computed over that result are then wrong, usually too high, and the query still looks right.GROUP BY c.id with SUM or COUNT over the joined column. See
MySQL GROUP BY performance.
The same arithmetic goes wrong faster with 2 one-to-many joins in one query,
where the 2 sets of matches multiply against each other. Aggregate each in its
own subquery rather than joining both at once.
A missing ON gives you every combination
MySQL acceptsINNER JOIN with no ON clause and reads it as a cross join, so
a typo produces the product of both tables rather than an error. 2 tables of
10,000 rows give 100 million rows.
When a join returns a suspiciously round and suspiciously large number, check
for a missing ON before anything else. A comma between tables in FROM is
the same thing in older syntax, with the matching condition usually further
down in WHERE.
MySQL has no FULL OUTER JOIN
There is noFULL OUTER JOIN in MySQL. Combine the 2 halves with UNION:
UNION removes duplicate rows, which is what makes the 2 halves join up
cleanly. UNION ALL does not emulate a full outer join at all: it returns every
matched row twice, once from each half. When your data has duplicate rows you
need to keep, deduplicate on a key column instead of reaching for UNION ALL.
JOIN or subquery
A join and anIN subquery often express the same filter, and which one the
optimizer prefers depends on the data. When you only need to know whether a
related row exists and want none of its columns, EXISTS usually reads better
and lets the server stop at the first match.
Subqueries vs JOINs in MySQL compares them with
plans.
Troubleshooting
See also
- Reading EXPLAIN output — the plan that tells you which index a join used
- Foreign keys in MySQL — the constraint that decides which column to index
- MySQL GROUP BY performance — aggregating a one-to-many join back to 1 row
- How joins work — the tutorial, if you want the basics first

