A CTE (Common Table Expression) is a temporary, named result set that is defined and exists only during the execution of a single query.
Imagine you’ve written a query with three nested subqueries inside the FROM, two in the WHERE, and a JOIN to another subquery.
If you try to read or modify that script a week later… you probably won’t understand absolutely anything about what it does.
To solve this chaos, SQL Server introduced CTEs. Their main purpose is not to improve performance (which is usually the same as a subquery), but to improve the readability and organization of our queries.
Think of a CTE as a named temporary table that only exists during the execution of a single query.
Basic Syntax: The WITH Clause
A CTE is defined before your main query using the WITH keyword.
WITH VentasTotales AS (
-- Your complex query goes here
SELECT ClienteID, SUM(Total) as TotalGastado
FROM Pedidos
GROUP BY ClienteID
)
-- Here begins the real query that uses the CTE
SELECT
C.Nombre,
CTE.TotalGastado
FROM Clientes C
INNER JOIN VentasTotales CTE ON C.ClienteID = CTE.ClienteID
WHERE CTE.TotalGastado > 1000;
Notice the change in mindset:
- Define the “Total Sales” dataset.
- Use that clean dataset in the final query.
Chaining CTEs
The real magic emerges when you define multiple CTEs, where the second reads from the first, the third from the second, etc. This allows us to program in a “procedural” and logical way.
Imagine we want to:
Get sales per employee.
Calculate the average sales per employee for the company.
Filter employees who sold more than the average.
With subqueries it would be a nightmare. With CTEs it’s a walk in the park:
WITH VentasPorEmpleado AS (
-- Step 1: Raw grouped data
SELECT EmpleadoID, SUM(Importe) as Total
FROM Ventas
GROUP BY EmpleadoID
),
PromedioGlobal AS (
-- Step 2: Use step 1 to calculate the average
SELECT AVG(Total) as MediaEmpresa
FROM VentasPorEmpleado
)
-- Step 3: Cross both results
SELECT
V.EmpleadoID,
V.Total,
P.MediaEmpresa
FROM VentasPorEmpleado V
CROSS JOIN PromedioGlobal P
WHERE V.Total > P.MediaEmpresa;
The code reads from top to bottom, like a cooking recipe.
Recursive CTEs
So far, CTEs are “syntactic sugar” for organizing code. But there’s something normal subqueries cannot do and CTEs can: call themselves.
Recursive CTEs are used to traverse hierarchies (family trees, company organizational charts, nested product categories).
Structure of a Recursion
It consists of two parts joined by a UNION ALL:
- Anchor Member: The starting point.
- Recursive Member: The query that references the CTE itself (
NombreCTE) to find the “children”.
Let’s see it with an example of a company organizational chart.
We have an
Empleadostable withIDandJefeID. We want to list all employees and know at what level of the hierarchy they are.
It would look something like this:
WITH Organigrama AS (
-- 1. ANCHOR: Find the CEO (The one with no boss or JefeID is NULL)
SELECT EmpleadoID, Nombre, JefeID, 1 AS Nivel
FROM Empleados
WHERE JefeID IS NULL
UNION ALL
-- 2. RECURSIVE: Find employees whose Boss is in the previous step
SELECT
E.EmpleadoID,
E.Nombre,
E.JefeID,
O.Nivel + 1 -- Increase the level
FROM Empleados E
INNER JOIN Organigrama O ON E.JefeID = O.EmpleadoID
)
-- Use the final result
SELECT * FROM Organigrama ORDER BY Nivel;
How does it work?
- SQL executes the Anchor: Finds the CEO (Level 1).
- Executes the recursive part using the CEO as
Organigrama: Finds the directors (Level 2). - Executes the recursive part again using the directors: Finds the managers (Level 3).
- Repeats until it finds no one else and stops.
CTE vs Temporary Tables
Should I save intermediate data in a CTE or in a #TemporaryTable?
| Characteristic | CTE | Temporary Table (#) |
|---|---|---|
| Persistence | None. Only exists in the query. | Lasts until you close the session. |
| Storage | Memory (generally). | Disk (TempDB). |
| Indexes | You cannot create indexes on it. | Yes, you can create indexes. |
| Reusability | Only once. | You can query it multiple times. |
| Performance | Good for small/medium sets. | Better for massive volumes. |
