Chapter 15: Understanding LEFT, RIGHT, and FULL JOINs (Outer Joins)
While INNER JOIN
is fantastic for finding matching records, there are many scenarios where you need to retrieve data from one table even if there’s no corresponding match in the other table. For example:
- Listing all users, and for those who have placed orders, showing their order details (but still listing users without orders).
- Showing all products, and if they’ve been ordered, showing their order quantities (but still listing products that have never been ordered).
This is where Outer JOINs come into play. They include not only the rows that match in both tables but also the non-matching rows from one or both tables. The three main types of Outer JOINs are LEFT JOIN
, RIGHT JOIN
, and FULL JOIN
(though FULL JOIN
is not directly supported in MySQL but can be simulated).
What are Outer Joins?
Outer JOINs return rows from at least one of the tables, even if no matching rows are found in the other table. The non-matching columns from the unmatched side will have NULL
values in the result set.
LEFT JOIN
(orLEFT OUTER JOIN
):- Returns all rows from the left table (the first table mentioned in the
FROM
clause). - Returns matching rows from the right table.
- If there’s no match in the right table, the columns from the right table will have
NULL
values. - Think: “Give me everything from the left, and matching stuff from the right.”
- Returns all rows from the left table (the first table mentioned in the
RIGHT JOIN
(orRIGHT OUTER JOIN
):- Returns all rows from the right table (the second table mentioned in the
FROM
orJOIN
clause). - Returns matching rows from the left table.
- If there’s no match in the left table, the columns from the left table will have
NULL
values. - Think: “Give me everything from the right, and matching stuff from the left.”
- Note:
RIGHT JOIN
can always be rewritten as aLEFT JOIN
by simply swapping the table order.A RIGHT JOIN B
is the same asB LEFT JOIN A
. For consistency and readability, most developers prefer to useLEFT JOIN
exclusively.
- Returns all rows from the right table (the second table mentioned in the
FULL JOIN
(orFULL OUTER JOIN
):- Returns all rows when there is a match in either the left or the right table.
- If a row in the left table has no match in the right, the right-side columns will be
NULL
. - If a row in the right table has no match in the left, the left-side columns will be
NULL
. - Think: “Give me everything from both tables, showing NULLs where there’s no match.”
- Important for MySQL/MariaDB:
FULL JOIN
is not directly supported. You typically achieve the same result by combining aLEFT JOIN
and aRIGHT JOIN
with aUNION
clause.
SQL Syntax & Examples (MySQL/MariaDB Focus)
Let’s continue using our scriptbuzz_db
. Ensure your users
, products
, orders
, and order_items
tables are populated with some data, including users without orders, and products not in orders, to observe the NULL
behavior.
If jane_doe
(user_id 3) has no orders, and maybe you have a product not in order_items
, those will be good for demonstrating NULL
s.
1. LEFT JOIN
Examples
Syntax:
SELECT columns
FROM left_table
LEFT JOIN right_table
ON left_table.common_column = right_table.common_column;
Example 1: List all users and their order IDs (if they have any)
USE scriptbuzz_db;
SELECT
u.user_id,
u.username,
o.order_id,
o.total_amount
FROM users AS u
LEFT JOIN orders AS o
ON u.user_id = o.user_id
ORDER BY u.user_id, o.order_id;
Expected Output (example, assuming user 3 has no orders):
user_id | username | order_id | total_amount |
1 | john_doe | 1 | 150.75 |
1 | john_doe | 2 | 200.00 |
1 | john_doe | 4 | 30.00 |
2 | admin_user | 3 | 50.25 |
2 | admin_user | 5 | 100.00 |
3 | jane_doe | NULL | NULL |
What it does: All users are listed (users
is the left table). For users who have corresponding entries in the orders
table, their order_id
and total_amount
are displayed. For jane_doe
(user_id 3) who has no orders, the order_id
and total_amount
columns from the orders
table show NULL
, but jane_doe
herself is still included.
Example 2: List all products and count how many times they appear in order items
SELECT
p.product_id,
p.name,
COUNT(oi.product_id) AS times_ordered
FROM products AS p
LEFT JOIN order_items AS oi
ON p.product_id = oi.product_id
GROUP BY p.product_id, p.name
ORDER BY p.name;
Expected Output (example – assuming Laptop Pro is in 2 orders, Wireless Mouse in 2, Mech Keyboard in 2, others not in any order_items):
product_id | name | times_ordered |
7 | Gaming Monitor | 0 |
8 | Gaming Mouse | 0 |
1 | Laptop Pro | 2 |
3 | Mechanical Keyboard | 2 |
4 | USB-C Hub | 0 |
6 | USB Keyboard | 0 |
5 | Webcam 1080p | 0 |
2 | Wireless Mouse | 2 |
What it does: All products are listed. If a product has appeared in order_items
, it counts how many times. If a product has never appeared in order_items
, COUNT(oi.product_id)
correctly returns 0 because COUNT()
ignores NULL
s. This is a common pattern for “products never ordered” or similar reports.
2. RIGHT JOIN
Examples
Syntax:
SELECT columns
FROM left_table
RIGHT JOIN right_table
ON left_table.common_column = right_table.common_column;
As mentioned, RIGHT JOIN
is just the inverse of LEFT JOIN
.
Example 3: List all orders and the username of the customer (even if a theoretical order had no matching user)
-- This query is just for demonstration. In a properly constrained database,
-- an order *must* have a user, so the right table (orders) would always
-- have a match in the left table (users).
SELECT
u.username,
o.order_id,
o.total_amount
FROM users AS u
RIGHT JOIN orders AS o
ON u.user_id = o.user_id
ORDER BY o.order_id;
What it does: Returns all rows from orders
(the right table) and matching rows from users
. If there were an order
with a user_id
that didn’t exist in the users
table, that order would still be listed, and u.username
would be NULL
. Given our foreign key, this scenario won’t occur with valid data.
Equivalent LEFT JOIN
:
SELECT
u.username,
o.order_id,
o.total_amount
FROM orders AS o -- Orders is now the LEFT table
LEFT JOIN users AS u -- Users is now the RIGHT table
ON o.user_id = u.user_id
ORDER BY o.order_id;
Both queries yield the same result, but the LEFT JOIN
version is generally preferred for consistency.
3. Simulating FULL JOIN
in MySQL/MariaDB
Since FULL JOIN
isn’t directly available, you combine a LEFT JOIN
and a RIGHT JOIN
(or two LEFT JOIN
s by swapping tables) using the UNION
operator. UNION
combines the result sets of two or more SELECT
statements, removing duplicate rows. UNION ALL
includes duplicates.
Scenario: We want to list all users, all products, and see if they’re linked in order_items
(as user_id
is linked to orders
, and orders
linked to order_items
, and order_items
linked to products
). This is a complex FULL JOIN
for demonstration.
Let’s simplify to a FULL JOIN
between users
and orders
to show users without orders and orders without users (if any exist).
-- Part 1: LEFT JOIN (all users, matching orders)
SELECT
u.username,
o.order_id,
o.total_amount
FROM users AS u
LEFT JOIN orders AS o ON u.user_id = o.user_id
UNION -- UNION removes duplicate rows
-- Part 2: RIGHT JOIN (all orders, matching users, but primarily to catch orders without a user)
-- Note: Given the FK, orders should always have a user.
-- This part primarily catches cases where users.user_id might be NULL and orders.user_id is not.
-- Or if an order references a non-existent user (which FKs prevent).
SELECT
u.username,
o.order_id,
o.total_amount
FROM users AS u
RIGHT JOIN orders AS o ON u.user_id = o.user_id
WHERE u.user_id IS NULL; -- This WHERE clause is crucial to select only unmatched RIGHT rows
-- if you use UNION, but with UNION ALL you'd exclude this.
-- For a true FULL JOIN, the WHERE IS NULL is applied AFTER UNION.
-- The canonical way is (LEFT JOIN) UNION (RIGHT JOIN WHERE IS NULL on LEFT side)
Corrected Canonical FULL OUTER JOIN
Simulation in MySQL:
-- Get all users and their orders
SELECT
u.username,
o.order_id,
o.total_amount
FROM users AS u
LEFT JOIN orders AS o ON u.user_id = o.user_id
UNION ALL -- Use UNION ALL to include all rows from both parts before filtering, then filter if necessary
-- Get all orders that don't have a matching user (if any, due to data integrity issues or lack of FK)
SELECT
u.username, -- Will be NULL if no user match
o.order_id,
o.total_amount
FROM users AS u
RIGHT JOIN orders AS o ON u.user_id = o.user_id
WHERE u.user_id IS NULL; -- Only include rows from the right join where the left side didn't match
-- (i.e., orders that don't have a user)
Explanation of the FULL JOIN
simulation:
- The
LEFT JOIN
part gets all users and their matched orders. Any user without an order will haveNULL
s for order details. - The
RIGHT JOIN
part (withWHERE u.user_id IS NULL
) is specifically designed to catch anyorders
that don’t have a matchinguser
. Given a foreign key, this part should return no rows in a healthy database. If it did return rows, it would indicate data integrity issues (orders pointing to non-existent users). UNION ALL
combines the results.UNION
(withoutALL
) would remove duplicates, which might be desirable if an order could hypothetically exist without a user, and a user without an order, and they both appeared in the left/right part of the query. For a true “all rows from both sides with NULLs where no match,”UNION ALL
followed by a distinct on the result is effectivelyFULL JOIN
.
Because of foreign key constraints, FULL JOIN
on users
and orders
in a well-designed scriptbuzz_db
won’t show NULL
users for orders, as every order must have a user.
When to Use Which Join?
INNER JOIN
: When you only want to see data where there’s a match in both tables. (e.g., “users who have placed orders,” “products that have been ordered”).LEFT JOIN
: When you want to see all data from the “left” table, and matching data from the “right” table (withNULL
s for non-matches on the right). (e.g., “all users and their orders if any,” “all products and their order counts”). This is the most frequently used Outer JOIN.RIGHT JOIN
: Less common, but useful if your primary focus is the “right” table. Can always be rewritten as aLEFT JOIN
by swapping table order.FULL JOIN
: When you want to see all data from both tables, regardless of matches, withNULL
s where there are no matches. (e.g., “all users and all products, and how they relate through orders, showing those with no relation”). Use theUNION
simulation in MySQL/MariaDB.
Common Pitfalls and Best Practices
- Understanding
NULL
s: Be clear about which side of the join will produceNULL
s for non-matches. This is the defining characteristic of Outer JOINs. - Join Condition: A correct
ON
clause is essential. An incorrect one can lead to massive result sets or incorrect pairings. - Performance: Outer JOINs can be more expensive than
INNER JOIN
s, especially on large datasets. Ensure your join columns are indexed. - Choose the Right Join: Select the join type that accurately reflects the relationship you want to display and the data you need to include (or exclude).
Notes:
- Outer JOINs (
LEFT
,RIGHT
,FULL
) include rows even when there’s no match in the joined table, populating unmatched columns withNULL
. LEFT JOIN
: Returns all rows from the left table and matching rows from the right. (Most common outer join).RIGHT JOIN
: Returns all rows from the right table and matching rows from the left. (Less common, can be swapped forLEFT JOIN
).FULL JOIN
: Returns all rows when there’s a match in either table. (Not directly supported in MySQL/MariaDB, simulated withLEFT JOIN UNION RIGHT JOIN
).- The choice of join type depends entirely on whether you want to include non-matching rows from one or both tables.
Small Exercise
Using your scriptbuzz_db
:
- List all users, and for each user, show the total
total_amount
of their orders. Include users who have placed no orders (their total amount should beNULL
or0
). (Hint: UseLEFT JOIN
,GROUP BY
, andCOALESCE
orIFNULL
for theNULL
SUM
if you want0
instead ofNULL
).- Bonus: Use
COALESCE(SUM(o.total_amount), 0)
orIFNULL(SUM(o.total_amount), 0)
to display0
instead ofNULL
for users without orders.
- Bonus: Use
- List all products, and show the
product_id
,name
, and theorder_id
of anyorder_items
they are part of. Include products that have never been part of anorder_item
. - Find all categories (from the
products
table) and the number of distinct products in each. Include categories that might not have any products (if your categories were in a separate table, but for now just group by the existingcategory
column inproducts
). ShowNULL
for categories if they don’t have products (or 0 withCOALESCE
).
You’re now equipped with the full range of powerful joining techniques! This opens up a vast array of possibilities for querying complex, related data in your applications. Next, we’ll look at subqueries, another way to combine queries.