Database
 sql >> Base de Dados >  >> RDS >> Database

Fundamentos de expressões de tabela, Parte 6 - CTEs recursivos


Este artigo é a sexta parte de uma série sobre expressões de tabela. No mês passado, na Parte 5, abordei o tratamento lógico de CTEs não recursivos. Este mês eu abordo o tratamento lógico de CTEs recursivos. Eu descrevo o suporte do T-SQL para CTEs recursivos, bem como elementos padrão que ainda não são suportados pelo T-SQL. Eu forneço soluções T-SQL logicamente equivalentes para os elementos sem suporte.

Dados de amostra


Para dados de amostra, usarei uma tabela chamada Employees, que contém uma hierarquia de funcionários. A coluna empid representa o ID do funcionário do subordinado (o nó filho) e a coluna mgrid representa o ID do funcionário do gerente (o nó pai). Use o código a seguir para criar e preencher a tabela Employees no banco de dados tempdb:
SET NOCOUNT ON; USE tempdb;DROP TABLE SE EXISTE dbo.Employees;GO CREATE TABLE dbo.Employees( empid INT NOT NULL CONSTRAINT PK_Employees PRIMARY KEY, mgrid INT NULL CONSTRAINT FK_Employees_Employees REFERENCES dbo.Employees, empname VARCHAR(25) NOT NULL, salário DINHEIRO CHECK (empid <> mgrid)); INSERT INTO dbo.Employees(empid, mgrid, empname, salário) VALUES(1, NULL, 'David' , $10000.00), (2, 1, 'Eitan' , $7000.00), (3, 1, 'Ina' , $7500.00) , (4, 2, 'Seraph' , $ 5.000,00), (5, 2, 'Jiru' , $ 5.500,00), (6, 2, 'Steve' , $4.500,00), (7, 3, 'Aaron', $5.000,00), ( 8, 5, 'Lilach' , $3.500,00), (9, 7, 'Rita', $3.000,00), (10, 5, 'Sean', $3.000,00), (11, 7, 'Gabriel', $3.000,00), (12, 9, 'Emilia' , $ 2.000,00), (13, 9, 'Michael', $ 2.000,00), (14, 9, 'Didi', $ 1.500,00); CREATE UNIQUE INDEX idx_unc_mgrid_empid ON dbo.Employees(mgrid, empid) INCLUDE(empname, salario);

Observe que a raiz da hierarquia de funcionários—o CEO—tem um NULL na coluna mgrid. Todos os demais funcionários têm um gerente, portanto, sua coluna mgrid é definida para o ID de funcionário de seu gerente.

A Figura 1 tem uma representação gráfica da hierarquia do funcionário, mostrando o nome do funcionário, ID e sua localização na hierarquia.

Figura 1:hierarquia de funcionários

CTEs recursivos


CTEs recursivos têm muitos casos de uso. Uma categoria clássica de usos tem a ver com o manuseio de estruturas de grafos, como nossa hierarquia de funcionários. Tarefas envolvendo grafos geralmente requerem percorrer caminhos de comprimento arbitrário. Por exemplo, dado o ID do funcionário de algum gerente, retorne o funcionário de entrada, bem como todos os subordinados do funcionário de entrada em todos os níveis; ou seja, subordinados diretos e indiretos. Se você tivesse um pequeno limite conhecido no número de níveis que precisa seguir (o grau do gráfico), poderá usar uma consulta com uma junção por nível de subordinados. No entanto, se o grau do gráfico for potencialmente alto, em algum momento se torna impraticável escrever uma consulta com muitas junções. Outra opção é usar código iterativo imperativo, mas essas soluções tendem a ser longas. É aqui que os CTEs recursivos realmente brilham - eles permitem que você use soluções declarativas, concisas e intuitivas.

Começarei com o básico de CTEs recursivos antes de chegar às coisas mais interessantes, onde abordo os recursos de pesquisa e ciclismo.

Lembre-se de que a sintaxe de uma consulta em um CTE é a seguinte:
WITH [ ( ) ]
AS
(

)
;
Aqui mostro um CTE definido usando a cláusula WITH, mas como você sabe, você pode definir vários CTEs separados por vírgulas.

