Hello! Welcome back to your SQL course.
In the last two lessons, we explored how to use Common Table Expressions (CTEs), first to clarify a single logical step, and then to chain multiple CTEs into a clean, sequential data pipeline. You've learned how to build complex queries from scratch in a structured way.
Today, we'll apply that knowledge to a different, but equally critical, real-world task: improving existing code. The learning outcome for this lesson is to refactor a query with nested subqueries into a more maintainable CTE-based structure. In any technical job, you will inevitably encounter complex, hard-to-read "legacy" code. Being able to understand, clean up, and improve it is a highly valued skill.
This process is analogous to refactoring a large, monolithic function in a language like C into a set of smaller, well-defined functions. You're not changing what the code does, but you're dramatically improving its structure, making it easier to read, debug, and maintain.
The Problem: "Spaghetti SQL"
Complex business questions often lead to SQL queries where one subquery is nested inside another, which might be nested inside another. This is often called "Spaghetti SQL" because tracing the logic is like trying to follow a single noodle in a bowl of spaghetti.
Consider a query like this:
-- This is a conceptual example of a messy query
SELECT *
FROM employees e
WHERE e.department_id IN (
SELECT department_id
FROM departments d
WHERE d.avg_sales > (
SELECT AVG(s.total)
FROM sales s
WHERE s.region = d.region
)
);
While the database can execute this, it's very difficult for a human to read. What is the business logic? If it produces an error or an incorrect result, how would you debug it? This is the problem we'll solve today.
Refactoring with CTEs allows us to untangle this mess. We can isolate each logical block, give it a meaningful name, and arrange the query in a clear, top-to-bottom sequence.
A Guided Refactoring Example
Let's watch a practical demonstration of refactoring. The following video first builds a query using nested subqueries to solve a problem and then rewrites that exact query using CTEs, allowing for a direct comparison of the two approaches.
SQL WITH Clause | How to write SQL Queries using WITH Clause | SQL CTE (Common Table Expression)
This video from techTFQ provides a perfect walkthrough. It first presents a complex problem and solves it using messy, nested subqueries, highlighting the readability issues. Then, it refactors that same query into a clean, maintainable CTE-based structure.
This video is best watched in two parts: The 'Before' State (06:17 - 15:02): Watch this segment to see how the presenter builds a query with multiple, nested subqueries to find stores with sales better than the average. Notice how the final query becomes hard to read, with one query nested inside another. The 'After' State (15:02 - 20:24): Now, watch how the presenter refactors this complex query into two CTEs, total_sales and average_sales. Pay close attention to how the final SELECT statement becomes incredibly simple and readable, and how the total_sales logic is defined once and reused. You'll see that both queries produce the same result, but the CTE version is far superior in terms of clarity and maintainability.
As the video demonstrates, refactoring provides several key benefits:
- Readability: The main query logic is no longer hidden in nested parentheses. It's a clean
SELECTfrom named, logical blocks. - Maintainability: If you need to change how "total sales" are calculated, you only need to edit the
total_salesCTE. The change is isolated and easy to perform. - Reusability: The
total_salesresult was calculated once and then used by theaverage_salesCTE. The subquery version had to repeat that logic, which is inefficient and error-prone.
A Step-by-Step Method for Untangling Queries
Now that you've seen the "before and after," let's establish a formal process for refactoring. The goal is to be methodical, not just haphazardly copy-pasting code.
Simplify Complex Queries with Postgres CTEs in Supabase
This article, 'Simplify Complex Queries with Postgres CTEs in Supabase', provides an excellent, detailed walkthrough of this exact refactoring process. It starts with a piece of 'spaghetti SQL' for a music app and methodically untangles it.
Read the sections 'The Problem with Complex SQL Queries' and 'Solving Spaghetti SQL with CTEs'. Then, jump to the subsection 'Tips for Testing and Debugging CTEs'. First, look at the initial query. Try to mentally trace the logic – it's difficult! Then, follow the author's process. Notice how they break the problem down into logical chunks (user_recent_play_count, user_top_genre, etc.) and create a CTE for each. Crucially, absorb the advice on debugging: After writing each CTE, run a SELECT * FROM your_cte_name to ensure it works as expected before building the next one. This iterative, test-as-you-go approach is a fundamental professional habit.
From the article, we can summarize the refactoring process into these steps:
- Understand the Goal: Before touching the code, read the query and figure out the business question it's trying to answer.
- Identify the Logical Blocks: Look at the innermost subqueries first. What piece of information is each one trying to produce? Give it a mental name (e.g., "this finds all active promotions," "this gets user play counts").
- Extract the Innermost Logic: Take the innermost subquery and make it your first CTE. Give it a descriptive name.
- Test As You Go: Immediately after creating your first CTE, comment out the rest of the old query and write
SELECT * FROM your_new_cte;. Run it and verify the results. This ensures your foundation is solid before you build on it. - Build Outward: Move to the next subquery that depended on the one you just extracted. Rewrite it as a second CTE, but instead of using a subquery, have it
SELECTfrom your first CTE. Test this new CTE. - Repeat: Continue this process, replacing each layer of subqueries with a new, tested CTE that references a previous one.
- Construct the Final Query: Once all the messy logic is encapsulated in a clean sequence of CTEs, write your final, simple
SELECTstatement that orchestrates them.
Test your understanding!
You are given a messy query that finds all employees who earn more than the average salary of their respective department, but only for departments located in 'New York'.
The messy query:
SELECT
e.employee_name,
e.salary
FROM
employees e
JOIN
departments d ON e.department_id = d.department_id
WHERE
d.location = 'New York' AND e.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.department_id = e.department_id
);
This query uses a correlated subquery, which can be inefficient and hard to read. Refactor it using CTEs. Describe the two CTEs you would create and the final SELECT statement.
Show answer
Here is the refactored structure following our method:
-
CTE 1:
department_averages- Purpose: Calculate the average salary for every single department. This isolates the aggregation logic.
- Code: It would
GROUP BY department_idon theemployeestable and calculateAVG(salary).
-
CTE 2:
new_york_departments- Purpose: Get the list of department IDs that are located in 'New York'. This isolates the location filter.
- Code: It would
SELECT department_id FROM departments WHERE location = 'New York'.
-
Final
SELECTStatement:- Purpose: Bring all the pieces together.
- Code:
- Join
employeeswithdepartment_averagesondepartment_id. - Join the result with
new_york_departmentsondepartment_id(this effectively filters for only NY departments). - In the
WHEREclause, compare the employee'ssalaryto theavg_dept_salaryfrom thedepartment_averagesCTE.
- Join
The complete refactored query:
WITH department_averages AS (
-- Step 1: Calculate the average salary for each department
SELECT
department_id,
AVG(salary) AS avg_dept_salary
FROM
employees
GROUP BY
department_id
),
new_york_departments AS (
-- Step 2: Identify the departments in New York
SELECT
department_id
FROM
departments
WHERE
location = 'New York'
)
-- Final Step: Join the data and apply the final filter
SELECT
e.employee_name,
e.salary
FROM
employees e
JOIN
department_averages da ON e.department_id = da.department_id
JOIN
new_york_departments nyd ON e.department_id = nyd.department_id
WHERE
e.salary > da.avg_dept_salary;
Notice how each CTE has one clear job, and the final SELECT is just for joining and filtering based on the pre-calculated results. It tells a story from top to bottom.
Conclusion
You have now completed the core lessons on Common Table Expressions. You know how to build queries with them and, just as importantly, how to refactor existing queries to use them. This is a powerful combination of skills that separates a beginner from a proficient SQL user.
Key Takeaways:
- Nested subqueries ("spaghetti SQL") are hard to read, debug, and maintain.
- Refactoring involves methodically extracting nested logic into a sequence of named CTEs.
- A reliable process is to start from the innermost query, extract it to a CTE, test it, and build outwards.
- The final result is a query that is self-documenting, modular, and much easier to work with for you and your future teammates.
Preview of the Next Lesson:
We are now going to shift our focus from reading data to manipulating it. In the next module, you will learn the fundamentals of Data Definition Language (DDL) and Data Manipulation Language (DML). We will start by learning how to CREATE, ALTER, and DROP tables, which are the foundational commands for defining the very structure of your database.