¿La forma más sencilla de realizar una autounión recursiva?

100

¿Cuál es la forma más sencilla de realizar una autounión recursiva en SQL Server? Tengo una mesa como esta:

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

Y quiero poder obtener los registros solo relacionados con una jerarquía que comience con una persona específica. Entonces, si solicitara la jerarquía de CJ por PersonID = 1, obtendría:

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

Y para EB obtendría:

PersonID | Initials | ParentID
2          EB         1
4          SW         2

Estoy un poco atascado en esto, no puedo pensar en cómo hacerlo aparte de una respuesta de profundidad fija basada en un montón de combinaciones. Esto serviría como sucede porque no tendremos muchos niveles, pero me gustaría hacerlo correctamente.

¡Gracias! Chris.

Chris
fuente
2
¿Qué versión de SQL Server estás usando? es decir, Sql 2000, 2005, 2008?
boydc7
2
SO preguntas sobre consultas recursivas: stackoverflow.com/search?q=sql-server+recursive
OMG Ponies

Respuestas:

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

Al agregar la condición de pedido, puede preservar el orden del árbol:

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

Al cambiar la ORDER BYcondición, puede cambiar el orden de los hermanos.

Quassnoi
fuente
7
+1, excepto que Chris necesitaría en PersonID = theIdYouAreLookingForlugar de ParentID IS NULL.
Heinzi
He publicado una nueva pregunta en SO, stackoverflow.com/questions/13535003/…
Kishore Kumar
@Aaroninus: el nodo principal está definido por la consulta superior (ancla) en la WITHcláusula. Si necesita información específica, cree un violín en sqlfiddle.com y publique el enlace aquí.
Quassnoi
24

Usando CTE puede hacerlo de esta manera

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
fuente
2
Buena respuesta completa con el importante DONDE PersonID = @PersonID
Oli B
5

La consulta de Quassnoi con un cambio para tabla grande. Padres con más hijos que 10: formateado como str (5) el número de fila ()

CON q COMO 
        (
        SELECT m. *, CAST (str (ROW_NUMBER () OVER (ORDER BY m.ordernum), 5) AS VARCHAR (MAX)) COLLATE Latin1_General_BIN AS bc
        DESDE #tm
        DONDE ParentID = 0
        UNIÓN TODOS
        SELECCIONE m. *, Q.bc + '.' + str (ROW_NUMBER () OVER (PARTICIÓN POR m.ParentID ORDER BY m.ordernum), 5) COLLATE Latin1_General_BIN
        DESDE #tm
        ÚNETE q
        ON m.parentID = q.DBID
        )
SELECCIONE *
DESDE q
PEDIR POR
        antes de Cristo

Guille
fuente
2

SQL 2005 o posterior, los CTE son el camino estándar a seguir según los ejemplos que se muestran.

SQL 2000, puede hacerlo usando 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

(que funcionará en 2005, simplemente no es la forma estándar de hacerlo. Dicho esto, si encuentra que es la forma más fácil de trabajar, hágalo funcionar)

Si realmente necesita hacer esto en SQL7, puede hacer aproximadamente lo anterior en un sproc pero no pudo seleccionarlo; SQL7 no admite UDF.

eftpotrm
fuente
2

Verifique lo siguiente para ayudar a comprender el concepto de recursividad 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
fuente