Самый простой способ сделать рекурсивное самосоединение?

101

Каков самый простой способ выполнить рекурсивное самосоединение в SQL Server? У меня есть такая таблица:

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

И я хочу иметь возможность получать записи, относящиеся только к иерархии, начиная с конкретного человека. Итак, если бы я запросил иерархию CJ по PersonID = 1, я бы получил:

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

А для EB я бы получил:

PersonID | Initials | ParentID
2          EB         1
4          SW         2

Я немного застрял в этом, не могу придумать, как это сделать, кроме ответа с фиксированной глубиной, основанного на кучке объединений. Так получится, потому что у нас не будет много уровней, но я хотел бы сделать это как следует.

Спасибо! Крис.

Крис
источник
2
Какую версию SQL Server вы используете? т.е. SQL 2000, 2005, 2008?
boydc7
2
SO вопросы относительно рекурсивных запросов: stackoverflow.com/search?q=sql-server+recursive
OMG Ponies

Ответы:

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

Добавив условие упорядочивания, вы можете сохранить порядок дерева:

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

Изменяя ORDER BYусловие, вы можете изменить порядок братьев и сестер.

Quassnoi
источник
7
+1, за исключением того, что Крису понадобится PersonID = theIdYouAreLookingForвместо ParentID IS NULL.
Heinzi
Я опубликовал новый вопрос по SO, stackoverflow.com/questions/13535003/…
Кишор Кумар,
@Aaroninus: родительский узел определяется самым верхним (якорным) запросом в WITHпредложении. Если вам нужна конкретика, создайте скрипку на sqlfiddle.com и разместите ссылку здесь.
Quassnoi
25

Используя CTE, вы можете сделать это так

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
Адриан Стандер
источник
2
Хороший полный ответ с важным WHERE PersonID = @PersonID
Оли Б
5

Запрос Quassnoi с изменением для большой таблицы. Родители с большим количеством потомков, чем 10: форматирование row_number () как str (5)

С q AS 
        (
        ВЫБРАТЬ m. *, CAST (str (ROW_NUMBER () OVER (ORDER BY m.ordernum), 5) AS VARCHAR (MAX)) COLLATE Latin1_General_BIN AS bc
        ОТ #tm
        ГДЕ ParentID = 0
        СОЮЗ ВСЕ
        ВЫБЕРИТЕ m. *, Q.bc + '.' + str (ROW_NUMBER () OVER (РАЗДЕЛЕНИЕ ПО m.ParentID ORDER BY m.ordernum), 5) COLLATE Latin1_General_BIN
        ОТ #tm
        ПРИСОЕДИНЯЙТЕСЬ q
        НА m.parentID = q.DBID
        )
ВЫБРАТЬ *
ОТ q
СОРТИРОВАТЬ ПО
        До нашей эры

Guille
источник
2

SQL 2005 или новее, CTE - это стандартный способ работы в соответствии с показанными примерами.

SQL 2000, вы можете сделать это с помощью 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

(который будет работать в 2005 году, это просто не стандартный способ сделать это. Тем не менее, если вы обнаружите, что это более простой способ работы, бегите с ним)

Если вам действительно нужно сделать это в SQL7, вы можете сделать примерно то же самое в sproc, но не можете выбрать из него - SQL7 не поддерживает UDF.

Eftpotrm
источник
2

Проверьте следующее, чтобы понять концепцию рекурсии 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)
Премчандра Сингх
источник