<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 19 of 54 16 minIntermediate

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.

A join puts rows from two tables side by side. 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.

Query.sql
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;
Output
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      Khan
  • o and c are table aliases. They are short names for the tables in this query, and o.OrderDate means “OrderDate from Orders”.
  • ON says which rows belong together, almost always a foreign key = the primary key it points at.
  • INNER is optional: a plain JOIN is an inner join.
Interactive

INNER JOIN: a line for every match

Join.sql
SELECT c.CustomerId, c.FirstName, o.OrderId, o.CustomerIdFROM dbo.Customers AS cINNER JOIN dbo.Orders AS o    ON o.CustomerId = c.CustomerId;
Customers c
Orders o
1cust 1
2cust 2
3cust 1
4cust 4
5cust 6
6cust 3
7cust 4
8cust 8
9cust 2
10cust 6
matchednot in the result
CustomerIdFirstNameOrderIdCustomerId
11Asha11
21Asha31
32Ben22
42Ben92
53Chloe63
64Dev44
74Dev74
86Farid56
96Farid106
108Zoë88
10 rows: every order found its customer. Ella (5) and Grace (7) have no orders, so an INNER JOIN leaves them out. Asha, Ben, Dev and Farid appear twice, once per order: the “one” side is repeated for each match on the “many” side. Click a customer to see their lines.

A customer with two orders appears twice, once paired with each order. A customer with no orders (Ella, Grace) does not appear at all. That is what “inner” means: only matched rows. The next lesson keeps the unmatched ones.

Ambiguous column names

Both tables have a CustomerId column. If you name it without saying which table, SQL Server refuses:

Query.sql
SELECT OrderId, CustomerId, FirstNameFROM dbo.Orders AS oJOIN dbo.Customers AS c ON c.CustomerId = o.CustomerId;
Output
Msg 209, Level 16, State 1, Line 1
Ambiguous column name 'CustomerId'.

Joining more than two tables

Each extra 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:

Query.sql
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;
Output
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.80

The order of the tables in the FROM clause does not change the result of inner joins. Start wherever reads naturally. Here the question is about products, so the query starts from the order lines and fetches the rest:

Query.sql
SELECT 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;
Output
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

The ON condition can be any true-or-false test. This one pairs each order line with its product only when the price paid was lower than the list price, which finds discounts:

Query.sql
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;
Output
OrderId  Name          ListPrice  PricePaid
-------  ------------  ---------  ---------
      7  Cement 25 kg       7.20       7.00

The old comma syntax

Older code lists the tables with commas and puts the join condition in WHERE. It gives the same result:

Query.sql
SELECT o.OrderId, c.FirstNameFROM dbo.Orders AS o, dbo.Customers AS cWHERE c.CustomerId = o.CustomerId  AND o.Status = 'Pending';
Output
OrderId  FirstName
-------  ---------
      8  Zoë
      9  Ben

Prefer JOIN … 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).

Quick check

Customers has 8 rows and Orders has 10, and every order has a valid CustomerId. How many rows does Customers JOIN Orders ON o.CustomerId = c.CustomerId return?

Quick check

Why does SELECT CustomerId FROM dbo.Orders o JOIN dbo.Customers c ON … fail?

Practice

List the lines of all Pending orders: the order number, the customer’s first name, the product name and the quantity, sorted by order number and then product name.

Show a hint

Join Orders, Customers, OrderItems and Products, then filter on o.Status.

Show the solution
Solution.sql
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;
Output
OrderId  FirstName  Product              Quantity
-------  ---------  -------------------  --------
      8  Zoë        Claw hammer                 1
      8  Zoë        Plywood sheet 18 mm         2
      9  Ben        Wall plugs (100)            4

Remember

  • 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.
0 of 54 lessons completed