Skip to main content

Command Palette

Search for a command to run...

Advanced SQL Techniques - CTE

Common Table Expression

Published
โ€ข8 min readโ€ขView as Markdown

A Common Table Expression (CTE) is a temporary, named result set that you can reference within a SELECT, INSERT, UPDATE, or DELETE statement. Think of it as a temporary view that exists only for the duration of a single query, making complex queries more readable and manageable.


The Anatomy of a CTE

A CTE is defined using the WITH clause. The basic syntax is straightforward and consists of two main parts: the CTE definition and the final query that uses it.

Basic Syntax:

WITH CteName (column1, column2, ...) AS (
    -- CTE query definition (a SELECT statement)
    SELECT ...
    FROM ...
    WHERE ...
)
-- Main query that uses the CTE
SELECT ...
FROM CteName;
  • WITH clause: This keyword initiates the CTE.

  • CteName: This is the temporary name you assign to the result set.

  • (column1, column2, ...): This is an optional list of column names for the CTE. If omitted, the columns from the SELECT statement in the CTE's definition are used.

  • AS (...): The SELECT query inside the parentheses defines the logic and the data for the CTE.

  • Main Query: The final SELECT (or INSERT, UPDATE, DELETE) statement that references CteName as if it were a regular table.

A single WITH clause can also define multiple CTEs, separated by commas.

WITH Cte1 AS (
    -- Query for the first CTE
),
Cte2 AS (
    -- Query for the second CTE, which can reference Cte1
)
SELECT ...
FROM Cte2;

Why Use CTEs?

CTEs offer several significant advantages over traditional subqueries or temporary tables.

  • Readability and Maintainability: CTEs break down complex logic into distinct, logical steps. Instead of nesting subqueries deep inside one another, you can define each logical unit as a separate CTE, making the final query much easier to read, debug, and maintain. ๐Ÿ“–

  • Recursion: This is the superpower of CTEs. They can reference themselves to query hierarchical data, like organizational charts, file systems, or bills of materials. This is impossible to do with simple subqueries.

  • Reusability within a Query: A CTE can be referenced multiple times within the same main query, which helps to avoid repeating the same logic.


Types of CTEs

There are two main types of CTEs: non-recursive and recursive.

Non-Recursive CTEs

These are the most common type. They are simple, named result sets used to simplify complex joins and subqueries.

Example Scenario: Find all employees who earn more than the average salary in their respective departments.

Let's assume we have an employees table with employee_id, name, department, and salary.

Without a CTE, you might write a correlated subquery or a join with an aggregated subquery, which can be clunky.

With a CTE, the logic is clean:

-- Step 1: Define a CTE to calculate the average salary for each department
WITH DepartmentAvgSalary AS (
    SELECT
        department,
        AVG(salary) AS avg_salary_for_dept
    FROM
        employees
    GROUP BY
        department
)
-- Step 2: Join the original table with the CTE to find the employees
SELECT
    e.name,
    e.department,
    e.salary,
    das.avg_salary_for_dept
FROM
    employees AS e
JOIN
    DepartmentAvgSalary AS das
ON
    e.department = das.department
WHERE
    e.salary > das.avg_salary_for_dept;

This query is easy to follow: first, we calculate the average salary per department and name it DepartmentAvgSalary. Then, we use that result set to filter our employees table.

Recursive CTEs

A recursive CTE is one that references itself. It's perfect for traversing hierarchical or graph-like data structures. A recursive CTE must have at least two parts:

  1. Anchor Member: The base query that runs first and returns the initial set of rows. It does not reference the CTE itself.

  2. Recursive Member: The query that references the CTE. It's joined with the anchor member using UNION ALL. This member is executed repeatedly, with its input being the output from the previous execution, until it returns no more rows.

Example Scenario: Display an organizational hierarchy, starting from the CEO.

Imagine an employees table with employee_id, name, and manager_id. The CEO is the employee whose manager_id is NULL.

WITH RECURSIVE EmployeeHierarchy AS (
    -- 1. Anchor Member: Select the top-level employee (the CEO)
    SELECT
        employee_id,
        name,
        manager_id,
        0 AS hierarchy_level -- Start level at 0
    FROM
        employees
    WHERE
        manager_id IS NULL

    UNION ALL

    -- 2. Recursive Member: Join employees with their managers
    SELECT
        e.employee_id,
        e.name,
        e.manager_id,
        h.hierarchy_level + 1 -- Increment level for each step down
    FROM
        employees AS e
    JOIN
        EmployeeHierarchy AS h
    ON
        e.manager_id = h.employee_id -- The recursive join condition
)
-- Final query to select all data from the hierarchy
SELECT
    *
