Inner & Outer Joins

Joins combine rows from two or more tables based on a related column — RecipeBox's recipes, users, and reviews tables are only useful together once you can join them. This lesson covers INNER JOIN and LEFT [OUTER] JOIN, SQLite's two long-standing join types, plus the real story behind RIGHT JOIN and FULL OUTER JOIN support.

INNER JOIN

INNER JOIN (or just JOIN — the INNER keyword is optional and implied) returns only the rows that have a match on both sides. A recipe with zero reviews simply won't appear at all.

-- Every recipe paired with its author (a recipe always has exactly one author)
SELECT r.title, u.username AS author
FROM recipes r
INNER JOIN users u ON u.id = r.user_id;

-- Recipes together with each of their reviews (recipes with 0 reviews are excluded)
SELECT r.title, rv.rating, rv.comment
FROM recipes r
JOIN reviews rv ON rv.recipe_id = r.id;

LEFT [OUTER] JOIN

LEFT JOIN keeps every row from the left (first-listed) table, matching in rows from the right table where possible — and filling the right side's columns with NULL where there's no match. This is exactly what you need to find recipes with zero reviews.

-- Every recipe, with review columns NULL when a recipe has no reviews yet
SELECT r.title, rv.rating
FROM recipes r
LEFT JOIN reviews rv ON rv.recipe_id = r.id;

-- Isolate recipes that have never been reviewed at all
SELECT r.title
FROM recipes r
LEFT JOIN reviews rv ON rv.recipe_id = r.id
WHERE rv.id IS NULL;
Why the WHERE clause matters here: The WHERE rv.id IS NULL filter is what narrows a "recipes with all their reviews" result down to "recipes with no reviews" — without it, you'd get every recipe, reviewed or not.

RIGHT JOIN and FULL OUTER JOIN

For a long time, SQLite genuinely did not support RIGHT JOIN or FULL OUTER JOIN — only INNER JOIN and LEFT [OUTER] JOIN existed. That changed with SQLite 3.39.0 (released June 2022), which added both.

-- RIGHT JOIN: works on SQLite 3.39.0+. Keeps every user, even ones with no recipes.
SELECT u.username, r.title
FROM recipes r
RIGHT JOIN users u ON u.id = r.user_id;

-- FULL OUTER JOIN: works on SQLite 3.39.0+. Keeps unmatched rows from both sides.
SELECT u.username, r.title
FROM users u
FULL OUTER JOIN recipes r ON r.user_id = u.id;

You'll still run into older SQLite builds in the wild — bundled with older Android or Python installs, for instance — where these two keywords raise a syntax error. Two workarounds cover both cases without needing them:

-- RIGHT JOIN workaround: just flip the table order and use LEFT JOIN instead
SELECT u.username, r.title
FROM users u
LEFT JOIN recipes r ON r.user_id = u.id;

-- FULL OUTER JOIN workaround: UNION two LEFT JOINs run in opposite directions
SELECT u.username, r.title
FROM users u
LEFT JOIN recipes r ON r.user_id = u.id
UNION
SELECT u.username, r.title
FROM recipes r
LEFT JOIN users u ON u.id = r.user_id;
Rule of thumb: If you're targeting a modern SQLite (3.39.0+, which is now the common case), RIGHT JOIN and FULL OUTER JOIN work as expected. If you're not sure of the SQLite version — especially on older embedded or mobile platforms — the flip-and-LEFT JOIN / UNION patterns above always work.
Next up: Head to Self Joins, Cross Joins & Unions to join a table to itself and combine result sets with UNION.