Lesson 16: Stop Using Subqueries (Try This Pattern Instead)
π Full written solution: https://interview-kit-fe.vercel.app/sql-lesson-16-ctes-common-table-expressions-with π» Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/sql-lesson-16-ctes-common-table-expressions-with Chapters: 0:00 Meet Kai and Why We're Here 0:18 What is a CTE Really? 0:37 The Kitchen Analogy 0:56 The Basic Syntax Pattern 1:09 Why Use CTEs Over Subqueries? 1:24 Building a Recursive CTE 1:40 The Recursive Trap Warning 1:55 CTE vs Temporary Tables 2:09 When NOT to Use a CTE 2:26 Common Syntax Mistakes 2:38 Ordering Matters in CTEs 2:52 Optimization Considerations 3:07 Recap and Key Takeaways 3:23 Kai's Sign-off In SQL Lesson 16, Kai demystifies Common Table Expressions (CTEs) using a relatable kitchen analogy to show how they transform messy, multi-step queries into modular logic. Learn the exact syntax patterns for both standard and recursive CTEs, discover why temporary tables aren't always the answer, and identify critical ordering traps that break your code. By the end, you'll know when to use CTEs, when to avoid them, and how to optimize your SQL without over-engineering β Kai Watch next: - Lesson: How Do Apps Survive a Million Users Without Crashing?: https://youtu.be/wHWdfeklLG4 - Common Mistake: Forgetting the Script Tag Placement #shorts: https://youtu.be/Ipc-RKtAhSg - Mistake: The Single Point of Failure #shorts: https://youtu.be/g9qGNtAsvYs
1. Introduction: The Spaghetti Problem and the Modular Solution
There is a distinct smell that every senior software engineer recognizes instantly: the smell of unmanageable complexity. In the world of relational databases, this smell often manifests as "spaghetti queries." These are massive blocks of SQL code where subqueries nest inside subqueries, filtering criteria repeat endlessly, and the logic flows from left to right like a tangled mess of wires.
When you find yourself rewriting the same join condition five times or trying to remember which filter applied in step three versus step four, you have hit a wall. Interviewers love these scenarios because they test not just your ability to retrieve data, but your ability to architect a solution that is maintainable and readable. They want to see if you can break a monolithic problem into smaller, manageable pieces.
That specific pattern of decomposition? That is the power of Common Table Expressions (CTEs).
In this lesson, we aren't just learning syntax; we are adopting a philosophy of modular thinking. We will explore how CTEs act as temporary named tables that exist only for the duration of your query. By defining logic in a step-by-step fashion before joining them into a final result, you transform opaque logic into clear documentation. Whether you are preparing for a technical interview or optimizing production SQL pipelines, mastering CTEs is the single most impactful shift you can make to improve your code quality today.
2. Problem Overview
Imagine you are tasked with generating a report on employee performance. You need to calculate metrics based on departments, roll up sales figures from multiple tables, and then find employees who exceed a specific threshold in their hierarchy.
If you tried to do this without a structured approach, you might end up with a query that looks like this (hypothetically):
SELECT e.name, d.dept_name,
SUM(s.amount) as total_sales
FROM sales s
JOIN employees e ON e.id = s.emp_id
JOIN departments d ON e.dept_id = d.id
WHERE s.amount > 1000
AND e.manager_id IN (
SELECT emp2.id
FROM employees emp2
WHERE emp2.level > 3
)
GROUP BY e.name, d.dept_name;
This works, but notice the repetition of join conditions and the difficulty of debugging the logic flow. If you need to add another filter for "Year 2023", you have to hunt through multiple places.
The challenge is to restructure this so that we can define intermediate resultsβlike filtering high-level managers or pre-calculating sales totalsβand treat them as distinct entities before combining them. This is exactly what a Common Table Expression allows us to do. It lets us say, "First, let's identify the elite managers," and then "Next, let's find the big sellers," and finally "Combine them."
3. Example
To visualize this, imagine we have three tables: Employees, Departments, and Sales.
Input Data Scenario: We want to list all employees who are either in the 'Engineering' department OR are managers of more than 5 direct reports.
Standard Approach (Messy):
You might try using a massive OR clause with multiple joins or nested subqueries inside the WHERE condition, making it hard to read.
CTE Approach (Clean):
- Step 1 (CTE A): Create a list of employees in Engineering.
- Step 2 (CTE B): Create a list of employees who have more than 5 reports.
- Step 3 (Main Query): Combine the results of Step 1 and Step 2 using
UNIONorIN.
By defining these steps first, we separate the "what" from the "how."
4. Intuition: The Kitchen Analogy
The most effective way to understand a CTE is to imagine you are cooking a complex multi-course meal for a restaurant.
Think of your SQL query as the final plating process.
- Without CTEs: You might try chopping onions, dicing carrots, and mincing garlic all at the same time while stirring a pot of soup boiling on the stove. If you make a mistake with the onions, you have to stop the whole process. It's chaotic.
- With CTEs: You use labeled bowls. You set aside your chopped onions in a bowl labeled
Prepped_Onions. You set aside your cooked carrots in a bowl labeledCooked_Carrots.
When it's time to assemble the final dish, you don't go back to the raw vegetables or the knife block. You simply reach into your bowl of Prepped_Onions and use them. If you need more onions later, you look at that same bowl.
In SQL terms:
- The Bowl is the CTE.
- The Recipe Step is the query definition inside the
WITHclause. - The Final Dish is your main
SELECTstatement that pulls from the bowls.
This separation of concerns is why senior engineers love CTEs. It turns a complex, monolithic logic flow into a linear series of readable steps.
5. Brute Force Solution: Nested Subqueries
Before we dive into CTEs, let's look at how you might have solved this before knowing the trick. We call this the "Brute Force" approach because it relies on repeating logic or deeply nesting queries.
Idea: Calculate everything you need inside the final SELECT or WHERE clause using nested subqueries.
Algorithm: Identify every unique filter or aggregation needed and embed it directly into the main query's conditions.
Advantages: No special syntax required; standard SQL works everywhere.
Disadvantages:
- Readability: As you nest deeper (e.g.,
SELECT ... WHERE ... IN (...) AND ... IN (...)), the code becomes unreadable quickly. - Debugging: You cannot inspect the intermediate result of a subquery without executing it separately. If the logic is wrong, you don't know which part caused the failure until the very end.
- Repetition: If two tables share the same join condition, you repeat yourself, violating the DRY (Don't Repeat Yourself) principle.
Complexity: The algorithmic complexity remains O(N), but the cognitive complexity skyrockets as nesting depth increases.
6. Optimal Solution: Structured Common Table Expressions (CTEs)
The optimal solution is to extract intermediate logic into named CTEs before the final selection.
Step-by-Step Approach:
- Define with
WITH: Start your query with theWITHkeyword. This signals the database that a temporary result set is coming up. - Name Your Logic: Create an alias (e.g.,
Prepped_Onions, orHigh_Performers). - Isolate Complexity: Write the logic for each distinct part of your business rule in its own block. You can filter, aggregate, and join as needed here.
- Reference Clearly: In your final
SELECTstatement, reference these named temporary tables just like you would a standard database table.
Why this works: The database optimizer is generally smart enough to merge CTEs efficiently, but more importantly, you become smart enough to understand the data flow instantly. You are essentially writing pseudo-code that happens to be executable SQL.
7. Python Code (SQL Implementation)
While CTEs are a SQL feature, we will write them in standard ANSI-SQL syntax which works across PostgreSQL, MySQL (8.0+), Oracle, and Snowflake. We will assume a generic employees table structure for this example.
import psycopg2
# Connection setup (simulated for Python context)
conn = psycopg2.connect(host="localhost", database="company_db")
cur = conn.cursor()
# SQL Query using Common Table Expressions
query = """
WITH EngineeringTeam AS (
-- Step 1: Define the Engineering Team
SELECT e.emp_id, e.name, e.salary
FROM employees e
WHERE e.department = 'Engineering'
),
HighLevelManagers AS (
-- Step 2: Define Managers with many reports (Intermediate Logic)
SELECT e.emp_id, e.name, e.level
FROM employees e
JOIN employee_reports r ON e.emp_id = r.manager_id
GROUP BY e.emp_id, e.name, e.level
HAVING COUNT(r.report_id) > 5
),
CombinedHighValue AS (
-- Step 3: Combine logic or prepare for final join
SELECT 'Engineering' as category, emp_id, name, salary FROM EngineeringTeam
UNION ALL
SELECT 'Manager' as category, emp_id, name, salary
FROM HighLevelManagers
)
-- Final Selection
SELECT * FROM CombinedHighValue
ORDER BY category, salary DESC;
"""
# Execute and fetch results
cur.execute(query)
results = cur.fetchall()
for row in results:
print(row)
cur.close()
conn.close()
8. Code Walkthrough
Let's dissect the Python script and SQL logic line by line.
WITH EngineeringTeam AS (...): We declare a CTE namedEngineeringTeam. TheASkeyword is optional but recommended for clarity. Inside the parentheses, we define the query that populates this "virtual table." Note that we selectemp_id,name, andsalary.WHERE e.department = 'Engineering': We filter strictly here. If a row doesn't match, it disappears from theEngineeringTeamCTE immediately.HighLevelManagers AS (...): This is our second named table. Here we perform aJOINwithemployee_reports. Crucially, we useGROUP BYandHAVING. This aggregation happens inside the CTE definition, isolating the logic from the final output.CombinedHighValue AS (...): This CTE combines our previous two results. We useUNION ALLto merge them. Notice we added acategorycolumn here to distinguish them in the final output. This is a great place to normalize your data before the final step.- Final
SELECT * FROM CombinedHighValue: Now, this looks like any standard SQL query! You are selecting from a table you defined yourself. The complexity is hidden behind the modular names we chose earlier.
9. Dry Run: Walking Through an Example
Let's trace a dry run with simplified data to see how the logic flows.
Scenario:
- Employee A: Department='Engineering', Level=2, Reports=3.
- Employee B: Department='Sales', Level=5, Reports=8 (Manager).
- Employee C: Department='HR', Level=10, Reports=0.
Execution Flow:
- CTE
EngineeringTeamRuns First:- Scans
employeestable. - Filters for
Department = 'Engineering'. - Finds Employee A.
- Stores {A, Engineering, ...} in memory.
- Scans
- CTE
HighLevelManagersRuns Second:- Joins
employeesandemployee_reports. - Groups by manager.
- Checks
HAVING COUNT > 5. - Finds Employee B (8 reports). Employee A is excluded (3 reports).
- Stores {B, Manager, ...} in memory.
- Joins
- CTE
CombinedHighValueRuns Third:- Takes the result of Step 1 (Employee A) and Step 2 (Employee B).
- Labels them 'Engineering' and 'Manager'.
- Final Query Executes:
- Scans the temporary result set of Step 3.
- Returns rows for A and B, sorted by category then salary.
Result: Employee A and Employee B are returned. Employee C is never seen by the final query because they failed the specific criteria in their respective CTEs early on.
10. Complexity Analysis
One of the biggest questions regarding CTEs is performance. Does this "virtual table" overhead slow us down?
- Time Complexity: Theoretically, adding a CTE adds a linear factor to the parsing and planning phase (O(N) relative to the number of steps). However, modern SQL optimizers (like in PostgreSQL or Snowflake) are very smart. They often "flatten" the query plan, executing the necessary sub-queries without actually creating physical temporary tables unless forced. The execution time is usually comparable to a flattened nested query, but with the benefit of logical clarity.
- Space Complexity: Since CTEs are temporary result sets, they hold data in memory (or disk buffers) during execution. If you create a massive CTE without limits, you increase memory pressure. This is why we must be careful not to materialize huge intermediate datasets unnecessarily.
11. Alternative Solutions: Recursion and Temporary Tables
Alternative 1: Recursive CTEs Sometimes, your data is hierarchical (an org chart). You cannot solve this with standard CTEs easily. You need a Recursive CTE. This looks like our standard CTE but adds a recursive step that references the CTE name itself.
- Structure:
WITH RECURSIVE cte AS (SELECT ... UNION ALL SELECT ... FROM cte) - Use Case: Finding all subordinates of a CEO, or traversing a graph structure.
- Warning: Always ensure your recursive query has an
EXITcondition (usually in theWHEREclause of the recursive part) to prevent infinite loops that crash the database server.
Alternative 2: Temporary Tables (CREATE TEMP TABLE)
In some legacy systems or very complex reports, you might create a physical temporary table before the main query.
- Pros: Can be indexed explicitly; exists until you drop it.
- Cons: Requires extra steps (Create -> Insert -> Drop) which increases boilerplate code and transaction overhead. CTEs are generally preferred for single-query readability unless you need to persist the intermediate data across multiple distinct queries in the same transaction.
12. Edge Cases and Common Mistakes
Even senior engineers make mistakes with CTEs. Here is how to avoid them:
- Missing Semicolons: In many tools (like SQL Server Management Studio or specific IDE configurations), you must end a
WITHstatement with a semicolon;. MySQL usually doesn't require this, but being explicit never hurts in cross-tool environments. - Reserved Keywords as Names: Never name your CTE
Order,Group, orSelectwithout quoting it (e.g.,"Order"). This will crash the parser. Use unique names likeEmployeeDataorSalesMetrics. - Circular References: You cannot reference a table in your main query inside its own CTE definition if that dependency hasn't been established yet. The flow must be Definition -> Usage.
- Over-Engineering: Don't use a CTE for a simple join of three tables if it adds unnecessary boilerplate. If the query is clean as is, keep it simple. Clarity wins over complexity.
- Assuming Performance Gains: A CTE doesn't always mean faster execution. Sometimes a flat query allows the optimizer to use an index scan more effectively. Always test your execution plan before defaulting to CTEs for massive datasets.
13. Interview Questions and Similar Problems
If you are preparing for a coding interview, here are the likely follow-ups:
- "Write a SQL query to find employees who earn more than their managers." (Classic Self-Join problem, solvable with CTEs or just direct joins).
- "Find all duplicate emails in a table." (Group By problem, excellent for testing
HAVING COUNT > 1logic inside a CTE). - "Generate a report of monthly sales trends using window functions." (CTEs often used to pre-aggregate data before applying
LAG()orLEAD()functions).
Similar Problems:
- LeetCode 570: Manage Employees: Uses self-joins, but a CTE can make the logic cleaner.
- LeetCode 2189: Constructing a Sentence: While wordy in SQL, demonstrates breaking complex string manipulation into steps (CTE style).
14. Key Takeaways
To summarize what we have built today:
- Modular Logic: CTEs allow you to define temporary, named result sets that exist only for the duration of your query.
- Readability First: They are essential for transforming complex queries into readable blocks of logic, effectively turning "spaghetti" into a structured flowchart.
- Strategic Usage: Use CTEs to break down big problems into small pieces, but avoid them if the join is straightforward and involves only two or three tables.
- Recursion Power: For hierarchical data (org charts), recursive CTEs are your go-to tool for traversing relationships safely.
- **
The solution
WITH RECURSIVE ManagerTree AS (
SELECT EmployeeID, ManagerID
FROM Employees
UNION ALL
SELECT e.EmployeeID, m.ManagerID
FROM Employees e
JOIN ManagerTree m ON e.ManagerID = m.EmployeeID
)
SELECT * FROM ManagerTreeReady to try it yourself? Solve Sql problems with instant feedback in the practice sandbox.
Practice now β


