Joining Multiple Tables
Progress Tracking
Log in to save this lesson and continue from where you left off.
SELECT
e.first_name,
e.last_name,
p.title AS project_title,
p.budget
FROM linkedin_employees e
JOIN linkedin_emp_projects ep
ON e.id = ep.emp_id
JOIN linkedin_projects p
ON ep.project_id = p.id;Plan Before You Write
When facing a complex request, don’t start typing immediately. Sketch out the path first:
- What data is needed? (columns from which tables)
- How do the tables connect? (foreign key relationships)
- What’s the logical path? (A → B → C → D)
Drawing boxes with arrows on paper sounds old-school, but it catches mistakes before they’re written.
Each JOIN must connect to something already in your query. You can’t jump from customers directly to categories if they’re only linked through orders and products.
Join Order: When It Matters
For INNER JOINs, the order doesn’t matter. The optimizer rearranges them for performance. But OUTER JOINs are different: the order affects which rows survive.
-- All customers, their orders (if any), returns on those orders (if any)
SELECT
c.name,
o.id AS order_id,
r.reason
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
LEFT JOIN returns r
ON o.id = r.order_id;
-- Customer with no orders: NULLs for both order and return
-- Customer with order but no return: order data, NULL return
-- Customer with order and return: all dataEach LEFT JOIN preserves rows from the previous result. Customers without orders still appear. Orders without returns still appear.
When mixing INNER and OUTER joins, think carefully about which rows are preserved at each step. An INNER JOIN in the middle can eliminate rows you wanted to keep.
Mixing Join Types
Different JOIN types can be mixed in the same query.
-- Only customers with orders, but show all returns for those orders
SELECT
c.name,
o.id,
r.return_reason
FROM customers c
INNER JOIN orders o
ON c.id = o.customer_id
LEFT JOIN returns r
ON o.id = r.order_id;The INNER JOIN filters to customers with orders. The LEFT JOIN then preserves all those orders, even ones without returns. If you changed INNER JOIN to LEFT JOIN, customers without any orders would also appear.
The Row Multiplication Problem
Here’s a trap that gets everyone at least once. You join tables, your aggregates are wrong, and you spend an hour figuring out why.
The culprit: many-to-many relationships multiplying rows. Say you join customers to orders (fine so far), then join orders to a tags table where each order can have multiple tags. Suddenly each order appears three times — once per tag — and your SUM(revenue) is 3x too high.
If an order can have multiple tags, categories, or other attributes, joining that table multiplies your fact rows. Your SUM() will be wrong.
How to catch this — check row counts at each step to find where the explosion happens:
| id | first_name | last_name | city | address | phone_number |
|---|---|---|---|---|---|
| 8 | John | Joseph | San Francisco | 928-386-8164 | |
| 7 | Jill | Michael | Austin | 813-297-0692 | |
| 4 | William | Daniel | Denver | 813-368-1200 | |
| 5 | Henry | Jackson | Miami | 808-601-7513 | |
| 13 | Emma | Isaac | Miami | 808-690-5201 |
| id | cust_id | order_date | order_details | total_order_cost |
|---|---|---|---|---|
| 1 | 3 | 2019-03-04 | Coat | 100 |
| 2 | 3 | 2019-03-01 | Shoes | 80 |
| 3 | 3 | 2019-03-07 | Skirt | 30 |
| 4 | 7 | 2019-02-01 | Coat | 25 |
| 5 | 7 | 2019-03-10 | Shoes | 80 |
Write a query using UNION ALL to check the row count of each source table, then compare with the JOIN result.
Common Mistakes
- Wrong join path — you can’t join products directly to customers if they’re only connected through orders. Follow the foreign key chain.
- Forgetting that dimension tables can have duplicates — if your
customerstable has multiple rows per customer (slowly changing dimensions, data quality issues), every join against that table multiplies your rows. Always check for duplicates withGROUP BY id HAVING COUNT(*) > 1before trusting aggregate results. - Mixing
OUTERjoins without thinking through preservation —LEFT JOINthenINNER JOINoften defeats theLEFT JOIN. Be intentional. - Illegible queries without aliases — with 5+ tables, aliases aren’t optional. Pick meaningful ones and use them consistently.
Salary Less Than Twice The Average
Join three tables to compare manager salaries with their team averages.
| empl_id | manager_empl_id |
|---|---|
| E849 | |
| E850 | E849 |
| E851 | E849 |
| E852 | E850 |
| E853 | E850 |
| empl_id | empl_name | empl_city | empl_dob | empl_pin | salary |
|---|---|---|---|---|---|
| E849 | Steven M. Jones | Hicksville | 1988-03-29 | 1490 | 80000 |
| E850 | Marilynn M. Walters | New York | 1978-12-26 | 9624 | 30000 |
| E851 | Kyle M. Massey | Lake Katrine | 1977-09-22 | 1563 | 40000 |
| E852 | Cody A. Mosby | Anaheim | 1965-03-18 | 4883 | 22000 |
| E853 | David J. Mintz | Houston | 1977-01-04 | 8001 | 18000 |
Write a query to get the list of managers whose salary is less than twice the average salary of employees reporting to them. For these managers, output their ID, salary and the average salary of employees reporting to them.
Key Takeaways
- Chain
JOINs to connect any number of tables - Plan your path before writing — sketch the table relationships
JOINorder matters forOUTERjoins, not forINNERjoins- Watch for row multiplication from many-to-many relationships
- Check row counts at each step when debugging
What’s Next
Congratulations — you’ve completed Module 3. You can now combine data from any number of tables using UNION and all types of JOINs. These skills show up in almost every real-world SQL task. In the next module, you’ll learn subqueries and CTEs, which let you build complex logic step by step.