SQL FULL OUTER JOIN: Syntax, Examples & Practice
SQL FULL OUTER JOIN returns every row from both tables. Rows that match are combined; rows with no match on either side are still returned, with NULL in the other table's columns. FULL JOIN is the same thing.
What Is a FULL OUTER JOIN in SQL?
Take two lists from the Playground database: customers who have placed an order (buyers) and customers who have written a review (reviewers). Some people are on both lists, some on only one. A FULL OUTER JOIN lines the lists up and keeps everybody:
WITH buyers AS (
SELECT DISTINCT user_id FROM orders
), reviewers AS (
SELECT DISTINCT user_id FROM reviews
)
SELECT b.user_id AS buyer_id,
r.user_id AS reviewer_id
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id
ORDER BY COALESCE(b.user_id, r.user_id)
LIMIT 8;
▶ Run this query in the SQL Playground
| buyer_id | reviewer_id |
|---|---|
| 1 | NULL |
| 2 | NULL |
| NULL | 3 |
| 4 | NULL |
| 5 | 5 |
| 6 | 6 |
| 7 | 7 |
| 8 | 8 |
Read the two columns together. Customers 5 to 8 appear in both: they ordered and reviewed. Customers 1, 2 and 4 have a NULL on the right: they ordered but never reviewed. Customer 3 has a NULL on the left: a review but no order. No one is dropped from either list.
The two WITH blocks are CTEs, named subqueries that act as the two tables. An INNER JOIN would keep only customers 5 to 8.
FULL OUTER JOIN Syntax
SELECT columns
FROM table_a
FULL OUTER JOIN table_b
ON table_a.key = table_b.key;
| Part | What it does |
|---|---|
FROM table_a | The left table. All of its rows are kept. |
FULL OUTER JOIN table_b | The right table. All of its rows are kept too. |
ON … | The matching rule. Rows that satisfy it are combined into one. |
FULL JOIN | The same join. OUTER is optional. |
Database support
| Database | FULL OUTER JOIN |
|---|---|
| PostgreSQL | Supported |
| SQL Server | Supported |
| Oracle | Supported |
| SQLite | Supported from version 3.39 (2022) |
| MySQL, MariaDB | Not supported. Use the UNION workaround below. |
How FULL OUTER JOIN Works
| Row | In the result | Columns from the other table |
|---|---|---|
| Matches a row on the other side | Once per match | Filled in |
| Left row with no match | Once | Right columns are NULL |
| Right row with no match | Once | Left columns are NULL |
A FULL OUTER JOIN is the result of a LEFT JOIN plus the right-table rows that the LEFT JOIN left out. Counting the four joins on the same two lists shows how they relate:
WITH buyers AS (
SELECT DISTINCT user_id FROM orders
), reviewers AS (
SELECT DISTINCT user_id FROM reviews
)
SELECT 'INNER JOIN' AS join_type, COUNT(*) AS rows_returned
FROM buyers b INNER JOIN reviewers r ON r.user_id = b.user_id
UNION ALL
SELECT 'LEFT JOIN', COUNT(*)
FROM buyers b LEFT JOIN reviewers r ON r.user_id = b.user_id
UNION ALL
SELECT 'RIGHT JOIN', COUNT(*)
FROM buyers b RIGHT JOIN reviewers r ON r.user_id = b.user_id
UNION ALL
SELECT 'FULL OUTER JOIN', COUNT(*)
FROM buyers b FULL OUTER JOIN reviewers r ON r.user_id = b.user_id;
▶ Run this query in the SQL Playground
| join_type | rows_returned |
|---|---|
| INNER JOIN | 84 |
| LEFT JOIN | 114 |
| RIGHT JOIN | 104 |
| FULL OUTER JOIN | 134 |
| Join | Rows | Who is in the result |
|---|---|---|
| INNER JOIN | 84 | Ordered and reviewed |
| LEFT JOIN | 114 | Every buyer |
| RIGHT JOIN | 104 | Every reviewer |
| FULL OUTER JOIN | 134 | Everyone on either list |
The arithmetic always holds: LEFT + RIGHT − INNER = FULL, here 114 + 104 − 84 = 134.
FULL OUTER JOIN vs LEFT, RIGHT and INNER JOIN
| INNER | LEFT | RIGHT | FULL OUTER | |
|---|---|---|---|---|
| Matching rows | Kept | Kept | Kept | Kept |
| Unmatched left rows | Dropped | Kept | Dropped | Kept |
| Unmatched right rows | Dropped | Dropped | Kept | Kept |
| NULLs can appear in | Neither side | Right columns | Left columns | Both sides |
Use a LEFT JOIN when one table is the master list and the other adds optional detail. Use FULL OUTER JOIN when neither table is the master: two lists that should agree, or two reports to merge. The SQL JOINs overview shows all six join types together.
FULL OUTER JOIN vs UNION
| FULL OUTER JOIN | UNION | |
|---|---|---|
| Combines | Columns, side by side, matched on a key | Rows, stacked on top of each other |
| Tables need | A column to match on | The same number and type of columns |
| A row present in both | One combined row | One row (UNION) or two (UNION ALL) |
| Tells you which side a row came from | Yes, by which columns are NULL | Only if you add a label column |
A UNION of the two lists would give the same 134 customers, but as a single column with no way to see who is on which list.
Finding Rows That Do Not Match
The most common real use: reconciling two sets of data.
After a FULL OUTER JOIN, a NULL key on either side marks a row that exists in only one table. Keep just those rows and you have every mismatch, from both directions, in one query:
WITH buyers AS (
SELECT DISTINCT user_id FROM orders
), reviewers AS (
SELECT DISTINCT user_id FROM reviews
)
SELECT COALESCE(b.user_id, r.user_id) AS user_id,
CASE WHEN b.user_id IS NULL THEN 'reviewed but never ordered'
ELSE 'ordered but never reviewed' END AS issue
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id
WHERE b.user_id IS NULL
OR r.user_id IS NULL
ORDER BY user_id
LIMIT 6;
▶ Run this query in the SQL Playground
| user_id | issue |
|---|---|
| 1 | ordered but never reviewed |
| 2 | ordered but never reviewed |
| 3 | reviewed but never ordered |
| 4 | ordered but never reviewed |
| 15 | ordered but never reviewed |
| 17 | reviewed but never ordered |
COALESCE picks whichever id is not NULL, so there is one id column whatever side the row came from. Grouping the same join gives a summary of all three situations:
WITH buyers AS (
SELECT DISTINCT user_id FROM orders
), reviewers AS (
SELECT DISTINCT user_id FROM reviews
)
SELECT CASE WHEN b.user_id IS NULL THEN 'reviewed only'
WHEN r.user_id IS NULL THEN 'ordered only'
ELSE 'ordered and reviewed' END AS segment,
COUNT(*) AS customers
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id
GROUP BY segment
ORDER BY segment;
▶ Run this query in the SQL Playground
| segment | customers |
|---|---|
| ordered and reviewed | 84 |
| ordered only | 30 |
| reviewed only | 20 |
The same shape reconciles any two sources: a payments file against the orders table, last month's customer list against this month's, a staging table against production. Rows with a NULL on the left are new or unexpected; rows with a NULL on the right are missing.
| You want | WHERE clause after the FULL OUTER JOIN |
|---|---|
| Rows only in the left table | WHERE b.key IS NULL |
| Rows only in the right table | WHERE a.key IS NULL |
| Rows in exactly one table | WHERE a.key IS NULL OR b.key IS NULL |
| Rows in both | WHERE a.key IS NOT NULL AND b.key IS NOT NULL (an INNER JOIN) |
Merging Two Summaries With FULL OUTER JOIN and COALESCE
FULL OUTER JOIN is also how two summaries are put side by side when either can be missing a row. Count orders per customer and reviews per customer separately, then join the two results. A customer with orders but no reviews, or the reverse, still gets a row:
WITH o AS (
SELECT user_id, COUNT(*) AS orders FROM orders GROUP BY user_id
), r AS (
SELECT user_id, COUNT(*) AS reviews FROM reviews GROUP BY user_id
)
SELECT COALESCE(o.user_id, r.user_id) AS user_id,
COALESCE(o.orders, 0) AS orders,
COALESCE(r.reviews, 0) AS reviews
FROM o
FULL OUTER JOIN r
ON r.user_id = o.user_id
ORDER BY user_id
LIMIT 8;
▶ Run this query in the SQL Playground
| user_id | orders | reviews |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 1 | 0 |
| 3 | 0 | 3 |
| 4 | 3 | 0 |
| 5 | 1 | 1 |
| 6 | 2 | 1 |
| 7 | 3 | 1 |
| 8 | 3 | 1 |
| Expression | Why it is needed |
|---|---|
COALESCE(o.user_id, r.user_id) | The id may be NULL on either side, so neither column alone identifies every row. |
COALESCE(o.orders, 0) | A customer missing from the orders summary has NULL, which should read as 0. |
orders and reviews tables on user_id would pair every order with every review of the same customer and inflate the counts.To add names, join the result to users on the combined key:
WITH buyers AS (
SELECT DISTINCT user_id FROM orders
), reviewers AS (
SELECT DISTINCT user_id FROM reviews
)
SELECT u.id, u.first_name,
CASE WHEN b.user_id IS NOT NULL THEN 'yes' ELSE 'no' END AS has_ordered,
CASE WHEN r.user_id IS NOT NULL THEN 'yes' ELSE 'no' END AS has_reviewed
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id
JOIN users u
ON u.id = COALESCE(b.user_id, r.user_id)
ORDER BY u.id
LIMIT 6;
▶ Run this query in the SQL Playground
| id | first_name | has_ordered | has_reviewed |
|---|---|---|---|
| 1 | Jennifer | yes | no |
| 2 | Donna | yes | no |
| 3 | Kenneth | no | yes |
| 4 | Michael | yes | no |
| 5 | Robert | yes | yes |
| 6 | Deborah | yes | yes |
FULL OUTER JOIN in MySQL
MySQL and MariaDB do not have FULL OUTER JOIN. The standard workaround is a LEFT JOIN and a RIGHT JOIN combined with UNION, which removes the matched rows that both halves return:
SELECT a.id, a.name, b.id, b.value
FROM a
LEFT JOIN b ON b.a_id = a.id
UNION
SELECT a.id, a.name, b.id, b.value
FROM a
RIGHT JOIN b ON b.a_id = a.id;
Running that pattern on the buyers and reviewers lists returns the same number of rows as the real FULL OUTER JOIN:
WITH buyers AS (
SELECT DISTINCT user_id FROM orders
), reviewers AS (
SELECT DISTINCT user_id FROM reviews
)
SELECT COUNT(*) AS rows_from_union
FROM (
SELECT b.user_id AS buyer_id, r.user_id AS reviewer_id
FROM buyers b LEFT JOIN reviewers r ON r.user_id = b.user_id
UNION
SELECT b.user_id, r.user_id
FROM buyers b RIGHT JOIN reviewers r ON r.user_id = b.user_id
) AS emulated;
▶ Run this query in the SQL Playground
| rows_from_union |
|---|
| 134 |
UNION also removes genuine duplicate rows. If the tables can contain identical rows that must be kept, use this version instead: all the LEFT JOIN rows, plus only the right-table rows that had no match.
SELECT a.id, a.name, b.id, b.value
FROM a
LEFT JOIN b ON b.a_id = a.id
UNION ALL
SELECT a.id, a.name, b.id, b.value
FROM a
RIGHT JOIN b ON b.a_id = a.id
WHERE a.id IS NULL;
FULL OUTER JOIN With a WHERE Clause
A WHERE condition on a column of either table is not true for the rows where that table is NULL, so it removes them. Filtering on one side quietly turns the FULL OUTER JOIN into a LEFT, RIGHT or INNER JOIN:
| WHERE condition on | Unmatched rows that survive | Effectively a |
|---|---|---|
| A left-table column | Left rows only | LEFT JOIN |
| A right-table column | Right rows only | RIGHT JOIN |
| Columns of both tables | None | INNER JOIN |
To filter and still keep both sides complete, filter each table before the join, in its own subquery or CTE. That is what the buyers and reviewers CTEs do. To restrict buyers to 2024 without losing reviewers, put the date condition inside the CTE:
WITH buyers AS (
SELECT DISTINCT user_id FROM orders
WHERE order_date >= '2024-01-01'
), reviewers AS (
SELECT DISTINCT user_id FROM reviews
WHERE reviewed_at >= '2024-01-01'
)
SELECT CASE WHEN b.user_id IS NULL THEN 'reviewed only'
WHEN r.user_id IS NULL THEN 'ordered only'
ELSE 'ordered and reviewed' END AS segment,
COUNT(*) AS customers
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id
GROUP BY segment
ORDER BY segment;
▶ Run this query in the SQL Playground
| segment | customers |
|---|---|
| ordered and reviewed | 25 |
| ordered only | 31 |
| reviewed only | 22 |
Where FULL OUTER JOIN Is Used
| Task | Left table ⟗ right table |
|---|---|
| Reconcile payments received against invoices issued | invoices ⟗ payments |
| Compare a data migration: rows in the old system vs the new | old_table ⟗ new_table |
| Customers this year vs last year (new, lost, retained) | last_year ⟗ this_year |
| Budget vs actual spend by department | budget ⟗ actuals |
| Stock on the system vs stock counted in the warehouse | system_stock ⟗ counted_stock |
| Two monthly summaries merged into one report | summary_a ⟗ summary_b |
| Employees in HR vs employees in payroll | hr ⟗ payroll |
What these have in common is that neither side is the reference. If one side is known to be complete, a LEFT JOIN from it is simpler and says so.
Common FULL OUTER JOIN Mistakes
1. Filtering one side in WHERE
SELECT b.user_id, r.user_id
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id
WHERE b.user_id < 50;
WITH b AS (
SELECT user_id FROM buyers
WHERE user_id < 50
)
SELECT b.user_id, r.user_id
FROM b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id;
For reviewer-only rows b.user_id is NULL, so the WHERE test fails and they disappear. The join has become a LEFT JOIN.
2. Using one side's key as the id
SELECT b.user_id, …
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id;
SELECT COALESCE(b.user_id, r.user_id) AS user_id, …
FROM buyers b
FULL OUTER JOIN reviewers r
ON r.user_id = b.user_id;
Either key can be NULL. Grouping or sorting by only one of them lumps all the other side's unmatched rows together under NULL.
3. Joining detail tables directly
SELECT o.user_id, COUNT(*)
FROM orders o
FULL OUTER JOIN reviews r
ON r.user_id = o.user_id
GROUP BY o.user_id;
WITH o AS (SELECT user_id, COUNT(*) AS n FROM orders GROUP BY user_id),
r AS (SELECT user_id, COUNT(*) AS n FROM reviews GROUP BY user_id)
SELECT COALESCE(o.user_id, r.user_id), o.n, r.n
FROM o FULL OUTER JOIN r ON r.user_id = o.user_id;
When both tables have several rows per key the join multiplies them. Reduce each to one row per key before joining.
4. Writing FULL OUTER JOIN in MySQL
SELECT *
FROM a
FULL OUTER JOIN b
ON b.a_id = a.id;
SELECT * FROM a LEFT JOIN b ON b.a_id = a.id
UNION
SELECT * FROM a RIGHT JOIN b ON b.a_id = a.id;
MySQL does not implement the keyword. See FULL OUTER JOIN in MySQL.
FULL OUTER JOIN Performance
| Tip | Why it helps |
|---|---|
| Index the join columns on both tables | Both sides are looked up against each other, so both need an index. |
| Reduce each side before joining | Filtering or aggregating in a subquery means fewer rows to match, and it keeps the join correct. |
| Use a narrower join when you can | If one side is always complete, LEFT JOIN gives the optimiser more options and is easier to read. |
| Expect the MySQL workaround to cost more | It runs two joins and a duplicate-removal step. The UNION ALL form avoids the last one. |
FULL OUTER JOIN Cheat Sheet
| Goal | Pattern |
|---|---|
| Every row of both tables | FROM a FULL OUTER JOIN b ON b.key = a.key |
| One key column for every row | COALESCE(a.key, b.key) |
| Rows in only one table | WHERE a.key IS NULL OR b.key IS NULL |
| Label which side a row came from | CASE WHEN a.key IS NULL THEN … WHEN b.key IS NULL THEN … END |
| Zeros instead of NULL | COALESCE(a.value, 0) |
| Filter without losing rows | Filter each table in a subquery or CTE before the join |
| MySQL | LEFT JOIN … UNION … RIGHT JOIN |
Practice FULL OUTER JOIN Queries
7 exercises on the real Playground tables.
Write each query yourself first. Every solution has been run against the Playground database.
-
1Easy Build two lists, products that have been ordered and products that have been reviewed, and FULL OUTER JOIN them. Show the first ten rows by product id. Table:
order_items, reviewsShow solution
WITH ordered AS ( SELECT DISTINCT product_id FROM order_items ), reviewed AS ( SELECT DISTINCT product_id FROM reviews ) SELECT o.product_id AS ordered_id, r.product_id AS reviewed_id FROM ordered o FULL OUTER JOIN reviewed r ON r.product_id = o.product_id ORDER BY COALESCE(o.product_id, r.product_id) LIMIT 10;▶ Run this query in the SQL Playground
A NULL in
reviewed_idmeans the product was ordered but never reviewed, and the reverse forordered_id. -
2Medium Count the products that were ordered only, reviewed only, or both. Table:
order_items, reviewsShow solution
WITH ordered AS ( SELECT DISTINCT product_id FROM order_items ), reviewed AS ( SELECT DISTINCT product_id FROM reviews ) SELECT CASE WHEN o.product_id IS NULL THEN 'reviewed only' WHEN r.product_id IS NULL THEN 'ordered only' ELSE 'ordered and reviewed' END AS segment, COUNT(*) AS products FROM ordered o FULL OUTER JOIN reviewed r ON r.product_id = o.product_id GROUP BY segment ORDER BY segment;▶ Run this query in the SQL Playground
The side that is NULL identifies the segment. The result has 3 segments.
-
3Medium Compare customers who ordered in 2023 with customers who ordered in 2024. List the ones who ordered in exactly one of the two years, with the year. Table:
ordersShow solution
WITH y2023 AS ( SELECT DISTINCT user_id FROM orders WHERE order_date >= '2023-01-01' AND order_date < '2024-01-01' ), y2024 AS ( SELECT DISTINCT user_id FROM orders WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01' ) SELECT COALESCE(a.user_id, b.user_id) AS user_id, CASE WHEN a.user_id IS NULL THEN '2024 only' ELSE '2023 only' END AS year FROM y2023 a FULL OUTER JOIN y2024 b ON b.user_id = a.user_id WHERE a.user_id IS NULL OR b.user_id IS NULL ORDER BY user_id;▶ Run this query in the SQL Playground
The WHERE keeps only rows with a NULL on one side: 69 customers ordered in one year but not the other.
-
4Medium For each customer id, show the number of orders and the number of payments, with 0 where a customer has none of one kind. Show the first ten by id. Table:
orders, paymentsShow solution
WITH o AS ( SELECT user_id, COUNT(*) AS orders FROM orders GROUP BY user_id ), p AS ( SELECT user_id, COUNT(*) AS payments FROM payments GROUP BY user_id ) SELECT COALESCE(o.user_id, p.user_id) AS user_id, COALESCE(o.orders, 0) AS orders, COALESCE(p.payments, 0) AS payments FROM o FULL OUTER JOIN p ON p.user_id = o.user_id ORDER BY user_id LIMIT 10;▶ Run this query in the SQL Playground
Each table is reduced to one row per customer before the join, so nothing is multiplied.
-
5Medium Write the FULL OUTER JOIN of buyers and reviewers the MySQL way, with LEFT JOIN, RIGHT JOIN and UNION. Show the first eight rows. Table:
orders, reviewsShow solution
WITH buyers AS ( SELECT DISTINCT user_id FROM orders ), reviewers AS ( SELECT DISTINCT user_id FROM reviews ) SELECT buyer_id, reviewer_id FROM ( SELECT b.user_id AS buyer_id, r.user_id AS reviewer_id FROM buyers b LEFT JOIN reviewers r ON r.user_id = b.user_id UNION SELECT b.user_id, r.user_id FROM buyers b RIGHT JOIN reviewers r ON r.user_id = b.user_id ) AS full_join ORDER BY COALESCE(buyer_id, reviewer_id) LIMIT 8;▶ Run this query in the SQL Playground
The LEFT JOIN supplies every buyer, the RIGHT JOIN every reviewer, and UNION removes the customers that both halves return. The outer query only sorts the combined rows.
-
6Challenging List the customers who have reviewed a product but never placed an order, with their name and city. Table:
orders, reviews, usersShow solution
WITH buyers AS ( SELECT DISTINCT user_id FROM orders ), reviewers AS ( SELECT DISTINCT user_id FROM reviews ) SELECT u.id, u.first_name, u.last_name, u.city FROM buyers b FULL OUTER JOIN reviewers r ON r.user_id = b.user_id JOIN users u ON u.id = r.user_id WHERE b.user_id IS NULL ORDER BY u.id;▶ Run this query in the SQL Playground
b.user_id IS NULLkeeps reviewer-only rows. 20 customers reviewed without ordering. A LEFT JOIN from reviewers would also work here, because only one side is needed. -
7Challenging For each month of 2024, show the number of orders placed and reviews written, in one row per month. Table:
orders, reviewsShow solution
WITH o AS ( SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders FROM orders WHERE order_date >= '2024-01-01' GROUP BY month ), r AS ( SELECT strftime('%Y-%m', reviewed_at) AS month, COUNT(*) AS reviews FROM reviews WHERE reviewed_at >= '2024-01-01' GROUP BY month ) SELECT COALESCE(o.month, r.month) AS month, COALESCE(o.orders, 0) AS orders, COALESCE(r.reviews, 0) AS reviews FROM o FULL OUTER JOIN r ON r.month = o.month ORDER BY month;▶ Run this query in the SQL Playground
Two monthly summaries merged on the month. FULL OUTER JOIN guarantees a month appears even if it has orders but no reviews, or the reverse.
strftimeis SQLite's date function.
📝 Quiz — FULL OUTER JOIN
Test your understanding · 0/10 answered
What does a FULL OUTER JOIN return?
Matching rows are combined, and unmatched rows from both tables are kept with NULLs on the other side.
Is FULL JOIN different from FULL OUTER JOIN?
They are the same join; the OUTER keyword can be left out.
In a FULL OUTER JOIN result, where can NULLs appear because of a missing match?
An unmatched left row has NULL right columns, and an unmatched right row has NULL left columns.
Which database does not support FULL OUTER JOIN?
MySQL has no FULL OUTER JOIN. It is emulated with a LEFT JOIN and a RIGHT JOIN combined by UNION.
How do you emulate FULL OUTER JOIN in MySQL?
The LEFT JOIN gives all left rows, the RIGHT JOIN all right rows, and UNION removes the matched rows that appear in both.
LEFT JOIN returns 114 rows, RIGHT JOIN 104 and INNER JOIN 84 for two lists of unique keys. How many rows does FULL OUTER JOIN return?
LEFT + RIGHT − INNER = 114 + 104 − 84 = 134.
After a FULL OUTER JOIN of a and b, which WHERE clause returns rows found in only one table?
A NULL key on either side marks a row with no partner. "<>" does not work, because comparing with NULL is never true.
Why wrap the keys in COALESCE(a.key, b.key)?
Unmatched rows have a NULL key on one side. COALESCE returns whichever one exists.
What happens if you add WHERE a.status = 'x' after a FULL OUTER JOIN?
For b-only rows a.status is NULL, so the condition is not true. The join now behaves like a LEFT JOIN.
When is FULL OUTER JOIN a better choice than LEFT JOIN?
FULL OUTER JOIN suits reconciling or merging two sources where rows can be missing from either one.
FULL OUTER JOIN Interview Questions
Q1. What is a FULL OUTER JOIN?
A join that returns all rows from both tables. Rows satisfying the ON condition are combined; rows without a match on either side are returned with NULLs in the other table's columns.
Q2. What is the difference between FULL OUTER JOIN and LEFT JOIN?
LEFT JOIN keeps unmatched rows of the left table only. FULL OUTER JOIN also keeps the unmatched rows of the right table. FULL equals LEFT plus the right-only rows.
Q3. What is the difference between FULL OUTER JOIN and INNER JOIN?
INNER JOIN returns only matched rows. FULL OUTER JOIN returns those plus every unmatched row from both tables.
Q4. What is the difference between FULL OUTER JOIN and UNION?
A join matches rows on a key and places columns side by side. UNION stacks the rows of two queries with the same columns and does no matching. They answer different questions, though UNION is used to build a full join in MySQL.
Q5. How do you write a FULL OUTER JOIN in MySQL?
SELECT … FROM a LEFT JOIN b ON … UNION SELECT … FROM a RIGHT JOIN b ON …. To keep duplicate rows, use UNION ALL and add WHERE a.key IS NULL to the second query.
Q6. How do you find rows that exist in one table but not the other, in both directions?
FULL OUTER JOIN on the key and filter WHERE a.key IS NULL OR b.key IS NULL. A CASE expression on which key is NULL labels the direction.
Q7. How many rows can a FULL OUTER JOIN return?
At least as many as the larger table, and for unique keys exactly LEFT + RIGHT − INNER. With many-to-many matches it can exceed the sum of both tables.
Q8. Why use COALESCE with a FULL OUTER JOIN?
Because the key and any other column can be NULL on either side. COALESCE(a.key, b.key) gives a single non-NULL key, and COALESCE(value, 0) turns missing measures into zeros.
Q9. What does a WHERE filter do to a FULL OUTER JOIN?
A condition on one table's columns removes the rows where that table is NULL, reducing the join to a LEFT, RIGHT or INNER JOIN. Filter in subqueries before the join to keep both sides complete.
Q10. Can you chain FULL OUTER JOINs across three tables?
Yes, but the join key for the third table must use COALESCE of the first two keys, for example ON c.id = COALESCE(a.id, b.id), otherwise rows that came only from b fail to match.
Q11. When would you use FULL OUTER JOIN in practice?
For reconciliation and comparison: payments against invoices, old system against new after a migration, this period's customers against last period's, or budget against actuals.
Q12. Is FULL OUTER JOIN slower than other joins?
It can be, because the database must track unmatched rows on both sides and has fewer join strategies available. Indexes on both join columns and reducing each input first keep it reasonable.
Frequently Asked Questions
What is FULL OUTER JOIN in SQL?
FULL OUTER JOIN returns every row from both tables. Matching rows are combined into one, and rows with no match in the other table are still returned, with NULL in the missing columns.
Is FULL JOIN the same as FULL OUTER JOIN?
Yes. FULL JOIN is the short form; the OUTER keyword is optional and does not change the result.
Does MySQL support FULL OUTER JOIN?
No. In MySQL and MariaDB, write a LEFT JOIN and a RIGHT JOIN of the same tables and combine them with UNION. PostgreSQL, SQL Server, Oracle and SQLite 3.39 or later support FULL OUTER JOIN directly.
What is the difference between FULL OUTER JOIN and LEFT JOIN?
LEFT JOIN keeps all rows of the left table and only the matching rows of the right. FULL OUTER JOIN keeps all rows of both, so unmatched right-table rows also appear.
What is the difference between FULL OUTER JOIN and UNION?
FULL OUTER JOIN matches rows on a key and returns their columns side by side. UNION appends the rows of one query to another and does not match anything.
How do I find rows that do not match in either table?
Use a FULL OUTER JOIN and add WHERE a.key IS NULL OR b.key IS NULL. The result contains the rows that exist in only one of the two tables.
Why does FULL OUTER JOIN return NULL values?
A row with no match in the other table has no data for that table's columns, so they are NULL. With FULL OUTER JOIN this can happen on either side.
When should I use FULL OUTER JOIN?
When you need to compare or merge two sets of data and rows may be missing from either one, such as reconciling two systems or combining two summaries into one report.