What you'll practice: the logical order in which SQL clauses actually execute, and why it matters. You write a query in the order
SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, but the database evaluates it in a completely different order:FROM -> pick your tables / join them WHERE -> filter raw rows GROUP BY -> collapse rows into groups HAVING -> filter groups SELECT -> pick (and compute) the columns to return ORDER BY -> sort the result LIMIT -> take the top N rowsUnderstanding this order explains every "weird" SQL error you'll hit for the rest of your career.
- Recite the logical order of SQL clause execution
- Understand why you can't reference a
SELECTalias inWHERE - Understand why
HAVINGcan reference aggregates butWHEREcan't - Understand why
ORDER BYcan reference aSELECTalias - Reason about what a query returns by mentally executing the clauses in order
psql -U postgres -d sql_exercise \
-f 02-sql-fundamentals/08-order-of-sql/schema.sql
psql -U postgres -d sql_exercise \
-f 02-sql-fundamentals/08-order-of-sql/seed.sqlSET search_path TO sqlf_order;
SELECT
(SELECT COUNT(*) FROM customer) AS customers,
(SELECT COUNT(*) FROM product) AS products,
(SELECT COUNT(*) FROM orders) AS orders;
-- Expected: 15, 15, 50Return the sum of orders.total across every row. Alias it total_revenue.
Hint: SELECT SUM(total) AS total_revenue FROM orders;
Return the total revenue from orders placed in 2024.
Hint: WHERE order_date >= '2024-01-01' then SUM(total).
Question: think about the order of execution. WHERE runs before the SUM, so it's filtering rows, not the already-computed total.
For each customer, return the customer_id and the total they've spent across all orders. Sort by total descending.
Hint: GROUP BY customer_id.
Try this query:
SELECT
customer_id,
SUM(total) AS spent
FROM orders
WHERE spent > 500 -- <-- this will fail
GROUP BY customer_id;Question: what error do you get? Why? Explain it in terms of the logical order of execution.
Now fix it two ways:
- Move the filter to
HAVING. - Keep the logic in
WHEREby avoiding the alias entirely (which isn't possible here — why?).
Return every customer who has spent more than $1,000 total. Show customer_id, total_spent.
Hint: SUM(total) in SELECT, HAVING SUM(total) > 1000.
Return every customer whose 2024 orders total more than $500. Show customer_id and spent_2024.
Hint: WHERE order_date >= '2024-01-01' ... GROUP BY customer_id HAVING SUM(total) > 500.
Question: what would happen if you put the date filter in HAVING instead of WHERE? Would the result be the same? Would the query be slower?
Return each customer's customer_id and their total spending aliased spent, then sort by spent descending.
Hint: ORDER BY spent DESC (works because ORDER BY runs after SELECT).
Return the top 5 customers (by total spend) who live in either 'USA' or 'UK'. Show the customer's full name (concatenated), their country, and their total spend.
Hint: you'll need to use every clause:
FROM customer c JOIN orders o ON o.customer_id = c.idWHERE c.country IN ('USA', 'UK')GROUP BY c.id, c.first_name, c.last_name, c.countryORDER BY total_spent DESCLIMIT 5
Return each product.category with the total revenue from orders of products in that category. Sort descending.
Hint: orders → join product → GROUP BY product.category → SUM(orders.total).
Try this query:
SELECT customer_id, SUM(total)
FROM orders
WHERE SUM(total) > 500 -- <-- this will fail
GROUP BY customer_id;Question: what error do you get? Explain it using the clause execution order.
Return the top 3 (category, year) combinations by total revenue. Show the category, the year (extracted from order_date), and the total revenue. Sort by total revenue descending.
Hint: EXTRACT(YEAR FROM order_date) gives you the year as a number. Then GROUP BY both category and year, and LIMIT 3.
Return the first_name and last_name of every customer who has never ordered anything from the 'Books' category.
Hint: one way:
SELECT first_name, last_name
FROM customer
WHERE id NOT IN (
SELECT o.customer_id
FROM orders o
JOIN product p ON p.id = o.product_id
WHERE p.category = 'Books'
);Think about what order this query runs in: the subquery is evaluated first (just like a FROM/WHERE block), then its result is used by the outer WHERE.
Return each month of 2023 with the total revenue for that month. Columns: month (as a date truncated to month) and revenue. Sort by month.
Hint: DATE_TRUNC('month', order_date) in PostgreSQL.
Return the first_name, last_name, and order count of every customer who has placed more than 3 orders.
Hint: JOIN, GROUP BY, HAVING COUNT(*) > 3. Then reason: which part of the query runs where?
Without running it, describe — in order — what each clause of the following query does:
SELECT c.country, SUM(o.total) AS revenue
FROM customer c
JOIN orders o ON o.customer_id = c.id
WHERE o.order_date >= '2023-01-01'
GROUP BY c.country
HAVING SUM(o.total) > 1000
ORDER BY revenue DESC
LIMIT 3;Write your answer as a 7-step trace: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. Then run the query to check.
Read the official Postgres docs section on logical query processing, then explain in one paragraph where DISTINCT fits into the order.
Return each order with a running total of revenue per customer, ordered by order_date. Use SUM(total) OVER (PARTITION BY customer_id ORDER BY order_date). Window functions run at roughly the same stage as SELECT — after GROUP BY and HAVING, before ORDER BY.
Rewrite exercise 8 (top 5 US/UK customers) using a WITH (common table expression) to name the intermediate "customer totals" step. Does it make the query easier to read?
DROP SCHEMA sqlf_order CASCADE;