Hello everybody!
How are you ?
In this article I would like to demonstrate to you How to Create Recursive Queries with Common Table Expressions (CTE) in SQL Server. The CTE feature has been available since the 2005 release, and even today, many people are unaware of this interesting and useful feature in everyday life.
Introduction
A CTE is very similar to a subquery or derived table, with the advantage that the dataset can be used more than once in the query, gaining performance (in this situation) and also improving code readability. For these reasons, the use of ETC has been widespread as a replacement for other solutions cited.
Example query with CTE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
;WITH CTE1 AS ( SELECT Id_Bairro, Nm_Cliente, COUNT(Id_Pedido) AS Qt_Pedidos FROM Clientes LEFT JOIN Pedidos ON Clientes.Id_Cliente = Pedidos.Id_Pedido GROUP BY Id_Bairro, Nm_Cliente ), CTE2 AS ( SELECT Nm_Bairro, COUNT(Nm_Cliente) AS Qt_Clientes, SUM(Qt_Pedidos) AS Qt_Pedidos FROM Bairro LEFT JOIN CTE1 ON Bairro.Id_Bairro = CTE1.Id_Bairro GROUP BY Nm_Bairro ) SELECT Nm_Bairro, Qt_Clientes, Qt_Pedidos, CAST(Qt_Pedidos AS NUMERIC(15, 2)) / Qt_Clientes AS Media FROM CTE2; |
Query example with Subquery:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
SELECT Nm_Bairro, Qt_Clientes, Qt_Pedidos, CAST(Qt_Pedidos AS NUMERIC(15, 2)) / Qt_Clientes AS Media FROM ( SELECT Nm_Bairro, COUNT(Nm_Cliente) AS Qt_Clientes, SUM(Qt_Pedidos) AS Qt_Pedidos FROM Bairro LEFT JOIN ( SELECT Id_Bairro, Nm_Cliente, COUNT(Id_Pedido) AS Qt_Pedidos FROM Clientes LEFT JOIN Pedidos ON Clientes.Id_Cliente = Pedidos.Id_Pedido GROUP BY Id_Bairro, Nm_Cliente ) AS Q1 ON Bairro.Id_Bairro = Q1.Id_Bairro GROUP BY Nm_Bairro ) AS Q2 |
In addition, a CTE provides the significant advantage of being able to self-reference, thus creating a recursive CTE, which is performed repeatedly to return subsets of data until the full result is obtained.
Recursive CTE is often used to return hierarchical data, such as displaying employees on an organization chart, where they can have multiple hierarchical levels, and I will demonstrate here an example of how recursive CTE works.
Recursive CTE Examples
Example of recursive CTE using a sequence of numbers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
WITH CTE_Numerico (Nivel, Numero) AS ( -- Âncora (nível 1) SELECT 1 AS Nivel, 1 AS Numero UNION ALL -- Níveis recursivos (Níveis N) SELECT Nivel + 1, Numero + Numero FROM CTE_Numerico WHERE Numero < 2048 ) SELECT * FROM CTE_Numerico |
Example of recursive CTE using hierarchy:
Testing mass creation - CLR Corporation employees
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
IF (OBJECT_ID('tempdb..#Funcionarios') IS NOT NULL) DROP TABLE #Funcionarios CREATE TABLE #Funcionarios ( Id_Empregado INT IDENTITY(1, 1) NOT NULL, Nm_Empregado VARCHAR(60) NOT NULL, Id_Superior INT NULL ) INSERT INTO #Funcionarios(Nm_Empregado, Id_Superior) VALUES ('Edvaldo Neves', NULL), ('Fabricio Amante', 1), ('Caio Lima', 1), ('Tiago Castro', 2) INSERT INTO #Funcionarios(Nm_Empregado, Id_Superior) VALUES ('Reginaldo Oliveira', 3), ('Fábio Merazzi', 3), ('Dirceu Resende', 2), ('Luiz Vitor Neves', 2) INSERT INTO #Funcionarios(Nm_Empregado, Id_Superior) VALUES ('Vithor Lima', 1), ('Edimar Lellis', 9), ('Lucas Fardim', 9), ('Aquila Loureiro', 9) INSERT INTO #Funcionarios(Nm_Empregado, Id_Superior) VALUES ('Rodrigo Almeida', 9), ('Flávio Castro', NULL), ('Raul Farias', 14), ('Logan Castro', 1) INSERT INTO #Funcionarios(Nm_Empregado, Id_Superior) VALUES('Fábio Amante', 3), ('Ariel Neves', 3), ('Leandro Galon', 7), ('Lucas Keller', 7) INSERT INTO #Funcionarios(Nm_Empregado, Id_Superior) VALUES('Richardson Folha', 19), ('Rafaela Giugliet', 20) SELECT * FROM #Funcionarios |
Given the above structure, I want to create the hierarchy levels of these employees. How would we do this without using CTE?
Example with manual hierarchy
A very common way to create a data hierarchy in SQL Server for a few levels is through manual insertions.
Although it is quite simple (basically, Ctrl + C and Ctrl + V), when we need to automate this work or we have many levels in our hierarchy (it may even be variable), this solution ends up not being very well, leading us to look for a solution. robust.
Example of Manual Hirarchy Creation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
IF (OBJECT_ID('tempdb..#Hiearquia') IS NOT NULL) DROP TABLE #Hiearquia CREATE TABLE [#Hiearquia] ( [Id_Empregado] INT NOT NULL, [Nm_Empregado] VARCHAR(60) NOT NULL, [Id_Superior] INT, [Nivel] INT ) -- Nível 1 INSERT INTO #Hiearquia SELECT Id_Empregado, Nm_Empregado, Id_Superior, 1 AS Nivel FROM #Funcionarios WHERE Id_Superior IS NULL -- Nível 2 INSERT INTO #Hiearquia SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, 2 AS Nivel FROM #Funcionarios A JOIN #Hiearquia B ON A.Id_Superior = B.Id_Empregado WHERE NOT EXISTS(SELECT NULL FROM #Hiearquia WHERE Id_Empregado = A.Id_Empregado) -- Nível 3 INSERT INTO #Hiearquia SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, 3 AS Nivel FROM #Funcionarios A JOIN #Hiearquia B ON A.Id_Superior = B.Id_Empregado WHERE NOT EXISTS(SELECT NULL FROM #Hiearquia WHERE Id_Empregado = A.Id_Empregado) -- Nível 4 INSERT INTO #Hiearquia SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, 4 AS Nivel FROM #Funcionarios A JOIN #Hiearquia B ON A.Id_Superior = B.Id_Empregado WHERE NOT EXISTS(SELECT NULL FROM #Hiearquia WHERE Id_Empregado = A.Id_Empregado) -- Nível 5 INSERT INTO #Hiearquia SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, 5 AS Nivel FROM #Funcionarios A JOIN #Hiearquia B ON A.Id_Superior = B.Id_Empregado WHERE NOT EXISTS(SELECT NULL FROM #Hiearquia WHERE Id_Empregado = A.Id_Empregado) SELECT * FROM #Hiearquia |
Example with LOOP WHILE
In order to create a more robust than the previous one that works with N levels of hierarchy, we can use a LOOP WHILE for this, which will create an iteration on each level from the anchor (first level) until the insert finds no more records to inserted.
Example of insertion using LOOP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
IF (OBJECT_ID('tempdb..#Hiearquia') IS NOT NULL) DROP TABLE #Hiearquia CREATE TABLE [#Hiearquia] ( [Id_Empregado] INT NOT NULL, [Nm_Empregado] VARCHAR(60) NOT NULL, [Id_Superior] INT, [Nivel] INT ) -- Nível 1 INSERT INTO #Hiearquia SELECT Id_Empregado, Nm_Empregado, Id_Superior, 1 AS Nivel FROM #Funcionarios WHERE Id_Superior IS NULL DECLARE @Nivel INT = 2 WHILE(1=1) BEGIN INSERT INTO #Hiearquia SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, @Nivel AS Nivel FROM #Funcionarios A JOIN #Hiearquia B ON A.Id_Superior = B.Id_Empregado WHERE NOT EXISTS(SELECT NULL FROM #Hiearquia WHERE Id_Empregado = A.Id_Empregado) IF (@@ROWCOUNT <= 0) BREAK SET @Nivel += 1 END SELECT * FROM #Hiearquia |
As you can see, I used a while loop to query the table itself, thus chaining the data and assembling the hierarchy.
But what about recursive CTE? Could she return this same result with just 1 SELECT?
A: Of course! Using this feature, we can create a query that will return the expected result with just one query.
Example with Recursive CTE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
;WITH CTE_Recursivo AS ( -- Nível 1 (Âncora) SELECT Id_Empregado, Nm_Empregado, Id_Superior, 1 AS Nivel FROM #Funcionarios WHERE Id_Superior IS NULL UNION ALL -- Níveis 2-N SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, B.Nivel + 1 AS Nivel FROM #Funcionarios A JOIN CTE_Recursivo B ON A.Id_Superior = B.Id_Empregado ) SELECT * FROM CTE_Recursivo |
Controlling the degree of recursion with MAXRECURSION
Now that you've seen how to use recursive CTE and how it can be practical, it's time to understand the risk of building a recursive CTE in an “infinite loop”. This happens when you don't program your query's recursion well and it can end up reaching N levels and even crash your instance and consume so much resources trying to resolve the query.
To avoid this kind of situation, there is the query hint MAXRECURSION, which allows you to specify the maximum level of recursion. At this level, SQL will abort the rest of the execution and display a severely 16 error message, as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
;WITH CTE_Recursivo AS ( -- Nível 1 (Âncora) SELECT Id_Empregado, Nm_Empregado, Id_Superior, 1 AS Nivel FROM Dacasa.dbo.Funcionarios_Teste WHERE Id_Superior IS NULL UNION ALL -- Níveis 2-N SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, B.Nivel + 1 AS Nivel FROM Dacasa.dbo.Funcionarios_Teste A JOIN CTE_Recursivo B ON A.Id_Superior = B.Id_Empregado ) SELECT * FROM CTE_Recursivo OPTION(MAXRECURSION 1) |
Error message:
The statement terminated. 1 has been exhausted before statement completion.
Note that using hint OPTION (MAXRECURSION 1), only the primary level and most 1 hierarchy level were returned (1 and 2 levels). If we used the MAXRECURSION 2 value, the primary level and more 2 hierarchy levels would be returned (level 1 to level 3).
Since the exception raised by hint MAXRECURSION is of severity 16, it causes an interruption in the rest of the routine and if it is a job, this job will return a failure. If you don't want this behavior and just want to limit the level of recursion, but without causing routine error, just wrap your CTE in a block of TRY..CATCH:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
BEGIN TRY ;WITH CTE_Recursivo AS ( -- Nível 1 (Âncora) SELECT Id_Empregado, Nm_Empregado, Id_Superior, 1 AS Nivel FROM Dacasa.dbo.Funcionarios_Teste WHERE Id_Superior IS NULL UNION ALL -- Níveis 2-N SELECT A.Id_Empregado, A.Nm_Empregado, A.Id_Superior, B.Nivel + 1 AS Nivel FROM Dacasa.dbo.Funcionarios_Teste A JOIN CTE_Recursivo B ON A.Id_Superior = B.Id_Empregado ) SELECT * FROM CTE_Recursivo OPTION(MAXRECURSION 1) END TRY BEGIN CATCH END CATCH |
Note: The hint parameter MAXRECURSION supports values from 1 to 32.767. If you want to use a recursion level above this value, simply do not use this hint or specify the value 0, which means no recursion limit.
Another way to demonstrate a very interesting recursive use of CTE is to generate number sequences:
Example of recursive CTE to generate a sequence of 200 numbers:
1 2 3 4 5 6 7 8 |
;WITH CTE_Numerico AS ( SELECT 1 AS Numero UNION ALL SELECT Numero + 1 FROM CTE_Numerico ) SELECT * FROM CTE_Numerico OPTION (MAXRECURSION 199) |
Using this code above, it was possible to limit 200 records using hint MAXRECURSION (199) by limiting 199 recursion levels. However, you can ensure this limitation in another way (and without raising an exception):
1 2 3 4 5 6 7 8 9 |
WITH CTE_Numerico AS ( SELECT 1 AS Numero UNION ALL SELECT Numero + 1 FROM CTE_Numerico WHERE Numero < 200 ) SELECT * FROM CTE_Numerico OPTION (MAXRECURSION 249) |
So notice that even specifying that the recursion level is 249, that is, the maximum recursion level is 250 records (and indeed, 250 records should have been generated), as I limited the amount in 200 records inside the query. CTE, it was only possible to return the 200 records, making hint MAXRECURSION unnecessary in this case.
This showed that it is possible to limit the records of a recursive CTE (in certain situations) even without using hint MAXRECURSION.
Conclusion
In this article, I was able to demonstrate what a CTE is, in what situations we can use it, how it is built, and some of the advantages it has over other solutions, such as derived tables and subquery.
In addition, it was possible to explain how a recursive CTE works, how it can be assembled, and some examples where it can be especially useful.
I hope you enjoyed this article and that it was helpful to you!
A hug and see you next!
What are the disadvantages of using CTE? At the company I work for we had serious problems with queries that used CTE. By pre-processing them in a separate table and indexing them, performance improved. What do you think about it? Can CTE have performance issues when used with many re-registrations?
Great! Congratulations! Worth $ 18 thousand ok?
Kkkkkkkkkk #whatever
Dirceu ball show. Thanks
Great post!
Wow, Bubuquinha, Fardim ?!
I will be forced to file a lawsuit… Kkkkkkkkkk
Excellent post. I learned a lot. Congratulations!!!