JOIN

JOIN

1. JOIN Concept

JOIN is a method to connect two or more tables to query data.

┌─────────────────┐     ┌─────────────────┐
│     users       │     │     orders      │
├─────────────────┤     ├─────────────────┤
│ id │ name       │     │ id │ user_id    │
├────┼────────────┤     ├────┼────────────┤
│ 1  │ John Kim   │◄────│ 1  │ 1          │
│ 2  │ Jane Lee   │◄────│ 2  │ 1          │
│ 3  │ Mike Park  │     │ 3  │ 2          │
└────┴────────────┘     └────┴────────────┘
         ↑ users.id = orders.user_id

2. Practice Table Setup

-- Users table
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255)
);

-- Orders table
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    product_name VARCHAR(200),
    amount NUMERIC(10, 2),
    order_date DATE DEFAULT CURRENT_DATE
);

-- Sample data
INSERT INTO users (name, email) VALUES
('John Kim', 'kim@email.com'),
('Jane Lee', 'lee@email.com'),
('Mike Park', 'park@email.com'),
('Sarah Choi', 'choi@email.com');  -- User with no orders

INSERT INTO orders (user_id, product_name, amount) VALUES
(1, 'Laptop', 1500000),
(1, 'Mouse', 50000),
(2, 'Keyboard', 100000),
(2, 'Monitor', 300000),
(3, 'Headset', 150000),
(NULL, 'Gift Set', 80000);  -- Order without user

3. INNER JOIN

Returns only data that matches in both tables.

-- Basic syntax
SELECT columns
FROM table1
INNER JOIN table2 ON table1.column = table2.column;

-- Query user and order information
SELECT
    users.name,
    users.email,
    orders.product_name,
    orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;

Result:

  name          email        product_name   amount
──────────┼──────────────────┼──────────────┼──────────
 John Kim  kim@email.com     Laptop        1500000
 John Kim  kim@email.com     Mouse           50000
 Jane Lee  lee@email.com     Keyboard       100000
 Jane Lee  lee@email.com     Monitor        300000
 Mike Park park@email.com    Headset        150000

Use Table Aliases

SELECT u.name, o.product_name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

JOIN Implies INNER JOIN

-- INNER can be omitted
SELECT u.name, o.product_name
FROM users u
JOIN orders o ON u.id = o.user_id;

4. LEFT (OUTER) JOIN

Returns all rows from left table + matching rows from right table. Unmatched rows are filled with NULL.

SELECT
    u.name,
    o.product_name,
    o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

Result:

   name     │ product_name │  amount
────────────┼──────────────┼──────────
 John Kim   │ Laptop       │ 1500000
 John Kim   │ Mouse        │   50000
 Jane Lee   │ Keyboard     │  100000
 Jane Lee   │ Monitor      │  300000
 Mike Park  │ Headset      │  150000
 Sarah Choi │ NULL         │ NULL      ← User with no orders included

Find Users Without Orders

SELECT u.name, u.email
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

5. RIGHT (OUTER) JOIN

Returns all rows from right table + matching rows from left table.

SELECT
    u.name,
    o.product_name,
    o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

Result:

   name    │ product_name │  amount
───────────┼──────────────┼──────────
 John Kim  │ Laptop       │ 1500000
 John Kim  │ Mouse        │   50000
 Jane Lee  │ Keyboard     │  100000
 Jane Lee  │ Monitor      │  300000
 Mike Park │ Headset      │  150000
 NULL      │ Gift Set     │   80000   ← Order without user included

6. FULL (OUTER) JOIN

Returns all rows from both tables. Unmatched rows are filled with NULL.

SELECT
    u.name,
    o.product_name,
    o.amount
FROM users u
FULL JOIN orders o ON u.id = o.user_id;

Result:

   name     │ product_name │  amount
────────────┼──────────────┼──────────
 John Kim   │ Laptop       │ 1500000
 John Kim   │ Mouse        │   50000
 Jane Lee   │ Keyboard     │  100000
 Jane Lee   │ Monitor      │  300000
 Mike Park  │ Headset      │  150000
 Sarah Choi │ NULL         │ NULL      ← User without orders
 NULL       │ Gift Set     │   80000   ← Order without user

7. CROSS JOIN

Returns all possible combinations (Cartesian product).

-- Color and size tables
CREATE TABLE colors (name VARCHAR(20));
CREATE TABLE sizes (name VARCHAR(10));

INSERT INTO colors VALUES ('Red'), ('Blue'), ('Black');
INSERT INTO sizes VALUES ('S'), ('M'), ('L');

-- All combinations
SELECT c.name AS color, s.name AS size
FROM colors c
CROSS JOIN sizes s;

Result:

 color │ size
───────┼──────
 Red   │ S
 Red   │ M
 Red   │ L
 Blue  │ S
 Blue  │ M
 Blue  │ L
 Black │ S
 Black │ M
 Black │ L

8. SELF JOIN

Joins a table with itself.

-- Employee-Manager relationship
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    manager_id INTEGER REFERENCES employees(id)
);

INSERT INTO employees (name, manager_id) VALUES
('CEO', NULL),
('VP', 1),
('Manager A', 2),
('Manager B', 2),
('Employee', 3);

-- Query employee and manager names
SELECT
    e.name AS employee,
    m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Result:

  employee  │ manager
────────────┼─────────
 CEO        │ NULL
 VP         │ CEO
 Manager A  │ VP
 Manager B  │ VP
 Employee   │ Manager A

9. Multiple Table JOIN

Connect 3 or more tables.

-- Add category table
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

-- Products table
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    category_id INTEGER REFERENCES categories(id),
    name VARCHAR(200),
    price NUMERIC(10, 2)
);

-- Order items table
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER REFERENCES orders(id),
    product_id INTEGER REFERENCES products(id),
    quantity INTEGER
);

-- JOIN 3 tables
SELECT
    u.name AS user_name,
    p.name AS product_name,
    c.name AS category_name,
    oi.quantity
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN categories c ON p.category_id = c.id;

10. JOIN Conditions and WHERE

ON vs WHERE

-- ON: Table join condition
-- WHERE: Result filtering

-- LEFT JOIN + WHERE
SELECT u.name, o.product_name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100000;  -- NULL rows removed

-- LEFT JOIN + Additional condition in ON
SELECT u.name, o.product_name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.amount > 100000;
-- All users retained, only matching orders connected

Composite JOIN Conditions

SELECT *
FROM table1 t1
JOIN table2 t2 ON t1.col1 = t2.col1 AND t1.col2 = t2.col2;

11. USING Clause

Simplifies joins when column names are the same.

-- Using ON
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id;

-- Using USING (when column names match)
-- If orders.user_id and users.user_id are the same:
SELECT * FROM orders
JOIN users USING (user_id);

12. NATURAL JOIN

Automatically joins on all columns with the same name. (Not recommended)

-- Joins on all columns with same name
SELECT * FROM orders
NATURAL JOIN users;

-- May produce unintended results, explicit ON recommended

13. JOIN Visualization

INNER JOIN:         LEFT JOIN:          RIGHT JOIN:         FULL JOIN:
    ┌───┐              ┌───┐              ┌───┐              ┌───┐
   ┌┼───┼┐            ┌┼───┼┐            ┌┼───┼┐            ┌┼───┼┐
  ┌┼│███│┼┐          ┌┼│███│ │          │ │███│┼┐          ┌┼│███│┼┐
  │ │███│ │          ││████│ │          │ │████││          ││█████││
  └┼│███│┼┘          └┼│███│ │          │ │███│┼┘          └┼│███│┼┘
   └┼───┼┘            └┼───┘ │          │ └───┼┘            └─────┼┘
    └───┘              └─────┘          └─────┘              └─────┘
   A ∩ B               All A            All B              A ∪ B

14. Practice Examples

Practice 1: Basic JOIN

-- 1. Users who have ordered and their order info
SELECT u.name, o.product_name, o.amount, o.order_date
FROM users u
INNER JOIN orders o ON u.id = o.user_id
ORDER BY o.order_date DESC;

-- 2. Total order amount per user
SELECT u.name, SUM(o.amount) AS total_amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
ORDER BY total_amount DESC;

Practice 2: OUTER JOIN

-- 1. All users (regardless of orders)
SELECT
    u.name,
    COALESCE(SUM(o.amount), 0) AS total_amount,
    COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
ORDER BY total_amount DESC;

-- 2. Find users who haven't ordered
SELECT u.name, u.email
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

-- 3. Find orders without users
SELECT o.id, o.product_name, o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL;

Practice 3: Complex Condition JOIN

-- 1. Users who ordered 1,000,000 or more
SELECT DISTINCT u.name, u.email
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount >= 1000000;

-- 2. Users who ordered within last 30 days
SELECT DISTINCT u.name
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days';

Practice 4: Multiple Table JOIN

-- Connect categories → products → orders
SELECT
    c.name AS category,
    p.name AS product,
    u.name AS customer,
    oi.quantity,
    p.price * oi.quantity AS subtotal
FROM categories c
JOIN products p ON c.id = p.category_id
JOIN order_items oi ON p.id = oi.product_id
JOIN orders o ON oi.order_id = o.id
JOIN users u ON o.user_id = u.id
ORDER BY c.name, p.name;

15. Performance Considerations

Use Indexes

-- Create indexes on foreign key columns
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);

SELECT Only Needed Columns

-- Bad example
SELECT * FROM users u JOIN orders o ON u.id = o.user_id;

-- Good example
SELECT u.name, o.product_name, o.amount
FROM users u JOIN orders o ON u.id = o.user_id;

Check Execution Plan with EXPLAIN

EXPLAIN SELECT u.name, o.product_name
FROM users u
JOIN orders o ON u.id = o.user_id;

Next Steps

Learn about aggregate functions and GROUP BY in 07_Aggregation_and_Grouping.md!

to navigate between lessons