MySQL Joins

Joins are how you combine data from multiple tables. Understanding different join types and when to use them is essential for any database developer.

Sample Schema

SQL
CREATE TABLE departments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    budget DECIMAL(12,2)
);

CREATE TABLE employees (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    department_id INT,
    salary DECIMAL(10,2),
    hire_date DATE,
    FOREIGN KEY (department_id) REFERENCES departments(id)
);

CREATE TABLE projects (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    budget DECIMAL(12,2)
);

CREATE TABLE employee_projects (
    employee_id INT,
    project_id INT,
    role VARCHAR(50),
    hours_logged DECIMAL(6,2) DEFAULT 0,
    PRIMARY KEY (employee_id, project_id),
    FOREIGN KEY (employee_id) REFERENCES employees(id),
    FOREIGN KEY (project_id) REFERENCES projects(id)
);

All Join Types

SQL
-- INNER JOIN: Only matching rows
SELECT e.name, d.name AS department
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;
-- 10 employees in 3 departments → only 10 rows (employees without departments excluded)

-- LEFT JOIN: All left + matching right
SELECT e.name, d.name AS department
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;
-- Includes employees with NULL department_id

-- RIGHT JOIN: All right + matching left
SELECT e.name, d.name AS department
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;
-- Includes departments with no employees

-- FULL OUTER JOIN (MySQL doesn't support, use UNION)
SELECT e.name, d.name AS department
FROM employees e LEFT JOIN departments d ON e.department_id = d.id
UNION
SELECT e.name, d.name AS department
FROM employees e RIGHT JOIN departments d ON e.department_id = d.id;

-- CROSS JOIN: Every combination
SELECT e.name, p.name AS project
FROM employees e
CROSS JOIN projects p;

-- Self JOIN: Table with itself
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- Many-to-Many
SELECT
    e.name AS employee,
    p.name AS project,
    ep.role,
    ep.hours_logged
FROM employees e
JOIN employee_projects ep ON e.id = ep.employee_id
JOIN projects p ON ep.project_id = p.id
ORDER BY e.name, p.name;

Practical JOIN Patterns

SQL
-- Department summary
SELECT
    d.name AS department,
    COUNT(e.id) AS employee_count,
    ROUND(AVG(e.salary), 2) AS avg_salary,
    MAX(e.salary) AS max_salary,
    MIN(e.salary) AS min_salary,
    SUM(e.salary) AS total_payroll
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
GROUP BY d.id, d.name
ORDER BY total_payroll DESC;

-- Find departments with no employees
SELECT d.name
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
WHERE e.id IS NULL;

-- Monthly revenue report
SELECT
    DATE_FORMAT(o.created_at, '%Y-%m') AS month,
    p.category,
    COUNT(DISTINCT o.id) AS orders,
    SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.status = 'completed'
GROUP BY month, p.category
ORDER BY month DESC, revenue DESC;

-- Top spending customers
SELECT
    u.name,
    COUNT(DISTINCT o.id) AS total_orders,
    SUM(o.amount) AS total_spent,
    MAX(o.created_at) AS last_order
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
HAVING total_spent > 1000
ORDER BY total_spent DESC
LIMIT 20;

-- Find duplicate emails
SELECT email, COUNT(*) AS count
FROM users
GROUP BY email
HAVING count > 1;

-- Latest order per customer
SELECT u.name, o.product, o.amount, o.created_at
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at = (
    SELECT MAX(created_at) FROM orders WHERE user_id = u.id
);

💡 Tip: LEFT JOIN + WHERE right_table.id IS NULL is the standard pattern for finding "orphaned" records — rows in one table that have no matching rows in another.

Next: Indexes and Performance