What Is a CTE in SQL and How to Use It

A Common Table Expression (CTE) is a temporary result set that is returned by a single statement to be used further within the same statement. As it is a temporary result, it is not stored anywhere and does not consume disk space. However, you can reference it in the same manner as any table.

Alternatively, we can view a CTE as a virtual table containing columns and rows of data. This virtual table is created as the result of the query execution, used by another query, and deleted after that main query execution.

Since the introduction of SQL Server 2005, CTEs have become a popular method for database specialists who apply them to make complex queries easier to read and maintain: as SQL rules demand naming each CTE, the entire code becomes clearer to the reader.

Basic syntax and usage of CTEs

The CTE syntax is as follows:

WITH cte_name (column_1, column_2, column_3,...)   
AS (cte_query_definition)
 
SELECT * 
FROM cte_name;

Such queries consist of two parts, and the first part is the CTE. Common Table Expressions are always introduced by the keyword WITH (that's why a CTE is also called an SQL WITH clause, and you can see this definition in many resources).

Thus, the parameters of the query with a CTE are:

  • WITH – the introductory keyword necessary to define using the CTE. Only one WITH keyword is allowed.
  • cte_name – the current Common Table Expression name.
  • cte_query_definition – the actual CTE you apply.

The second part of the query syntax is a SELECT statement. You should write it right after the CTE without any columns, semicolons, or other punctuation symbols.

Now, let us delve into the practical usage of the CTEs.

Using the simple WITH clause

The simplest query format contains one CTE: the SQL Server first runs the query in the WITH clause, fetches the data, and stores it within a temporary relation. Then the main query uses that temporary relation and delivers the final result.

In the example below, we use the SQL WITH clause to retrieve the list of top products by units sold.

WITH TopSoldProducts AS
(
    SELECT 
        p.ProductID,
        p.Name AS ProductName,
        SUM(sod.OrderQty) AS TotalQuantitySold
    FROM 
        Production.Product p
    JOIN Sales.SalesOrderDetail sod ON p.ProductID = sod.ProductID
    GROUP BY 
        p.ProductID, p.Name
    )

SELECT 
    ProductID,
    ProductName,
    TotalQuantitySold
FROM 
    TopSoldProducts;

Simple WITH clause

Querying multiple CTEs in a single query

Some work scenarios require multiple CTEs to bring the result. There is no need to write several separate queries because SQL allows combining multiple CTEs in a single query with one of the SET operators, such as UNION or UNION ALL.

Note the following rules for constructing a query with multiple CTEs:

  • Only one WITH keyword is allowed.
  • CTEs must be separated with commas.
  • A comma is not needed before the main query.
  • Only one main query is allowed.

Let us apply CTEs to retrieve the information about products along with their associated subcategories and categories:

WITH ProductCategoryCTE AS (
    SELECT
        ProductCategoryID,
        Name AS CategoryName
    FROM
        Production.ProductCategory
),

ProductSubcategoryCTE AS (
    SELECT
        ProductSubcategoryID,
        Name AS SubcategoryName,
        ProductCategoryID
    FROM
        Production.ProductSubcategory
),

ProductDetailCTE AS (
    SELECT
        p.ProductID,
        p.Name AS ProductName,
        p.ProductNumber,
        p.Color,
        p.ListPrice,
        ps.SubcategoryName,
        pc.CategoryName
    FROM
        Production.Product AS p
    LEFT JOIN
        ProductSubcategoryCTE AS ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
    LEFT JOIN
        ProductCategoryCTE AS pc ON ps.ProductCategoryID = pc.ProductCategoryID
)

SELECT
    pd.ProductID,
    pd.ProductName,
    pd.ProductNumber,
    pd.Color,
    pd.ListPrice,
    pd.SubcategoryName,
    pd.CategoryName
FROM
    ProductDetailCTE AS pd
ORDER BY
    pd.ProductID;

Multiple CTEs in one query

Advanced usage of CTEs

In the previous section, we examined the scenarios of referencing simple CTEs, single or multiple in one query. Let us now consider more challenging cases - recursive CTEs and nested CTEs.

Recursive CTE

A recursive CTE involves referencing itself - the result subset returned by the WITH clause is repeatedly referenced until getting the desired result.

The recursive CTE syntax is as follows:

WITH RECURSIVE cte_name AS (
    cte_query_definition (the anchor member)
 
    UNION ALL
 
    cte_query_definition (the recursive member)
)
 
SELECT *
FROM cte_name;

As you see, the basic structure of the recursive CTE syntax is similar to the standard syntax of the non-recursive CTEs. However, there are significant differences that you need to notice when building queries with recursive CTEs:

  • WITH RECURSIVE is used to introduce CTE instead of just WITH.
  • The first query definition is called the anchor member.
  • The second part of the CTE that will reference itself is called the recursive member.
  • UNION or UNION ALL connects the anchor member with the recursive member.

The most common use case for applying recursive CTEs is querying hierarchical data, such as organizational structures, menus, routes, etc.

The following example showcases a recursive CTE, which provides a hierarchical view of the employees within the organization. This view illustrates the organizational structure, where each employee is associated with their manager's EmployeeID and their level in the hierarchy (denoted as EmployeeLevel). This kind of report is beneficial in various professional situations.

DECLARE @MyEmployees TABLE
(
    ManagerID INT,
    EmployeeID INT,
    Title NVARCHAR(100),
    FullName NVARCHAR(200)
);

INSERT INTO @MyEmployees (ManagerID, EmployeeID, Title, FullName)
VALUES
    (NULL, 1, 'CEO', 'Cedric Thornfield'),
    (1, 2, 'VP of Engineering', 'Beatrice Wetherby'),
    (1, 3, 'VP of Product', 'Rupert Blakemore'),

    -- Level 2: Team Lead
    (2, 4, 'Team Lead', 'Rosalind Hawkesworth'),
    (3, 5, 'Team Lead', 'Percival Greyson'),
    
    -- Level 3: Lead
    (4, 6, 'Lead', 'Victoria Radcliffe'),
    (5, 7, 'Lead', 'Julian Crayford'),
    
    -- Level 4: Regular employees
    (6, 8, 'Developer', 'Elliot Stratton'),
    (6, 9, 'Developer', 'Dorothy Winters'),
    (7, 10, 'Developer', 'Leonard Wycliffe'),
    (7, 11, 'Developer', 'Camilla Tyndall');

WITH EmployeeHierarchy(ManagerID, EmployeeID, Title, FullName, EmployeeLevel) AS
(
    -- Base case: CEO (Level 0)
    SELECT ManagerID, EmployeeID, Title, FullName, 0 AS EmployeeLevel
    FROM @MyEmployees
    WHERE ManagerID IS NULL
    UNION ALL
    -- Recursive case: Get all employees reporting to the previous level
    SELECT e.ManagerID, e.EmployeeID, e.Title, e.FullName, EmployeeLevel + 1
    FROM @MyEmployees AS e
        INNER JOIN EmployeeHierarchy AS eh
        ON e.ManagerID = eh.EmployeeID
)
-- Final selection of all employees in hierarchy
SELECT ManagerID, EmployeeID, Title, FullName, EmployeeLevel
FROM EmployeeHierarchy
ORDER BY ManagerID, EmployeeLevel;

Recursive CTE

Another advanced CTE usage technique is working with nested CTEs.

Nested CTE

Like subqueries, CTEs can also be nested, or, in plain words, we can use a CTE definition inside another CTE definition.

The syntax of the nested CTE is as follows:

WITH cte_expression_1 AS (
    WITH cte_expression_2 AS (
        cte_query_definition -- Defines cte_expression_2
    )
    SELECT * FROM cte_expression_2
)
SELECT FROM cte_expression_1;

In this example, cte_expression_1 references the cte_expression_2. When the CTE is defined, we can use it in the subsequent expressions. The most common use case for nested CTEs is applying aggregate functions inside one another, such as when we need to find the average minimum or maximum, define the group comparisons, etc.

The below example demonstrates using the nested query to process order data to identify and rank customers based on their total spending, then filter and display those who have spent more than 350.

DECLARE @Orders TABLE (
    OrderID INT,
    CustomerName NVARCHAR(100),
    OrderDate DATE,
    Amount DECIMAL(10, 2)
);

INSERT INTO @Orders (OrderID, CustomerName, OrderDate, Amount)
VALUES 
(1, 'Alexander Montgomery', '2025-03-01', 250.00),
(2, 'Beatrice Fitzgerald', '2025-03-02', 150.00),
(3, 'Charlotte Winchester', '2025-03-03', 200.00),
(4, 'Edward Kensington', '2025-03-01', 300.00),
(5, 'Beatrice Fitzgerald', '2025-03-05', 180.00),
(6, 'David Harrington', '2025-03-02', 500.00),
(7, 'Evelyn Pembroke', '2025-03-03', 120.00),
(8, 'Edward Kensington', '2025-03-04', 100.00),
(9, 'Charlotte Winchester', '2025-03-04', 350.00),
(10, 'David Harrington', '2025-03-05', 180.00);

-- Using nested CTEs to process the data
WITH CustomerOrders AS (
    -- Outer CTE: Group orders by CustomerName and calculate the total amount spent by each customer
    SELECT CustomerName, SUM(Amount) AS TotalAmount
    FROM @Orders
    GROUP BY CustomerName
),
RankedCustomers AS (
    -- Nested CTE level 1: Rank customers based on the total amount spent
    SELECT CustomerName, TotalAmount,
            RANK() OVER (ORDER BY TotalAmount DESC) AS Rank
    FROM CustomerOrders
),
FilteredCustomers AS (
    -- Nested CTE level 2: Filter customers who spent more than 350
    SELECT CustomerName, TotalAmount, Rank
    FROM RankedCustomers
    WHERE TotalAmount > 350
)
-- Select and display customer names, total amount spent, and rank from the filtered result
SELECT CustomerName, TotalAmount, Rank
FROM FilteredCustomers;

Nested CTE

CTEs vs. other SQL features

Discussing SQL Common Table Expressions (CTEs), it's essential to highlight other SQL features that bear resemblance to CTEs in various aspects. Specifically, these include temporary tables and subqueries. Let's examine how these features differ and determine the most fitting application for each.

CTEs vs. Temporary table

Temporary tables are genuine tables that support indexes and constraints. They remain available during the session and can be accessed by multiple users. In contrast, CTEs are transient datasets that exist solely within a specific query and are only accessible during that query's execution.

Their objectives vary as well. CTEs primarily aid SQL developers in enhancing code clarity. On the other hand, temporary tables prove beneficial when handling large datasets or when a particular result set needs multiple references.

CTEs vs. Subqueries

CTEs and SQL subqueries share many similarities, but their differences dictate their optimal use cases.

As established, CTEs are positioned at the beginning of a query and must have designated names. In contrast, subqueries are integrated inline and don't necessitate naming.

Subqueries are one-time-use within a query, while CTEs can be referenced multiple times and support recursion. It's also worth noting that subqueries are compatible with the WHERE clause using IN and EXISTS, unlike CTEs.

When to use CTEs vs. Subqueries

CTEs are optimal for:

  • Enhancing query readability. While CTEs don't typically influence query performance, they assist SQL developers in comprehending the code.
  • Recursive queries. CTEs are handy for querying hierarchical data since subqueries lack recursive capabilities.

Subqueries are optimal for:

  • Employing IN or EXISTS in the WHERE clause. CTEs are not suitable here.
  • Modifying data in a table with information from another table. Subqueries efficiently extract the required data portion and integrate it into an UPDATE statement for another table.

In various scenarios, the choice between a subquery or a CTE is at the developer's discretion since both tools can serve multiple operational contexts.


🛠 Feature 📋 Characteristics 🔄 Reusability 📈 Best use case
CTEs (Common Table Expressions) Transient datasets within a query, named and positioned at the start. Multi-use Enhancing query readability and recursive queries.
Temporary tables Genuine tables with indexes and constraints, accessible during a session. Multi-use Handling large datasets or when results need multiple references.
Subqueries Inline queries without naming, compatible with WHERE clause using IN/EXISTS. Single-use Using IN/EXISTS in a WHERE clause or modifying data in another table.

Conclusion

In the realm of SQL Server, Common Table Expressions (CTEs) stand out as a potent tool for database developers and administrators. They provide a more readable format for complex queries, serve as a building block for recursive operations, and make it easier to manage and debug code. This not only enhances the developer experience but also aids in optimizing query performance.

It's essential, however, to understand when and how to use CTEs effectively. Having proper tools, such as dbForge SQL Complete can also help the practitioners significantly by providing a comprehensive set of features for SQL coding, including context-based suggestions, code beautifying, debugging, and more to simplify all code writing tasks and ensure the highest quality of the results.

Frequently Asked Questions

What is a CTE in SQL?

A Common Table Expression (CTE) is a temporary named result set that you can reference within a SELECT, INSERT, UPDATE, or DELETE statement. It exists only for the duration of the query and improves the readability and maintainability of complex SQL.

What is the difference between a CTE and a subquery?

CTEs are named and can be referenced multiple times within a query, while subqueries aren't named, are nested within another query, and must be repeated if needed again. Unlike subqueries, CTEs can be recursive. Also, CTEs are generally more readable, especially for complex logic.

How to use multiple CTEs in a single SQL query?

You can define multiple CTEs by separating them with commas within the WITH clause, as in the following example.

WITH cte_1 AS (
    SELECT * FROM table_1
),
cte_2 AS (
    SELECT * FROM table_2
)

SELECT * 
FROM cte_1 
JOIN cte_2 ON cte_1.id = cte_2.id;
What are the limitations of CTEs in SQL Server?

CTEs in SQL Server aren't physically stored and can't be indexed. A CTE must be immediately followed by a single SELECT, INSERT, UPDATE, or DELETE statement that references it. A CTE defined in a query can't be referenced in subsequent, separate SQL statements, even within the same session or transaction. Also, recursive CTEs have a default maximum recursion level of 100, but it can be modified.

What is the syntax of CTE in SQL?

The following in the basic syntax.

WITH cte_name AS (
    cte_query_definition
)
SELECT * FROM cte_name;

WITH introduces the CTE, followed by its name and the query definition in parentheses.

You can add column names after the CTE name so that these names appear in the CTE result set instead of the column names from the SELECT statement within the CTE. This helps ensure clarity and meaningful column names, especially when the CTE involves complex calculations or joins. The order of the column names specified after the CTE name should match the order of the columns in the SELECT statement within the CTE.

WITH cte_name (column1, column2) AS (
    SELECT colA, colB
    FROM table_name
)
SELECT column1, column2
FROM cte_name;

In this example, column1 and column2 are the names that will appear in the result set of the CTE, replacing the original column names colA and colB, respectively.