| |

SQL 37 🛢️ Combining All Rows with FULL OUTER JOIN

The FULL OUTER JOIN returns all rows from both tables, matching rows where the join condition is satisfied and filling the unmatched side with NULLs where it is not. It is the union of a LEFT JOIN and a RIGHT JOIN: every left row appears, every right row appears, and rows that match appear once with both sides populated. The result is the complete picture of two tables, including the rows on each side that have no counterpart on the other.

The FULL OUTER JOIN is the least commonly used of the outer joins, but it solves a specific problem that neither the LEFT JOIN nor the RIGHT JOIN can solve alone: finding rows that exist in one table but not the other, in both directions, in a single query. It is the tool for reconciliation, for auditing, for comparing two datasets that should match, and for reports that need to show every entity from both sides whether or not a relationship exists.

Not every database supports FULL OUTER JOIN. MySQL does not support it directly and requires a UNION of a LEFT JOIN and a RIGHT JOIN to simulate it. PostgreSQL, SQL Server, Oracle, and SQLite support it natively. This chapter covers the syntax, the behavior, the unmatched-row detection in both directions, the aggregate patterns, the MySQL workaround, and the use cases where the full outer join is the right tool.

Key point: FULL OUTER JOIN returns all rows from both tables. Matched rows appear once with both sides populated; unmatched rows appear with NULLs on the missing side. Use WHERE left.key IS NULL OR right.key IS NULL to find rows that exist in only one table. MySQL does not support FULL OUTER JOIN and requires a UNION of LEFT and RIGHT joins.


Why FULL OUTER JOIN exists

The bidirectional unmatched problem. A LEFT JOIN finds left rows with no match. A RIGHT JOIN finds right rows with no match. Neither finds both in a single query. The FULL OUTER JOIN does, which makes it the natural tool for reconciliation.

The completeness problem. A report that must show every customer and every order, whether or not they relate, needs a full outer join. A LEFT JOIN would drop orders with no customer; a RIGHT JOIN would drop customers with no order. Only the full outer join preserves both.

The symmetry problem. The full outer join is the only join that treats both tables as equally important. Every other join preserves one side and drops the unmatched rows from the other. The full outer join makes no such choice.

The comparison problem. When two datasets are supposed to match, the full outer join reveals where they do not. It is the SQL equivalent of a diff: it shows what is in A but not B, what is in B but not A, and what is in both.

The audit problem. Reconciliation reports need to show the source rows, the target rows, and the differences. The full outer join produces this in a single result set.


a. Basic FULL OUTER JOIN syntax

The syntax places the left table first, then FULL OUTER JOIN and the right table, with the matching condition in ON.

SELECT c.name, o.order_id
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id;

The FULL JOIN spelling is also valid in some databases. FULL OUTER JOIN is the standard form.

The result contains:

nameorder_idSource
Alice101matched
Alice102matched
Bob103matched
CarolNULLleft only (customer with no orders)
NULL104right only (order with no customer)

Every customer appears, every order appears, and the matched rows appear once.


b. The shape of the result

The full outer join produces three kinds of rows:

Row typeLeft columnsRight columns
MatchedPopulatedPopulated
Left onlyPopulatedNULL
Right onlyNULLPopulated

A row is “left only” when the left table has a row with no match in the right table. Its right columns are NULL. A row is “right only” when the right table has a row with no match in the left table. Its left columns are NULL.

The presence of NULL does not indicate an error. It indicates that the row exists on one side of the join but not the other.


c. Finding rows that exist in only one table

The full outer join combined with a filter on the NULL keys finds rows that exist in one table but not the other.

SELECT c.customer_id, o.customer_id
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL OR o.customer_id IS NULL;

The result contains:

  • Customers with no orders (left only)
  • Orders with no customer (right only)

This is the reconciliation query. It shows the rows on each side that have no counterpart on the other.

To find only left-only rows:

WHERE c.customer_id IS NULL

To find only right-only rows:

WHERE o.customer_id IS NULL

