📖 Scenario: You work in a small bookstore's database team. The store wants to analyze its sales data to find the total sales per author and then identify authors with sales above a certain threshold.
🎯 Goal: Build a PostgreSQL query using multiple Common Table Expressions (CTEs) to first calculate total sales per book, then total sales per author, and finally select authors with sales above a threshold.
📋 What You'll Learn
Create a CTE named book_sales that sums sales per book.
Create a second CTE named author_sales that sums sales per author using book_sales.
Use a final SELECT statement to get authors with total sales greater than a threshold.
Use exact CTE names and column names as specified.
💡 Why This Matters
🌍 Real World
Bookstores and many businesses use multiple CTEs to organize complex queries for reporting and analysis.
💼 Career
Knowing how to write multiple CTEs is important for data analysts and database developers to write clear and efficient SQL queries.
Progress0 / 4 steps
1
Create the book_sales CTE
Write a CTE named book_sales that selects book_id and sums quantity as total_quantity from the sales table, grouping by book_id.
PostgreSQL
Hint
Use WITH book_sales AS (SELECT book_id, SUM(quantity) AS total_quantity FROM sales GROUP BY book_id).
2
Add the author_sales CTE
Add a second CTE named author_sales that joins book_sales with books on book_id, then sums total_quantity as author_total grouped by author.
PostgreSQL
Hint
Join book_sales with books and group by b.author.
3
Add a threshold variable
Create a variable named sales_threshold and set it to 100 to filter authors with sales above this number.
PostgreSQL
Hint
Use SELECT 100 AS sales_threshold to define the threshold.
4
Select authors with sales above the threshold
Write a final SELECT statement that selects author and author_total from author_sales where author_total is greater than 100.
PostgreSQL
Hint
Filter authors with author_total > 100.
Practice
(1/5)
1. What is the main purpose of using multiple CTEs (Common Table Expressions) in a single PostgreSQL query?
easy
A. To speed up the database server hardware
B. To create multiple databases at once
C. To permanently store data in the database
D. To break a complex query into smaller, readable parts
Solution
Step 1: Understand what CTEs do
CTEs let you write parts of a query separately with names, making complex queries easier to read.
Step 2: Identify the purpose of multiple CTEs
Using multiple CTEs helps organize and simplify complex logic by splitting it into smaller named queries.
Final Answer:
To break a complex query into smaller, readable parts -> Option D
Quick Check:
Multiple CTEs = Query readability [OK]
Hint: Think of CTEs as named steps in a recipe [OK]
Common Mistakes:
Confusing CTEs with permanent tables
Thinking CTEs improve hardware speed
Assuming CTEs create databases
2. Which of the following is the correct syntax to define two CTEs named cte1 and cte2 in one PostgreSQL query?
easy
A. WITH cte1 AS (SELECT * FROM table1), cte2 AS (SELECT * FROM table2) SELECT * FROM cte1 JOIN cte2 ON cte1.id = cte2.id;
B. WITH cte1 (SELECT * FROM table1); WITH cte2 (SELECT * FROM table2); SELECT * FROM cte1, cte2;
C. WITH cte1 = SELECT * FROM table1, cte2 = SELECT * FROM table2 SELECT * FROM cte1 JOIN cte2;
D. WITH cte1 AS SELECT * FROM table1, cte2 AS SELECT * FROM table2 SELECT * FROM cte1 JOIN cte2;
Solution
Step 1: Recall correct CTE syntax
CTEs start with WITH, each named, followed by AS and a query in parentheses, separated by commas.
Step 2: Check each option's syntax
WITH cte1 AS (SELECT * FROM table1), cte2 AS (SELECT * FROM table2) SELECT * FROM cte1 JOIN cte2 ON cte1.id = cte2.id; correctly uses commas, AS, and parentheses. Others miss parentheses or use wrong separators.
Final Answer:
WITH cte1 AS (SELECT * FROM table1), cte2 AS (SELECT * FROM table2) SELECT * FROM cte1 JOIN cte2 ON cte1.id = cte2.id; -> Option A
Quick Check:
Correct CTE syntax = WITH cte1 AS (SELECT * FROM table1), cte2 AS (SELECT * FROM table2) SELECT * FROM cte1 JOIN cte2 ON cte1.id = cte2.id; [OK]
Hint: Remember: WITH cte_name AS (query), separate multiple CTEs by commas [OK]
Common Mistakes:
Omitting parentheses around CTE queries
Using semicolons between CTEs
Using '=' instead of AS
3. Given the following query, what will be the output?
WITH cte1 AS (SELECT 1 AS num), cte2 AS (SELECT num * 2 AS double_num FROM cte1) SELECT * FROM cte2;
medium
A. [{num: 2}]
B. [{num: 1, double_num: 2}]
C. [{double_num: 2}]
D. Syntax error
Solution
Step 1: Analyze cte1
cte1 returns one row with column num = 1.
Step 2: Analyze cte2 using cte1
cte2 selects num * 2 as double_num, so double_num = 1 * 2 = 2.
Step 3: Final SELECT from cte2
Only double_num column is selected, so output is [{double_num: 2}].
Final Answer:
[{double_num: 2}] -> Option C
Quick Check:
cte2 output columns = double_num [OK]
Hint: Check which columns each CTE outputs carefully [OK]
Common Mistakes:
Assuming cte2 includes all columns from cte1
Expecting num column in final output
Thinking query has syntax errors
4. Identify the error in this query using multiple CTEs:
WITH cte1 AS (SELECT id, name FROM users), cte2 AS (SELECT id, age FROM users) SELECT cte1.id, cte1.name, cte2.age FROM cte1 JOIN cte2 ON cte1.id = cte2.user_id;
medium
A. CTEs must be defined with semicolons
B. Column cte2.user_id does not exist
C. Missing comma between CTEs
D. Cannot join two CTEs in one query
Solution
Step 1: Check CTE definitions
Both cte1 and cte2 select id and other columns from users table correctly.
Step 2: Check JOIN condition
Query joins on cte1.id = cte2.user_id, but cte2 has no user_id column, only id.
Step 3: Identify error
Using cte2.user_id causes a column not found error.
Final Answer:
Column cte2.user_id does not exist -> Option B
Quick Check:
Check column names in JOIN = Column cte2.user_id does not exist [OK]
Hint: Verify column names in JOIN conditions carefully [OK]
Common Mistakes:
Assuming user_id exists in cte2
Thinking semicolons separate CTEs
Believing CTEs cannot be joined
5. You want to calculate the average order amount per customer and then find customers whose average order is above 100. Which query correctly uses multiple CTEs to achieve this?
hard
A. WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders GROUP BY customer_id), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg;
B. WITH avg_orders AS (SELECT customer_id, SUM(amount) AS avg_amount FROM orders GROUP BY customer_id), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg;
C. WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg;
D. WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders GROUP BY customer_id) SELECT * FROM avg_orders WHERE avg_amount > 100;
Solution
Step 1: Calculate average order per customer
WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders GROUP BY customer_id), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg; correctly uses AVG(amount) with GROUP BY customer_id in avg_orders CTE.
Step 2: Filter customers with avg_amount > 100
WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders GROUP BY customer_id), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg; uses a second CTE high_avg to select customers meeting the condition.
Step 3: Final SELECT returns customers with high average orders
WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders GROUP BY customer_id), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg; selects all from high_avg, giving correct result.
Final Answer:
WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders GROUP BY customer_id), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg; -> Option A
Quick Check:
Correct aggregation and filtering = WITH avg_orders AS (SELECT customer_id, AVG(amount) AS avg_amount FROM orders GROUP BY customer_id), high_avg AS (SELECT customer_id FROM avg_orders WHERE avg_amount > 100) SELECT * FROM high_avg; [OK]
Hint: Use one CTE for aggregation, another for filtering [OK]
Common Mistakes:
Using SUM instead of AVG for average calculation
Missing GROUP BY clause
Not filtering in a separate CTE
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.