Skip to main content
Relational databases store related data in separate tables on purpose — it reduces duplication and keeps updates consistent. But when you need to answer questions that span multiple tables, you need JOINs. A JOIN combines rows from two (or more) tables based on a matching condition. This page teaches you every major join type, when to use each one, and the mistakes that trip up beginners.

Why JOINs Exist

Consider: your employee table stores department_id, but not the department name. To display employee names alongside their department names you must connect the two tables.
Notice that Gopal Krishna disappears — they have no matching department. That is exactly INNER JOIN behavior, and understanding when that is correct (versus using LEFT JOIN) is the core skill of this page.

The Sample Tables

department employee (abbreviated)

JOIN Syntax

The ON clause specifies the matching condition — almost always a primary key on one side and a foreign key on the other. Use table aliases (short names like e and d) to keep your queries readable, especially when joining multiple tables.

INNER JOIN

An INNER JOIN returns only the rows that have a matching value in both tables. Rows with no match on either side are excluded.
Result: Every employee with a non-null department_id that exists in the department table. Gopal Krishna (NULL department) is excluded. Any department with no employees is also excluded.

INNER JOIN with WHERE and ORDER BY

Practice: HR employees

Write a query that returns the name and salary of all employees in the Human Resources department, sorted by salary descending.

LEFT JOIN

A LEFT JOIN returns all rows from the left (first) table, plus the matching rows from the right table. Where there is no match, the right table’s columns are filled with NULL.
Result: All 10 employees appear. Gopal Krishna’s department_name shows as NULL.

Find Unassigned Employees

LEFT JOIN + IS NULL is the standard pattern for finding rows in the left table that have no match in the right table.
This returns only the employees who have not been assigned to any department — a common data-quality check.

Practice: Departments with no employees

Write a query that lists department names that currently have no employees. (Hint: join from department to employee and look for NULL on the employee side.)

RIGHT JOIN

A RIGHT JOIN returns all rows from the right (second) table, plus matching rows from the left. It is the mirror of LEFT JOIN.
Result: All departments appear — even Marketing and Finance if they have no employees — with NULL in the employee columns where there is no match.
SQLite does not natively support RIGHT JOIN. You can achieve the same result by swapping the table order and using a LEFT JOIN:

FULL OUTER JOIN

A FULL OUTER JOIN returns all rows from both tables. Rows with matches are merged; unmatched rows on either side appear with NULL for the other table’s columns.
SQLite does not support FULL OUTER JOIN directly. Emulate it by combining a LEFT JOIN and a RIGHT JOIN (rewritten as LEFT JOIN) with UNION ALL, then deduplicating:

Joining Three or More Tables

You chain multiple JOIN clauses. Each new JOIN adds one more table to the result.
Work through joins one at a time when building complex queries: start with two tables, verify the result, then add the next join.

Self-JOIN

A self-join connects a table to itself. A common use case is a management hierarchy — where each employee row has a manager_id that references another employee_id in the same table.
You must alias the table twice (here as e and m) so SQL can distinguish which “copy” of the table each reference refers to.

The Cartesian Product Trap

If you forget the ON clause, SQL returns the Cartesian product — every row in the left table paired with every row in the right table. For tables with 25 and 5 rows, that is 125 rows of meaningless combinations.

Choosing the Right JOIN

INNER JOIN

Use when you only want rows that have a match on both sides. Excludes unmatched rows entirely.“Show me employees who have a department.”

LEFT JOIN

Use when you want every row from the left table, whether or not it has a match. Unmatched right columns become NULL.“Show me all employees, with their department if they have one.”

RIGHT JOIN

Use when you want every row from the right table. Equivalent to swapping the table order in a LEFT JOIN.“Show me all departments, with their employees if any exist.”

FULL OUTER JOIN

Use when you need every row from both sides regardless of matches. Useful for data reconciliation.“Show me all employees and all departments, matched where possible.”

Complete Practice: Departmental salary report

Write a query that returns: department_name, the count of employees in each department, and the average salary — but only for departments that have at least one employee. Sort by average salary descending.
Using INNER JOIN here naturally excludes departments with no employees. If you want to include zero-headcount departments, switch to LEFT JOIN and change HAVING COUNT(e.employee_id) >= 1 to >= 0.

Next: Advanced SQL

Go beyond the basics — write subqueries, CTEs, window functions, CASE expressions, and learn how indexes and views work.