CTE vs subquery performance in PostgreSQL - Performance Comparison
We want to understand how using a Common Table Expression (CTE) compares to a subquery in terms of time taken as data grows.
How does the choice between CTE and subquery affect the work the database does?
Analyze the time complexity of these two queries:
-- Using a CTE
WITH recent_orders AS (
SELECT * FROM orders WHERE order_date > CURRENT_DATE - INTERVAL '30 days'
)
SELECT customer_id, COUNT(*) FROM recent_orders GROUP BY customer_id;
-- Using a subquery
SELECT customer_id, COUNT(*) FROM (
SELECT * FROM orders WHERE order_date > CURRENT_DATE - INTERVAL '30 days'
) AS recent_orders
GROUP BY customer_id;
Both queries find how many orders each customer made in the last 30 days, but one uses a CTE and the other a subquery.
Look for repeated work done by the database:
- Primary operation: Scanning the orders table to filter recent orders.
- How many times: Once per query, but CTE may cause repeated scans if referenced multiple times.
As the number of orders grows, the database must scan more rows to find recent orders.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | Scan 10 rows once |
| 100 | Scan 100 rows once |
| 1000 | Scan 1000 rows once |
Pattern observation: The scan grows linearly with the number of rows in the orders table.
Time Complexity: O(n)
This means the time grows roughly in direct proportion to the number of rows scanned.
[X] Wrong: "CTEs always run faster than subqueries because they look cleaner."
[OK] Correct: CTEs can sometimes cause the database to run the same work multiple times, making them slower than subqueries in some cases.
Understanding how query structure affects performance helps you write efficient database code and explain your choices clearly.
"What if the CTE is referenced multiple times in the query? How would that affect the time complexity compared to a subquery?"
Practice
Solution
Step 1: Understand CTE behavior in PostgreSQL before v12
CTEs are treated as optimization fences, meaning the planner executes them separately and does not optimize across them.Step 2: Compare with subqueries
Subqueries can be inlined and optimized with the rest of the query, often leading to better performance.Final Answer:
CTEs act as optimization fences, potentially slowing queries. -> Option CQuick Check:
CTEs before v12 block optimization = A [OK]
- Thinking subqueries are always slower
- Assuming CTEs and subqueries perform the same
- Believing subqueries can't be inside SELECT
Solution
Step 1: Recall CTE syntax
CTEs start with WITH, followed by the CTE name, AS, and a query in parentheses.Step 2: Validate each option
WITH cte_name AS (SELECT * FROM table_name) SELECT * FROM cte_name; matches the correct syntax. Options A, B, and C have incorrect order or keywords.Final Answer:
WITH cte_name AS (SELECT * FROM table_name) SELECT * FROM cte_name; -> Option AQuick Check:
CTE syntax starts with WITH name AS (query) [OK]
- Placing WITH after SELECT
- Using CREATE keyword for CTE
- Misplacing parentheses
-- Query 1 (CTE):
WITH recent_orders AS (SELECT * FROM orders WHERE order_date > '2024-01-01')
SELECT customer_id, COUNT(*) FROM recent_orders GROUP BY customer_id;
-- Query 2 (Subquery):
SELECT customer_id, COUNT(*) FROM (SELECT * FROM orders WHERE order_date > '2024-01-01') AS recent_orders GROUP BY customer_id;Solution
Step 1: Understand PostgreSQL 11 optimization
In PostgreSQL 11, CTEs act as optimization fences, so Query 1 executes the CTE separately.Step 2: Compare with subquery behavior
Subqueries can be inlined and optimized with the main query, so Query 2 can be faster.Final Answer:
Query 2 runs faster because subqueries allow better optimization in PostgreSQL 11. -> Option BQuick Check:
PostgreSQL 11: subqueries optimize better than CTEs [OK]
- Assuming CTEs always optimize well
- Thinking both queries have same speed
- Believing Query 1 has syntax error
WITH recent_orders AS SELECT * FROM orders WHERE order_date > '2024-01-01'
SELECT * FROM recent_orders;Solution
Step 1: Check CTE syntax
CTE query must be enclosed in parentheses after AS keyword.Step 2: Identify the missing parentheses
The query lacks parentheses around SELECT * FROM orders WHERE ...Final Answer:
Missing parentheses around the CTE query. -> Option AQuick Check:
CTE query must be in parentheses [OK]
- Omitting parentheses around CTE query
- Thinking semicolon is required after CTE
- Believing CTE names can't be simple
Solution
Step 1: Understand PostgreSQL 13 CTE behavior
Starting with PostgreSQL 12, CTEs can be inlined and optimized like subqueries.Step 2: Evaluate optimization strategies
Replacing CTEs with subqueries is not necessary; keeping CTEs maintains readability and performance.Final Answer:
Keep CTEs because PostgreSQL 13 can inline them, improving speed. -> Option DQuick Check:
PostgreSQL 12+ inlines CTEs for better optimization [OK]
- Thinking subqueries are deprecated
- Replacing CTEs blindly without testing
- Assuming temporary tables always improve speed
Ready to prove it?
Pass the test and this pattern moves unknown
