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.