Em CTEs regulares e não recursivas, a expressão de tabela interna é baseada em uma consulta que é avaliada apenas uma vez. Em CTEs recursivos, a expressão de tabela interna é baseada normalmente em duas consultas conhecidas como membros , embora você possa ter mais de dois para lidar com algumas necessidades especializadas. Os membros são separados normalmente por um operador UNION ALL para indicar que seus resultados são unificados.

Um membro pode ser um membro âncora — significando que é avaliado apenas uma vez; ou pode ser um membro recursivo — o que significa que é avaliado repetidamente, até retornar um conjunto de resultados vazio. Aqui está a sintaxe de uma consulta em um CTE recursivo que é baseado em um membro âncora e um membro recursivo:
WITH [ ( ) ]
AS
(

UNION ALL

)
;
O que torna um membro recursivo é ter uma referência ao nome CTE. Essa referência representa o conjunto de resultados da última execução. Na primeira execução do membro recursivo, a referência ao nome CTE representa o conjunto de resultados do membro âncora. Na execução N, onde N> 1, a referência ao nome CTE representa o conjunto de resultados da execução (N – 1) do membro recursivo. Como mencionado, uma vez que o membro recursivo retorna um conjunto vazio, ele não é executado novamente. Uma referência ao nome CTE na consulta externa representa os conjuntos de resultados unificados da execução única do membro âncora e todas as execuções do membro recursivo.

O código a seguir implementa a tarefa mencionada acima de retornar a subárvore de funcionários para um determinado gerenciador de entrada, passando a ID de funcionário 3 como o funcionário raiz neste exemplo:
DECLARE @root AS INT =3; WITH C AS( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid)SELECT empid, mgrid, empnameFROM C;

A consulta âncora é executada apenas uma vez, retornando a linha para o funcionário raiz de entrada (funcionário 3 em nosso caso).

A consulta recursiva une o conjunto de funcionários do nível anterior - representado pela referência ao nome CTE, apelidado de M para gerentes - com seus subordinados diretos da tabela Empregados, apelidado de S para subordinados. O predicado de junção é S.mgrid =M.empid, o que significa que o valor mgrid do subordinado é igual ao valor empid do gerente. A consulta recursiva é executada quatro vezes da seguinte forma:
  1. Devolva subordinados do empregado 3; saída:funcionário 7
  2. Retorne subordinados do funcionário 7; saída:funcionários 9 e 11
  3. Devolução de subordinados dos funcionários 9 e 11; saída:12, 13 e 14
  4. Devolução de subordinados dos funcionários 12, 13 e 14; saída:conjunto vazio; paradas de recursão

A referência ao nome CTE na consulta externa representa os resultados unificados da execução única da consulta âncora (funcionário 3) com os resultados de todas as execuções da consulta recursiva (funcionários 7, 9, 11, 12, 13 e 14). Este código gera a seguinte saída:
empid mgrid empname------ ------ --------3 1 Ina7 3 Aaron9 7 Rita11 7 Gabriel12 9 Emilia13 9 Michael14 9 Didi

Um problema comum em soluções de programação é o código descontrolado, como loops infinitos, normalmente causados ​​por um bug. Com CTEs recursivos, um bug pode levar a uma execução de runway do membro recursivo. Por exemplo, suponha que em nossa solução para retornar os subordinados de um funcionário de entrada, você tenha um bug no predicado de junção do membro recursivo. Em vez de usar ON S.mgrid =M.empid, você usou ON S.mgrid =S.mgrid, assim:
DECLARE @root AS INT =3; WITH C AS( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =S .mgrid)SELECT empid, mgrid, empnameFROM C;

