Common Table Expressions, operator WITH

Common Table Expressions is a temporary dataset that can be accessed in subsequent queries. The WITH operator is used to write a common table expressions.

MySQL 8.4
-- Example of using the WITH clause
WITH Aeroflot_trips AS
    (SELECT TRIP.* FROM Company
        INNER JOIN Trip ON Trip.company = Company.id WHERE name = 'Aeroflot')

SELECT plane, COUNT(plane) AS amount FROM Aeroflot_trips GROUP BY plane;

An expression with WITH is considered "temporary" because the result is not stored somewhere permanently in the database schema, but acts as a temporary representation that exists only for the duration of the query, that is, it is available only during the execution of a SELECT, INSERT, UPDATE, DELETE, or MERGE statement. It is only valid in the query it belongs to, allowing you to improve the structure of the query without polluting the global namespace.

WITH statement syntax

MySQL 8.4
WITH cte_name [(column [, ...])] AS (subquery)
    [, cte_name [(column [, ...])] AS (subquery)] ...
  • [ ]optional part
  • ...can be repeated
cte_name

The name of the common table expression. If you need several expressions, list them separated by commas.

column

Column names of the table expression, separated by commas. If you omit them, they are taken from the subquery.

Optional
subquery

A query whose result can be used in other parts of the SQL query by the name cte_name.

Subquery

Query examples

  1. We create a common table expression Aeroflot_trips containing all the flights made by the Aeroflot airline
MySQL 8.4
WITH Aeroflot_trips AS
    (SELECT plane, town_from, town_to FROM Company
        INNER JOIN Trip ON Trip.company = Company.id WHERE name = 'Aeroflot')

SELECT * FROM Aeroflot_trips;
planetown_fromtown_to
IL-86MoscowRostov
IL-86RostovMoscow
  1. Similarly, we create a common table expression Aeroflot_trips, but with renamed columns
MySQL 8.4
WITH Aeroflot_trips (aeroflot_plane, town_from, town_to) AS
    (SELECT plane, town_from, town_to FROM Company
        INNER JOIN Trip ON Trip.company = Company.id WHERE name = 'Aeroflot')

SELECT * FROM Aeroflot_trips;
aeroflot_planetown_fromtown_to
IL-86MoscowRostov
IL-86RostovMoscow
  1. Using the WITH operator, we define several common table expressions
MySQL 8.4
WITH Aeroflot_trips AS
    (SELECT TRIP.* FROM Company
        INNER JOIN Trip ON Trip.company = Company.id WHERE name = 'Aeroflot'),
    Don_avia_trips AS
    (SELECT TRIP.* FROM Company
        INNER JOIN Trip ON Trip.company = Company.id WHERE name = 'Don_avia')

SELECT * FROM Don_avia_trips UNION SELECT * FROM  Aeroflot_trips;
idcompanyplanetown_fromtown_totime_outtime_in
11811TU-134RostovMoscow1900-01-01T06:12:00.000Z1900-01-01T08:01:00.000Z
11821TU-134MoscowRostov1900-01-01T12:35:00.000Z1900-01-01T14:30:00.000Z
11871TU-134RostovMoscow1900-01-01T15:42:00.000Z1900-01-01T17:39:00.000Z
11881TU-134MoscowRostov1900-01-01T22:50:00.000Z1900-01-02T00:48:00.000Z
11951TU-154RostovMoscow1900-01-01T23:30:00.000Z1900-01-02T01:11:00.000Z
11961TU-154MoscowRostov1900-01-01T04:00:00.000Z1900-01-01T05:45:00.000Z
11452IL-86MoscowRostov1900-01-01T09:35:00.000Z1900-01-01T11:23:00.000Z
11462IL-86RostovMoscow1900-01-01T17:55:00.000Z1900-01-01T20:01:00.000Z

Working with recursion in CTE

CTEs can also be used to perform recursive queries, which allow iterative data processing, for example, working with hierarchical data structures such as manager-subordinate relationships.

Syntax of a recursive CTE

A recursive CTE consists of two parts separated by the UNION ALL operator:

  • The initial dataset, which does not contain recursive references.
  • The recursive part: a query that refers to the CTE to continue the recursion.
MySQL 8.4
WITH RECURSIVE cte_name [(column [, ...])] AS (
    -- Initial dataset
    SELECT expression [, ...]
    FROM table_name
    [WHERE condition]

    UNION ALL

    -- Recursive part
    SELECT expression [, ...]
    FROM cte_name
    INNER JOIN table_name ON join_condition
    [WHERE condition]
)

SELECT * FROM cte_name;
  • [ ]optional part
  • ...can be repeated
cte_name

The name of the table expression. The recursive part refers to it to continue the recursion.

column

Column names of the table expression. Without the list, the names come from the initial part.

Optional
expression

Columns and calculations that each part selects. Both parts select the same number of columns. The initial part sets the column types, and the recursive part's values must fit them. The recursive part often calculates something like level + 1.

table_name
The table with the source data, for example Employees.
join_condition

How the table rows are linked to the rows found at the previous step, for example e.managerId = s.id.

condition

Filters rows: in the initial part, which rows to start from; in the recursive part, which rows to add at the next step.

Optional

Example: manager-subordinate hierarchy

Consider the Employees table, which contains employee IDs and their managers:

idnamemanagerId
1John Smith<NULL>
2Michael Johnson1
3Robert Williams1
4James Brown2
5David Jones2
6Richard Davis3

We need to find all subordinates of John Smith (id=1) at all hierarchy levels.

MySQL 8.4
WITH RECURSIVE Subordinates AS (
    -- Initial dataset
    SELECT id, name, managerId
    FROM Employees
    WHERE managerId = 1

    UNION ALL

    -- Recursive part: subordinates of subordinates
    SELECT e.id, e.name, e.managerId
    FROM Employees e
    INNER JOIN Subordinates s ON e.managerId = s.id
)

SELECT * FROM Subordinates;
idnamemanagerId
2Michael Johnson1
3Robert Williams1
4James Brown2
5David Jones2
6Richard Davis3

Steps for executing a recursive CTE

  1. Initial dataset: selects all employees whose managerId=1 (direct subordinates of John Smith).
  2. Recursive part: for each employee selected in the initial dataset, selects their subordinates (where managerId equals the id of the chosen employee).
  3. Union: unites the results of the initial dataset and the recursive parts using UNION ALL.
  4. Recursion: the process repeats for each new set of subordinates until all hierarchy levels are retrieved.

Conclusion

Common table expressions have been added to SQL to simplify complex long queries, especially those with multiple subqueries. Their main task is to improve readability, ease of writing queries and their further support. It does this by hiding large and complex queries in named expressions, which are then used in the main query.