Tips for Using Common Table Expressions

Greg Larsen explores using multiple Common Table Expressions (CTEs), the MAXRECUSION option, how to use a CTE within a cursor, and what you cannot do within a CTE.

I introduced you to Common Table Expression (CTE) in my last article. In that article, I covered the basic syntax of a CTE and provided a couple of examples of using CTEs. This article will be an extension of that article. This article explores using multiple CTEs, the MAXRECUSION option, how to use a CTE within a cursor, and what you cannot do within a CTE.

Using Multiple CTEs

Multiple CTEs can be used within the scope of a single SELECT, UPDATE, DELETE, INSERT or CREATE VIEW statement. There are a number of different flavors of how multiple CTEs can be used. In this section, I will describe two different ways that you can use multiple CTEs to support querying SQL Server.

For my first example, I will be creating two different CTEs that are used to join some data in a SELECT statement. To do this I will be defining my two CTEs using a WITH statement and then referencing each of the defined CTEs within my SELECT statement. Being able to define two different CTEs with separate logic, and then joining the CTE output in a single SELECT statement makes it easier to write and debug your CTE code, one CTE at a time. Additionally being able to do this makes the code much easier to read, and maintain.

WITH
---------------
-- First CTE --
---------------
SalesDollarPerYear_CTE (SalesPersonID, FullName,Title, SalesTerritory, Year2002, Year2003, Year2004 )
AS (
SELECT
pvt.[SalesPersonID]
,pvt.[FullName]
,pvt.[Title]
,pvt.[SalesTerritory]
,pvt.[2002]
,pvt.[2003]
,pvt.[2004]
FROM (SELECT
soh.[SalesPersonID]
,c.[FirstName] + ' ' + COALESCE(c.[MiddleName], '') + ' ' + c.[LastName] AS [FullName]
,e.[Title]
,st.[Name] AS [SalesTerritory]
,soh.[SubTotal]
,YEAR(DATEADD(m, 6, soh.[OrderDate])) AS [FiscalYear]
FROM [Sales].[SalesPerson] sp
INNER JOIN [Sales].[SalesOrderHeader] soh
ON sp.[SalesPersonID] = soh.[SalesPersonID]
INNER JOIN [Sales].[SalesTerritory] st
ON sp.[TerritoryID] = st.[TerritoryID]
INNER JOIN [HumanResources].[Employee] e
ON soh.[SalesPersonID] = e.[EmployeeID]
INNER JOIN [Person].[Contact] c
ON e.[ContactID] = c.ContactID
) AS soh
PIVOT
(
SUM([SubTotal])
FOR [FiscalYear]
IN ([2002], [2003], [2004])
) AS pvt),
---------------
-- Second CTE --
---------------
SalesCountPerYear_CTE (SalesPersonID, NumberOfOrders, MaxDate)
AS
(
SELECT SalesPersonID, COUNT(*), MAX(OrderDate)
FROM Sales.SalesOrderHeader
GROUP BY SalesPersonID
)
-------------------
-- Use Both CTEs --
-------------------
SELECT * FROM SalesDollarPerYear_CTE Y
JOIN SalesCountPerYear_CTE S
ON Y.SalesPersonID = S.SalesPersonID

In this example the first CTE SalesDollarsPerYear_CTE is defined using a WITH statement like normal. This CTE returns rolled up Sales.SalesOrder history information by sales person. The rolled up information summarizes the order SubTotal amounts by fiscal year. The second CTE is defined right after the first CTE, using the same WITH clause, by placing a comma after the first CTE. Once both CTEs are defined, an easy to read SELECT statement references each CTE. In this case, I joined the output of each CTE based on SalesPersonID.

Not only can you define multiple CTEs and reference them in a single SELECT statement, but you can also have a CTE that references another CTE. In order to do this all you need to do is define the referenced CTE prior to using it. Here is an example where my first CTE is referenced inside the second CTE definition.

