Le moyen le plus simple de faire une auto-jointure récursive?

101

Quelle est la manière la plus simple d'effectuer une auto-jointure récursive dans SQL Server? J'ai une table comme celle-ci:

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

Et je veux pouvoir obtenir les enregistrements uniquement liés à une hiérarchie commençant par une personne spécifique. Donc, si je demandais la hiérarchie de CJ par PersonID = 1, j'obtiendrais:

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

Et pour les EB, j'aurais:

PersonID | Initials | ParentID
2          EB         1
4          SW         2

Je suis un peu coincé sur ce point, je ne peux pas penser comment le faire en dehors d'une réponse à profondeur fixe basée sur un tas de jointures. Cela ferait comme cela se passe parce que nous n'aurons pas beaucoup de niveaux, mais je voudrais le faire correctement.

Merci! Chris.

Chris
la source
2
Quelle version de SQL Server utilisez-vous? c'est-à-dire Sql 2000, 2005, 2008?
boydc7
2
Questions SO concernant les requêtes récursives: stackoverflow.com/search?q=sql-server+recursive
OMG Ponies

Réponses:

113
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

En ajoutant la condition de commande, vous pouvez conserver l'ordre de l'arborescence:

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

En modifiant la ORDER BYcondition, vous pouvez modifier l'ordre des frères et sœurs.

Quassnoi
la source
7
+1, sauf que Chris aurait besoin à la PersonID = theIdYouAreLookingForplace de ParentID IS NULL.
Heinzi
J'ai posté une nouvelle question sur SO, stackoverflow.com/questions/13535003/…
Kishore Kumar
@Aaroninus: le nœud parent est défini par la requête la plus haute (ancre) dans la WITHclause. Si vous avez besoin de détails, veuillez créer un violon sur sqlfiddle.com et publier le lien ici.
Quassnoi
25

En utilisant les CTE, vous pouvez le faire de cette façon

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
la source
2
Bonne réponse complète avec l'important WHERE PersonID = @PersonID
Oli B
5

La requête Quassnoi avec un changement pour une grande table. Parents avec plus d'enfants que 10: Formater en str (5) le row_number ()

AVEC 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
        WHERE ParentID = 0
        UNION TOUT
        SELECT m. *, Q.bc + '.' + str (ROW_NUMBER () OVER (PARTITION BY m.ParentID ORDER BY m.ordernum), 5) COLLATE Latin1_General_BIN
        DE #tm
        REJOINDRE q
        ON m.parentID = q.DBID
        )
SÉLECTIONNER *
DE q
COMMANDÉ PAR
        avant JC

guille
la source
2

SQL 2005 ou version ultérieure, les CTE sont la méthode standard à suivre selon les exemples présentés.

SQL 2000, vous pouvez le faire en utilisant des UDF -

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

(qui fonctionnera en 2005, ce n'est tout simplement pas la manière standard de le faire. Cela dit, si vous trouvez que c'est la manière la plus simple de travailler, exécutez-la)

Si vous avez vraiment besoin de faire cela dans SQL7, vous pouvez faire à peu près ce qui précède dans un sproc mais vous ne pouvez pas en sélectionner - SQL7 ne prend pas en charge les UDF.

eftpotrm
la source
2

Vérifiez ce qui suit pour vous aider à comprendre le concept de récursivité 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
la source