Dado pelo menos um funcionário com um ID de gerente não nulo presente na tabela, a execução do membro recursivo continuará retornando um resultado não vazio. Lembre-se de que a condição de término implícita para o membro recursivo é quando sua execução retorna um conjunto de resultados vazio. Se ele retornar um conjunto de resultados não vazio, o SQL Server o executará novamente. Você percebe que se o SQL Server não tivesse um mecanismo para limitar as execuções recursivas, ele continuaria executando o membro recursivo repetidamente para sempre. Como medida de segurança, o T-SQL oferece suporte a uma opção de consulta MAXRECURSION que limita o número máximo permitido de execuções do membro recursivo. Por padrão, esse limite é definido como 100, mas você pode alterá-lo para qualquer valor SMALLINT não negativo, com 0 representando nenhum limite. Definir o limite máximo de recursão para um valor positivo N significa que a tentativa de execução N + 1 do membro recursivo é abortada com um erro. Por exemplo, executar o código com erros acima como está significa que você confia no limite máximo de recursão padrão de 100, portanto, a execução 101 do membro recursivo falha:
empid mgrid empname------ ------ --------3 1 Ina2 1 Eitan3 1 Ina...
Msg 530, Level 16, State 1, Line 121
A instrução terminou. A recursão máxima 100 foi esgotada antes da conclusão da instrução.
Diga que em sua organização é seguro assumir que a hierarquia de funcionários não excederá 6 níveis de gerenciamento. Você pode reduzir o limite máximo de recursão de 100 para um número muito menor, como 10 por segurança, assim:
DECLARE @root AS INT =3; WITH C AS( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =S .mgrid)SELECT empid, mgrid, empnameFROM COPTION (MAXRECURSION 10);

Agora, se houver um bug resultando em uma execução descontrolada do membro recursivo, ele será descoberto na 11ª tentativa de execução do membro recursivo em vez da execução 101:
empid mgrid empname------ ------ --------3 1 Ina2 1 Eitan3 1 Ina...
Msg 530, Level 16, State 1, Line 149
A instrução terminou. A recursão máxima 10 foi esgotada antes da conclusão da instrução.
É importante notar que o limite máximo de recursão deve ser usado principalmente como uma medida de segurança para abortar a execução de código fugitivo com bugs. Lembre-se que quando o limite é excedido, a execução do código é abortada com um erro. Você não deve usar essa opção para limitar o número de níveis a serem visitados para fins de filtragem. Você não quer que um erro seja gerado quando não houver nenhum problema com o código.

Para fins de filtragem, você pode limitar o número de níveis a serem visitados programaticamente. Você define uma coluna chamada lvl que rastreia o número do nível do funcionário atual em comparação com o funcionário raiz de entrada. Na consulta âncora você define a coluna lvl para a constante 0. Na consulta recursiva você define para o valor lvl do gerente mais 1. Para filtrar tantos níveis abaixo da raiz de entrada quanto @maxlevel, adicione o predicado M.lvl <@ maxlevel para a cláusula ON da consulta recursiva. Por exemplo, o código a seguir retorna o funcionário 3 e dois níveis de subordinados do funcionário 3:
DECLARE @root AS INT =3, @maxlevel AS INT =2; WITH C AS( SELECT empid, mgrid, empname, 0 AS lvl FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, M.lvl + 1 AS lvl FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid AND M.lvl <@maxlevel)SELECT empid, mgrid, empname, lvlFROM C;

Essa consulta gera a seguinte saída:
empid mgrid empname lvl------ ------ -------- ----3 1 Ina 07 3 Aaron 19 7 Rita 211 7 Gabriel 2

Cláusulas padrão SEARCH e CYCLE


O padrão ISO/IEC SQL define duas opções muito poderosas para CTEs recursivos. Uma é uma cláusula chamada SEARCH que controla a ordem de busca recursiva, e outra é uma cláusula chamada CYCLE que identifica ciclos nos caminhos percorridos. Aqui está a sintaxe padrão para as duas cláusulas:
7.18

Função

Especifique a geração de informações de ordenação e detecção de ciclo no resultado de expressões de consulta recursiva.

Formato

::=
[ ]
AS
[ ]
::=
| |
::=
SEARCH SET
::=
PROFUNDIDADE PRIMEIRO POR | BREADTH FIRST BY
::=
::=
CYCLE SET TO
DEFAULT USING
::= [ { }… ]
::=
::=
::=
::=
::=