USE AdventureWorks;
GO
WITH
---------------
-- First CTE --
---------------
ProductSales_CTE (ProductName, TotalSold)
AS
(
SELECT P.Name, COUNT(*)
FROM Sales.SalesOrderDetail SOD
RIGHT OUTER JOIN Production.Product P
ON SOD.ProductID = P.ProductID
GROUP BY P.Name
),
---------------------------------------------
-- Second CTE that reference the first CTE --
---------------------------------------------
WorstBestProduct_CTE (ProductName, TotalSold)
AS
(
SELECT TOP 1 ProductName, TotalSold
FROM ProductSales_CTE
ORDER BY TotalSold DESC
UNION
SELECT TOP 1 ProductName, TotalSold
FROM ProductSales_CTE
ORDER BY TotalSold ASC
)
--------------------------
-- Using the Second CTE --
--------------------------
SELECT ProductName, TotalSold
FROM WorstBestProduct_CTE;

Here you can see that I first defined a CTE named “ProductSales_CTE”. This CTE calculates the number of items sold for each Production.Product . The second CTE “WorstBestProduct_CTE” references the ProductSales_CTE. In this second CTE, I return the Productname that sold the fewest number of items, as well as the ProductName that sold the most number of items. This was done by referencing the “ProductSales_CTE” twice in my second CTE. The two different SELECT statements sort the ProductSales_CTE output on TotalSales, first in descending order, than in ascending order, and select the first record returned. After both CTEs were defined, the final SELECT statement only needs to reference the “WorstBestProduct_CTE” to execute both CTEs.

MAXRECUSION

When you are writing recursive CTEs, there is a possibility that the code you write could cause an infinite loop. An infinite loop would occur if the recursive part of your CTE always returned at least one row. Here is an example of a recursive CTE that causes an infinite loop.

USE AdventureWorks;
GO
WITH MyCTE(Number) AS
(
SELECT 1 AS Number
UNION ALL
SELECT R.Number + 1
FROM MyCTE AS R
)
SELECT Number
FROM MyCTE;

This is a very simple recursive CTE. This CTE code demonstrates what happens when an infinite loop is coded. This CTE just generates a list of numbers starting from 1 and increments them by 1 for each new number returned from the CTE. Because the recursive part of this CTE does not indicate a stopping point the MAXRECUSION setting will control when this recursion query will stop.

MAXRECUSION can be used to control the number times the recursive part of the query (the SELECT statement that references the CTE) is executed. The default value for MAXRECUSION is 100. If you execute the above code, you will find that after 100 executions of the second SELECT statement in “MyCTE” the following error will occur:

Msg 530, Level 16, State 1, Line 1

The statement terminated. The maximum recursion 100 has been exhausted before statement completion.

After the error occurs, if you look at the output produced, you will see that 101 different numbers were created.

If the logic of your CTE and the data that your CTE works against requires more iteration than the default value for MAXRECUSION, you can override the default number by using the MAXRECURSION query hint. Here is an example where I have modified the above TSQL batch to perform 200 iteration of the recursive SELECT statement in “MyCTE”.

WITH MyCTE(Number) AS
(
SELECT 1 AS Number
UNION ALL
SELECT R.Number + 1
FROM MyCTE AS R
)
SELECT Number
FROM MyCTE 
OPTION(MAXRECURSION 200);
GO

Note that since my CTE is still an infinite loop CTE, that when 200 iteration are reached, an error similar error to above is still thrown.

The maximum number you can set when using the MAXRECUSIVE query hint is 32,767. That does not mean if your data and CTE needs 40,000 iterations to complete that you cannot build a CTE. If you need your CTE to perform more than 32,767 recursive iterations, you can also set the MAXRECUSIVE value to 0 (zero). This tells the optimizer to let this query run through as many iterations as needed to complete your CTE. Here is a modified example of my infinite loop CTE where I set the MAXRECURSIVE value to 0.

WITH MyCTE(Number) AS
(
SELECT 1 AS Number
UNION ALL
SELECT R.Number + 1
FROM MyCTE AS R
WHERE R.Number < 100000
)
SELECT Number
FROM MyCTE 
OPTION(MAXRECURSION 0);
GO

As you can see I have now change my infinite loop CTE so the recursive SELECT statement stops after returning 100000 different numbers. Note, that if your recursive part of your query never returns an empty set, then your CTE query will become an infinite loop. Therefore, be careful when you set the MAXRECURSIVE value to 0.

