在SQL中递归链接记录

时间:2015-02-25 23:35:56

标签: sql sql-server

假设我们有一个由10个工作组成的表。有时工作可以参考其他工作。例如,我们说每项工作要么是完整的穿梭路线,要么是穿梭路线的一部分。如果它是一个完整的穿梭路线,那么[prev]和[next]字段都将为空。如果它只是路线的一条腿,那么这些字段中至少有一个将引用另一个作业,即路径序列中之前或之后的作业。

这是表格的样子。

enter image description here

我希望能够在表中查询1)[grab_me]为真的所有工作,或者2)它是任何一条腿(工作)[grab_me]的路线的一部分field设置为true。所以基本上,"抓住这些工作,以及他们所依附的任何东西。"

在这种情况下,所需的结果如下:

enter image description here

我已经能够通过两个递归CTE的联合来实现它,但我对SQL的理解并不是我想要的任何地方,我觉得它更简单方式。

这是我当前的设置:

declare @t table (job int, prev int, next int, grab_me bit)
insert  @t (job, prev, next, grab_me)
values  (1, null, null, 0),
        (2, null, 3, 0),
        (3, 2, 4, 0),
        (4, 3, null, 0),
        (5, null, null, 1),
        (6, null, null, 1),
        (7, null, 8, 0),
        (8, 7, 9, 1),
        (9, 8, 10, 0),
        (10, 9, null, 0)

select * from @t;

with cte_1 as (
    select  job, prev, next, grab_me
    from    @t
    where   grab_me = 1

    union all

    select  child.job, child.prev, child.next, child.grab_me
    from    @t child
    join    cte_1 parent
    on      parent.prev = child.job
),
cte_2 as (
    select  job, prev, next, grab_me
    from    @t
    where   grab_me = 1

    union all

    select  child.job, child.prev, child.next, child.grab_me
    from    @t child
    join    cte_2 parent
    on      parent.next = child.job
)

select * from cte_1 union select * from cte_2;

2 个答案:

答案 0 :(得分:4)

如果引入level列,则可以使用单个cte执行此操作。父节点的级别为0,取决于您前往的方式为正数或负数。使用此伪列,您可以使递归查询起作用:

with cte_1 as (
    select  job, prev, next, grab_me, 0 as level
    from    @t
    where   grab_me = 1

    union all

    select  child.job, child.prev, child.next, child.grab_me, parent.level + 1
    from    @t child
    join    cte_1 parent
    on      parent.prev = child.job and parent.level >= 0

    union all

    select  child.job, child.prev, child.next, child.grab_me, parent.level - 1
    from    @t child
    join    cte_1 parent
    on      parent.next = child.job and parent.level <= 0
)
select * from cte_1 ;

result
job         prev        next        grab_me level
----------- ----------- ----------- ------- -----------
5           NULL        NULL        1       0
6           NULL        NULL        1       0
8           7           9           1       0
7           NULL        8           0       1
9           8           10          0       -1
10          9           NULL        0       -2

(6 row(s) affected)

答案 1 :(得分:3)

这是一种技术上使用一个CTE的变体。我必须明确指出递归应该遍历哪个方向以避免无限循环。在这个问题之前我不知道在一个CTE中可能有multiple recursive members

WITH
CTE
AS
(
    select  job, prev, next, grab_me, 1 AS UsePrev, 1 AS UseNext
    from    @t
    where   grab_me = 1

    UNION ALL

    select  child.job, child.prev, child.next, child.grab_me, 1 AS UsePrev, 0 AS UseNext
    from    
        @t child 
        inner join cte parent on 
            parent.prev = child.job
            AND parent.UsePrev = 1

    UNION ALL

    select  child.job, child.prev, child.next, child.grab_me, 0 AS UsePrev, 1 AS UseNext
    from    
        @t child 
        inner join cte parent on 
            parent.next = child.job
            AND parent.UseNext = 1
)
SELECT * FROM CTE;

结果集:

job    prev    next    grab_me    UsePrev    UseNext
5      NULL    NULL    1          1          1
6      NULL    NULL    1          1          1
8      7       9       1          1          1
7      NULL    8       0          1          0
9      8       10      0          0          1
10     9       NULL    0          0          1

执行计划与第一个变体有很大不同。整个批处理的执行计划包括填充表变量,原始查询和新查询,INSERT为22%,原始查询为55%,新查询为23%。它太大了,不适合屏幕。

因此,新变体可能更有效,但必须使用真实数据进行检查。

单个CTE,第二个版本

事实证明,我们可以在没有两个递归成员的情况下做到这一点:

WITH
CTE
AS
(
    select  job, prev, next, grab_me, 1 AS UsePrev, 1 AS UseNext
    from    @t
    where   grab_me = 1

    UNION ALL

    select  child.job, child.prev, child.next, child.grab_me
        , CASE WHEN parent.prev = child.job AND parent.UsePrev = 1 THEN 1 ELSE 0 END AS UsePrev
        , CASE WHEN parent.next = child.job AND parent.UseNext = 1 THEN 1 ELSE 0 END AS UseNext
    from
        @t AS child 
        inner join cte AS parent on
            (parent.prev = child.job AND parent.UsePrev = 1)
            OR
            (parent.next = child.job AND parent.UseNext = 1)
)
SELECT * FROM CTE;

上一版本的执行计划再次简单,估计效率更高。

如果批处理包括初始化,原始查询,我的第一个变体和我的第二个变体,查询成本是:插入19%,原始48%,我的第一个版本20%,我的第二个版本13%。

插入

insert

原始查询

original query

第一个变体

first variant

第二个变体

second variant

最终变体

在第二个版本中,我们可以移除额外的列UsePrevUseNext并使用现有列prevnext来实现此目的。逻辑仍然是一样的。如果使用父级prev链接从父级转到子级,则将此子级的next链接(指向此父级)设置为NULL,以便我们不会回溯再次到同一个父母。同样,如果我们使用父级next链接从父级转到子级,则将此子级的prev链接设置为NULL

WITH
CTE
AS
(
    select  job, prev, next, grab_me
    from    @t
    where   grab_me = 1

    UNION ALL

    select  child.job
        , CASE WHEN parent.next = child.job THEN NULL ELSE child.prev END AS prev
        , CASE WHEN parent.prev = child.job THEN NULL ELSE child.next END AS next
        , child.grab_me
    from
        @t AS child 
        inner join cte AS parent on
            (parent.prev = child.job)
            OR
            (parent.next = child.job)
)
SELECT * FROM CTE;

执行计划与第二个变体非常相似,只有一个Compute Scalar更少:

third and final variant

BTW,@cha在另一个答案中建议的查询执行计划与我的第一个变体的执行计划完全相同。