No momento em que escrevo, o T-SQL não suporta essas opções, mas nos links a seguir você pode encontrar solicitações de melhoria de recursos solicitando sua inclusão no T-SQL no futuro e votar neles:
  • Adicione suporte para a cláusula ISO/IEC SQL SEARCH para CTEs recursivos em T-SQL
  • Adicione suporte para a cláusula ISO/IEC SQL CYCLE para CTEs recursivos em T-SQL

Nas seções a seguir, descreverei as duas opções padrão e também fornecerei soluções alternativas logicamente equivalentes que estão atualmente disponíveis no T-SQL.

cláusula SEARCH


A cláusula SEARCH padrão define a ordem de busca recursiva como BREADTH FIRST ou DEPTH FIRST por algumas colunas de ordenação especificadas e cria uma nova coluna de sequência com as posições ordinais dos nós visitados. Você especifica BREADTH FIRST para pesquisar primeiro em cada nível (largura) e depois nos níveis (profundidade). Você especifica DEPTH FIRST para pesquisar primeiro para baixo (profundidade) e depois transversalmente (largura).

Você especifica a cláusula SEARCH logo antes da consulta externa.

Vou começar com um exemplo de ordem de busca recursiva em largura. Apenas lembre-se de que esse recurso não está disponível no T-SQL, portanto, você não poderá executar os exemplos que usam as cláusulas padrão no SQL Server ou no Banco de Dados SQL do Azure. O código a seguir retorna a subárvore do funcionário 1, solicitando uma ordem de busca em largura por empid, criando uma coluna de sequência chamada seqbreadth com as posições ordinais dos nós visitados:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid) PESQUISE LARGURA PRIMEIRO POR empid SET seqbreadth------------------------------------------------------ ---- SELECT empid, mgrid, empname, seqbreadthFROM CORDER BY seqbreadth;

Aqui está a saída desejada deste código:
empid mgrid empname seqbreadth------ ------ -------- -----------1 NULL David 12 1 Eitan 23 1 Ina 34 2 Seraph 45 2 Jiru 56 2 Steve 67 3 Aaron 78 5 Lilach 89 7 Rita 910 5 Sean 1011 7 Gabriel 1112 9 Emilia 1213 9 Michael 1314 9 Didi 14

Não se confunda com o fato de que os valores seqbreadth parecem ser idênticos aos valores empid. Isso é apenas por acaso, como resultado da maneira como atribuí manualmente os valores empid nos dados de amostra que criei.

A Figura 2 apresenta uma representação gráfica da hierarquia de funcionários, com uma linha pontilhada mostrando a amplitude da primeira ordem em que os nós foram pesquisados. Os valores empid aparecem logo abaixo dos nomes dos funcionários e os valores seqbreadth atribuídos aparecem no canto inferior esquerdo de cada caixa.

Figura 2:abrangência da pesquisa primeiro

O que é interessante notar aqui é que dentro de cada nível, os nós são pesquisados ​​com base na ordem de coluna especificada (empid no nosso caso), independentemente da associação pai do nó. Por exemplo, observe que no quarto nível Lilach é acessado primeiro, Rita em segundo, Sean em terceiro e Gabriel por último. Isso porque, entre todos os funcionários do quarto nível, essa é a ordem deles com base em seus valores empíricos. Não é como se Lilach e Sean fossem revistados consecutivamente porque são subordinados diretos do mesmo gerente, Jiru.

É bastante simples criar uma solução T-SQL que forneça o equivalente lógico da opção padrão SAERCH DEPTH FIRST. Você cria uma coluna chamada lvl como mostrei anteriormente para rastrear o nível do nó em relação à raiz (0 para o primeiro nível, 1 para o segundo e assim por diante). Em seguida, você calcula a coluna de sequência de resultados desejada na consulta externa usando uma função ROW_NUMBER. Conforme a ordenação da janela você especifica primeiro o lvl, e depois a coluna de ordenação desejada (empid no nosso caso). Aqui está o código da solução completa:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname, 0 AS lvl FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, M.lvl + 1 AS lvl FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, mgrid, empname, ROW_NUMBER() OVER(ORDER BY lvl, empid) AS seqbreadthFROM CORDER BY seqbreadth;

