Module 3: Working with Multiple Tables35 min

INNER JOIN Fundamentals

Progress Tracking

Log in to save this lesson and continue from where you left off.

Log in

Where the Real Work Begins

If you’ve been working with single tables, you’ve only seen a fraction of what SQL can do. In practice, almost every meaningful question requires pulling data from multiple tables:

  • “Show me orders with customer names.”
  • “Get products with their category labels.”
  • “Find employees with their department info.”

All of these need JOINs.

INNER JOIN is the workhorse. You’ll use it constantly. It connects rows from two tables where a condition is met, and drops everything that doesn’t match.

The Basic Pattern

Here’s the syntax you’ll write hundreds of times:

SQL
SELECT columns
FROM table1
INNER JOIN table2
  ON table1.column = table2.column;

The ON clause is where you specify how the tables connect. In real databases, this is almost always a foreign key pointing to a primary key — exactly the relationships covered in Lesson 3.1.

A Realistic Example

Say your project manager asks: “Can you pull a list of orders with customer details? We need to send shipping notifications.”

Table: customers
idfirst_namelast_namecityaddressphone_number
8JohnJosephSan Francisco928-386-8164
7JillMichaelAustin813-297-0692
4WilliamDanielDenver813-368-1200
5HenryJacksonMiami808-601-7513
13EmmaIsaacMiami808-690-5201
Table: orders
idcust_idorder_dateorder_detailstotal_order_cost
132019-03-04Coat100
232019-03-01Shoes80
332019-03-07Skirt30
472019-02-01Coat25
572019-03-10Shoes80
1
Try INNER JOIN

Join orders to customers using cust_id. Select the order id, order_date, customer first_name, and city.

Tables: orders, customers

Orders live in one table, customer details in another. The JOIN brings them together. Only orders with valid customers show up.

INNER JOIN vs JOIN

You’ll see both INNER JOIN and just JOIN in the wild. They’re identical. INNER JOIN is preferred because it’s explicit about what’s happening, but your team might have a style preference.

Always Use Table Aliases

Notice the AS o and AS c? Those aliases aren’t optional in practice. When you’re joining 3, 4, 5 tables, you need short names or your query becomes unreadable.

SQL
-- This gets painful fast:
SELECT
  orders.id,
  customers.first_name,
  products.name
FROM orders
INNER JOIN customers
  ON orders.cust_id = customers.id
INNER JOIN products
  ON orders.product_id = products.id;

-- Much better:
SELECT
  o.id,
  c.first_name,
  p.name
FROM orders o
INNER JOIN customers c
  ON o.cust_id = c.id
INNER JOIN products p
  ON o.product_id = p.id;
Meaningful aliases

Pick meaningful aliases. ‘o’ for orders, ‘c’ for customers, ‘p’ for products.

What INNER JOIN Actually Does

Here’s the mental model: INNER JOIN finds matching pairs. If a row from the left table has no match in the right table, it’s gone. Same in reverse.

This matters. A lot. If you have orders that point to deleted customers (bad data happens), those orders won’t appear. If you have customers who never ordered, they won’t show up either.

Total Cost Of Orders

Table: customers
idfirst_namelast_namecityaddressphone_number
8JohnJosephSan Francisco928-386-8164
7JillMichaelAustin813-297-0692
4WilliamDanielDenver813-368-1200
5HenryJacksonMiami808-601-7513
13EmmaIsaacMiami808-690-5201
Table: orders
idcust_idorder_dateorder_detailstotal_order_cost
132019-03-04Coat100
232019-03-01Shoes80
332019-03-07Skirt30
472019-02-01Coat25
572019-03-10Shoes80
2
Total Cost Of Orders
View solution

Find the total cost of each customer's orders. Output customer's id, first name, and the total order cost. Order records by customer's first name alphabetically.

Tables: customers, orders

Qualifying Column Names

When both tables have a column called id or name or created_at, you have to tell SQL which one you mean:

SQL
-- SQL doesn’t know which 'id' you want:
SELECT id  -- Error: ambiguous column name
FROM orders
JOIN customers ON ...

