Lab 07: Outer Joins

Time: 30 minutes | Level: Foundations | DB: MySQL 8 / PostgreSQL 15

Overview

Learn LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN (PostgreSQL), finding unmatched rows, the anti-join pattern, and CROSS JOIN.


Step 1: Setup

docker run -d --name mysql-lab -e MYSQL_ROOT_PASSWORD=rootpass mysql:8.0
for i in $(seq 1 30); do docker exec mysql-lab mysql -uroot -prootpass -e "SELECT 1" 2>/dev/null && break || sleep 2; done

docker run -d --name pg-lab -e POSTGRES_PASSWORD=rootpass postgres:15
sleep 10

docker exec mysql-lab mysql -uroot -prootpass << 'EOF'
CREATE DATABASE joinlab;
USE joinlab;

CREATE TABLE customers (
    customer_id  INT NOT NULL AUTO_INCREMENT,
    name         VARCHAR(100) NOT NULL,
    email        VARCHAR(100),
    city         VARCHAR(50),
    PRIMARY KEY (customer_id)
);

CREATE TABLE orders (
    order_id    INT NOT NULL AUTO_INCREMENT,
    customer_id INT,
    total       DECIMAL(10,2),
    order_date  DATE,
    status      VARCHAR(20) DEFAULT 'pending',
    PRIMARY KEY (order_id),
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

CREATE TABLE categories (
    cat_id   INT NOT NULL AUTO_INCREMENT,
    cat_name VARCHAR(50) NOT NULL,
    PRIMARY KEY (cat_id)
);

CREATE TABLE products (
    product_id  INT NOT NULL AUTO_INCREMENT,
    name        VARCHAR(100),
    cat_id      INT,
    price       DECIMAL(10,2),
    PRIMARY KEY (product_id),
    FOREIGN KEY (cat_id) REFERENCES categories(cat_id)
);

INSERT INTO customers (name, email, city) VALUES
('Alice Brown',   '[email protected]',  'New York'),
('Bob Chen',      '[email protected]',    'Chicago'),
('Carol Davis',   '[email protected]',  'Houston'),
('David Evans',   '[email protected]',  'Phoenix'),
('Eve Foster',    NULL,               'Boston');   -- no email

INSERT INTO orders (customer_id, total, order_date, status) VALUES
(1, 299.99,  '2024-01-10', 'completed'),
(1, 149.50,  '2024-01-25', 'completed'),
(2, 89.99,   '2024-02-05', 'completed'),
(3, 499.00,  '2024-02-18', 'shipped'),
(1, 75.00,   '2024-03-01', 'pending'),
(NULL, 55.00,'2024-03-05', 'pending');  -- guest order (no customer)

INSERT INTO categories (cat_name) VALUES
('Electronics'), ('Books'), ('Clothing'), ('Sports');

INSERT INTO products (name, cat_id, price) VALUES
('Laptop',      1, 999.99),
('Phone',       1, 499.99),
('SQL Guide',   2,  39.99),
('T-Shirt',     3,  19.99),
('Running Shoes',3, 89.99),
('Yoga Mat',    NULL, 29.99);  -- uncategorized product
EOF

Step 2: LEFT JOIN

📸 Verified Output:

💡 LEFT JOIN preserves ALL rows from the left (first) table. Rows with no match in the right table get NULL for right-table columns. David and Eve have no orders — they still appear.


Step 3: RIGHT JOIN

📸 Verified Output:

💡 RIGHT JOIN is the mirror of LEFT JOIN. In practice, most developers rewrite RIGHT JOINs as LEFT JOINs by swapping table order — it's the same result but easier to read.


Step 4: Finding Unmatched Rows — Anti-Join Pattern

📸 Verified Output:

📸 Verified Output:

💡 The LEFT JOIN ... WHERE b.id IS NULL pattern is called an anti-join. It efficiently finds rows in table A with no matching row in table B.


Step 5: FULL OUTER JOIN (PostgreSQL)

MySQL does NOT support FULL OUTER JOIN directly. PostgreSQL does.

PostgreSQL:

📸 Verified Output (PostgreSQL):

MySQL workaround for FULL OUTER JOIN:


Step 6: CROSS JOIN

📸 Verified Output:

💡 CROSS JOIN is intentional when you need all combinations (e.g., generate a size × color matrix, create test data). With large tables, result grows as m×n rows — be careful!


Step 7: Multi-Table Outer Join

📸 Verified Output:


Step 8: Capstone — Category Coverage Report

📸 Verified Output:

Cleanup:


Summary

Join Type
Returns
Syntax

INNER JOIN

Only matching rows

JOIN b ON a.id = b.a_id

LEFT JOIN

All left + matching right

LEFT JOIN b ON ...

RIGHT JOIN

All right + matching left

RIGHT JOIN b ON ...

FULL OUTER JOIN

All rows from both tables

FULL OUTER JOIN (PG only)

CROSS JOIN

All combinations

CROSS JOIN b

Anti-join

Unmatched from left

LEFT JOIN b ... WHERE b.id IS NULL

Next: Lab 08 — Subqueries

Last updated