To find both in a single result:

WHERE c.customer_id IS NULL OR o.customer_id IS NULL

This is the pattern that the full outer join exists for. The LEFT JOIN finds one direction; the RIGHT JOIN finds the other; the FULL OUTER JOIN finds both in one query.


d. Aggregate patterns with FULL OUTER JOIN

Aggregating over a full outer join requires care, because the unmatched rows have NULLs in the columns from one side.

SELECT
  COALESCE(c.name, 'No customer') AS customer,
  COUNT(o.order_id) AS order_count
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.name;

The COALESCE replaces the NULL name with a label for the orders that have no customer. The COUNT(o.order_id) counts the orders; the unmatched customers have zero orders because their o.order_id is NULL.

FunctionBehavior on unmatched rows
COUNT(*)Counts the row
COUNT(left.key)Counts only left matches
COUNT(right.key)Counts only right matches
SUM(left.col)NULL if no left match
COALESCE(SUM(left.col), 0)Zero if no left match

The choice of which column to count depends on what the query is measuring. To count matches, count the key from the other side. To count rows on one side, count the key from that side.


e. The MySQL workaround

MySQL does not support FULL OUTER JOIN. The equivalent result is produced by a UNION of a LEFT JOIN and a RIGHT JOIN.

SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id

UNION

SELECT c.name, o.order_id
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;

The UNION removes duplicate rows. A row that appears in both the left join and the right join — a matched row — is included only once.

The UNION ALL variant does not remove duplicates, so matched rows would appear twice. For the full outer join simulation, UNION is the correct choice.

The same pattern works in any database that lacks full outer join support, including older versions of MySQL and some other systems.


f. When to use FULL OUTER JOIN

The full outer join is appropriate in specific situations.

SituationUse FULL OUTER JOIN
Reconciliation between two systemsYes
Finding rows in either table with no matchYes
Report that must show every entity from both sidesYes
Replacing an INNER JOINNo
Replacing a LEFT JOINNo
Replacing a RIGHT JOINNo
Compatibility with MySQLUse UNION workaround

The full outer join is not a replacement for the other joins. It is a specific tool for a specific problem: showing the complete picture of two tables, including the rows that have no relationship.

The performance characteristics of a full outer join are similar to a left or right join. The optimizer may implement it as a hash join or a merge join, and the cost depends on the size of the tables and the availability of indexes on the join columns.


Complete Example Session

-- ============================================
-- PART 1: CREATE SAMPLE TABLES
-- ============================================
CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY,
    name        VARCHAR(50)
);

CREATE TABLE orders (
    order_id    INTEGER PRIMARY KEY,
    customer_id INTEGER,
    total       NUMERIC(10, 2)
);

INSERT INTO customers VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Carol');

INSERT INTO orders VALUES
(101, 1, 250.00),
(102, 1, 180.00),
(103, 2, 320.00),
(104, 9, 500.00);
-- ============================================
-- PART 2: BASIC FULL OUTER JOIN
-- ============================================
SELECT c.name, o.order_id, o.total
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id;
-- Alice (2 orders), Bob (1 order), Carol (no order),
-- order 104 (no customer)
-- ============================================
-- PART 3: FIND ROWS IN EITHER TABLE WITH NO MATCH
-- ============================================
SELECT c.customer_id, o.customer_id
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL OR o.customer_id IS NULL;
-- Carol (no orders), order 104 (no customer)
-- ============================================
-- PART 4: FIND ONLY LEFT-ONLY ROWS
-- ============================================
SELECT c.customer_id, c.name
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.customer_id IS NULL;
-- Carol
-- ============================================
-- PART 5: FIND ONLY RIGHT-ONLY ROWS
-- ============================================
SELECT o.order_id, o.total
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL;
-- Order 104
-- ============================================
-- PART 6: AGGREGATE WITH COALESCE
-- ============================================
SELECT
  COALESCE(c.name, 'No customer') AS customer,
  COUNT(o.order_id) AS order_count
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY COALESCE(c.name, 'No customer');
-- Alice: 2, Bob: 1, Carol: 0, No customer: 1
-- ============================================
-- PART 7: MYSQL WORKAROUND
-- ============================================
SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id