-- Be explicit:
SELECT o.id
FROM orders o
JOIN customers c ON ...
Qualify even unique columns

Even when a column name is unique, qualifying it anyway is good practice. It makes the query self-documenting. When someone reads o.order_date, they know exactly where that column lives.

Adding WHERE Clauses

JOINs and WHERE work together. The JOIN connects tables, then WHERE filters the combined result.

3
Filter Joined Results

Add a WHERE clause to filter to orders over 100 from customers in the US.

Tables: orders, customers

Conceptually, first the tables get joined, then the combined rows get filtered. (The query optimizer might reorder things for performance, but the logical result is the same.)

Order Details

Table: customers
idfirst_namelast_namecityaddressphone_number
8JohnJosephSan Francisco928-386-8164
7JillMichaelAustin813-297-0692
4WilliamDanielDenver813-368-1200
5HenryJacksonMiami808-601-7513
13EmmaIsaacMiami808-690-5201
Table: orders
idcust_idorder_dateorder_detailstotal_order_cost
132019-03-04Coat100
232019-03-01Shoes80
332019-03-07Skirt30
472019-02-01Coat25
572019-03-10Shoes80
4
Order Details
View solution

Find order details made by Jill and Eva. Consider the Jill and Eva as first names of customers. Output the order date, details and cost along with the first name. Order records based on the customer id in ascending order.

Tables: customers, orders

JOINs with Aggregation: The Trap Everyone Falls Into

JOINs work with GROUP BY, but there’s a classic bug that shows up constantly in code reviews:

Never GROUP BY names alone

Different people can share the same name. You’ll merge their data and get wrong numbers.

SQL
-- THE BUG: Two customers named 'John' get merged
SELECT
  c.first_name,
  COUNT(o.id) AS order_count
FROM customers c
INNER JOIN orders o
  ON c.id = o.cust_id
GROUP BY c.first_name;
-- If you have 3 Johns, this counts all their orders together!

-- THE FIX: Always group by the unique key
SELECT
  c.id,
  c.first_name,
  COUNT(o.id) AS order_count,
  SUM(o.total_order_cost) AS total_spent
FROM customers c
INNER JOIN orders o
  ON c.id = o.cust_id
GROUP BY c.id, c.first_name;
What are you counting?

When aggregating joined data, ask: “What am I counting?” COUNT(o.id) counts orders. COUNT(c.id) counts customers (but would give wrong results here since each customer appears multiple times with multiple orders).

Matching Similar Hosts and Guests

Table: airbnb_hosts
host_idnationalitygenderage
0USAM28
1USAF29
2ChinaF31
3ChinaM24
4MaliM30
Table: airbnb_guests
guest_idnationalitygenderage
0MaliM21
1ChinaF23
2MaliF27
3AustraliaF24
4LuxembourgM19
5
Matching Similar Hosts and Guests
View solution

Find matching hosts and guests pairs in a way that they are both of the same gender and nationality. Output the host id and the guest id of matched pair.

Tables: airbnb_hosts, airbnb_guests

Mistakes That Show Up All the Time

  • Forgetting the ON clause — INNER JOIN requires ON. You’ll get a syntax error, not a cross join.
  • Joining on the wrong columns — just because two columns have similar names doesn’t mean they should be joined. Look for foreign key → primary key connections.
  • Being surprised when rows disappear — INNER JOIN only returns matches. If you need to keep rows without a match, you want LEFT JOIN (next lesson).
  • Grouping by non-unique fields — include the primary key (usually id) in your GROUP BY. Names and emails can have duplicates in messy real-world data.

Key Takeaways

  • INNER JOIN returns only rows that match in both tables
  • ON specifies the connection (usually foreign key = primary key)
  • Always use table aliases, and make them meaningful
  • Qualify column names, even when not strictly required
  • When aggregating, GROUP BY unique keys (id), never just names

What’s Next

INNER JOIN works great when you only want matching rows. But what about the question: “Show me all customers, including those who haven’t ordered yet”? That’s where LEFT JOIN comes in, and it’s probably the most important join type for analytics work.