Em seguida, vamos examinar a ordem de pesquisa DEPTH FIRST. De acordo com o padrão, o código a seguir deve retornar a subárvore do funcionário 1, solicitando uma ordem de busca em profundidade por empid, criando uma coluna de sequência chamada seqdepth com as posições ordinais dos nós pesquisados:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid) PROFUNDIDADE DE PESQUISA PRIMEIRO POR empid SET seqdepth---------------------------------------- SELECT empid, mgrid, empname, seqdepthFROM CORDER BY seqdepth;

A seguir está a saída desejada deste código:
empid mgrid empname seqdepth------ ------ -------- ---------1 NULL David 12 1 Eitan 24 2 Seraph 35 2 Jiru 48 5 Lilach 510 5 Sean 66 2 Steve 73 1 Ina 87 3 Aaron 99 7 Rita 1012 9 Emilia 1113 9 Michael 1214 9 Didi 1311 7 Gabriel 14

Observando a saída de consulta desejada, provavelmente é difícil descobrir por que os valores de sequência foram atribuídos da maneira que foram. A Figura 3 deve ajudar. Ele tem uma representação gráfica da hierarquia de funcionários, com uma linha pontilhada refletindo a primeira ordem de pesquisa em profundidade, com os valores de sequência atribuídos no canto inferior esquerdo de cada caixa.

Figura 3:profundidade de pesquisa primeiro

Criar uma solução T-SQL que forneça o equivalente lógico da opção padrão SEARCH DEPTH FIRST é um pouco complicado, mas factível. Vou descrever a solução em duas etapas.

Na Etapa 1, para cada nó você computa um caminho de classificação binário que é feito de um segmento por ancestral do nó, com cada segmento refletindo a posição de classificação do ancestral entre seus irmãos. Você constrói este caminho de forma semelhante à forma como calcula a coluna lvl. Ou seja, comece com uma string binária vazia para o nó raiz na consulta âncora. Então, na consulta recursiva, você concatena o caminho do pai com a posição de classificação do nó entre os irmãos depois de convertê-lo em um segmento binário. Você usa a função ROW_NUMBER para calcular a posição, particionada pelo pai (mgrid em nosso caso) e ordenada pela coluna de ordenação relevante (empid em nosso caso). Você converte o número da linha em um valor binário de tamanho suficiente dependendo do número máximo de filhos diretos que um pai pode ter em seu gráfico. BINARY(1) suporta até 255 filhos por pai, BINARY(2) suporta 65.535, BINARY(3):16.777.215, BINARY(4):4.294.967.295. Em uma CTE recursiva, as colunas correspondentes nas consultas âncora e recursivas devem ser do mesmo tipo e tamanho , portanto, certifique-se de converter o caminho de classificação em um valor binário do mesmo tamanho em ambos.

Aqui está o código que implementa a Etapa 1 em nossa solução (assumindo que BINARY(1) é suficiente por segmento, o que significa que você não tem mais de 255 subordinados diretos por gerente):
DECLARE @root AS INT =1; WITH C AS(SELECT empid, mgrid, empname, CAST(0x AS VARBINARY(900)) AS sortpath FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M. sortpath + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid) AS BINARY(1)) AS VARBINARY(900)) AS sortpath FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, mgrid, empname, sortpathFROM C;

Este código gera a seguinte saída:
empid mgrid empname sortpath------ ------ -------- -----------1 NULL David 0x2 1 Eitan 0x013 1 Ina 0x027 3 Aaron 0x02019 7 rita 0x02010111 7 Gabriel 0x02010212 9 Emilia 0x0201010113 9 Michael 0x0201010214 9 DIDI 0x02010101034 2 Seraph 0x01015 2 Jiru 0x01026 2 Steve 0x010108 5 LILLC 
Observe que usei uma string binária vazia como o caminho de classificação para o nó raiz da única subárvore que estamos consultando aqui. Caso o membro âncora possa potencialmente retornar múltiplas raízes de subárvore, simplesmente comece com um segmento baseado em um cálculo ROW_NUMBER na consulta âncora, semelhante à maneira como você o calcula na consulta recursiva. Nesse caso, seu caminho de classificação terá um segmento a mais.

