Introduction: The Problem With Deeply Nested Subqueries
As analytical requirements grow, writing subqueries inside subqueries creates queries that are written and read inside-out:
What Is a CTE? (The Core Mental Model)
SELECT *
FROM employees
WHERE salary > 50000
)
SELECT *
FROM high_salary;
employees
high_salary
Qualified Records
Basic CTE Syntax & Structure
Every basic CTE consists of five fundamental syntax components:
WITH: The keyword that begins the CTE definition block.cte_name: The custom descriptive name assigned to the temporary result.AS: Introduces the query definition.(...): Parentheses enclosing the innerSELECTquery.- Main Query: The final statement that queries from
cte_name.
CTE Step-by-Step Walkthrough
Consider three employee records:
| id | name | salary | CTE Filter (salary > 50000) |
|---|---|---|---|
| 1 | Rahul | $60,000 | Included in high_salary |
| 2 | Amit | $40,000 | Excluded |
| 3 | Priya | $80,000 | Included in high_salary |
The CTE first filters rows into a virtual named dataset (high_salary). The outer query then reads from high_salary as if it were a clean, pre-filtered table.
CTE vs. Subquery: Major Conceptual Comparison
Comparing identical analytical tasks written with a Subquery vs a CTE:
SELECT *
FROM (
SELECT department, AVG(salary) AS avg_sal
FROM employees
GROUP BY department
) AS dept_stats
WHERE avg_sal > 70000;
WITH dept_stats AS (
SELECT department, AVG(salary) AS avg_sal
FROM employees
GROUP BY department
)
SELECT *
FROM dept_stats
WHERE avg_sal > 70000;
└── (Subquery inside FROM)
└── WHERE filter
↓
Step 2: SELECT * FROM named_step
CTE With Aggregation: Computing Totals Before Filtering
In data analytics, you often need to calculate metrics in Step 1 and apply secondary filters in Step 2:
SELECT
department,
SUM(amount) AS total_sales
FROM orders
GROUP BY department
)
SELECT *
FROM department_sales
WHERE total_sales > 100000;
Multiple Chained CTEs (Comma Separation)
You can define multiple sequential CTEs within a single WITH statement by separating them with commas:
monthly_totals AS (
SELECT
DATE_TRUNC('month', order_date) AS order_month,
SUM(amount) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
),
top_performing_months AS (
-- CTE 2 queries CTE 1!
SELECT *
FROM monthly_totals
WHERE revenue > 5000
)
SELECT *
FROM top_performing_months
ORDER BY revenue DESC;
monthly_totals
top_performing_months
Executive Report
CTE With JOIN Operations
A CTE can encapsulate complex joins, simplifying subsequent downstream queries:
SELECT
c.id AS customer_id,
c.name AS customer_name,
o.amount
FROM customers AS c
INNER JOIN orders AS o ON c.id = o.customer_id
)
SELECT customer_name, SUM(amount) AS total_spent
FROM customer_orders
GROUP BY customer_name;
CTE With CASE Categorization
Computing categorical labels in a CTE allows you to filter on that newly created label in the main query:
SELECT
name,
salary,
CASE
WHEN salary >= 90000 THEN 'High'
WHEN salary >= 65000 THEN 'Medium'
ELSE 'Low'
END AS salary_tier
FROM employees
)
SELECT *
FROM employee_tiers
WHERE salary_tier = 'High';
CTE With Date & Time Analysis
Aggregating time buckets with DATE_TRUNC inside a CTE:
SELECT
DATE_TRUNC('quarter', order_date) AS quarter_start,
SUM(amount) AS total_revenue
FROM orders
GROUP BY DATE_TRUNC('quarter', order_date)
)
SELECT *
FROM quarterly_revenue
ORDER BY quarter_start ASC;
Multi-Step Analytical Pipelines
A complete analytics pipeline converting raw order events into actionable management insights:
- Raw Transactions: Granular timestamped orders.
- Step 1 (Monthly Aggregation): Total monthly sales volume.
- Step 2 (Growth Benchmark): Identifying high-performing months.
- Final Result: Clean executive dashboard view.
CTE Statement Scope & Lifecycle
CTE Created
CTE Scope Immediately Ends
Recursive CTEs: Conceptual Preview
In advanced SQL, a Recursive CTE is a specialized CTE that references itself to iteratively traverse hierarchical trees:
(Recursive CTE syntax and mechanics will be explored in depth in a dedicated advanced module).
CTE vs. Temporary Table (CREATE TEMP TABLE)
| Attribute | CTE (Common Table Expression) | Temporary Table (TEMP TABLE) |
|---|---|---|
| Scope | Single SQL statement only | Entire database user session |
| Storage | Inline query-scoped expression | Physical temporary database object |
| Indexing | Cannot be indexed directly | Can create custom indexes |
| Best Use Case | Modularizing single multi-step queries | Heavy multi-query analytical scripts |
Common CTE Mistakes to Avoid
Writing WITH cte1 AS (...), WITH cte2 AS (...). Use WITH once, separated by commas.
Omitting parentheses around the inner query causes a syntax error.
Attempting to query the CTE name after the terminating semicolon causes "relation does not exist".
Using generic names like t1 or temp instead of descriptive domain names like monthly_revenue.
Practical CTE Query Exercises
SELECT
department,
COUNT(*) AS headcount,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department
)
SELECT *
FROM dept_rollup
WHERE avg_salary > 70000;
SELECT
DATE_TRUNC('month', order_date) AS sales_month,
SUM(amount) AS total_revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
),
spike_months AS (
SELECT *
FROM monthly_sales
WHERE total_revenue > 3000
)
SELECT *
FROM spike_months
ORDER BY total_revenue DESC;
SELECT id, name, department, salary
FROM employees
WHERE salary > 70000
)
SELECT * FROM high_salary;
| id | Name | Department | Salary | Status |
|---|---|---|---|---|
| 1 | Rahul Sharma | Engineering | $95,000 | ✓ Captured in CTE |
| 3 | Priya Patel | Engineering | $110,000 | ✓ Captured in CTE |
| 5 | Rohan Gupta | Sales | $75,000 | ✓ Captured in CTE |
SQL CTE Best Practices
- Use Meaningful Domain Names: Name CTEs by their analytical purpose (e.g.
monthly_revenue, nottemp1). - Keep Each CTE Focused on One Step: Avoid creating a single giant 200-line CTE when two 20-line CTEs are much cleaner.
- Refactor Deep Subqueries into CTEs: Improve code review readability for team data analytics workflows.
- Remember Single-Statement Scope: Use Temporary Tables only if intermediate results must persist across multiple distinct queries.
What You Should Know Now: Checklist
- ✓Definition: CTEs assign a temporary name to a query result for use in the main query.
- ✓Syntax:
WITH cte_name AS (...) SELECT ... FROM cte_name; - ✓Multiple CTEs: Defined with a single
WITHkeyword and separated by commas. - ✓Statement Scope: CTEs exist only for the execution lifetime of that single query.
- ✓CTE vs Subquery: CTEs organize multi-step transformations top-to-bottom rather than inside-out.