Chapter 3
Chapter 3 — RIGHT JOIN, CROSS JOIN, Self-Joins, and Multi-Table Joins
The previous chapter covered the two joins you'll write 90% of the time — INNER JOIN and LEFT JOIN. This chapter handles the rest of the toolkit: RIGHT JOIN (rarely needed, but good to recognise), CROSS JOIN (combinatorial queries and test data), self-joins (comparing rows in the same table), and the practical mechanics of chaining three or more tables together without losing your mind.
books, authors, book_authors, publishers,
customers, orders, order_items, reviews.
Setup scripts are in Chapter 1 if you need to recreate the tables.
1. RIGHT JOIN
A RIGHT JOIN is the mirror image of a LEFT JOIN. It keeps all rows from the right table and fills in NULLs for columns from the left table wherever there is no match.
Syntax
Example — find publishers with no books yet
We want every publisher in the publishers table, even those that haven't had any books
added to the books table yet.
| publisher_id | publisher | book_title |
|---|---|---|
| 7 | Horizon Press | NULL |
| 11 | Velvet Pages | NULL |
The rewrite rule — prefer LEFT JOIN
Any RIGHT JOIN can be rewritten as a LEFT JOIN by swapping the tables. Most developers use LEFT JOIN exclusively because it reads left-to-right the way you think about it: "start with this table, optionally attach that table."
When RIGHT JOIN is genuinely useful
You might see RIGHT JOIN in legacy code or when someone finds it easier to express a query that way. Accept it, understand it, but in new code always convert to LEFT JOIN.
2. CROSS JOIN — Cartesian Products
A CROSS JOIN produces every combination of rows from two tables. If table A has 5 rows and table B has 4 rows, the result has 5 × 4 = 20 rows. There is no ON condition — every row is paired with every other row.
Syntax
Use case 1 — pricing matrix
Generate every combination of book format (hardcover, paperback, ebook) and discount tier (0%, 10%, 20%) to build a pricing grid:
| format | discount | sale_price |
|---|---|---|
| Ebook | 0.00 | 14.99 |
| Ebook | 0.10 | 13.49 |
| Ebook | 0.20 | 11.99 |
| Hardcover | 0.00 | 14.99 |
| Hardcover | 0.10 | 13.49 |
| Hardcover | 0.20 | 11.99 |
| Paperback | 0.00 | 14.99 |
| Paperback | 0.10 | 13.49 |
| Paperback | 0.20 | 11.99 |
Use case 2 — generate test data
Cross joining a list of first names with last names quickly builds a large customer test set:
Implicit CROSS JOIN — a common accident
If you list two tables in FROM without a join condition, MySQL performs a CROSS JOIN silently. This is how beginners accidentally create million-row result sets:
3. Self-Join — a Table Joined to Itself
A self-join is not a special JOIN keyword — it's just joining a table to itself using two different aliases. It's the standard solution whenever rows in the same table relate to each other.
Classic example — employee/manager hierarchy
Add a staff table to the bookshop schema to demonstrate:
Self-join — show each staff member with their manager
| employee | employee_role | manager | manager_role |
|---|---|---|---|
| Sandra Holt | General Manager | NULL | NULL |
| Kenji Mori | Buyer | Tom Briggs | Buying Manager |
| Amara Osei | Buyer | Tom Briggs | Buying Manager |
| Lena Fischer | Events Coordinator | Rita Cho | Events Manager |
| Tom Briggs | Buying Manager | Sandra Holt | General Manager |
| Rita Cho | Events Manager | Sandra Holt | General Manager |
Self-join — find pairs of books by the same publisher
Self-joins also work for comparing sibling rows — finding books that share something:
a.id < b.id ensures
each pair appears exactly once and a row is never paired with itself. Use <= only
if you want self-pairs (a row matching itself).
4. Joining Three or More Tables
Multi-table joins are nothing more than chaining — each JOIN adds another "column source" to the growing result set. MySQL evaluates them left to right unless you explicitly hint otherwise. The key to readable multi-table queries is consistent, short table aliases.
Three-table join — books with their authors and publisher
Five-table join — full order line detail
A realistic reporting query: every order line with customer name, book title, author, and price paid.
line_total in a SUM. To aggregate safely,
either pick one author per book (MAX/MIN on author name), or aggregate before joining authors.
Mixing INNER and LEFT JOIN in the same query
You can — and often should — mix join types. Inner joins for required relationships, left joins for optional ones:
Alias discipline — a worked example
With five tables in a query, ambiguous column names become a real problem. Always qualify every column with its alias. A consistent naming convention helps:
Putting It Together — A Worked Exercise
Build a query that answers: "Which customers have never left a review for any book they purchased?"
This requires four tables: customers, orders, order_items, and reviews.
r.customer_id = c.customer_id to the WHERE clause, it effectively turns the
LEFT JOIN into an INNER JOIN — any row with a non-matching review would be filtered out.
Keeping it in ON means "match only reviews that belong to this customer;
if none exist, keep the row anyway." Chapter 2 covered this pattern in detail.
Quick Reference
| Join type | What it returns | Typical use | Watch out for |
|---|---|---|---|
| RIGHT JOIN | All rows from right table + matching rows from left (NULLs where no match) | "Find right-table rows with no left-table partner" — though usually rewritten as LEFT JOIN | Mixing LEFT and RIGHT in one query; just rewrite as LEFT JOIN |
| CROSS JOIN | Every row in A × every row in B — no condition | Pricing matrices, test data generation, calendar grids | Accidental cross join via comma-FROM; exponential row count on large tables |
| Self-join | Rows from the same table related to each other via an FK pointing back to the same table | Manager/employee hierarchies, comparing sibling rows, finding related records in one table | Forgetting LEFT JOIN at the top of a hierarchy; duplicate pairs without a.id < b.id |
| 3+ table join | Progressively wider result set — each JOIN adds column sources | Any real reporting query that spans bridge/junction tables | Fan-out from many-to-many: rows multiply per join leg; inflates SUM/COUNT aggregates |
a.id < b.id in self-join |
Ensures each pair appears once; no self-pairs | Finding books by the same publisher, duplicate-detection | Use <= only if you explicitly want self-pairs |
| Condition in ON vs WHERE | ON condition applied during join (keeps LEFT JOIN soft); WHERE applied after (can harden to INNER) | Filtering on the optional table's columns — must go in ON | Moving optional-table filter to WHERE silently converts LEFT to INNER JOIN |