O que resta a fazer na Etapa 2 é criar a coluna seqdepth do resultado desejado calculando os números das linhas que são ordenados pelo caminho de classificação na consulta externa, assim
DECLARE @root AS INT =1; WITH C AS(SELECT empid, mgrid, empname, CAST(0x AS VARBINARY(900)) AS sortpath FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M. sortpath + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid) AS BINARY(1)) AS VARBINARY(900)) AS sortpath FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, mgrid, empname, ROW_NUMBER() OVER(ORDER BY caminho de classificação) AS seqdepthFROM CORDER BY seqdepth;

Esta solução é o equivalente lógico de usar a opção padrão SEARCH DEPTH FIRST mostrada anteriormente junto com sua saída desejada.

Uma vez que você tenha uma coluna de sequência refletindo a primeira ordem de busca de profundidade (seqdepth em nosso caso), com um pouco mais de esforço você pode gerar uma representação visual muito boa da hierarquia. Você também precisará da coluna lvl acima mencionada. Você ordena a consulta externa pela coluna de sequência. Você retorna algum atributo que deseja usar para representar o nó (digamos, empname em nosso caso), prefixado por alguma string (digamos ' | ') tempos de lvl replicados. Aqui está o código completo para obter essa representação visual:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, empname, 0 AS lvl, CAST(0x AS VARBINARY(900)) AS sortpath FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.empname, M.lvl + 1 AS lvl, CAST(M.sortpath + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid) AS BINARY(1)) AS VARBINARY(900)) AS sortpath FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, ROW_NUMBER() OVER(ORDER BY sortpath) AS seqdepth, REPLICATE(' | ', lvl) + empname AS empFROM CORDER BY seqdepth;

Este código gera a seguinte saída:
empid seqdepth emp------ ------------- --------------------1 1 David2 2 | Eitan4 3 | | Serafim5 4 | | Jiru8 5 | | | Lilach10 6 | | | Sean6 7 | | Steve3 8 | Ina7 9 | | Aaron9 10 | | | Rita12 11 | | | | Emília13 12 | | | | Miguel14 13 | | | | Didi11 14 | | | Gabriel

Isso é bem legal!

cláusula CYCLE


Estruturas de grafos podem ter ciclos. Esses ciclos podem ser válidos ou inválidos. Um exemplo de ciclos válidos é um gráfico que representa rodovias e estradas conectando cidades e vilas. Isso é o que chamamos de gráfico cíclico . Por outro lado, um gráfico que representa uma hierarquia de funcionários, como no nosso caso, não deve ter ciclos. Isso é o que chamamos de acíclico gráfico. No entanto, suponha que devido a alguma atualização errônea, você acabe com um ciclo sem querer. Por exemplo, suponha que, por engano, você atualize o ID de gerente do CEO (funcionário 1) para o funcionário 14 executando o seguinte código:
ATUALIZAR Funcionários SET mgrid =14WHERE empid =1;

Agora você tem um ciclo inválido na hierarquia de funcionários.

Independentemente de os ciclos serem válidos ou inválidos, quando você percorre a estrutura do gráfico, certamente não deseja continuar seguindo um caminho cíclico repetidamente. Em ambos os casos, você deseja parar de seguir um caminho assim que uma não primeira ocorrência do mesmo nó for encontrada e marcar esse caminho como cíclico.

Então, o que acontece quando você consulta um gráfico que tem ciclos usando uma consulta recursiva, sem nenhum mecanismo para detectá-los? Isso depende da implementação. No SQL Server, em algum momento você atingirá o limite máximo de recursão e sua consulta será abortada. Por exemplo, supondo que você executou a atualização acima que introduziu o ciclo, tente executar a seguinte consulta recursiva para identificar a subárvore do funcionário 1:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid)SELECT empid, mgrid, empnameFROM C;