Using a CTE Within a Cursor

A CTE can also be referenced within a cursor definition. Here is a simple CTE that is referenced in a cursor that retrieves employee records one at a time.

USE [AdventureWorks]
GO
DECLARE Emp_Cursor CURSOR FOR
----------------------
-- CTE defined here --
----------------------
WITH Employee_CTE (EmployeeID, FirstName, MiddleName, LastName, Title, Phone,
EmailAddress, EmployeeAddress, EmployeeCity)
AS (
SELECT
e.[EmployeeID]
,c.[FirstName]
,c.[MiddleName]
,c.[LastName]
,e.[Title] AS [JobTitle]
,c.[Phone]
,c.[EmailAddress]
,a.[AddressLine1]
,a.[City]
FROM [HumanResources].[Employee] e
INNER JOIN [Person].[Contact] c
ON c.[ContactID] = e.[ContactID]
INNER JOIN [HumanResources].[EmployeeAddress] ea
ON e.[EmployeeID] = ea.[EmployeeID]
INNER JOIN [Person].[Address] a
ON ea.[AddressID] = a.[AddressID])
SELECT LastName + ' ,' + FirstName AS EmpName,
EmailAddress
FROM Employee_CTE;
DECLARE @EmpName varchar(100);
DECLARE @Email varchar(100);
DECLARE @I INT;
SET @I = 0;
OPEN Emp_Cursor;
FETCH NEXT FROM Emp_Cursor INTO @EmpName, @Email;
WHILE @@FETCH_STATUS = 0
BEGIN;
SET @I = @I + 1;
PRINT 'Employer #' + CAST(@I AS CHAR(3));
PRINT 'Employname = ' + @EmpName + CHAR(13) +
'Email = ' + @Email + CHAR(13) + CHAR(13);
FETCH NEXT FROM Emp_Cursor INTO @EmpName, @Email;
END;
CLOSE Emp_Cursor;
DEALLOCATE Emp_Cursor;

In this example, I created a CTE named “Employee_CTE”. In order to use that CTE in a cursor, I had to define it right inside the “DECLARE Emp_Cursor CURSOR” statement. Some people make the mistake of trying to define their cursor right before the “DECLARE …CURSOR” statement. When you do this, you will get an error. Once I have defined my CTE inside my cursor definition, the rest of the cursor processing is similar to processing any other cursor.

What Cannot Be Used Within a CTE

Not all T-SQL statements and clauses can be used within a CTE. In some cases certain statements/clauses just cannot be used in a recursive CTEs.

Below, is a list of those statements and/or clauses that cannot be used in ANY CTE.

  • COMPUTE or COMPUTE BY
  • ORDER BY (except when a TOP clause is specified)
  • INTO
  • OPTION clause with query hints
  • FOR XML
  • FOR BROWSE

Here are the statements and/or clauses that cannot be used in a recursive CTE:

  • SELECT DISTINCT
  • GROUP BY
  • HAVING
  • Scalar aggregation (meaning you can’t use min or max)
  • TOP
  • LEFT, RIGHT, OUTER JOIN (INNER JOIN is allowed)
  • Subqueries
  • A hint applied to a recursive reference to a CTE inside a CTE_query_definition.

Simplify Your T-SQL using CTEs

CTEs are very much like creating a view or temporary table, but they can be called recursively. Being able to define multiple CTEs that join two complicated T-SQL statements makes your T-SQL code easier to write, debug and maintain. The power of multiple CTEs and recursion allows you to write your code in a series of multiple building blocks of simple code. By using a CTE, you can simplify your complicated T-SQL logic.

» See All Articles by Columnist Gregory A. Larsen

Gregory Larsen
Gregory Larsen
Gregory A. Larsen is a DBA at Washington State Department of Health (DOH). Greg is responsible for maintaining SQL Server and other database management software. Greg works with customers and developers to design and implement database changes, and solve database/application related problems. Greg builds homegrown solutions to simplify and streamline common database management tasks, such as capacity management.

Get the Free Newsletter!

Subscribe to Cloud Insider for top news, trends & analysis

Latest Articles