Предположим, у меня есть таблица как:

id    |   parentId   |  name
 1          NULL         A
 2            1          B
 3            2          C
 4            1          E
 5            3          E

Я пытаюсь написать скалярную функцию, которую можно назвать:

SELECT dbo.GetId('A/B/C/E'), что даст "5", если мы воспользуемся приведенной выше справочной таблицей. Функция выполнит следующие шаги:

  1. Найдите идентификатор "A", равный 1
  2. Найдите идентификатор "B", родительский элемент которого - "A" (id: 1), который будет id: 2
  3. Найдите идентификатор 'C', родительский элемент которого 'B' (id: 2), который будет id: 3
  4. Найдите идентификатор "E", родительский элемент которого - "C" (id: 3), который будет id: 5

Я пытался сделать это с помощью цикла WHILE, но он очень быстро усложнялся ... Просто подумал, что должен быть простой способ сделать это.

0
Denis 13 Май 2016 в 00:14

3 ответа

Лучший ответ

Я думаю, что у меня это основано на рекомендации @ SeanLange использовать рекурсивный CTE (выше в комментариях):

CREATE FUNCTION GetID 
(
    @path VARCHAR(MAX)
)

/* TEST:
SELECT dbo.GetID('A/B/C/E')

*/
RETURNS INT 
AS
BEGIN
    DECLARE @ID INT;

    WITH cte AS (
        SELECT p.id ,
               p.parentId ,
               CAST(p.name AS VARCHAR(MAX)) AS name
        FROM tblT p
        WHERE parentId IS NULL

        UNION ALL
        SELECT p.id ,
               p.parentId ,
               CAST(pcte.name + '/' + p.name AS VARCHAR(MAX)) AS name
        FROM dbo.tblT p
        INNER JOIN cte pcte ON
            pcte.id = p.parentId
    )
    SELECT @ID = id
    FROM cte
    WHERE name = @path

    RETURN @ID
END
0
Denis 12 Май 2016 в 21:59

Версия CTE не оптимизирована для получения иерархических данных. (См. блог MSDN)

Вы должны сделать что-то вроде указанного ниже. Он протестирован на 10 миллионах записей и в 300 раз быстрее, чем версия CTE :)

Declare @table table(Id int, ParentId int, Name varchar(10))
insert into @table values(1,NULL,'A')
insert into @table values(2,1,'B')
insert into @table values(3,2,'C')
insert into @table values(4,1,'E')
insert into @table values(5,3,'E')

DECLARE @Counter tinyint = 0;

IF OBJECT_ID('TEMPDB..#ITEM') IS NOT NULL
DROP TABLE #ITEM

CREATE TABLE #ITEM
(
 ID int not null 
,ParentID int
,Name VARCHAR(MAX)
,lvl int not null
,RootID int not null
)

INSERT INTO #ITEM
    (ID,lvl,ParentID,Name,RootID)
SELECT   Id
        ,0 AS LVL
        ,ParentId
        ,Name
        ,Id AS RootID
FROM            
    @table
WHERE
        ISNULL(ParentId,-1) = -1

WHILE @@ROWCOUNT > 0
    BEGIN
        SET @Counter += 1
        insert into #ITEM(ID,ParentId,Name,lvl,RootID)
        SELECT  ci.ID
                ,ci.ParentId
                ,ci.Name
                ,@Counter as cntr
                ,ch.RootID
        FROM    
            @table AS ci
        INNER JOIN
            #ITEM AS pr 
        ON
            CI.ParentId=PR.ID
        LEFT OUTER JOIN
            #ITEM AS ch
        ON  ch.ID=pr.ID
        WHERE           
                ISNULL(ci.ParentId, -1) > 0
            AND PR.lvl = @Counter - 1
END

select * from #ITEM
2
M.S. 13 Май 2016 в 03:29

Вот пример функционального rcte на основе ваших данных образца и требований, как я их понимаю.

if OBJECT_ID('tempdb..#Something') is not null
    drop table #Something

create table #Something
(
    id int
    , parentId int
    , name char(1)
)

insert #Something
select 1, NULL, 'A' union all
select 2, 1, 'B' union all
select 3, 2, 'C' union all
select 4, 1, 'E' union all
select 5, 3, 'E'

declare @Root char(1) = 'A';

with MyData as
(
    select *
    from #Something
    where name = @Root

    union all

    select s.*
    from #Something s
    join MyData d on d.id = s.parentId
)

select *
from MyData

Обратите внимание, что если вы измените значение своей переменной, вывод изменится. Я бы сделал это встроенной табличной функцией.

0
Sean Lange 12 Май 2016 в 21:38