Module 4: Subqueries & CTEs40 min

CTE Fundamentals

Progress Tracking

Log in to save this lesson and continue from where you left off.

Log in

Finally, Readable Complex Queries

Nested subqueries are a nightmare to read. You start at the innermost level, work your way out, try to hold the whole thing in your head. There’s a better way.

CTEs (Common Table Expressions) let you name each step and lay them out top-to-bottom. They’re like creating temporary variables that hold query results. Once you start using them, you’ll wonder how you ever lived without them.

Your First CTE

SQL
WITH big_orders AS (
  SELECT *
  FROM orders
  WHERE total_order_cost > 200
)
SELECT * FROM big_orders;
Table: techcorp_workforce
idfirst_namelast_namedepartmentsalaryphone_numberjoining_date
1SarahMitchellHR95000555-01012021-03-15
2MichaelChenHR88000555-01022022-06-01
3EmilyRodriguezHR825002021-09-20
4DavidParkHR80000555-01042023-01-10
5LisaThompsonHR650002021-04-05
1
CTE for High Earners

Write a CTE that finds employees earning over 100000, then select from it.

Tables: techcorp_workforce

You define high_earners first, then use it like a table in the main query. Compare this to the equivalent nested subquery and tell me which you’d rather debug at 11pm.

Basic Syntax

SQL
WITH cte_name AS (
  SELECT ...
  FROM ...
  WHERE ...
)
SELECT *
FROM cte_name;
CTE must come first

The CTE must come before the main query. You can’t define it after SELECT.

The CTE Pattern for Finding Max

Remember the subquery pattern for finding the maximum? Here’s how CTEs make it cleaner:

SQL
-- Subquery approach (works, but nested)
SELECT
  company,
  profits
FROM companies
WHERE profits = (
  SELECT MAX(profits)
  FROM companies
);

-- CTE approach (clearer flow)
WITH max_profit AS (
  SELECT MAX(profits) AS highest
  FROM companies
)
SELECT
  c.company,
  c.profits
FROM companies c
JOIN max_profit m
  ON c.profits = m.highest;

The CTE approach separates “find the max” from “find who has it.” Each step is clear and testable.

Most Profitable Financial Company

Use a CTE to find the maximum profit, then join back to find the company.

Table: forbes_global_2010_2014
companysectorindustrycontinentcountrymarketvaluesalesprofitsassetsrank
ICBCFinancialsMajor BanksAsiaChina215.6148.742.73124.91
China Construction BankFinancialsRegional BanksAsiaChina174.4121.334.22449.54
Agricultural Bank of ChinaFinancialsRegional BanksAsiaChina141.1136.4272405.48
JPMorgan ChaseFinancialsMajor BanksNorth AmericaUnited States229.7105.717.32435.320
Berkshire HathawayFinancialsInvestment ServicesNorth AmericaUnited States309.1178.819.5493.417
2
Most Profitable Financial Company
View solution

Find the most profitable company from the financial sector. Output the result along with the continent.

Tables: forbes_global_2010_2014

Why CTEs Beat Nested Subqueries

Here’s a real example. You want each department’s salary as a percentage of company total.

The Nested Way (Hard to Follow)

SQL
SELECT
  d.department,
  d.total_salary,
  d.total_salary * 100.0 / NULLIF(c.company_total, 0) AS pct
FROM (
  SELECT
    department,
    SUM(salary) AS total_salary
  FROM employees
  GROUP BY department
) AS d
CROSS JOIN (
  SELECT SUM(salary) AS company_total
  FROM employees
) AS c;

The CTE Way (Clear and Logical)

Table: employee
idfirst_namelast_nameagesexemployee_titledepartmentsalarytargetbonusemailcityaddressmanager_id
5MaxGeorge26MSalesSales1300200150Max@company.comCalifornia2638 Richards Avenue1
13KattyBond56FManagerManagement1500000300Katty@company.comArizona1
11RicherdGear57MManagerManagement2500000300Richerd@company.comAlabama1
10JenniferDion34FSalesSales1000200150Jennifer@company.comAlabama13
19GeorgeJoe50MManagerManagement1000000300George@company.comFlorida1003 Wyatt Street1
3
Try CTE for Salary Percentages

Use two CTEs to calculate each department salary as a percentage of the company total.

Tables: employee

Same result, but you can read it top-to-bottom: first we calculate department totals, then company total, then combine them.

Average Salaries

Use a CTE to calculate department averages, then JOIN back to show each employee with their department’s average.

Table: employee
idfirst_namelast_nameagesexemployee_titledepartmentsalarytargetbonusemailcityaddressmanager_id
5MaxGeorge26MSalesSales1300200150Max@company.comCalifornia2638 Richards Avenue1
13KattyBond56FManagerManagement1500000300Katty@company.comArizona1
11RicherdGear57MManagerManagement2500000300Richerd@company.comAlabama1
10JenniferDion34FSalesSales1000200150Jennifer@company.comAlabama13
19GeorgeJoe50MManagerManagement1000000300George@company.comFlorida1003 Wyatt Street1
4
Average Salaries
View solution

Compare each employee's salary with the average salary of the corresponding department. Output the department, first name, and salary of employees along with the average salary of that department.

Tables: employee

CTE Scope

CTEs exist only within their statement. Once the query finishes, they’re gone.

If you need something persistent, you’d create a temporary table or a view. But for most analysis, CTEs are exactly what you need.

Naming Conventions

Name your CTEs well. Good names are documentation.

  • customer_orders tells you what’s in it
  • temp1 tells you nothing
  • dept_avg is clear and concise
  • x is lazy and confusing
Descriptive CTE names

Future you (and your teammates) will thank you for descriptive CTE names.

Pre-Aggregating Before Joining

One of the most common patterns: aggregate data in a CTE, then join it back to add context.

SQL
WITH dept_stats AS (
  SELECT
    department,
    AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department
)
SELECT
  e.first_name,
  e.salary,
  d.avg_salary
FROM employees e
JOIN dept_stats d
  ON e.department = d.department;

Now every employee row shows alongside their department’s average. Want employees above average? Add WHERE e.salary > d.avg_salary.

Highest Salary In Department

Use a CTE to find department-level maximums, then filter or join to find the top earners.

Table: employee
idfirst_namelast_nameagesexemployee_titledepartmentsalarytargetbonusemailcityaddressmanager_id
5MaxGeorge26MSalesSales1300200150Max@company.comCalifornia2638 Richards Avenue1
13KattyBond56FManagerManagement1500000300Katty@company.comArizona1
11RicherdGear57MManagerManagement2500000300Richerd@company.comAlabama1
10JenniferDion34FSalesSales1000200150Jennifer@company.comAlabama13
19GeorgeJoe50MManagerManagement1000000300George@company.comFlorida1003 Wyatt Street1
5
Highest Salary In Department
View solution

Find the employee with the highest salary per department. Output the department name, employee's first name along with the corresponding salary.

Tables: employee
6
Try CTE with Filtering

Write a CTE that totals salary per department, then show only departments where the total exceeds 150000.

Tables: employee

Key Takeaways

  • CTEs use WITH to create named temporary result sets
  • They make complex queries readable by laying out steps top-to-bottom
  • The CTE must come before the main SELECT
  • CTEs exist only within their statement
  • Name CTEs descriptively; they’re documentation
  • The pre-aggregate-and-join pattern is incredibly common

What’s Next

A single CTE is powerful. Multiple CTEs are transformative. Next, you’ll learn to chain CTEs that build on each other, reference the same CTE multiple times, and break complex analysis into logical steps.