Menu

SQLite Self Join: Querying a Table Against Itself

How a self join works in SQLite — pairing rows from the same table using aliases, with worked examples for employee/manager and hierarchical data.

This page includes runnable editors — edit, run, and see output instantly.

A Self Join Is Just a Join With Aliases

There's nothing special about a self join. It's an ordinary JOIN where both sides happen to be the same table. The trick is that SQLite needs a way to tell the two copies apart, so you give each one an alias.

You reach for one whenever a row in a table refers to another row in the same table. The classic case: an employees table where each row has a manager_id pointing at another employee:

Ada has no manager. Boris and Cleo report to Ada. Diego and Esme report to Boris. The relationship lives entirely inside one table — and that's exactly where a self join earns its keep.

The Basic Shape

To get each employee paired with their manager's name, join employees to itself. One copy plays the role of "employee," the other plays "manager":

Read it as two tables that happen to share storage. e is the employee row; m is the manager row. The join condition e.manager_id = m.id lines them up: for each employee, find the row in m whose id matches the employee's manager_id.

Notice Ada didn't show up. Her manager_id is NULL, and INNER JOIN drops rows that don't match.

Keeping the Unmatched Rows: LEFT JOIN

If you want everyone in the result, including the people without a manager, switch to LEFT JOIN:

Now Ada appears with NULL in the manager column. Same self-join mechanics, just with the join type doing what LEFT JOIN always does — keep every row from the left side, fill blanks where there's no match.

This is the form you usually want when displaying a list of people. "No manager" is information; dropping the row isn't.

The Aliases Aren't Optional

Try the join without aliases and SQLite has no idea what you mean:

SELECT name, manager_id FROM employees JOIN employees ON manager_id = id;
-- Error: ambiguous column name: name

Every column appears twice — once from each copy of the table — and SQLite can't pick. Aliases solve this by giving each instance its own name. Pick aliases that describe the role the row is playing, not the table:

  • e and m for employee/manager.
  • parent and child for hierarchies.
  • a and b when you're comparing arbitrary pairs.

The alias is the whole reason a self join reads cleanly.

Finding Pairs Within a Table

Self joins aren't only for hierarchies. Any time you want to compare rows in the same table, the pattern fits. Here's a list of products, where we want every pair priced the same:

Two things to spot. First, a.price = b.price is the actual matching condition. Second, a.id < b.id is what stops the query from returning each pair twice (once as (Mug, Notebook), again as (Notebook, Mug)) and from pairing every row with itself. That < trick is worth remembering — it shows up any time you're listing pairs.

Going Two Levels Up

A self join handles one hop in a hierarchy. Want each employee's grand-manager? Join three times:

Each new alias represents one more level up the tree. This works fine for two or three hops, but it falls apart fast — you'd need to know the depth of the hierarchy at query-writing time, and add a join for each level. That's the wall that recursive CTEs were invented to break through.

When Not to Reach for a Self Join

A self join is the right tool when you need columns from both sides of the relationship in the result. If you only need to filter — say, find every employee whose manager is Ada — a subquery often reads better:

No alias gymnastics, and the intent is unmistakable. The rule of thumb: do you want data from both rows in the output? Self join. Do you just need a value to compare against? Subquery.

For arbitrary-depth hierarchies (org charts, file trees, threaded comments), neither pattern scales. That's recursive CTE territory.

Next: Subqueries

Self joins and subqueries solve overlapping problems, and knowing which one fits saves you a lot of squinting at SQL later. The next page goes deep on subqueries — scalar, correlated, and IN forms — and where each one shines.

Frequently Asked Questions

What is a self join in SQLite?

A self join is a regular JOIN where a table is joined with itself. You give the same table two different aliases so SQLite can treat them as separate row sources, then match rows on a column that relates one row to another — most often a parent/child relationship like employee/manager.

Why do I need aliases in a self join?

Without aliases, SQLite can't tell which copy of the table you mean when you write a column name. Giving each instance its own alias (like e for employee and m for manager) lets you write e.manager_id = m.id unambiguously. The aliases aren't optional — the query won't parse without them.

When should I use a self join versus a subquery?

Use a self join when you want columns from both rows in the result — say, the employee's name and the manager's name on the same line. Use a subquery when you only need to filter or look up a single value. For deeply nested hierarchies, neither fits well; a recursive CTE is the right tool.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED