加入递归查询

Join with recursive query

设置

我有下表(简化):

CREATE TABLE Category(
    CategoryId              int           NOT NULL PRIMARY KEY,
    ParentCategoryId        int           NULL,
    Name                    nvarchar(255) NOT NULL,
    FOREIGN KEY (ParentCategoryId) REFERENCES Category(CategoryId) ON UPDATE NO ACTION ON DELETE NO ACTION);

CREATE TABLE TimeSlot(
    TimeSlotId              int           NOT NULL PRIMARY KEY,
    CategoryId              int           NOT NULL,
    FOREIGN KEY (CategoryId) REFERENCES Category(CategoryId) ON UPDATE NO ACTION ON DELETE NO ACTION);

CREATE TABLE PersonTimeSlotAssignment(
    PersonId                int           NOT NULL,
    TimeSlotId              int           NOT NULL,
    PRIMARY KEY (PersonId, TimeSlotId),
    FOREIGN KEY (TimeSlotId) REFERENCES TimeSlot(TimeSlotId) ON UPDATE NO ACTION ON DELETE NO ACTION);

这里是一些测试数据:

INSERT INTO Category(CategoryId, ParentCategoryId, Name) VALUES (100, NULL, 'cat 1');
INSERT INTO Category(CategoryId, ParentCategoryId, Name) VALUES (110, 100, 'cat 1.1');
INSERT INTO Category(CategoryId, ParentCategoryId, Name) VALUES (111, 110, 'cat 1.1.1');
INSERT INTO Category(CategoryId, ParentCategoryId, Name) VALUES (120, 100, 'cat 1.2');
INSERT INTO Category(CategoryId, ParentCategoryId, Name) VALUES (200, NULL, 'cat 2');

INSERT INTO TimeSlot(TimeSlotId, CategoryId) VALUES (301, 111);
INSERT INTO TimeSlot(TimeSlotId, CategoryId) VALUES (302, 120);
INSERT INTO TimeSlot(TimeSlotId, CategoryId) VALUES (303, 200);

INSERT INTO PersonTimeSlotAssignment(PersonId, TimeSlotId) VALUES (401, 301);
INSERT INTO PersonTimeSlotAssignment(PersonId, TimeSlotId) VALUES (401, 302);
INSERT INTO PersonTimeSlotAssignment(PersonId, TimeSlotId) VALUES (402, 302);
INSERT INTO PersonTimeSlotAssignment(PersonId, TimeSlotId) VALUES (402, 303);

我能做什么

SELECT ts.TimeSlotId, pc.Name 
    FROM PersonTimeSlotAssignment 
    JOIN TimeSlot AS ts ON PersonTimeSlotAssignment.TimeSlotId = ts.TimeSlotId 
    JOIN Category AS pc ON ts.CategoryId = pc.CategoryId
    WHERE PersonTimeSlotAssignment.PersonId = @PERSON_ID;

这为某个人提供了此人分配到的所有 TimeSlots 的列表以及 TimeSlot 所属的叶类别的名称。例如,对于 ID 为 401 的人,它给出:

TimeSlotId  Name
---------------------
301         cat 1.1.1
302         cat 1.2

通过以下递归查询,我还可以从某个类别中获取所有祖先直到根类别:

;WITH Parents AS (
    SELECT * FROM Category 
        WHERE CategoryId=@CATEGORY_ID
        UNION ALL SELECT c.* FROM Category c JOIN Parents p ON p.ParentCategoryId=c.CategoryId
    ) 
    SELECT Name FROM Parents;

例如,对于 ID 为 111 的类别,我得到:

Name
---------
cat 1.1.1
cat 1.1
cat 1

我想做什么

我需要的是分配给一个人的时间段列表,以及该时间段的类别名称,直到根类别。因此,对于 ID 为 401 的人,结果应如下所示:

TimeSlotId  Name
---------------------
301         cat 1.1.1
301         cat 1.1
301         cat 1
302         cat 1.2
302         cat 1

我无法弄清楚如何组合以上两个查询以获得预期的结果。

我试过的

我希望这些方面的东西可以奏效:

;WITH Parents AS (
    SELECT * FROM Category 
        WHERE CategoryId=<<'How to get CategoryId for each assigned TimeSlot here?'>>
        UNION ALL SELECT c.* FROM Category c JOIN Parents p ON p.ParentCategoryId=c.CategoryId
    ) 
SELECT ts.TimeSlotId, pc.Name 
    FROM PersonTimeSlotAssignment 
    JOIN TimeSlot AS ts ON PersonTimeSlotAssignment.TimeSlotId = ts.TimeSlotId 
    JOIN Parents AS pc ON <<'How should this look like?'>>
    WHERE PersonTimeSlotAssignment.PersonId = @PERSON_ID;

这将处理递归类别并提供基于 PersonId 或 TimeSlotId 的所有数据:

