Maneira mais simples de fazer uma auto-junção recursiva?

100

Qual é a maneira mais simples de fazer uma autojunção recursiva no SQL Server? Eu tenho uma mesa assim:

PersonID | Initials | ParentID
1          CJ         NULL
2          EB         1
3          MB         1
4          SW         2
5          YT         NULL
6          IS         5

E eu quero ser capaz de obter os registros relacionados apenas a uma hierarquia começando com uma pessoa específica. Portanto, se eu solicitasse a hierarquia de CJ por PersonID = 1, eu obteria:

PersonID | Initials | ParentID
1          CJ         NULL
2          EB         1
3          MB         1
4          SW         2

E para EBs eu obteria:

PersonID | Initials | ParentID
2          EB         1
4          SW         2

Estou um pouco preso nisso, não consigo pensar em como fazer isso sem uma resposta de profundidade fixa baseada em um monte de junções. Isso aconteceria como acontece porque não teremos muitos níveis, mas eu gostaria de fazê-lo corretamente.

Obrigado! Chris.

Chris
fonte
2
Qual versão do SQL Server você está usando? ou seja, Sql 2000, 2005, 2008?
boydc7
2
Perguntas sobre o SO relacionadas a consultas recursivas: stackoverflow.com/search?q=sql-server+recursive
OMG Ponies

Respostas:

112
WITH    q AS 
        (
        SELECT  *
        FROM    mytable
        WHERE   ParentID IS NULL -- this condition defines the ultimate ancestors in your chain, change it as appropriate
        UNION ALL
        SELECT  m.*
        FROM    mytable m
        JOIN    q
        ON      m.parentID = q.PersonID
        )
SELECT  *
FROM    q

Ao adicionar a condição de pedido, você pode preservar a ordem da árvore:

WITH    q AS 
        (
        SELECT  m.*, CAST(ROW_NUMBER() OVER (ORDER BY m.PersonId) AS VARCHAR(MAX)) COLLATE Latin1_General_BIN AS bc
        FROM    mytable m
        WHERE   ParentID IS NULL
        UNION ALL
        SELECT  m.*,  q.bc + '.' + CAST(ROW_NUMBER() OVER (PARTITION BY m.ParentID ORDER BY m.PersonID) AS VARCHAR(MAX)) COLLATE Latin1_General_BIN
        FROM    mytable m
        JOIN    q
        ON      m.parentID = q.PersonID
        )
SELECT  *
FROM    q
ORDER BY
        bc

Ao alterar a ORDER BYcondição, você pode alterar a ordem dos irmãos.

Quassnoi
fonte
7
+1, exceto que Chris precisaria em PersonID = theIdYouAreLookingForvez de ParentID IS NULL.
Heinzi
Eu postei uma nova pergunta no SO, stackoverflow.com/questions/13535003/…
Kishore Kumar
@Aaroninus: O nó pai é definido pela consulta superior (âncora) da WITHcláusula. Se precisar de detalhes, crie um violino em sqlfiddle.com e poste o link aqui.
Quassnoi
24

Usando CTEs, você pode fazer isso desta maneira

DECLARE @Table TABLE(
        PersonID INT,
        Initials VARCHAR(20),
        ParentID INT
)

INSERT INTO @Table SELECT     1,'CJ',NULL
INSERT INTO @Table SELECT     2,'EB',1
INSERT INTO @Table SELECT     3,'MB',1
INSERT INTO @Table SELECT     4,'SW',2
INSERT INTO @Table SELECT     5,'YT',NULL
INSERT INTO @Table SELECT     6,'IS',5

DECLARE @PersonID INT

SELECT @PersonID = 1

;WITH Selects AS (
        SELECT *
        FROM    @Table
        WHERE   PersonID = @PersonID
        UNION ALL
        SELECT  t.*
        FROM    @Table t INNER JOIN
                Selects s ON t.ParentID = s.PersonID
)
SELECT  *
FROm    Selects
Adriaan Stander
fonte
2
Boa resposta completa com o importante WHERE PersonID = @PersonID
Oli B
5

A consulta Quassnoi com uma mudança para uma mesa grande. Pais com mais filhos do que 10: formatando como str (5) o row_number ()

COM q AS 
        (
        SELECT m. *, CAST (str (ROW_NUMBER () OVER (ORDER BY m.ordernum), 5) AS VARCHAR (MAX)) COLLATE Latin1_General_BIN AS bc
        DE #tm
        ONDE ParentID = 0
        UNION ALL
        SELECIONE m. *, Q.bc + '.' + str (ROW_NUMBER () OVER (PARTITION BY m.ParentID ORDER BY m.ordernum), 5) COLLATE Latin1_General_BIN
        DE #tm
        PARTICIPAR q
        ON m.parentID = q.DBID
        )
SELECIONE *
DE q
ORDENAR POR
        aC

guilhotina
fonte
2

SQL 2005 ou posterior, CTEs são a maneira padrão de seguir os exemplos mostrados.

SQL 2000, você pode fazer isso usando UDFs -

CREATE FUNCTION udfPersonAndChildren
(
    @PersonID int
)
RETURNS @t TABLE (personid int, initials nchar(10), parentid int null)
AS
begin
    insert into @t 
    select * from people p      
    where personID=@PersonID

    while @@rowcount > 0
    begin
      insert into @t 
      select p.*
      from people p
        inner join @t o on p.parentid=o.personid
        left join @t o2 on p.personid=o2.personid
      where o2.personid is null
    end

    return
end

(que funcionará em 2005, mas não é a maneira padrão de fazer isso. Dito isso, se você achar que é a maneira mais fácil de trabalhar, use-a)

Se você realmente precisa fazer isso no SQL7, pode fazer aproximadamente o acima em um sproc, mas não pode selecionar a partir dele - o SQL7 não oferece suporte a UDFs.

eftpotrm
fonte
2

Verifique a seguir para ajudar a entender o conceito de recursão CTE

DECLARE
@startDate DATETIME,
@endDate DATETIME

SET @startDate = '11/10/2011'
SET @endDate = '03/25/2012'

; WITH CTE AS (
    SELECT
        YEAR(@startDate) AS 'yr',
        MONTH(@startDate) AS 'mm',
        DATENAME(mm, @startDate) AS 'mon',
        DATEPART(d,@startDate) AS 'dd',
        @startDate 'new_date'
    UNION ALL
    SELECT
        YEAR(new_date) AS 'yr',
        MONTH(new_date) AS 'mm',
        DATENAME(mm, new_date) AS 'mon',
        DATEPART(d,@startDate) AS 'dd',
        DATEADD(d,1,new_date) 'new_date'
    FROM CTE
    WHERE new_date < @endDate
    )
SELECT yr AS 'Year', mon AS 'Month', count(dd) AS 'Days'
FROM CTE
GROUP BY mon, yr, mm
ORDER BY yr, mm
OPTION (MAXRECURSION 1000)
Premchandra Singh
fonte