Where this session fits
By now you can filter, aggregate, and join. But some questions need an answer to compute the answer - "which products cost more than average?" needs the average first, then the comparison. That is a subquery: a query nested inside another. And when you stack several such steps, a CTE (Common Table Expression, written with WITH) lets you name each step so the whole thing stays readable. Both run against the same six Daybreak tables you already know.
WHERE, the IN (SELECT ...) pattern for "matches a list from another table", and the WITH habit that turns a scary nested query into a clean staircase of named steps - the single biggest readability upgrade in SQL.
Scalar subquery: a query inside a query 7 min live
A scalar subquery is a query that returns exactly one value - one number, one date, one name - which you then use anywhere a single value is allowed. The classic case: compare each row against an aggregate of the whole table, like "price above the average price". SQLite runs the inner query first, gets one number, then runs the outer query using it.
LiveProducts priced above the average4 min▶
You cannot write WHERE price > AVG(price) directly - an aggregate is not allowed in WHERE. So you compute the average in a subquery, wrapped in parentheses, and compare against that single value. Run it, then change > to < to flip to below-average.
SELECT name, price FROM products WHERE price > (SELECT AVG(price) FROM products) ORDER BY price DESC;
Self-studySubqueries can go in SELECT too2 min read▶
A scalar subquery is legal anywhere a single value is: in WHERE, in SELECT, even in HAVING. Putting one in the SELECT list lets you show a per-row value next to a whole-table figure - handy for "this price vs the average" side by side.
SELECT name, price,
(SELECT ROUND(AVG(price),2) FROM products) AS avg_price
FROM products
ORDER BY price DESC;
Every row shows the same avg_price - because the subquery is computed once over the whole table, not per row.
Subqueries with IN: matching a list 6 min live
The other everyday shape: a subquery that returns many values, which you match against with IN. Instead of "is this price above one number", it is "is this customer in the set of customers who ever had a refund". The inner query builds the list; the outer query keeps rows whose value appears in it.
LiveCustomers who have ever been refunded4 min▶
The inner query collects every customer_id that appears on a refunded order. The outer query then lists the customers whose id is IN that set. No join needed - a subquery reads cleanly when you only want to filter, not to pull columns from the other table.
SELECT name FROM customers WHERE customer_id IN (SELECT customer_id FROM orders WHERE status='refunded');
The "has ever done X" question. Support and growth teams ask this constantly: customers who ever churned, ever opened a ticket, ever bought equipment. The IN (SELECT ...) pattern answers all of them with the same shape - build the id list in the subquery, filter the main table against it. Swap the inner table and condition, keep the skeleton.
Self-studyNOT IN for the opposite set2 min read▶
Flip IN to NOT IN and you get the complement - here, customers who have never been refunded. It is the fastest way to ask "who is missing from this list?" without a join.
SELECT name FROM customers WHERE customer_id NOT IN (SELECT customer_id FROM orders WHERE status='refunded') ORDER BY name;
One caution for later warehouses: if the inner list can contain NULL, NOT IN behaves oddly. Daybreak's ids are never null, so it is safe here - but keep it in mind when you move to production data.
CTEs with WITH: naming your steps 8 min live
Subqueries are powerful, but stack a few and they nest inward - you read a big query from the deepest parentheses outward, which is exactly backwards from how you think. A CTE fixes this. WITH name AS (query) lets you name an intermediate result, then use that name like a table in the query below. The whole thing reads top-to-bottom.
LiveTop 5 customers by spend, with a CTE4 min▶
Here WITH spend AS (...) computes each customer's completed revenue once and names it spend. The query below then joins that named result to customers for readable names. Run it - you should see Noah Park on top around 232.
WITH spend AS ( SELECT o.customer_id, SUM(oi.quantity*oi.unit_price) AS total FROM orders o JOIN order_items oi ON o.order_id = oi.order_id WHERE o.status='completed' GROUP BY o.customer_id ) SELECT c.name, ROUND(s.total,2) AS total FROM spend s JOIN customers c ON c.customer_id = s.customer_id ORDER BY s.total DESC LIMIT 5;
WITH spend AS (...) as "let spend mean the result of this query". Everything after can use spend as if it were a real table. You can chain several with commas - each can reference the ones above it.
Self-studyWhy CTEs beat repeating a subquery2 min read▶
Beyond readability, a CTE lets you reference the same intermediate result more than once without copy-pasting the subquery. Name it once, use it twice. It also makes debugging easy: comment out the final SELECT, run just the CTE by itself, and check the intermediate rows.
- Readable: steps have names, so the query reads like prose.
- Reusable: reference the named result multiple times, no duplication.
- Debuggable: test each step in isolation before wiring them together.
Rewrite a nested subquery as a clean CTE ★ 16 min · everyone builds
Same answer as Part 1 - products priced above the average - but let us feel the difference between the nested form and the CTE form. You will see the identical result, and you will read the second one far more easily. This is the habit that pays off every day you write SQL.
Start from the nested version: the average lives inside the WHERE as a subquery. It works, but the "what is the average" step is buried in the condition.
Pull that step out: write WITH avg_p AS (SELECT AVG(price) AS a FROM products) to give the average a name.
Reference the named step: the main query joins nothing - it just compares price to avg_p.a, read from a clearly named source.
Read both out loud. The CTE version says "let avg_p be the average price; now show products above it" - top-to-bottom, no backwards nesting.
WITH avg_p AS ( SELECT AVG(price) AS a FROM products ) SELECT p.name, p.price FROM products p, avg_p WHERE p.price > avg_p.a ORDER BY p.price DESC;
The senior-analyst tell. When a reviewer opens a 40-line query, the first thing they judge is whether they can follow it. A wall of nested parentheses gets a "please refactor"; a stack of named CTE steps gets approved. Same result, very different reception - CTEs are how you write SQL that other people trust and reuse.
Try it yourself - this week ◐ 20-30 min total
- Write a scalar subquery that lists customers who signed up before the earliest order date. (Hint:
(SELECT MIN(order_date) FROM orders).) - Use
INto find every customer who has an active subscription. (Hint: the inner query pullscustomer_idfromsubscriptions WHERE cancel_date IS NULL.) - Take any two-step query you wrote in b3-b5 and rewrite it with a
WITHCTE. Read both versions - notice which one you would rather hand to a teammate. - Chain two CTEs: one that sums spend per customer, a second that keeps only those above 100, then select their names.
- Bring one question that needs "the value above/below an aggregate" to session b7 - window functions will let you compute per-group ranks without a subquery at all.
Official sources covered
This session teaches the subquery and CTE core of the major free SQL curricula, run on live Daybreak data instead of screenshots. Graded problem sets and certificates stay on the official sites (linked below). This page covers:
Three questions before you go 🎯 ◐ 90 seconds
1 · What does a scalar subquery return?
A scalar subquery returns exactly one value, which you can drop anywhere a single value is allowed - like the right side of a WHERE comparison.
2 · Why reach for a CTE (WITH) instead of nesting subqueries?
CTEs give intermediate results names, so a complex query reads like a recipe from the top down rather than from the deepest parentheses outward.
3 · What does WHERE price > (SELECT AVG(price) FROM products) do?
The subquery computes the average once, then the outer query keeps every product whose price is greater than that single number.