Table of Contents

Common Table Expression (CTE)

To get familiar with CTE, you can check documentation for Transact SQL: WITH common_table_expression

When CTEs are useful

  • Reusing the same SQL part in complex query
  • Recursive table processing

Defining simple CTE

CTE in LINQ To DB implements IQueryable and any IQueryable can be converted to CTE with the extension method AsCte("optional_name").

var employeeSubordinatesReport  =
    from e in db.Employee
    select new
    {
        e.EmployeeID,
        e.LastName,
        e.FirstName,
        NumberOfSubordinates = db.Employee
            .Where(e2 => e2.ReportsTo == e.ReportsTo)
            .Count(),
        e.ReportsTo
    };

// define  CTE named EmployeeSubordinatesReport
// employeeSubordinatesReport sub-query used as CTE body 
var employeeSubordinatesReportCte = employeeSubordinatesReport
                                     .AsCte("EmployeeSubordinatesReport");

The variable employeeSubordinatesReportCte can now be reused in other parts of linq query.

var result =
    from employee in employeeSubordinatesReportCte
    from manager in employeeSubordinatesReportCte
        .LeftJoin(manager => employee.ReportsTo == manager.EmployeeID)
    select new
    {
        employee.LastName,
        employee.FirstName,
        employee.NumberOfSubordinates,
        ManagerLastName = manager.LastName,
        ManagerFirstName = manager.FirstName,
        ManagerNumberOfSubordinates = manager.NumberOfSubordinates
   };

You are not limited in the number of CTEs, defined in a query, and they may reference each other. LINQ To DB will put them in the correct order and generate SQL with one limitation - there should be no circular references between CTEs.

WITH [EmployeeSubordinatesReport]
(
    [ReportsTo],
    [EmployeeID],
    [LastName],
    [FirstName],
    [NumberOfSubordinates]
)
AS
(
    SELECT
        [t2].[ReportsTo],
        [t2].[EmployeeID],
        [t2].[LastName],
        [t2].[FirstName],
        (
            SELECT
            Count(*)
            FROM
            [Employees] [t1]
            WHERE
            [t1].[ReportsTo] IS NULL AND [t2].[ReportsTo] IS NULL OR
            [t1].[ReportsTo] = [t2].[ReportsTo]
        ) as [c1]
    FROM
      [Employees] [t2]
)
SELECT
    [t3].[LastName] as [LastName1],
    [t3].[FirstName] as [FirstName1],
    [t3].[NumberOfSubordinates],
    [manager].[LastName] as [LastName2],
    [manager].[FirstName] as [FirstName2],
    [manager].[NumberOfSubordinates] as [NumberOfSubordinates1]
FROM
    [EmployeeSubordinatesReport] [t3]
        LEFT JOIN [EmployeeSubordinatesReport] [manager]
            ON [t3].[ReportsTo] = [manager].[EmployeeID]

Defining recursive CTE

Recursive CTEs are special because they are allowed to reference themselves! Because of this special ability, you can use recursive CTEs to solve problems other queries cannot. As an example, recursive CTEs are really good at working with hierarchical data such as org charts for bill of materials. (Further reading: Recursive CTEs Explained).

CTEs have limitations that are not handled by LINQ To DB, so you have to be aware of them before start of usage - Guidelines for Defining and Using Recursive Common Table Expressions

Since in C# language we can not use a variable's reference in its own initialization expression, we have created a function that helps in defining recursive queries: GetCte<TCteProjection>(cte => ...). TCteProjection is a required generic parameter that is needed for resolving the type of the lambda parameter.

The following example shows how to define a CTE to calculate the employee level in the hierarchy:

// defining class for representing Recursive CTE
class EmployeeHierarchyCTE
{
    public int EmployeeID;
    public string LastName;
    public string FirstName;
    public int? ReportsTo;
    public int HierarchyLevel;
}

using (var db = new NorthwindDB(context))
{
    var employeeHierarchyCte = db.GetCte<EmployeeHierarchyCTE>(employeeHierarchy =>
    {
        return
            (
                from e in db.Employee
                where e.ReportsTo == null
                select new EmployeeHierarchyCTE
                {
                    EmployeeID = e.EmployeeID,
                    LastName = e.LastName,
                    FirstName = e.FirstName,
                    ReportsTo = e.ReportsTo,
                    HierarchyLevel = 1
                }
            )
            .Concat
            (
                from e in db.Employee
                from eh in employeeHierarchy
                    .InnerJoin(eh => e.ReportsTo == eh.EmployeeID)
                select new EmployeeHierarchyCTE
                {
                    EmployeeID = e.EmployeeID,
                    LastName = e.LastName,
                    FirstName = e.FirstName,
                    ReportsTo = e.ReportsTo,
                    HierarchyLevel = eh.HierarchyLevel + 1
                }
            );
    });

    var result =
        from eh in employeeHierarchyCte
        orderby eh.HierarchyLevel, eh.LastName, eh.FirstName
        select eh;

    var data = result.ToArray();
}

Resulting SQL:

WITH [employeeHierarchy]
(
    [EmployeeID],
    [LastName],
    [FirstName],
    [ReportsTo],
    [HierarchyLevel]
)
AS
(
    SELECT
        [t1].[EmployeeID],
        [t1].[LastName],
        [t1].[FirstName],
        [t1].[ReportsTo],
        1 as [c1]
    FROM
        [Employees] [t1]
    WHERE
        [t1].[ReportsTo] IS NULL
    UNION ALL
    SELECT
        [t2].[EmployeeID],
        [t2].[LastName],
        [t2].[FirstName],
        [t2].[ReportsTo],
        [eh].[HierarchyLevel] + 1 as [c1]
    FROM
        [Employees] [t2]
            INNER JOIN [employeeHierarchy] [eh] ON [t2].[ReportsTo] = [eh].[EmployeeID]
)

SELECT
    [t3].[EmployeeID] as [EmployeeID2],
    [t3].[LastName] as [LastName2],
    [t3].[FirstName] as [FirstName2],
    [t3].[ReportsTo] as [ReportsTo2],
    [t3].[HierarchyLevel]
FROM
    [employeeHierarchy] [t3]
ORDER BY
    [t3].[HierarchyLevel],
    [t3].[LastName],
    [t3].[FirstName]

Database engines that support CTE

Database Engine Minimal version
Firebird 2.1
MS SQL 2008
MySQL 8.0.1
Oracle 11g Release 2
PostgreSQL 8.4
SQLite 3.8.3
IBM DB2 8
IBM Informix 14.10
ClickHouse

Known limitations

  • Oracle and Firebird DML operations that use CTE are not completely implemented.
  • Informix CTE are not yet implemented.