UNION

SELECT c.name, o.order_id
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;
-- Same result as FULL OUTER JOIN
-- ============================================
-- PART 8: FULL OUTER JOIN WITH ADDITIONAL FILTER
-- ============================================
SELECT c.name, o.order_id, o.total
FROM customers c
FULL OUTER JOIN orders o
  ON c.customer_id = o.customer_id
 AND o.total > 200;
-- Alice: order 101; Bob: order 103; Carol: NULL;
-- NULL: order 104 (preserved)
-- ============================================
-- PART 9: RECONCILIATION REPORT
-- ============================================
SELECT
  COALESCE(c.customer_id, o.customer_id) AS id,
  c.name,
  o.order_id,
  CASE
    WHEN c.customer_id IS NULL THEN 'order only'
    WHEN o.order_id IS NULL THEN 'customer only'
    ELSE 'matched'
  END AS status
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY id;
-- ============================================
-- PART 10: COMPARING TWO DATASETS
-- ============================================
-- Source table vs target table
SELECT
  COALESCE(s.id, t.id) AS id,
  s.value AS source_value,
  t.value AS target_value,
  CASE
    WHEN s.id IS NULL THEN 'missing in source'
    WHEN t.id IS NULL THEN 'missing in target'
    WHEN s.value <> t.value THEN 'value mismatch'
    ELSE 'match'
  END AS diff
FROM source s
FULL OUTER JOIN target t ON s.id = t.id
WHERE s.id IS NULL OR t.id IS NULL OR s.value <> t.value;

These ten parts cover creating the tables, a basic full outer join, finding rows in either table with no match, finding left-only rows, finding right-only rows, aggregating with COALESCE, the MySQL workaround, a full outer join with an additional filter, a reconciliation report, and comparing two datasets.


Quick Reference

FULL OUTER JOIN Syntax

FormExample
ExplicitFROM a FULL OUTER JOIN b ON a.id = b.id
ShortFROM a FULL JOIN b ON a.id = b.id

Row Types in the Result

Row typeLeft columnsRight columns
MatchedPopulatedPopulated
Left onlyPopulatedNULL
Right onlyNULLPopulated

Unmatched Detection

PatternPurpose
WHERE left.key IS NULLRight-only rows
WHERE right.key IS NULLLeft-only rows
WHERE left.key IS NULL OR right.key IS NULLBoth directions

Database Support

DatabaseFULL OUTER JOIN
PostgreSQLNative
SQL ServerNative
OracleNative
SQLiteNative (3.39+)
MySQLNot supported; use UNION

MySQL Workaround

StepQuery
1LEFT JOIN between the tables
2UNION
3RIGHT JOIN between the tables

Aggregation

FunctionBehavior
COUNT(*)Counts the row
COUNT(left.key)Counts left matches
COUNT(right.key)Counts right matches
COALESCE(col, default)Replaces NULL with a label

Best Practices

✅ Do This:

-- Use FULL OUTER JOIN for reconciliation
SELECT c.name, o.order_id
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id;

-- Find rows in either table with no match
WHERE c.customer_id IS NULL OR o.customer_id IS NULL;

-- Use COALESCE for labels
SELECT COALESCE(c.name, 'No customer') AS customer;

-- Use the UNION workaround for MySQL
LEFT JOIN ... UNION ... RIGHT JOIN ...

❌ Don’t Do This:

-- Use FULL OUTER JOIN when LEFT JOIN suffices
-- ❌ use the minimal join for the problem

-- Forget the MySQL limitation
SELECT ... FULL OUTER JOIN ...  -- ❌ fails in MySQL

-- Use UNION ALL instead of UNION in the workaround
LEFT JOIN ... UNION ALL ... RIGHT JOIN ...  -- ❌ duplicates

