SQL Lesson 6: JOINs — Combining Tables

📎 This article includes 2 downloadable practice files ↓

⏱ 3 min read

📘 SQL Beginner Course · Lesson 6 of 12

In this article
  1. Keys
  2. INNER JOIN
  3. Calculate across tables
  4. Three tables and a summary
  5. LEFT JOIN: keep everything from the left
  6. Other joins
  7. Where beginners go wrong
  8. Practice

The orders table stores customer_id 4, not “Rao Distributors”. Storing each fact once and linking by id keeps data consistent; JOIN brings it back together when you query. Think of it as VLOOKUP for whole tables at once.

Keys

  • Primary key: the unique id of each row (customers.customer_id).
  • Foreign key: a column pointing to another table’s primary key (orders.customer_id).

INNER JOIN

SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id;

o and c are aliases, short names for the tables. ON says how rows match. JOIN (same as INNER JOIN) keeps only rows that match in both tables.

Calculate across tables

SELECT o.order_id, p.name, o.qty, o.qty * p.price AS value
FROM orders o
JOIN products p ON p.product_id = o.product_id;

Three tables and a summary

SELECT c.region, SUM(o.qty * p.price) AS sales
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN products  p ON p.product_id  = o.product_id
WHERE o.status = 'Delivered'
GROUP BY c.region
ORDER BY sales DESC;

That’s a full sales-by-region report: joins, filter, group, sort.

LEFT JOIN: keep everything from the left

SELECT p.name
FROM products p
LEFT JOIN orders o ON o.product_id = p.product_id
WHERE o.order_id IS NULL;

LEFT JOIN keeps every product, with NULLs where no order matches. Filtering on IS NULL finds products never ordered. In shop.db that’s the UPS.

SELECT c.name, COUNT(o.order_id) AS orders
FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id;

Every customer appears, including any with zero orders. Note COUNT(o.order_id), not COUNT(*), which would count 1 for customers with no orders.

⚠️ If the join key isn’t unique on one side (two rows with the same product_id in a price list), every order matches twice and totals double. When totals look too big, check for duplicate keys.

Other joins

RIGHT JOIN is LEFT JOIN with the tables swapped; FULL OUTER JOIN keeps unmatched rows from both sides (supported in recent SQLite, PostgreSQL, SQL Server). Avoid comma joins without ON; they produce every combination of rows.

Where beginners go wrong

Mistake Symptom
Missing ON condition Millions of rows (cross join)
“ambiguous column name: name” Prefix with the alias: c.name, p.name
INNER JOIN when you need all rows Customers with no orders vanish; use LEFT JOIN
Filtering the right table in WHERE after a LEFT JOIN Turns it back into an inner join; move the condition into ON

Practice

Download shop.db and this lesson’s exercise file below. Open the database in the free DB Browser for SQLite (or sqlite3 shop.db), paste the exercises into the Execute SQL tab and write a query for each task. The expected result is printed under every task; answers are at the bottom of the file.

📎 Practice files for this article

  • 📄
    Practice database (SQLite)shop.db: 12 customers, 10 products and 300 orders (April-September 2026).
    ⬇ DB · 28 KB
  • 🗃️
    Lesson 6 exercisesTasks with the expected result under each, and answers at the bottom.
    ⬇ SQL · 3 KB

Free to use for learning. Files with macros (.bas) are plain text — import them with Alt+F11 → File → Import File, and always test on a copy.

✨ Ask AI about this article

Stuck on a step? Ask a question and the AI answers using this article.

Free · AI can be wrong