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


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условие, вы можете изменить порядок братьев и сестер.


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
СОРТИРОВАТЬ ПО
        До нашей эры


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.


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)
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.