Como esse código não define um limite máximo de recursão explícito, o limite padrão de 100 é assumido. O SQL Server interrompe a execução deste código antes da conclusão e gera um erro:
empid mgrid empname------ ------ --------1 14 David2 1 Eitan3 1 Ina7 3 Aaron9 7 Rita11 7 Gabriel12 9 Emilia13 9 Michael14 9 Didi1 14 David2 1 Eitan3 1 Ina7 3 Aaron...
Msg 530, Level 16, State 1, Line 432
A instrução terminou. A recursão máxima 100 foi esgotada antes da conclusão da instrução.
O padrão SQL define uma cláusula chamada CYCLE para CTEs recursivos, permitindo que você lide com ciclos em seu gráfico. Você especifica essa cláusula logo antes da consulta externa. Se uma cláusula SEARCH também estiver presente, especifique-a primeiro e depois a cláusula CYCLE. Aqui está a sintaxe da cláusula CYCLE:
CYCLE
SET TO DEFAULT
USING
A detecção de um ciclo é baseada na lista de colunas de ciclo especificada . Por exemplo, em nossa hierarquia de funcionários, você provavelmente gostaria de usar a coluna empid para essa finalidade. Quando o mesmo valor empid aparece pela segunda vez, um ciclo é detectado e a consulta não busca mais esse caminho. Você especifica uma nova coluna de marca de ciclo que indicará se um ciclo foi encontrado ou não, e seus próprios valores como a marca de ciclo e marca de não ciclo valores. Por exemplo, eles podem ser 1 e 0 ou 'Sim' e 'Não', ou qualquer outro valor de sua escolha. Na cláusula USING, você especifica um novo nome de coluna de matriz que conterá o caminho dos nós encontrados até agora.

Veja como você lidaria com uma solicitação para subordinados de algum funcionário raiz de entrada, com a capacidade de detectar ciclos com base na coluna empid, indicando 1 na coluna de marca de ciclo quando um ciclo é detectado e 0 caso contrário:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid) CYCLE empid SET cycle TO 1 DEFAULT 0------------------------------------ SELECT empid, mgrid, empnameFROM C;

Aqui está a saída desejada deste código:
empid mgrid empname cycle------ ------ -------- ------1 14 David 02 1 Eitan 03 1 Ina 07 3 Aaron 09 7 Rita 011 7 Gabriel 012 9 Emilia 013 9 Michael 014 9 Didi 01 14 David 14 2 Seraph 05 2 Jiru 06 2 Steve 08 5 Lilach 010 5 Sean 0

Atualmente, o T-SQL não oferece suporte à cláusula CYCLE, mas há uma solução alternativa que fornece um equivalente lógico. Envolve três etapas. Lembre-se de que anteriormente você construiu um caminho de classificação binário para lidar com a ordem de pesquisa recursiva. De maneira semelhante, como primeira etapa da solução, você cria um caminho de ancestrais baseado em cadeia de caracteres feito dos IDs de nó (IDs de funcionários em nosso caso) dos ancestrais que levam ao nó atual, incluindo o nó atual. Você deseja separadores entre os IDs de nó, incluindo um no início e um no final.

