31 Self-Join
A self-join is when a table joins to itself. You give it two aliases so PostgreSQL can tell which copy you mean, and use a condition that links rows together. Self-joins show up whenever the data models a relationship between rows of the same kind — employees and their managers, comments and their parent comments, predecessors and successors. For arbitrary-depth hierarchies, the right tool is a recursive CTE (covered in a later lesson); for one level, a self-join is simple and fast.
What you’ll learn
- Joining a table to itself with two aliases
- Choosing
INNERvsLEFTfor self-joins - Modeling a one-level hierarchy (employee → manager)
- A peek at recursive CTEs for arbitrary depth
- Naming columns for clarity
Example
In the join FROM employees_example e LEFT JOIN employees_example m ON e.manager_id = m.id, e is the employee row and m is the manager row. Both refer to the same physical table — PostgreSQL treats the aliases as independent copies. Using LEFT JOIN means employees without a manager (Alice) still appear, with NULL in the manager columns.
The count(e.id) (not count(*)) in the second query is the standard pattern: count(*) would count NULLs from unmatched rows, giving every leaf manager at least 1 direct report incorrectly. count(e.id) only counts non-NULL ids — actual reports.
The recursive CTE at the end is a preview of the CTE lesson. WITH RECURSIVE lets you walk an arbitrary depth — the anchor row (Dave) is the starting point; the recursive step joins back to chain to climb one level at a time.
Run the example
To run:
Common pitfalls
- Using
INNER JOINinstead ofLEFT JOINand silently losing the top of the hierarchy. The top row has no manager —INNER JOINdiscards it. - Forgetting that both
eandmare the same table — you canWHERE e.id <> m.idto exclude same-row matches if your condition might allow them. - Counting
*instead of the joined-table id when looking for “groups with no children” —count(child.id)is the correct idiom. - For arbitrary depth, repeatedly self-joining (
emp → mgr → mgr's mgr → ...) does not scale. UseWITH RECURSIVEinstead.
Symmetric relationships (friend-of) usually use a separate junction table, not a self-join. Asymmetric (manager-of, parent-of) fit nicely as a self-referencing column.
Add a sixth employee under Carol. Re-run the direct-reports query — Carol should now have one. Then modify the recursive CTE to start at Eve instead of Dave and see how the chain changes.
Downloads
SQL file: self-join.sql · All lesson files · Setup guide