FROM
    EmployeeHierarchy;

How it works:

  1. The anchor member runs once and finds the CEO (where manager_id IS NULL), assigning them hierarchy_level 0.

  2. The recursive member then takes the result from the anchor (the CEO) and finds all employees (e) whose manager_id matches the CEO's employee_id. These are the direct reports, and they are assigned hierarchy_level 1.

  3. The process repeats: the recursive member now takes the direct reports (level 1) as its input and finds their direct reports, assigning them hierarchy_level 2.

  4. This continues until the recursive member finds no more employees to add, and the final result set contains the entire organizational tree.


CTE vs. Temp Table

While both Common Table Expressions (CTEs) and temporary tables can solve similar joining problems by simplifying complex queries, they differ fundamentally in their scope, storage, and performance characteristics. In essence, a CTE is a disposable, named subquery that exists only for a single query's execution, whereas a temporary table is a physical table created in the database that persists for the entire session.

Here's a detailed breakdown of the differences:


Key Differences at a Glance

Feature

Common Table Expression (CTE)

Temporary Table

Lifecycle & Scope

Exists only for the duration of a single SELECT, INSERT, UPDATE, or DELETE statement.

Persists for the entire user session or until explicitly dropped.

Storage

Typically processed in memory; it's a logical construct, not physically stored.

Physically created in the tempdb database, involving disk I/O.

Performance

Generally faster for smaller, single-use result sets as it avoids disk I/O. Can be slower if referenced multiple times as it may be re-evaluated each time.

Often better for large datasets or when the intermediate result is used multiple times, as it can be indexed and has statistics.

Indexing

Cannot be indexed.

Can have indexes, constraints, and statistics, which can significantly boost performance.

Recursion

Uniquely supports recursive queries, making it ideal for hierarchical data.

Does not support recursion directly.

Readability

Excellent for improving the readability and structure of complex queries.

Can also simplify queries but requires separate CREATE and INSERT statements.

Use in Views

Can be used within a view definition.

Cannot be created or used within a view definition.


Performance Deep Dive ๐Ÿš€

The performance comparison isn't always straightforward and heavily depends on the specific scenario.

When to Favor a CTE for Performance:

  • Small, Simple Datasets: For intermediate results with a relatively small number of rows, CTEs are generally faster because they operate in memory and avoid the overhead of writing to and reading from the tempdb on disk.

  • Single Use: When the temporary result set is needed only once within the query, a CTE is a clean and efficient choice. The database optimizer can often integrate the CTE's logic directly into the main query's execution plan for better optimization.

When a Temporary Table Shines for Performance:

  • Large Datasets: When dealing with a large intermediate result set, materializing it into a temporary table can be more efficient. This prevents the database from having to hold a massive amount of data in memory.

  • Multiple Uses: If you need to reference the intermediate result set multiple times in subsequent queries within the same session, a temporary table is the clear winner. The data is written to disk once and can be read multiple times, avoiding the potential re-computation of a CTE.

  • Indexing and Statistics: This is a major advantage of temporary tables. You can create indexes on a temporary table, which can dramatically speed up joins and filtering in later parts of your script. The database optimizer can also use the statistics on the temporary table to create more efficient execution plans for subsequent queries.

Practical Scenarios

Use a CTE when:

  • You need to perform a recursive query, like traversing an organizational hierarchy.

  • You want to break down a complex query into logical, readable steps for a single operation.

  • The intermediate data set is small and will only be used once in the main query.

  • You are creating a view and need to simplify its logic.

Use a Temporary Table when:

  • You have a large amount of intermediate data that needs to be processed.

  • You need to reuse the same intermediate data across multiple, separate queries within the same session.

  • You need to create indexes on the intermediate data to improve the performance of subsequent operations.

  • You are building a complex multi-step data transformation process within a stored procedure.

Conclusion

Think of a CTE as a "disposable view" or a neat way to organize your thoughts within a single query. It's a logical construct designed for readability and simplicity. A temporary table, on the other hand, is a real, albeit short-lived, table that provides more power and flexibility for complex, multi-stage data manipulations, especially when performance at scale is a concern. The best choice ultimately depends on the complexity of your query, the size of your data, and how many times you need to access the intermediate results.

More from this blog

S

SQL Insights

31 posts