SQL Joins in PostgreSQL: INNER, LEFT, RIGHT, FULL & Self Joins
Learn every type of SQL join in PostgreSQL — INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN, and self joins — with clear examples for each.
SQL Joins in PostgreSQL
Joins combine rows from two or more tables based on a related column — this is the relational equivalent of MongoDB's embedded documents or a $lookup aggregation stage.
Assume two tables: employees(id, name, department_id) and departments(id, name).
INNER JOIN
Returns only rows that have a match in both tables.
SELECT e.name, d.name AS department
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;
An employee with no matching department (or a department with no employees) simply doesn't appear in the result.
LEFT JOIN (LEFT OUTER JOIN)
Returns every row from the left table, plus matching rows from the right — filling in NULL where there's no match.
SELECT e.name, d.name AS department
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;
-- every employee appears, even ones with no department assigned
RIGHT JOIN (RIGHT OUTER JOIN)
The mirror image of LEFT JOIN — every row from the right table, matched with the left where possible.
SELECT e.name, d.name AS department
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;
-- every department appears, even ones with zero employees
FULL OUTER JOIN
Returns everything from both tables — matched where possible, NULL-padded where not.
SELECT e.name, d.name AS department
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.id;
CROSS JOIN
Every row from the first table paired with every row from the second — the Cartesian product. No ON clause, and the result size is rows_in_a × rows_in_b.
SELECT e.name, d.name
FROM employees e
CROSS JOIN departments d;
Self Join
A table joined to itself — useful for hierarchical data, like employees and their managers.
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Common Errors & Troubleshooting
- Row count multiplying unexpectedly after a join — if the "one" side of a one-to-many relationship (like
departmentsjoined toemployees) gets duplicated per match, that's expected join behavior, not a bug — aggregate afterward if you need one row per department. - A
LEFT JOINbehaving like anINNER JOIN— aWHEREclause referencing the right table's column (e.g.WHERE d.name = 'Engineering') filters out theNULL-padded unmatched rows too, effectively turning it back into an inner join; move that condition into theONclause instead if you want to keep unmatched left rows. - Accidentally writing a
CROSS JOIN— forgetting theONcondition on a regular join (or a stray comma between table names in theFROMclause) produces a full Cartesian product — always double check every join has its matchingON.
Key Takeaways
INNER JOINonly keeps matched rows;LEFT/RIGHT/FULLOUTER joins keep unmatched rows too, filling gaps withNULL.LEFT JOINis the one you'll reach for most often — "give me all of table A, plus whatever matches from B."- A self join just means both sides of the
JOINreference the same table, usually with different aliases.