SQL Lesson 7: Subqueries

πŸ“Ž This article includes 2 downloadable practice files ↓

⏱ 3 min read

πŸ“˜ SQL Beginner Course Β· Lesson 7 of 12

In this article
  1. Compare with a single value
  2. IN and NOT IN with a list
  3. The row with the maximum
  4. Subquery in FROM (a derived table)
  5. CTE: the readable version
  6. EXISTS
  7. Where beginners go wrong
  8. Practice

Some questions need two steps: β€œproducts priced above the average” first needs the average. A subquery does the first step inside the main query.

Compare with a single value

SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

The inner query runs first and returns one number; the outer query uses it.

IN and NOT IN with a list

-- customers with at least one cancelled order
SELECT name FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders WHERE status = 'Cancelled');

-- customers with no orders in September 2026
SELECT name FROM customers
WHERE customer_id NOT IN (SELECT customer_id FROM orders
                          WHERE order_date BETWEEN '2026-09-01' AND '2026-09-30');
⚠️ If the subquery in NOT IN can return a NULL, NOT IN returns no rows at all. Add WHERE customer_id IS NOT NULL inside it, or use NOT EXISTS.

The row with the maximum

SELECT o.order_id, o.qty * p.price AS value
FROM orders o JOIN products p ON p.product_id = o.product_id
WHERE o.qty * p.price = (SELECT MAX(o2.qty * p2.price)
                         FROM orders o2 JOIN products p2 ON p2.product_id = o2.product_id);

Returns all orders tied for the largest value; ORDER BY … LIMIT 1 would hide ties.

Subquery in FROM (a derived table)

SELECT region, sales
FROM (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)
WHERE sales > 3000000;

CTE: the readable version

A WITH clause names the subquery first, which is easier to read and reuse:

WITH region_sales AS (
  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)
SELECT * FROM region_sales WHERE sales > 3000000;
πŸ’‘ Write the inner query on its own first, check its result, then wrap it. Debugging nested queries all at once is painful.

EXISTS

SELECT name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.qty >= 10);

True as soon as one matching row is found; safe with NULLs.

Where beginners go wrong

Error Cause
“subquery returns more than one row” Used = where IN is needed
NOT IN returns nothing NULL in the subquery result
Unreadable nesting Use a CTE

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 7 exercisesTasks with the expected result under each, and answers at the bottom.
    ⬇ SQL Β· 2 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