Joins & Aggregation
What a JOIN Is
A JOIN combines rows from two or more tables using a related column: a column whose values link a row in one table to a row in another.
Think of two lists in an office. One lists employees and their department number. The other lists department numbers and their names. A JOIN reads both lists and matches each employee to their department name.
Sample Tables
Employees
| employee_id | name | department_id |
|---|---|---|
| 1 | Aakash | 101 |
| 2 | Priya | 102 |
| 3 | Rahul | 101 |
| 4 | Neha | NULL |
Departments
| department_id | department_name |
|---|---|
| 101 | Engineering |
| 102 | HR |
| 103 | Finance |
department_id is the related column. Two rows are special:
- Neha has no department (
NULL). - Finance has no employees.
How each JOIN type treats these two rows is the whole difference between them.
Anatomy of a JOIN Query
SELECTe.name,d.department_nameFROM Employees e -- left table, alias eINNER JOIN Departments d -- right table, alias dON e.department_id = d.department_id; -- how rows match
| Part | Meaning |
|---|---|
FROM Employees e | The left table, given the alias (short name) e |
INNER JOIN Departments d | The right table, alias d |
ON e.department_id = d.department_id | The join condition: which rows match |
SELECT e.name, d.department_name | Which columns to return |
Aliases are optional, but they keep queries short and are required when a column name exists in both tables.
1. INNER JOIN
Returns only rows that match in both tables.
SELECT e.name, d.department_nameFROM Employees eINNER JOIN Departments dON e.department_id = d.department_id;
| name | department_name |
|---|---|
| Aakash | Engineering |
| Priya | HR |
| Rahul | Engineering |
Why: Neha's NULL matches no department, and no employee is in Finance (103), so neither appears.
JOIN on its own means INNER JOIN.
2. LEFT JOIN
Returns all rows from the left table, plus matching rows from the right. If there's no match, the right table's columns are NULL.
SELECT e.name, d.department_nameFROM Employees eLEFT JOIN Departments dON e.department_id = d.department_id;
| name | department_name |
|---|---|
| Aakash | Engineering |
| Priya | HR |
| Rahul | Engineering |
| Neha | NULL |
Why: every employee appears, even Neha, who has no department.
This answers the practice question: "Show all employees, including those without a department, with their department name when available." "All employees" means the Employees table must keep every row, so it goes on the left of a LEFT JOIN.
3. RIGHT JOIN
Returns all rows from the right table, plus matching rows from the left.
SELECT e.name, d.department_nameFROM Employees eRIGHT JOIN Departments dON e.department_id = d.department_id;
| name | department_name |
|---|---|
| Aakash | Engineering |
| Rahul | Engineering |
| Priya | HR |
| NULL | Finance |
Why: every department appears, even Finance, which has no employees.
A RIGHT JOIN is a LEFT JOIN with the tables swapped. Most teams write LEFT JOIN only, which keeps queries easier to read:
-- same result as the RIGHT JOIN aboveSELECT e.name, d.department_nameFROM Departments dLEFT JOIN Employees eON e.department_id = d.department_id;
4. FULL OUTER JOIN
Returns all rows from both tables, matching them where possible. Unmatched rows on either side get NULLs.
SELECT e.name, d.department_nameFROM Employees eFULL OUTER JOIN Departments dON e.department_id = d.department_id;
| name | department_name |
|---|---|
| Aakash | Engineering |
| Rahul | Engineering |
| Priya | HR |
| Neha | NULL |
| NULL | Finance |
Why: everyone from both tables appears: Neha without a department, Finance without employees.
SQL Server supports FULL OUTER JOIN. MySQL does not; there you combine a LEFT JOIN and a RIGHT JOIN with UNION.
5. CROSS JOIN
Returns every possible combination of rows from both tables. It has no ON condition. The result is called a Cartesian product.
SELECT e.name, d.department_nameFROM Employees eCROSS JOIN Departments d;
14 employees × 3 departments = 12 rows2 3Aakash–Engineering Aakash–HR Aakash–Finance4Priya–Engineering Priya–HR Priya–Finance5Rahul–Engineering Rahul–HR Rahul–Finance6Neha–Engineering Neha–HR Neha–FinanceWhy: each employee is paired with every department, ignoring their real department.
Real use: generating combinations, like every size for every T-shirt colour, or every day of a month for every store in a report. Careful: 10,000 × 10,000 rows = 100 million rows.
6. SELF JOIN
A self join joins a table to itself. It's not a new keyword; you use a normal JOIN with two different aliases for the same table. It's useful when a row points to another row in the same table, like an employee and their manager.
Employees (with managers)
| employee_id | name | manager_id |
|---|---|---|
| 1 | Amit | NULL |
| 2 | Priya | 1 |
| 3 | Rahul | 1 |
| 4 | Neha | 2 |
manager_id holds another employee's employee_id.
SELECTe.name AS employee,m.name AS managerFROM Employees e -- e = the employeeLEFT JOIN Employees m -- m = the same table, as the managerON e.manager_id = m.employee_id;
| employee | manager |
|---|---|
| Amit | NULL |
| Priya | Amit |
| Rahul | Amit |
| Neha | Priya |
Why LEFT JOIN: Amit has no manager. An INNER JOIN would drop him from the result.
Common Traps
1. Finding rows with no match. Use a LEFT JOIN and keep only rows where the right side is NULL:
-- employees without a departmentSELECT e.nameFROM Employees eLEFT JOIN Departments dON e.department_id = d.department_idWHERE d.department_id IS NULL; -- Neha-- departments without employeesSELECT d.department_nameFROM Departments dLEFT JOIN Employees eON e.department_id = d.department_idWHERE e.employee_id IS NULL; -- Finance
2. A filter in WHERE can turn a LEFT JOIN into an INNER JOIN.
-- Filter in WHERE: runs AFTER the join-- Neha's department_name is NULL, NULL = 'Engineering' is not true, so Neha is removedSELECT e.name, d.department_nameFROM Employees eLEFT JOIN Departments dON e.department_id = d.department_idWHERE d.department_name = 'Engineering';-- Aakash, Rahul-- Filter in ON: decides which departments may match, every employee staysSELECT e.name, d.department_nameFROM Employees eLEFT JOIN Departments dON e.department_id = d.department_idAND d.department_name = 'Engineering';-- Aakash Engineering, Priya NULL, Rahul Engineering, Neha NULL
3. NULL never matches NULL. NULL = NULL is not true in SQL, so rows with NULL in the join column never match anything, not even another NULL. That's why Neha drops out of the INNER JOIN.
4. One row can appear many times. If one left row matches three right rows, the result has three rows for it. Joining employees to a table of their projects returns one row per project, not one per employee.
5. Row order isn't guaranteed. SQL Server can return rows in any order. Add ORDER BY whenever order matters.
Quick Revision
| JOIN type | Returns | Neha (no dept) | Finance (no employees) |
|---|---|---|---|
INNER JOIN | Only matching rows | ✗ | ✗ |
LEFT JOIN | All left + matching right | ✓ (NULL dept) | ✗ |
RIGHT JOIN | All right + matching left | ✗ | ✓ (NULL name) |
FULL OUTER JOIN | All rows from both | ✓ | ✓ |
CROSS JOIN | Every combination (4 × 3 = 12) | ✓ paired with all | ✓ paired with all |
SELF JOIN | Table joined to itself | Used for employee → manager | — |
Interview tips:
LEFT JOIN=LEFT OUTER JOIN,RIGHT JOIN=RIGHT OUTER JOIN,FULL JOIN=FULL OUTER JOIN. The wordOUTERis optional.JOINalone =INNER JOIN.- "Show all X, even if..." almost always means
LEFT JOINwith X on the left. - "Find X that don't have Y" means
LEFT JOIN+WHERE y.id IS NULL.
Summary
1JOIN → combine tables on a related column (ON condition)2INNER → matches only3LEFT → all left rows, NULLs where no match4RIGHT → all right rows (usually rewritten as LEFT)5FULL OUTER → all rows from both sides6CROSS → every combination, no ON, rows = A × B7SELF → same table, two aliases (employee → manager)8Traps → WHERE on right table kills LEFT JOIN rows9 → NULL never matches NULL10 → one row can repeat for many matches11 → add ORDER BY for a fixed orderNext, GROUP BY and HAVING show how to summarise joined data, like counting employees per department.