-- Filter on the wrong side
WHERE c.customer_id IS NULL AND o.customer_id IS NULL  -- ❌ finds nothing

Common Pitfalls

PitfallWhy It HappensFix
FULL OUTER JOIN failsDatabase does not support itUse the UNION workaround
Duplicate matched rowsUsed UNION ALL in the workaroundUse UNION
Both directions not foundUsed AND instead of ORUse WHERE a IS NULL OR b IS NULL
Aggregate includes NULL groupUnmatched rows grouped by NULLUse COALESCE for a label
Confusing result rowsNot knowing which side is NULLUse a CASE column to label
Performance issuesLarge tables, no indexIndex the join columns

Real-World Examples

1. Basic Full Outer Join

SELECT c.name, o.order_id
FROM customers c FULL OUTER JOIN orders o ON c.customer_id = o.customer_id;

2. Reconciliation

SELECT c.customer_id, o.customer_id
FROM customers c FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL OR o.customer_id IS NULL;

3. Left-Only Rows

SELECT c.name
FROM customers c FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.customer_id IS NULL;

4. Right-Only Rows

SELECT o.order_id
FROM customers c FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL;

5. Aggregate with Label

SELECT COALESCE(c.name, 'No customer') AS customer, COUNT(o.order_id)
FROM customers c FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY COALESCE(c.name, 'No customer');

6. MySQL Workaround

SELECT c.name, o.order_id FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
UNION
SELECT c.name, o.order_id FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;

7. Reconciliation Report

SELECT COALESCE(c.id, o.id) AS id,
  CASE WHEN c.id IS NULL THEN 'order only'
       WHEN o.id IS NULL THEN 'customer only'
       ELSE 'matched' END AS status
FROM customers c FULL OUTER JOIN orders o ON c.id = o.id;

8. Dataset Comparison

SELECT COALESCE(s.id, t.id) AS id,
  CASE WHEN s.id IS NULL THEN 'missing in source'
       WHEN t.id IS NULL THEN 'missing in target'
       WHEN s.value <> t.value THEN 'mismatch'
       ELSE 'match' END AS diff
FROM source s FULL OUTER JOIN target t ON s.id = t.id;

9. Filter in ON

SELECT c.name, o.order_id
FROM customers c FULL OUTER JOIN orders o
  ON c.customer_id = o.customer_id AND o.total > 200;

10. Three Tables

SELECT c.name, o.order_id, p.name
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
FULL OUTER JOIN products p ON o.product_id = p.product_id;

Visual

FULL OUTER JOIN Behavior

┌──────────────────────────────────────────────────────────────┐
│  customers              orders                               │
│  ┌────┬────────┐        ┌─────┬──────┐                       │
│  │ id │ name   │        │ oid │ cid  │                       │
│  ├────┼────────┤        ├─────┼──────┤                       │
│  │ 1  │ Alice  │        │ 101 │ 1    │                       │
│  │ 2  │ Bob    │        │ 102 │ 1    │                       │
│  │ 3  │ Carol  │        │ 103 │ 2    │                       │
│  └────┴────────┘        │ 104 │ 9    │                       │
│                         └─────┴──────┘                       │
│                                                              │
│  FULL OUTER JOIN ON c.id = o.cid:                            │
│  ┌────────┬─────┬─────────────┐                              │
│  │ Alice  │ 101 │ matched     │                              │
│  │ Alice  │ 102 │ matched     │                              │
│  │ Bob    │ 103 │ matched     │                              │
│  │ Carol  │ NULL│ left only   │                              │
│  │ NULL   │ 104 │ right only  │                              │
│  └────────┴─────┴─────────────┘                              │
│                                                              │
│  Both sides are preserved.                                   │
└──────────────────────────────────────────────────────────────┘

The Three Row Types

