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