INNER JOIN: matching rows across tables
Combine rows from two or more tables where a condition matches: join syntax, table aliases, joining four tables at once, ambiguous columns, and why a wrong ON condition fails silently.
INNER JOIN returns one row for every pair of rows, one from each table, for which the ON condition is true. Rows that find no partner are left out.
SELECT o.OrderId, o.OrderDate, c.FirstName, c.LastNameFROM dbo.Orders AS oINNER JOIN dbo.Customers AS c ON c.CustomerId = o.CustomerIdORDER BY o.OrderId;OrderId OrderDate FirstName LastName
------- ---------- --------- --------
1 2024-02-03 Asha Patel
2 2024-02-10 Ben Carter
3 2024-03-01 Asha Patel
4 2024-03-15 Dev Singh
5 2024-04-02 Farid Khan
6 2024-04-20 Chloe Evans
7 2024-05-05 Dev Singh
8 2024-05-18 Zoë Müller
9 2024-06-01 Ben Carter
10 2024-06-12 Farid Khanoandcare table aliases. They are short names for the tables in this query, ando.OrderDatemeans “OrderDate from Orders”.ONsays which rows belong together, almost always a foreign key = the primary key it points at.INNERis optional: a plainJOINis an inner join.
Ambiguous column names
SELECT OrderId, CustomerId, FirstNameFROM dbo.Orders AS oJOIN dbo.Customers AS c ON c.CustomerId = o.CustomerId;Msg 209, Level 16, State 1, Line 1 Ambiguous column name 'CustomerId'.
Joining more than two tables
JOIN brings in one more table, with its own ON condition linking it to a table already in the query. This follows the path order → customer, order → lines → products, and works out each line’s total:
SELECT o.OrderId, c.FirstName, p.Name AS Product, oi.Quantity, oi.UnitPrice, oi.Quantity * oi.UnitPrice AS LineTotalFROM dbo.Orders AS oJOIN dbo.Customers AS c ON c.CustomerId = o.CustomerIdJOIN dbo.OrderItems AS oi ON oi.OrderId = o.OrderIdJOIN dbo.Products AS p ON p.ProductId = oi.ProductIdWHERE o.OrderId <= 3ORDER BY o.OrderId, p.Name;OrderId FirstName Product Quantity UnitPrice LineTotal
------- --------- ----------------- -------- --------- ---------
1 Asha Claw hammer 1 18.50 18.50
1 Asha Tape measure 8 m 2 9.75 19.50
2 Ben Cement 25 kg 10 7.20 72.00
2 Ben Sand 25 kg 10 4.10 41.00
3 Asha Cordless drill 1 89.00 89.00
3 Asha Wood screws (200) 2 11.90 23.80SELECT c.FirstName, c.City, p.Name AS Product, o.OrderDateFROM dbo.OrderItems AS oiJOIN dbo.Products AS p ON p.ProductId = oi.ProductIdJOIN dbo.Categories AS cat ON cat.CategoryId = p.CategoryIdJOIN dbo.Orders AS o ON o.OrderId = oi.OrderIdJOIN dbo.Customers AS c ON c.CustomerId = o.CustomerIdWHERE cat.Name = N'Tools'ORDER BY o.OrderDate;FirstName City Product OrderDate --------- --------- ---------------- ---------- Asha Leeds Claw hammer 2024-02-03 Asha Leeds Tape measure 8 m 2024-02-03 Asha Leeds Cordless drill 2024-03-01 Zoë Harrogate Claw hammer 2024-05-18 Farid York Tape measure 8 m 2024-06-12
ON can test more than keys
SELECT oi.OrderId, p.Name, p.Price AS ListPrice, oi.UnitPrice AS PricePaidFROM dbo.OrderItems AS oiJOIN dbo.Products AS p ON p.ProductId = oi.ProductId AND oi.UnitPrice < p.Price;OrderId Name ListPrice PricePaid
------- ------------ --------- ---------
7 Cement 25 kg 7.20 7.00The old comma syntax
SELECT o.OrderId, c.FirstNameFROM dbo.Orders AS o, dbo.Customers AS cWHERE c.CustomerId = o.CustomerId AND o.Status = 'Pending';OrderId FirstName
------- ---------
8 Zoë
9 BenJOIN … ON. It keeps each join condition next to its table, separate from the filters, and if you forget a condition you get a syntax error. With commas, a forgotten condition silently pairs every row with every row (a CROSS JOIN, two lessons on).
Show a hint
Show the solution
SELECT o.OrderId, c.FirstName, p.Name AS Product, oi.QuantityFROM dbo.Orders AS oJOIN dbo.Customers AS c ON c.CustomerId = o.CustomerIdJOIN dbo.OrderItems AS oi ON oi.OrderId = o.OrderIdJOIN dbo.Products AS p ON p.ProductId = oi.ProductIdWHERE o.Status = 'Pending'ORDER BY o.OrderId, p.Name;OrderId FirstName Product Quantity
------- --------- ------------------- --------
8 Zoë Claw hammer 1
8 Zoë Plywood sheet 18 mm 2
9 Ben Wall plugs (100) 4Remember
INNER JOIN returns one row for every pair of rows for which ON is true; unmatched rows are dropped. Use table aliases and prefix every column: o.OrderId, c.FirstName. Join a foreign key to the primary key it references; a wrong condition still runs, with wrong results. Add one JOIN … ON per extra table. For inner joins, the order of the tables does not change the result. Prefer JOIN … ON to commas in FROM: a forgotten condition becomes an error instead of a CROSS JOIN.