┌──────────────────────────────────────────────────────────────┐
│  MATCHED:                                                    │
│  ┌────────┬────────┐                                         │
│  │  left  │  right │                                         │
│  │  row   │  row   │                                         │
│  └────────┴────────┘                                         │
│                                                              │
│  LEFT ONLY:                                                  │
│  ┌────────┬────────┐                                         │
│  │  left  │  NULL  │                                         │
│  │  row   │        │                                         │
│  └────────┴────────┘                                         │
│                                                              │
│  RIGHT ONLY:                                                 │
│  ┌────────┬────────┐                                         │
│  │  NULL  │  right │                                         │
│  │        │  row   │                                         │
│  └────────┴────────┘                                         │
└──────────────────────────────────────────────────────────────┘

Unmatched Detection in Both Directions

┌──────────────────────────────────────────────────────────────┐
│  WHERE c.id IS NULL OR o.id IS NULL                          │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  c.id IS NULL  → right-only rows (orders with no customer)│
│  │  o.id IS NULL  → left-only rows (customers with no orders)│
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  This is the reconciliation query.                           │
└──────────────────────────────────────────────────────────────┘

MySQL Workaround

┌──────────────────────────────────────────────────────────────┐
│  FULL OUTER JOIN = LEFT JOIN UNION RIGHT JOIN                │
│                                                              │
│  LEFT JOIN  → matched + left-only                            │
│  RIGHT JOIN → matched + right-only                           │
│  UNION      → matched + left-only + right-only               │
│              (duplicates removed)                            │
│                                                              │
│  UNION ALL would keep duplicates, which is wrong.            │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
FULL OUTER JOINAll rows from both tables
Matched rowsAppear once with both sides populated
Left-only rowsLeft populated, right NULL
Right-only rowsLeft NULL, right populated
Unmatched detectionWHERE left.key IS NULL OR right.key IS NULL
Left-only detectionWHERE right.key IS NULL
Right-only detectionWHERE left.key IS NULL
MySQL supportNot supported; use UNION workaround
WorkaroundLEFT JOIN UNION RIGHT JOIN
Aggregate with labelCOALESCE(col, 'label')
Use caseReconciliation, auditing, dataset comparison

Key takeaways:

  • FULL OUTER JOIN returns all rows from both tables. Matched rows appear once with both sides populated. Unmatched rows appear with NULLs on the missing side. It is the union of a LEFT JOIN and a RIGHT JOIN.
  • The result has three kinds of rows: matched, left-only, and right-only. The matched rows have both sides populated. The left-only rows have NULLs in the right columns. The right-only rows have NULLs in the left columns.
  • WHERE left.key IS NULL OR right.key IS NULL finds rows in either table with no match. This is the reconciliation query. It shows what is in A but not B, what is in B but not A, and what is in both.
  • The FULL OUTER JOIN is the tool for reconciliation and auditing. It is not a replacement for the other joins. It solves the specific problem of showing the complete picture of two tables, including the rows that have no relationship.
  • MySQL does not support FULL OUTER JOIN. The equivalent result is produced by a UNION of a LEFT JOIN and a RIGHT JOIN. The UNION removes duplicates; UNION ALL does not, and using it would produce duplicate matched rows.
  • Aggregating over a FULL OUTER JOIN requires COALESCE for labels. Unmatched rows produce NULLs, and the GROUP BY includes a NULL group unless the column is replaced with a label.
  • Filter placement matters. A condition in ON preserves unmatched rows. The same condition in WHERE removes them and may turn the outer join into an inner join.

Remember: The FULL OUTER JOIN is the join that preserves everything. It returns every row from both tables, matches where the join condition is satisfied, and fills the unmatched side with NULLs where it is not. It is the union of a LEFT JOIN and a RIGHT JOIN, and it is the only join that treats both tables as equally important. Its primary use is reconciliation: finding rows that exist in one table but not the other, in both directions, in a single query. It is the SQL equivalent of a diff, and it is the tool for auditing, comparison, and completeness reports. MySQL does not support it directly, and the UNION workaround produces the same result. When the question is “show me everything from both sides, whether or not it matches,” the FULL OUTER JOIN is the answer.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!