SQL Joins Explained with Practical Examples
If you've ever worked with relational databases, you've encountered SQL JOINs. They're the backbone of querying data from multiple tables, and mastering them is essential for any developer working with databases. Yet, JOINs often confuse beginners—and even intermediate developers sometimes struggle to pick the right type.
In this comprehensive guide, we'll break down every type of SQL JOIN with visual diagrams, clear explanations, and practical examples you can run immediately. By the end, you'll confidently write complex queries that combine data from multiple tables.
In normalized databases, data is split across multiple tables to avoid redundancy. JOINs are how you reassemble that data for meaningful queries. Understanding JOINs is the difference between struggling with SQL and mastering it.
Setting Up Our Sample Data
Before diving into JOIN types, let's create two simple tables that we'll use throughout this tutorial. We'll work with employees and departments tables—a classic example that clearly demonstrates how JOINs work.
-- Create departments table
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50) NOT NULL,
location VARCHAR(100)
);
-- Create employees table
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(100) NOT NULL,
dept_id INT,
salary DECIMAL(10, 2),
hire_date DATE
);
-- Insert sample data
INSERT INTO departments VALUES
(1, 'Engineering', 'Building A'),
(2, 'Marketing', 'Building B'),
(3, 'Sales', 'Building C'),
(4, 'HR', 'Building D');
INSERT INTO employees VALUES
(101, 'Alice Johnson', 1, 85000, '2022-03-15'),
(102, 'Bob Smith', 1, 75000, '2023-01-10'),
(103, 'Carol Davis', 2, 65000, '2021-06-20'),
(104, 'David Wilson', 3, 70000, '2020-09-05'),
(105, 'Eve Martinez', NULL, 60000, '2024-02-01'),
(106, 'Frank Brown', 1, 90000, '2019-11-30');
employees
| emp_id | emp_name | dept_id |
|---|---|---|
| 101 | Alice | 1 |
| 102 | Bob | 1 |
| 103 | Carol | 2 |
| 104 | David | 3 |
| 105 | Eve | NULL |
| 106 | Frank | 1 |
departments
| dept_id | dept_name |
|---|---|
| 1 | Engineering |
| 2 | Marketing |
| 3 | Sales |
| 4 | HR |
Notice some key details in our data:
- Eve (emp_id 105) has
NULLfor dept_id — she's not assigned to any department - HR department (dept_id 4) has no employees assigned to it
- Engineering (dept_id 1) has multiple employees (Alice, Bob, Frank)
These edge cases will help us clearly see the differences between JOIN types.
The JOIN Types Overview
SQL provides several types of JOINs, each serving different purposes. Here's a quick visual overview before we dive deep into each one:
Visual comparison of all SQL JOIN types
INNER JOIN - The Most Common JOIN
INNER JOIN returns only the rows where there's a match in both tables. If a row in the left table has no matching row in the right table (or vice versa), it's excluded from the results.
SELECT
e.emp_id,
e.emp_name,
d.dept_name,
d.location
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
Result (5 rows)
| emp_id | emp_name | dept_name | location |
|---|---|---|---|
| 101 | Alice Johnson | Engineering | Building A |
| 102 | Bob Smith | Engineering | Building A |
| 103 | Carol Davis | Marketing | Building B |
| 104 | David Wilson | Sales | Building C |
| 106 | Frank Brown | Engineering | Building A |
- Eve Martinez is excluded because she has
NULLdept_id - HR department is excluded because no employees belong to it
LEFT JOIN (LEFT OUTER JOIN)
LEFT JOIN returns all rows from the left table and the matched rows from the right table. If there's no match, the result contains NULL values for columns from the right table.
SELECT
e.emp_id,
e.emp_name,
d.dept_name,
d.location
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;
Result (6 rows - all employees)
| emp_id | emp_name | dept_name | location |
|---|---|---|---|
| 101 | Alice Johnson | Engineering | Building A |
| 102 | Bob Smith | Engineering | Building A |
| 103 | Carol Davis | Marketing | Building B |
| 104 | David Wilson | Sales | Building C |
| 105 | Eve Martinez | NULL | NULL |
| 106 | Frank Brown | Engineering | Building A |
Notice that Eve Martinez now appears in the result, but with NULL values for department columns since she has no department assigned.
LEFT JOIN is perfect for finding records that don't have a match:
-- Find employees without a department
SELECT e.emp_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_id IS NULL;
RIGHT JOIN (RIGHT OUTER JOIN)
RIGHT JOIN is the opposite of LEFT JOIN. It returns all rows from the right table and the matched rows from the left table. Unmatched left table columns contain NULL.
SELECT
e.emp_id,
e.emp_name,
d.dept_name,
d.location
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;
Result (6 rows - all departments)
| emp_id | emp_name | dept_name | location |
|---|---|---|---|
| 101 | Alice Johnson | Engineering | Building A |
| 102 | Bob Smith | Engineering | Building A |
| 106 | Frank Brown | Engineering | Building A |
| 103 | Carol Davis | Marketing | Building B |
| 104 | David Wilson | Sales | Building C |
| NULL | NULL | HR | Building D |
Now we see the HR department in results (with NULL employee data), but Eve is excluded since she doesn't match any department.
Most developers prefer LEFT JOIN and simply swap the table order. This improves readability since we read SQL left-to-right. The following queries are equivalent:
-- These produce identical results:
SELECT * FROM A RIGHT JOIN B ON A.id = B.id;
SELECT * FROM B LEFT JOIN A ON B.id = A.id;
FULL OUTER JOIN
FULL OUTER JOIN returns all rows from both tables. Where there's a match, the row is combined. Where there's no match, the missing side contains NULL values.
-- MySQL doesn't support FULL OUTER JOIN directly
-- Use UNION of LEFT and RIGHT JOINs:
SELECT
e.emp_id,
e.emp_name,
d.dept_name,
d.location
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
UNION
SELECT
e.emp_id,
e.emp_name,
d.dept_name,
d.location
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.dept_id;
Result (7 rows - all records from both tables)
| emp_id | emp_name | dept_name | location |
|---|---|---|---|
| 101 | Alice Johnson | Engineering | Building A |
| 102 | Bob Smith | Engineering | Building A |
| 103 | Carol Davis | Marketing | Building B |
| 104 | David Wilson | Sales | Building C |
| 105 | Eve Martinez | NULL | NULL |
| 106 | Frank Brown | Engineering | Building A |
| NULL | NULL | HR | Building D |
CROSS JOIN - The Cartesian Product
CROSS JOIN returns the Cartesian product of both tables—every row from the first table combined with every row from the second table. If table A has 6 rows and table B has 4 rows, the result has 6 × 4 = 24 rows.
-- Explicit CROSS JOIN syntax
SELECT
e.emp_name,
d.dept_name
FROM employees e
CROSS JOIN departments d;
-- Alternative implicit syntax (comma-separated)
SELECT
e.emp_name,
d.dept_name
FROM employees e, departments d;
CROSS JOINs can produce massive result sets. A CROSS JOIN between a 1,000-row table and a 1,000-row table produces 1,000,000 rows. Common legitimate uses include:
- Generating test data
- Creating date/product combinations for reports
- Producing all possible pairs for analysis
SELF JOIN - Joining a Table to Itself
A SELF JOIN is when a table is joined with itself. This is useful for hierarchical data (like org charts) or comparing rows within the same table.
Let's modify our employees table to include a manager_id column:
-- Add manager relationship
ALTER TABLE employees ADD COLUMN manager_id INT;
UPDATE employees SET manager_id = 106 WHERE emp_id IN (101, 102);
UPDATE employees SET manager_id = 103 WHERE emp_id = 104;
-- Self join to find employees and their managers
SELECT
e.emp_name AS employee,
m.emp_name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
Result - Employee-Manager Relationships
| employee | manager |
|---|---|
| Alice Johnson | Frank Brown |
| Bob Smith | Frank Brown |
| Carol Davis | NULL |
| David Wilson | Carol Davis |
| Eve Martinez | NULL |
| Frank Brown | NULL |
Multiple JOINs - Combining More Than Two Tables
Real-world queries often join three or more tables. You simply chain JOIN clauses together:
-- Add a projects table
CREATE TABLE projects (
project_id INT PRIMARY KEY,
project_name VARCHAR(100),
dept_id INT
);
INSERT INTO projects VALUES
(1, 'Website Redesign', 2),
(2, 'Mobile App', 1),
(3, 'Data Analytics', 1);
-- Join three tables
SELECT
e.emp_name,
d.dept_name,
p.project_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
INNER JOIN projects p ON d.dept_id = p.dept_id
ORDER BY e.emp_name;
JOIN Types Comparison
| JOIN Type | Left Table Rows | Right Table Rows | Use Case |
|---|---|---|---|
| INNER JOIN | Only matched | Only matched | Find related records |
| LEFT JOIN | All rows | Only matched | Keep all left + find matches |
| RIGHT JOIN | Only matched | All rows | Keep all right + find matches |
| FULL OUTER | All rows | All rows | Combine everything |
| CROSS JOIN | All × All | All × All | Generate combinations |
| SELF JOIN | Table joined to itself | Hierarchies, comparisons | |
Performance Tips for JOINs
Always Index JOIN Columns
Ensure columns used in ON clauses have indexes. Without indexes, the database must scan entire tables for matches.
Filter Early with WHERE
Apply WHERE conditions before JOINing when possible. This reduces the number of rows processed by the JOIN.
Select Only Needed Columns
Avoid SELECT *. Specify only the columns you need to reduce data transfer and memory usage.
Use EXPLAIN to Analyze
Prefix your query with EXPLAIN to see the query execution plan and identify performance bottlenecks.
-- Create index on foreign key column
CREATE INDEX idx_emp_dept ON employees(dept_id);
-- Analyze query execution plan
EXPLAIN SELECT e.emp_name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
Common JOIN Mistakes to Avoid
Omitting the ON clause creates an accidental CROSS JOIN, potentially returning millions of rows:
-- ❌ WRONG - This is a CROSS JOIN!
SELECT * FROM employees e, departments d;
-- ✅ CORRECT
SELECT * FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
When both tables have columns with the same name, always use table aliases:
-- ❌ WRONG - Ambiguous column 'dept_id'
SELECT dept_id FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
-- ✅ CORRECT - Specify which table
SELECT e.dept_id FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
Remember that INNER JOIN excludes rows with NULL join keys. Use LEFT JOIN if you need to keep unmatched rows.
Practice Exercise
Try These Queries
- Find all employees in the Engineering department
- List all departments and their employee count (include departments with 0 employees)
- Find employees who earn more than their manager
- List all possible employee-project assignments
View Solutions
-- 1. Engineering employees
SELECT e.emp_name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_name = 'Engineering';
-- 2. Department employee counts
SELECT d.dept_name, COUNT(e.emp_id) AS employee_count
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name;
-- 3. Employees earning more than their manager
SELECT e.emp_name AS employee, e.salary AS emp_salary,
m.emp_name AS manager, m.salary AS mgr_salary
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id
WHERE e.salary > m.salary;
-- 4. All employee-project combinations (CROSS JOIN)
SELECT e.emp_name, p.project_name
FROM employees e
CROSS JOIN projects p;
Conclusion
SQL JOINs are fundamental to working with relational databases. Here's a quick recap:
- INNER JOIN: Returns only matching rows from both tables
- LEFT JOIN: Returns all rows from the left table, with matches from the right
- RIGHT JOIN: Returns all rows from the right table, with matches from the left
- FULL OUTER JOIN: Returns all rows from both tables
- CROSS JOIN: Returns the Cartesian product (all combinations)
- SELF JOIN: Joins a table to itself for hierarchical queries
The key to mastering JOINs is practice. Start with simple queries, visualize the Venn diagrams, and gradually work up to complex multi-table joins. Before long, you'll be writing sophisticated queries with confidence!
- Database Indexing: Boost Your Query Performance
- MySQL Official Documentation on JOINs
- SQL Performance Explained by Markus Winand