Aqui está o código para construir esse caminho de ancestrais:
DECLARE @root AS INT =1; WITH C AS(SELECT empid, mgrid, empname, CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, mgrid, empname, ancpathFROM C;

Esse código gera a seguinte saída, ainda seguindo caminhos cíclicos e, portanto, abortando antes da conclusão quando o limite máximo de recursão é excedido:
empid mgrid empname ancpath------ ------ -------- -------------------1 14 David . 1.2 1 Eitan .1.2.3 1 Ina .1.3.7 3 Aaron .1.3.7.9 7 Rita .1.3.7.9.11 7 Gabriel .1.3.7.11.12 9 Emilia .1.3.7.9.12.13 9 Michael .1.3.7.9. 13.14 9 Didi .1.3.7.9.14.1 14 David .1.3.7.9.14.1.2 1 Eitan .1.3.7.9.14.1.2.3 1 Ina .1.3.7.9.14.1.3.7 3 Aaron .1.3.7.9.14.1.3.7. ...
Msg 530, Level 16, State 1, Line 492
A instrução terminou. A recursão máxima 100 foi esgotada antes da conclusão da instrução.
Observando a saída, você pode identificar um caminho cíclico quando o nó atual aparece no caminho pela primeira vez, como no caminho '.1.3.7.9.14.1.' .

Na segunda etapa da solução, você produz a coluna da marca do ciclo. In the anchor query you simply set it to 0 since clearly a cycle cannot be found in the first node. In the recursive member you use a CASE expression that checks with a LIKE-based predicate whether the current node ID already appears in the parent’s path, in which case it returns the value 1, or not, in which case it returns the value 0.

Here’s the code implementing Step 2:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname, CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, 0 AS cycle FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, CASE WHEN M.ancpath LIKE '%.' + CAST(S.empid AS VARCHAR(4)) + '.%' THEN 1 ELSE 0 END AS cycle FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, mgrid, empname, ancpath, cycleFROM C;

This code generates the following output, again, still pursuing cyclic paths and failing once the max recursion limit is reached:
empid mgrid empname ancpath cycle------ ------ -------- ------------------- ------1 14 David .1. 02 1 Eitan .1.2. 03 1 Ina .1.3. 07 3 Aaron .1.3.7. 09 7 Rita .1.3.7.9. 011 7 Gabriel .1.3.7.11. 012 9 Emilia .1.3.7.9.12. 013 9 Michael .1.3.7.9.13. 014 9 Didi .1.3.7.9.14. 01 14 David .1.3.7.9.14.1. 12 1 Eitan .1.3.7.9.14.1.2. 03 1 Ina .1.3.7.9.14.1.3. 17 3 Aaron .1.3.7.9.14.1.3.7. 1...
Msg 530, Level 16, State 1, Line 532
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
The third and last step is to add a predicate to the ON clause of the recursive member that pursues a path only if a cycle was not detected in the parent’s path.

Here’s the code implementing Step 3, which is also the complete solution:
DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname, CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, 0 AS cycle FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, CASE WHEN M.ancpath LIKE '%.' + CAST(S.empid AS VARCHAR(4)) + '.%' THEN 1 ELSE 0 END AS cycle FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid AND M.cycle =0)SELECT empid, mgrid, empname, cycleFROM C;

This code runs to completion, and generates the following output:
empid mgrid empname cycle------ ------ -------- -----------1 14 David 02 1 Eitan 03 1 Ina 07 3 Aaron 09 7 Rita 011 7 Gabriel 012 9 Emilia 013 9 Michael 014 9 Didi 01 14 David 14 2 Seraph 05 2 Jiru 06 2 Steve 08 5 Lilach 010 5 Sean 0

You identify the erroneous data easily here, and fix the problem by setting the manager ID of the CEO to NULL, like so:
UPDATE Employees SET mgrid =NULLWHERE empid =1;

Run the recursive query and you will find that there are no cycles present.

Conclusão


Recursive CTEs enable you to write concise declarative solutions for purposes such as traversing graph structures. A recursive CTE has at least one anchor member, which gets executed once, and at least one recursive member, which gets executed repeatedly until it returns an empty result set. Using a query option called MAXRECURSION you can control the maximum number of allowed executions of the recursive member as a safety measure. To limit the executions of the recursive member programmatically for filtering purposes you can maintain a column that tracks the level of the current node with respect to the root node.

The SQL standard supports two very powerful options for recursive CTEs that are currently not available in T-SQL. One is a clause called SEARCH that controls the recursive search order, and another is a clause called CYCLE that detects cycles in the traversed paths. I provided logically equivalent solutions that are currently available in T-SQL. If you find that the unavailable standard features could be important future additions to T-SQL, make sure to vote for them here:
  • Add support for ISO/IEC SQL SEARCH clause to recursive CTEs in T-SQL
  • Add support for ISO/IEC SQL CYCLE clause to recursive CTEs in T-SQL

This article concludes the coverage of the logical aspects of CTEs. Next month I’ll turn my attention to physical/performance considerations of CTEs.