WITH Categories (PersonId, CategoryId, ParentCategoryId, TimeSlotId, Name, BASE)
AS
(
SELECT PersonId, c.CategoryId, c.ParentCategoryId, pts.TimeSlotId, c.Name, 0 AS BASE
FROM Category c
INNER JOIN TimeSlot ts ON c.CategoryId = ts.CategoryId
INNER JOIN PersonTimeSlotAssignment pts ON ts.TimeSlotId = pts.TimeSlotId
UNION ALL
SELECT PersonId, pc.CategoryId, pc.ParentCategoryId, TimeSlotId, pc.Name, BASE + 1
FROM Category pc
INNER JOIN Categories cs ON cs.ParentCategoryId = pc.CategoryId
)
SELECT * FROM Categories 
WHERE PersonId = 401
--WHERE TimeSlotId = 301

可能有更好的写法,但会按照您的要求进行,并且应该会带您到达需要去的地方。 'BASE' 不符合其原始目的,但仍显示您的人物和类别之间的相关性,例如BASE 0 表示该记录中的类别直接分配给此人。所以,我为此留下了它。谢谢

希望对您有所帮助:)

DECLARE @PersonId INT= 401;

WITH CTE AS
(
SELECT 
    t.*,
    c.CategoryId AS CategoryId_c,
    c.ParentCategoryId as ParentCategoryId_c,
    c.Name AS Name_c,
    c1.CategoryId AS CategoryId_c2,
    c1.ParentCategoryId AS ParentCategoryId_c2,
    c1.Name AS Name_c2,
    c2.CategoryId as CategoryId_c3,
    c2.ParentCategoryId AS ParentCategoryId_c3,
    c2.Name AS Name_c3
FROM 
PersonTimeSlotAssignment p
INNER JOIN TimeSlot t ON t.TimeSlotId=p.TimeSlotId
INNER JOIN Category c ON t.CategoryId=c.CategoryId
LEFT JOIN Category c1 ON c1.CategoryId=c.ParentCategoryId
LEFT JOIN Category c2 ON c2.CategoryId=c1.ParentCategoryId
WHERE p.PersonId=@PersonId
)

SELECT * FROM (
    SELECT TimeSlotId,Name_c  FROM CTE 
    UNION
    SELECT TimeSlotId,Name_c2 FROM CTE 
    UNION
    SELECT TimeSlotId,Name_c3 FROM CTE 
)a WHERE Name_c IS NOT NULL

用户定义函数 cross apply 在这种情况下非常有用。

--1. Create function
create function fn_Category(@id int)
returns table
as
return
with tbl as (
--anckor query
select CategoryId, ParentCategoryId,Name, 1 lvl
from Category where CategoryId = @id
union all
--recursive query
select c.CategoryId, c.ParentCategoryId,c.Name, lvl+1
from Category c
inner join tbl on tbl.ParentCategoryId=c.CategoryId--go up the tree
)
select * from tbl
go
--end of function

--2. and now we can use it
declare @PERSON_ID int = 401
SELECT ts.TimeSlotId, pc.Name 
    FROM PersonTimeSlotAssignment 
    JOIN TimeSlot AS ts ON PersonTimeSlotAssignment.TimeSlotId = ts.TimeSlotId 
    --JOIN Category AS pc ON ts.CategoryId = pc.CategoryId
    --use cross apply instead
    cross apply fn_Category(ts.CategoryId) pc 
    WHERE PersonTimeSlotAssignment.PersonId = @PERSON_ID;

声明 @PersonId int =401;

IF OBJECT_ID('tempdb.dbo.#TimeSlot') 不为空
下降 TABLE #TimeSlot

SELECT DISTINCT DENSE_RANK() OVER(ORDER BY t.TimeSlotId ) ID, t.TimeSlotId,c.CategoryId
进入#TimeSlot
来自 PersonTimeSlotAssignment p
INNER JOIN 时隙 t
ON p.TimeSlotId=t.TimeSlotId
INNER JOIN 类别 c ON c.CategoryId=t.CategoryId
WHERE PersonId=@PersonId

IF OBJECT_ID('tempdb.dbo.#Output') 不为空
下降 TABLE #输出
创建 TABLE #Output(timeSlotId INT,CategoryId INT)
声明@id INT=1 声明@timeSlotId 为 INT 声明@CategoryId INT
WHILE( SELECT id FROM #TimeSlot WHERE id=@id) 不为空
开始
SELECT @timeSlotId=TimeSlotId,@CategoryId=CategoryId 来自 #TimeSlot WHERE ID=@id
插入#Output SELECT @timeSlotId,@CategoryId
WHILE( SELECT ParentCategoryId FROM Category WHERE CategoryId=@CategoryId) 不为空
开始
SELECT @CategoryId=ParentCategoryId FROM Category WHERE CategoryId=@CategoryId
插入#Output SELECT @timeSlotId,@CategoryId
结束
SET @id=@id+1
结束

SELECT a.timeSlotId,c.Name FROM #Output a INNER JOIN 类别 c ON a.CategoryId=c.CategoryId