CTE vs. Subqueries : A Guide to Choosing the Right Approach
- Kirthana Ramanathan
- Oct 12
- 3 min read
Writing a complex SQL query often involves using the result of one query inside another. This leads to the eternal SQL dilemma: Should you use a nested subquery or define a CTE (Common Table Expression)? The answer is always query-specific, but understanding the strengths of each tool is crucial for writing efficient and maintainable code.
Let’s start with some simple examples to differentiate the two:
A subquery is a query nested inside another SQL statement. It’s more or less like a footnote – defined right where it’s used.
SELECT patient_id, glucose_value
FROM glucose_monitor
WHERE glucose_value > (SELECT AVG(glucose_value) FROM glucose_monitor);
A Common Table Expression or CTE is a temporary, named result that starts with the WITH clause and is defined before the main query. Think of it as a chapter outline that is defined upfront and then referenced by its name.
WITH high_glucose_patients AS (
SELECT patient_id FROM dexcom WHERE glucose_value > 180
)
SELECT * FROM high_glucose_patients
WHERE patient_id IN (SELECT patient_id FROM patients WHERE age > 60);
Now for the real question: When to Use Which?
Choose CTEs When:
1. Your query has multiple logical steps and you need it to be readable and organized.
2. You need the same result set multiple times.
3. When you are working with hierarchical data like organizational charts, categories, etc.
Choose Subqueries When:
1. You need a single value for a WHERE or SELECT clause.
2. You are checking for the existence of a certain type of data using EXISTS/ NOT EXISTS.
3. You need row by row calculations specific to each record.
Let’s take a closer look at a certain use case to understand the differences better.
Consider a food alert system for diabetic patients where you need to write a query to identity problematic food intake patterns:
The CTE Approach:
WITH patient_averages AS (
SELECT patient_id, AVG(calorie) AS avg_daily_calories
FROM daily_totals GROUP BY patient_id
),
ranked_foods AS (
SELECT f.patient_id, f.logged_food, f.calorie,
ROW_NUMBER() OVER (PARTITION BY f.patient_id ORDER BY f.calorie DESC) AS rank
FROM foodlog f
JOIN patient_averages p ON f.patient_id = p.patient_id
)
SELECT patient_id, logged_food, calorie
FROM ranked_foods
WHERE rank = 1 AND calorie > 500;
The subquery Approach:
SELECT patient_id, logged_food, calorie
FROM (
SELECT f.patient_id, f.logged_food, f.calorie,
ROW_NUMBER() OVER (PARTITION BY f.patient_id ORDER BY f.calorie DESC) AS rank
FROM foodlog f
JOIN (
SELECT patient_id, AVG(calorie) AS avg_daily_calories
FROM (SELECT patient_id, SUM(calorie) AS calorie FROM foodlog GROUP BY patient_id, DATE(time_of_intake)) daily_totals
GROUP BY patient_id
) pa ON f.patient_id = pa.patient_id
) ranked_foods
WHERE rank = 1 AND calorie > 500;
The CTE approach is not just easier to read; it's often more efficient. The patient_averages CTE calculates the averages once and reuses the result. In the subquery approach, the database might need to recalculate the average for every single food log entry. While this might be acceptable for a small hobby database, it will become significantly slower in a large organizational database with terabytes of data. The CTE's logical separation makes the query easier to debug, modify, and maintain.
The Bottom Line:
For simple, single-use values and EXISTS checks, a subquery is often the right tool. However, for multi-step analytical queries where readability and performance matter, default to using a CTE. It transforms a nested mess into a clean, logical story, making your code far more robust for the future.


