Olá pessoal!
Como vocês estão ?
Nesse artigo eu gostaria de demonstrar a vocês Como criar consultas recursivas com a CTE (Common Table Expressions ou expressões de tabela comuns) no SQL Server. O recurso de CTE está disponível desde a versão 2005, e até hoje, muitas pessoas não conhecem esse recurso tão interessante e útil no dia a dia.
Introdução
Uma CTE tem o uso bem similar ao de uma subquery ou tabela derivada, com a vantagem do conjunto de dados poder ser utilizado mais de uma vez na consulta, ganhando performance (nessa situação) e também, melhorando a legibilidade do código. Por estes motivos, o uso da CTE tem sido bastante difundido como substituição à outras soluções citadas.
Exemplo de consulta com 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; |
Exemplo de consulta com 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 |
Além disso, uma CTE fornece a significativa vantagem de ser capaz de se autorreferenciar, criando, portanto uma CTE recursiva, que é executada repetidamente para retornar subconjuntos de dados até que o resultado completo seja obtido.
A CTE recursiva é muito utilizada para retornar dados hierárquicos, como por exemplo, exibir funcionários em um organograma, no qual podem ter vários níveis hierárquicos e vou demonstrar aqui, um exemplo de como a CTE recursiva funciona.
Exemplos de CTE Recursiva
Exemplo de CTE recursiva utilizando uma sequência de números:
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 |
Exemplo de CTE recursiva utilizando hierarquia:
Criação da massa de testes – Funcionários da empresa CLR Corporation
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 |
Dada a estrutura acima, quero criar os níveis de hierarquia desses funcionários. Como faríamos isso sem usar o CTE ?
Exemplo com hierarquia manual
Uma forma muito comum de se criar uma hierarquia de dados no SQL Server para poucos níveis, é através de inserções manuais.
Embora seja bem simples (basicamente, Ctrl+C e Ctrl+V), quando precisamos automatizar esse trabalho ou temos muitos níveis na nossa hierarquia (podendo até ser variáveis), essa solução acaba não atendendo muito bem, nos levando a buscar uma solução mais robusta.
Exemplo de criação de hirarquia manual:
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 |
Exemplo com LOOP WHILE
Visando criar uma mais robusta que a anterior e que funcione com N níveis de hierarquia, podemos utilizar um LOOP WHILE para isso, que vai criar uma iteração em cada nível a partir da âncora (primeiro nível) até o insert não encontrar mais registros a serem inseridos.
Exemplo de inserção utilizando 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 |
Como vocês puderam observar, utilizei um loop while para conseguir consultar a própria tabela e assim, encadear os dados e montar a hierarquia.
Mas e a CTE recursiva? Ela poderia retornar esse mesmo resultado com apenas 1 SELECT?
R: É claro! Utilizando esse recurso, podemos criar uma consulta que vai retornar o resultado esperado com apenas uma consulta.
Exemplo com CTE Recursivo
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 |
Controlando o grau de recursão com MAXRECURSION
Agora que você já viu como utilizar a CTE recursiva e como ela pode ser prática, chegou a hora de entender o risco de se construir uma CTE recursiva em “loop infinito”. Isso acontece quando você não programa bem a recursão da sua query e ela pode acabar atingindo N níveis e até travar a sua instância e tanto consumir recursos tentando resolver a consulta.
Para evitar esse tipo de situação, existe a query hint MAXRECURSION, que permite especificar o nível máximo de recursão. Ao atingir esse nível, o SQL irá abortar o restante da execução e mostrar uma mensagem de erro com severidade 16, conforme demonstrado abaixo:
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) |
Mensagem de erro:
The statement terminated. The maximum recursion 1 has been exhausted before statement completion.
Vejam que utilizando o hint OPTION(MAXRECURSION 1), apenas o nível primário e mais 1 nível de hierarquia foram retornados (níveis 1 e 2). Caso utilizássemos o valor MAXRECURSION 2, o nível primário e mais 2 níveis de hierarquia seriam retornados (nível 1 ao nível 3).
Como a exceção gerada pelo hint MAXRECURSION é de severidade 16, ela causa uma interrupção no restante da rotina e caso seja um job, esse job irá retornar uma falha. Caso você não queira esse comportamento e deseja apenas limitar o nível de recursão, mas sem provocar erro na rotina, basta encapsular a sua CTE em um bloco de 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 |
Observação: O parâmetro do hint MAXRECURSION suporta valores de 1 a 32.767. Caso você queira utilizar um nível de recursão acima desse valor, basta não utilizar esse hint ou especificar o valor 0, que significa sem limite de recursividade.
Uma outra forma de demonstrar um uso de CTE recursiva bem interessante é para gerar sequências de números:
Exemplo de CTE recursiva para gerar uma sequência de 200 números:
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) |
Utilizando esse código acima, foi possível limitar os 200 registros utilizando o hint MAXRECURSION(199), limitando a 199 níveis de recursividade. Entretanto, é possível garantir essa limitação de uma outra forma (e sem gerar uma exceção):
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) |
Então reparem que, mesmo especificando que o nível de recursão é 249, ou seja, o nível máximo de recursão são 250 registros (e de fato, deveriam ter sido gerados 250 registros), como eu limitei a quantidade em 200 registros dentro da query da CTE, foi possível retornar apenas os 200 registros, fazendo com que o hint MAXRECURSION seja dispensável neste caso.
Com isso, foi possível demonstrar que é possível limitar os registros de um CTE recursivo (em determinadas situações) mesmo sem utilizar o hint MAXRECURSION.
Conclusão
Nesse artigo, pude demonstrar o que é uma CTE, em que situações podemos utilizá-la, como ela é construída e algumas das vantagens que ela possui sobre outras soluções, como tabelas derivadas e subquery.
Além disso, foi possível explicar como uma CTE recursiva funciona, como ela pode ser montada e alguns exemplos onde ela pode ser especialmente útil.
Espero que tenham gostado desse artigo e que ele tenha sido útil para vocês!
Um abraço e até a próxima!
Quais as desvantagens de usar CTE? Na empresa em que trabalho tivemos problemas sérios com consultas que usavam CTE. Ao pré-processa-las numa tabela separada e indexa-las, o desempenho melhorava. O que acha sobre isso? CTE podem apresentar problemas de desempenho quando utilizadas com muitos re registros?
Excelente! Parabéns! Vale US$ 18 mil ok?
Kkkkkkkkkk #quemdera
Show de bola Dirceu. Valeu
Excelente post!
Pô, Bubuquinha, Fardim?!
Vou ser obrigado a entrar com processo… Kkkkkkkkkk
Excelente post. Aprendi muito. Parabéns!!!