CTEs: Standalone, Multiple & Nested
Name a step with WITH … AS, chain several CTEs with commas, and build CTEs on top of earlier CTEs.
Nested subqueries are read from the inside out, which is unnatural. A CTE lets you write the same logic top to bottom, giving each step a name — like writing the steps of a recipe.
Definition
A CTE (Common Table Expression) is a named, temporary result set defined with the
WITHkeyword at the start of a statement. It exists only for the single statement that immediately follows it, and that statement can refer to it by name just like a table.
- Named — you choose a name (
team_totals) and use it in the main query. - Temporary — nothing is saved in the database; when the statement ends, the CTE disappears.
- Single statement — a CTE cannot be reused by the next query. For that you need a view (later topic).
How the database executes a CTE
| Step | What happens |
|---|---|
| 1. Run the CTE | The query inside WITH name AS ( … ) is evaluated |
| 2. Keep the result | The rows are available under that name |
| 3. Run the main query | SELECT … FROM team_totals uses the named result |
| 4. Discard it | When the statement ends, the CTE is gone |
Note: Logically a CTE is like a view that lives for one query. SQL Server does not necessarily store the CTE's rows — it usually inlines the CTE into the main query (and may evaluate it again each time it is referenced). It is a readability tool, not a caching tool. To store a result, use a temp table.
Syntax
sql
WITH cte_name [(column1, column2, ...)] AS (
SELECT ... -- the CTE query
),
another_cte AS ( -- more CTEs: comma, no second WITH
SELECT ... FROM cte_name
)
SELECT ... -- the ONE main statement that uses them
FROM another_cte;| Part | Meaning |
|---|---|
WITH | starts the CTE block — written only once |
cte_name | the name you will use like a table |
(column1, …) | optional column names (otherwise taken from the inner SELECT) |
AS ( … ) | the query that defines the CTE — brackets required |
, | separates multiple CTEs |
| main statement | SELECT (or INSERT / UPDATE / DELETE) that uses the CTEs |
Example — standalone CTE
sql
WITH team_totals AS (
SELECT team, SUM(runs) AS total
FROM players
GROUP BY team
)
SELECT *
FROM team_totals
WHERE total >= 700;| team | total |
|---|---|
| DC | 700 |
| RCB | 750 |
| RR | 850 |
WITH defines the CTE; the main query reads it by name. Same result as the derived-table version — but reads top to bottom.
Example — multiple CTEs
sql
WITH team_totals AS (
SELECT team, SUM(runs) AS total
FROM players
GROUP BY team
),
team_info AS (
SELECT team, team_name
FROM teams
)
SELECT i.team_name, t.total
FROM team_totals AS t
JOIN team_info AS i
ON t.team = i.team
ORDER BY t.total DESC;| team_name | total |
|---|---|
| Rajasthan Royals | 850 |
| Royal Challengers Bengaluru | 750 |
| Delhi Capitals | 700 |
| Mumbai Indians | 520 |
| Chennai Super Kings | 350 |
Step by step: define CTE 1 (team_totals) → add CTE 2 (team_info) after a comma → join them in the main query. WITH is written only once.
Example — nested CTEs (a CTE using a CTE)
sql
WITH totals AS (
SELECT team, SUM(runs) AS total
FROM players
GROUP BY team
),
ranked AS (
SELECT *, RANK() OVER (ORDER BY total DESC) AS rnk
FROM totals -- reads the CTE above
)
SELECT *
FROM ranked
WHERE rnk <= 3;| team | total | rnk |
|---|---|---|
| RR | 850 | 1 |
| RCB | 750 | 2 |
| DC | 700 | 3 |
How it works: totals sums runs per team → ranked reads totals and adds a rank → the main query keeps the top 3. Each CTE can use any CTE defined above it. You cannot filter on rnk in the same SELECT that creates it (window functions run after WHERE), so a CTE step is the standard solution.
Why use CTEs?
| Benefit | Explanation |
|---|---|
| Readability | Steps read top to bottom with meaningful names |
| No repetition | Reference the same CTE several times in the main query |
| Easy debugging | Run each step alone by changing the final SELECT to SELECT * FROM step_name |
| Enables recursion | Recursive CTEs (next topic) are only possible with WITH |
Common mistakes
- ❌ Writing
WITHtwice:WITH a AS (…) WITH b AS (…). ✅WITH a AS (…), b AS (…). - ❌ Using a CTE in a second statement after the main query → "Invalid object name". ✅ a CTE lives for one statement only.
- ❌
ORDER BYinside the CTE → not allowed in SQL Server (unless withTOP). ✅ sort in the main query. - ❌ In SQL Server, a statement before
WITHwithout a semicolon → syntax error. ✅ end the previous statement with;(many people write;WITH). - ❌ Referencing a CTE defined below the current one. ✅ define CTEs in dependency order.
SQL Server vs MySQL / PostgreSQL
Plain CTEs are identical in all three (MySQL needs 8.0+). Only recursive CTEs differ: PostgreSQL and MySQL require WITH RECURSIVE, SQL Server just uses WITH.
Interview tip
Q: CTE vs subquery — which is faster? Usually neither; SQL Server typically produces the same execution plan. Choose a CTE for readability, reuse inside one query, and recursion.
Key points to remember
- A CTE is a named, temporary result set defined with WITH … AS (…) for ONE statement only.
- Nothing is stored in the database; the CTE disappears when the statement ends.
- Multiple CTEs are separated by commas — WITH is written only once.
- A CTE can read any CTE defined above it, so logic can be built step by step (nested CTEs).
- CTEs are mainly a readability tool; performance is usually the same as the equivalent subquery.
- ORDER BY is not allowed inside a CTE in SQL Server (unless TOP is used); sort in the main query.
- In SQL Server the statement before WITH must end with a semicolon.
Try it yourself
Standalone CTE
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Multiple CTEs joined together
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Nested CTEs: totals → rank → top 3
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Practice: batting average from innings
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Revision Flashcards
Tap a card to flip it and see the answer.
