<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Joins and combining tables
All lessons in this course

Lesson 21 of 54 14 minIntermediate

Self joins and CROSS JOIN

Join a table to itself to compare its rows or walk a hierarchy, and use CROSS JOIN to pair every row with every row on purpose. Plus how to list each pair only once.

Nothing says the two sides of a join must be different tables. A self join uses the same table twice, under two aliases, to compare its rows with each other. A CROSS JOIN has no condition at all: every row of one side with every row of the other.

Interactive

A self join is a filtered CROSS JOIN

SelfJoin.sql
SELECT c1.FirstName AS Customer, c2.FirstName AS SameCityAs, c1.CityFROM dbo.Customers AS c1JOIN dbo.Customers AS c2    ON c2.City = c1.City;
c1 ↓ c2 →AshaLeedsBenYorkChloeLeedsDevBradfordEllaNULLFaridYorkGraceLeedsZoëHarrogate
Asha Leeds
Ben York
Chloe Leeds
Dev Bradford
Ella NULL
Farid York
Grace Leeds
Zoë Harrogate
15 of 64 pairs kept
Asha (1, Leeds) with Chloe (3, Leeds): kept, same city.
CustomerSameCityAsCity
1AshaAshaLeeds
2AshaChloeLeeds
3AshaGraceLeeds
4BenBenYork
5BenFaridYork
6ChloeAshaLeeds
7ChloeChloeLeeds
8ChloeGraceLeeds
9DevDevBradford
10FaridBenYork
11FaridFaridYork
12GraceAshaLeeds
13GraceChloeLeeds
14GraceGraceLeeds
15ZoëZoëHarrogate
Same city keeps the diagonal: every customer is paired with themself. Ella has no city, and NULL = NULL is not true, so her row and column stay empty.

Comparing rows of one table

Which customers live in the same city? Take Customers twice, as c1 and c2, and match on City. Without more conditions you also get everyone paired with themself, and every pair twice. c2.CustomerId > c1.CustomerId keeps each pair once:

Query.sql
SELECT c1.FirstName AS Customer, c2.FirstName AS SameCityAs, c1.CityFROM dbo.Customers AS c1JOIN dbo.Customers AS c2    ON c2.City = c1.City   AND c2.CustomerId > c1.CustomerIdORDER BY c1.City, c1.CustomerId, c2.CustomerId;
Output
Customer  SameCityAs  City
--------  ----------  -----
Asha      Chloe       Leeds
Asha      Grace       Leeds
Chloe     Grace       Leeds
Ben       Farid       York

The same pattern finds products that share a price:

Query.sql
SELECT a.Name, b.Name AS SamePriceAs, a.PriceFROM dbo.Products AS aJOIN dbo.Products AS b    ON b.Price = a.Price   AND b.ProductId > a.ProductId;
Output
Name              SamePriceAs     Price
----------------  --------------  -----
Timber 2x4 2.4 m  Safety goggles   6.80

Hierarchies: a foreign key to the same table

Staff have managers, and managers are staff. So the Staff table gets a ManagerId column that is a foreign key to its own primary key. The owner has no manager, so it allows NULL:

Query.sql
CREATE TABLE dbo.Staff (    StaffId   int          NOT NULL PRIMARY KEY,    Name      nvarchar(40) NOT NULL,    JobTitle  nvarchar(40) NOT NULL,    ManagerId int          NULL REFERENCES dbo.Staff (StaffId));INSERT INTO dbo.Staff (StaffId, Name, JobTitle, ManagerId) VALUES    (1, N'Mira',  N'Owner',           NULL),    (2, N'Tom',   N'Store manager',   1),    (3, N'Priya', N'Yard supervisor', 2),    (4, N'Sam',   N'Sales assistant', 2),    (5, N'Leo',   N'Yard hand',       3),    (6, N'Nina',  N'Bookkeeper',      1); SELECT s.Name AS Employee, s.JobTitle, m.Name AS ManagerFROM dbo.Staff AS sLEFT JOIN dbo.Staff AS m ON m.StaffId = s.ManagerIdORDER BY s.StaffId;
Output
Employee  JobTitle         Manager
--------  ---------------  -------
Mira      Owner            NULL
Tom       Store manager    Mira
Priya     Yard supervisor  Tom
Sam       Sales assistant  Tom
Leo       Yard hand        Priya
Nina      Bookkeeper       Mira

s is the employee and m is the same table playing the manager. It is a LEFT JOIN so that Mira, who has no manager, is not lost. Each extra level up needs one more join; for a whole tree of any depth there are recursive CTEs, in the next module.

CROSS JOIN: every combination

Sometimes you really do want every combination. A quantity price list for building materials pairs each product with each quantity. The quantities do not live in a table, so a VALUES list in FROM makes a small table on the spot:

Query.sql
SELECT p.Name, q.Qty, p.Price * q.Qty AS TotalFROM dbo.Products AS pCROSS JOIN (VALUES (1), (10), (50)) AS q (Qty)WHERE p.CategoryId = 2ORDER BY p.Name, q.Qty;
Output
Name          Qty   Total
------------  ---  ------
Cement 25 kg    1    7.20
Cement 25 kg   10   72.00
Cement 25 kg   50  360.00
Sand 25 kg      1    4.10
Sand 25 kg     10   41.00
Sand 25 kg     50  205.00

A CROSS JOIN of m rows with n rows returns m × n rows. That is useful for building calendars, grids, and “every store × every product” checklists. It is also what a forgotten join condition produces, which is why the row count is worth a glance whenever a join result looks too big.

Quick check

Customers has 8 rows and Categories has 7. How many rows does Customers CROSS JOIN Categories return?

Quick check

In a self join listing pairs, why use c2.CustomerId > c1.CustomerId rather than <>?

Practice

A builder wants two different products from the same category that cost less than 20 together. List each such pair once, with both names and the combined price.

Show a hint

Join Products to itself on CategoryId, keep each pair once with b.ProductId > a.ProductId, and filter on a.Price + b.Price < 20.

Show the solution
Solution.sql
SELECT a.Name, b.Name AS AndAlso, a.Price + b.Price AS TogetherFROM dbo.Products AS aJOIN dbo.Products AS b    ON b.CategoryId = a.CategoryId   AND b.ProductId > a.ProductIdWHERE a.Price + b.Price < 20;
Output
Name               AndAlso           Together
-----------------  ----------------  --------
Cement 25 kg       Sand 25 kg           11.30
Wood screws (200)  Wall plugs (100)     16.40

Remember

  • A self join uses one table twice under two aliases.
  • To list pairs once, require one key to be greater than the other.
  • Hierarchies use a foreign key to the same table; LEFT JOIN keeps the top of the tree.
  • CROSS JOIN returns every combination: m × n rows.
  • A VALUES list in FROM makes a small table inside the query.
0 of 54 lessons completed