Skip to content
SP StackPractices
beginner By StackPractices

SQL Joins — Visual Guide with Examples

A visual guide to SQL joins: INNER, LEFT, RIGHT, FULL OUTER, CROSS, and SELF joins with practical examples, performance tips, and common pitfalls.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

SQL joins combine rows from two or more tables based on a related column. Despite being one of the most fundamental SQL operations, joins are a common source of confusion and performance problems. This guide provides visual explanations, practical examples, and optimization strategies for every join type you will encounter in production.

When to Use

  • Combining related data from multiple tables (orders + customers)
  • Filtering data based on presence or absence in another table
  • Generating reports that aggregate data across entities
  • Checking referential integrity or orphaned records

INNER JOIN — Only Matching Rows

Returns rows where there is a match in both tables.

SELECT o.order_id, c.name, o.total
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;
Orders                    Customers              Result
┌────┬──────────┐        ┌────┬────────┐       ┌────┬────────┬───────┐
│ id │ customer │        │ id │ name   │       │ id │ name   │ total │
├────┼──────────┤        ├────┼────────┤       ├────┼────────┼───────┤
│ 1  │    101   │───────▶│101 │ Alice  │──────▶│ 1  │ Alice  │ 250   │
│ 2  │    102   │──┐     │102 │ Bob    │       │ 2  │ Bob    │ 100   │
│ 3  │    103   │  └────▶│103 │ Carol  │──────▶│ 3  │ Carol  │ 500   │
└────┴──────────┘        └────┴────────┘       └────┴────────┴───────┘
                                                      (no row for 104)

LEFT JOIN — All from Left, Matching from Right

Returns all rows from the left table, with matching rows from the right. Non-matching right rows are NULL.

SELECT c.name, o.order_id, o.total
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
Customers                 Orders                 Result
┌────┬────────┐          ┌────┬──────────┐      ┌────┬────────┬───────┐
│ id │ name   │          │ id │ customer │      │ id │ name   │ total │
├────┼────────┤          ├────┼──────────┤      ├────┼────────┼───────┤
│101 │ Alice  │─────────▶│ 1  │    101   │─────▶│101 │ Alice  │ 250   │
│102 │ Bob    │─────────▶│ 2  │    102   │─────▶│102 │ Bob    │ 100   │
│103 │ Carol  │─────────▶│ 3  │    103   │─────▶│103 │ Carol  │ 500   │
│104 │ Dave   │───✕──────│    │          │─────▶│104 │ Dave   │ NULL  │
└────┴────────┘          └────┴──────────┘      └────┴────────┴───────┘

Use LEFT JOIN to find customers without orders:

SELECT c.name
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.order_id IS NULL;

RIGHT JOIN — All from Right, Matching from Left

The mirror of LEFT JOIN. Returns all rows from the right table. Rarely used in practice — swap table order and use LEFT JOIN instead for readability.

FULL OUTER JOIN — All Rows from Both

Returns all rows when there is a match in either table. Non-matching rows from both sides are NULL.

SELECT c.name, o.order_id, o.total
FROM customers c
FULL OUTER JOIN orders o ON c.id = o.customer_id;
Result
┌─────┬────────┬───────┐
│ id  │ name   │ total │
├─────┼────────┼───────┤
│ 101 │ Alice  │ 250   │
│ 102 │ Bob    │ 100   │
│ 103 │ Carol  │ 500   │
│ 104 │ Dave   │ NULL  │  ◀── customer with no order
│  5  │ NULL   │ 75    │  ◀── orphaned order (no customer)
└─────┴────────┴───────┘

CROSS JOIN — Cartesian Product

Returns every combination of rows from both tables. Use sparingly — result size is table_a_rows × table_b_rows.

-- 3 colors × 4 sizes = 12 rows
SELECT c.color, s.size
FROM colors c
CROSS JOIN sizes s;

SELF JOIN — Joining a Table to Itself

Useful for hierarchical data (employees and managers, categories and subcategories).

SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Employees
┌────┬────────┬───────────┐
│ id │ name   │ manager_id│
├────┼────────┼───────────┤
│ 1  │ Alice  │ NULL      │  ◀── CEO
│ 2  │ Bob    │ 1         │
│ 3  │ Carol  │ 1         │
│ 4  │ Dave   │ 2         │
└────┴────────┴───────────┘

Result
┌─────────┬─────────┐
│employee │ manager │
├─────────┼─────────┤
│ Alice   │ NULL    │
│ Bob     │ Alice   │
│ Carol   │ Alice   │
│ Dave    │ Bob     │
└─────────┴─────────┘

Performance Optimization

Index the Join Columns

CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_customers_id ON customers(id);  -- usually primary key, already indexed

Avoid Joining on Calculated Values

-- Slow: function prevents index use
SELECT * FROM orders o
JOIN customers c ON UPPER(o.customer_email) = UPPER(c.email);

-- Fast: join on indexed column
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id;

Filter Before Joining

-- Slow: joins all orders, then filters
SELECT c.name, o.total
FROM customers c
JOIN orders o ON c.id = o.customer_id
WHERE o.created_at > '2024-01-01';

-- Faster: subquery reduces rows first
SELECT c.name, o.total
FROM customers c
JOIN (
  SELECT * FROM orders WHERE created_at > '2024-01-01'
) o ON c.id = o.customer_id;

Common Mistakes

  • Implicit INNER JOIN — using comma-separated tables without WHERE filters creates CROSS JOIN
  • Missing foreign key indexes — join columns must be indexed on the many side
  • LEFT JOIN with WHERE on right table — filters out NULL rows, effectively making it INNER JOIN
  • Joining on VARCHAR without collation awareness — case sensitivity surprises
  • N+1 queries in ORMs — fetching related data row-by-row instead of JOINing

FAQ

Which join is most commonly used? INNER JOIN and LEFT JOIN cover ~95% of production use cases.

Are JOINs expensive? They can be, but proper indexing makes most joins performant. The real cost is often transferring unnecessary columns.

Can I join more than two tables? Yes. databases can handle many joins, but each additional join adds complexity. Optimize and test with realistic data volumes.