Common Table Expressions (CTEs)
Common Table Expressions (CTEs)
Section titled โCommon Table Expressions (CTEs)โWhat it is
Section titled โWhat it isโCTEs (Common Table Expressions) allow defining temporary named result sets that exist only during query execution. Introduced in SQL Server 2005, they improve readability and enable recursive queries.
Before this feature
Section titled โBefore this featureโRequired derived tables or temp tables:
-- Derived table (hard to read)SELECT e.Name, d.DeptName, d.AvgSalaryFROM Employees eINNER JOIN ( SELECT DepartmentId, AVG(Salary) as AvgSalary, DepartmentName as DeptName FROM Employees GROUP BY DepartmentId, DepartmentName) d ON e.DepartmentId = d.DepartmentIdWHERE e.Salary > d.AvgSalary;
-- Temp table (more code)CREATE TABLE #DeptAvg ( DepartmentId INT, AvgSalary DECIMAL(10,2));INSERT INTO #DeptAvgSELECT DepartmentId, AVG(Salary)FROM EmployeesGROUP BY DepartmentId;
SELECT e.Name FROM Employees eINNER JOIN #DeptAvg d ON e.DepartmentId = d.DepartmentId;
DROP TABLE #DeptAvg;After this feature
Section titled โAfter this featureโCTEs provide clean, readable syntax:
-- Simple CTEWITH DepartmentAvg AS ( SELECT DepartmentId, AVG(Salary) as AvgSalary FROM Employees GROUP BY DepartmentId)SELECT e.Name, e.Salary, d.AvgSalaryFROM Employees eINNER JOIN DepartmentAvg d ON e.DepartmentId = d.DepartmentIdWHERE e.Salary > d.AvgSalary;
-- Multiple CTEsWITHActiveEmployees AS ( SELECT * FROM Employees WHERE Status = 'Active'),DeptSummary AS ( SELECT DepartmentId, COUNT(*) as EmpCount FROM ActiveEmployees GROUP BY DepartmentId)SELECT d.Name, ds.EmpCountFROM Departments dINNER JOIN DeptSummary ds ON d.Id = ds.DepartmentId;
-- Recursive CTE (hierarchical data)WITH EmployeeHierarchy AS ( -- Anchor member SELECT EmployeeId, Name, ManagerId, 1 as Level FROM Employees WHERE ManagerId IS NULL
UNION ALL
-- Recursive member SELECT e.EmployeeId, e.Name, e.ManagerId, eh.Level + 1 FROM Employees e INNER JOIN EmployeeHierarchy eh ON e.ManagerId = eh.EmployeeId)SELECT * FROM EmployeeHierarchyORDER BY Level, Name;Why this is better
Section titled โWhy this is betterโ- Readable: Named result sets are self-documenting
- Reusable: Reference CTE multiple times in same query
- Recursive: Enable hierarchical queries (org charts, categories)
- Maintainable: Easier to modify than nested subqueries
- Scoped: Exists only for the query (no cleanup needed)
Key notes / edge cases
Section titled โKey notes / edge casesโ- CTEs are not stored; theyโre executed with each reference
- Multiple references can impact performance (consider temp tables for large data)
- Recursive CTEs need termination condition to avoid infinite loops
- Canโt use ORDER BY in CTE definition (unless with TOP or OFFSET)
- Must be followed immediately by SELECT, INSERT, UPDATE, or DELETE
-- Performance consideration-- If CTE is referenced multiple times, consider temp tableWITH LargeCTE AS ( SELECT * FROM LargeTable WHERE ... -- expensive query)SELECT * FROM LargeCTEUNION ALLSELECT * FROM LargeCTE; -- CTE executed twice!
-- Recursive CTE with MAXRECURSION hintWITH NumberSequence AS ( SELECT 1 as Num UNION ALL SELECT Num + 1 FROM NumberSequence WHERE Num < 100)SELECT * FROM NumberSequenceOPTION (MAXRECURSION 100);Quick practice
Section titled โQuick practiceโ-
Write a CTE that finds all employees earning above average
Answer
WITH AvgSalary AS (SELECT AVG(Salary) as AverageFROM Employees)SELECT e.Name, e.SalaryFROM Employees e, AvgSalary aWHERE e.Salary > a.Average; -
Create a recursive CTE to generate numbers 1 to 10
Answer
WITH Numbers AS (SELECT 1 as NUNION ALLSELECT N + 1 FROM Numbers WHERE N < 10)SELECT * FROM Numbers; -
When would you use a temp table instead of a CTE?
Answer
Use temp tables when: 1) CTE is referenced many times (performance), 2) need to create indexes on results, 3) results need to persist across multiple statements.