Multiple CTEs in one query in PostgreSQL - Time & Space Complexity
When using multiple CTEs (Common Table Expressions) in one query, it's important to understand how the query's work grows as the data grows.
We want to know how the total steps needed change when the input data gets bigger.
Analyze the time complexity of the following query with multiple CTEs.
WITH cte1 AS (
SELECT * FROM orders WHERE order_date > '2023-01-01'
),
cte2 AS (
SELECT customer_id, COUNT(*) AS order_count FROM cte1 GROUP BY customer_id
),
cte3 AS (
SELECT customer_id FROM cte2 WHERE order_count > 5
)
SELECT * FROM customers WHERE id IN (SELECT customer_id FROM cte3);
This query filters recent orders, counts orders per customer, then selects customers with more than 5 orders.
Look for repeated work inside the query.
- Primary operation: Scanning and grouping rows in the orders table.
- How many times: Each CTE runs once in sequence, but the grouping scans all filtered orders.
As the number of orders grows, the work to scan and group them grows too.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 scans and groups |
| 100 | About 100 scans and groups |
| 1000 | About 1000 scans and groups |
Pattern observation: The work grows roughly in direct proportion to the number of orders.
Time Complexity: O(n)
This means the query's work grows linearly with the number of orders processed.
[X] Wrong: "Using multiple CTEs means the query runs multiple times and multiplies the work."
[OK] Correct: Each CTE runs once in order, and the total work depends mostly on the largest data scanned, not on the number of CTEs.
Understanding how multiple CTEs affect query time helps you write clear and efficient queries, a useful skill in real projects and interviews.
What if we added a join inside one of the CTEs? How would that change the time complexity?
Practice
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 DQuick Check:
Multiple CTEs = Query readability [OK]
- Confusing CTEs with permanent tables
- Thinking CTEs improve hardware speed
- Assuming CTEs create databases
cte1 and cte2 in one PostgreSQL query?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 AQuick 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]
- Omitting parentheses around CTE queries
- Using semicolons between CTEs
- Using '=' instead of AS
WITH cte1 AS (SELECT 1 AS num), cte2 AS (SELECT num * 2 AS double_num FROM cte1) SELECT * FROM cte2;
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 CQuick Check:
cte2 output columns = double_num [OK]
- Assuming cte2 includes all columns from cte1
- Expecting num column in final output
- Thinking query has syntax errors
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;
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 BQuick Check:
Check column names in JOIN = Column cte2.user_id does not exist [OK]
- Assuming user_id exists in cte2
- Thinking semicolons separate CTEs
- Believing CTEs cannot be joined
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 AQuick 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]
- Using SUM instead of AVG for average calculation
- Missing GROUP BY clause
- Not filtering in a separate CTE
Ready to prove it?
Pass the test and this pattern moves unknown
