【问题标题】:Select rows from hierarchy based on related nodes根据相关节点从层次结构中选择行
【发布时间】:2015-06-12 07:56:43
【问题描述】:

我有一个自引用表Foo

[Id] int NOT NULL,
[ParentId] int NULL,   --Foreign key to [Id]
[Type] char(1) NOT NULL

[Id] 是聚簇主键,在[ParentId][Type] 列上建立索引。

假设层次结构的最大深度为 1(子节点不能有子节点)。


我想获得满足以下条件的所有 Foo 行:

  • 类型是 A
  • 在其家谱中有一个B
  • 在其家谱中有一个C D

以下使用 JOIN 的查询返回了想要的结果,但是性能很糟糕

SELECT DISTINCT [Main].*

FROM Foo AS [Main]

--[Main] may not be root node
LEFT OUTER JOIN Foo AS [Parent]
    ON [Parent].[Id] = [Main].[ParentId]

--Must have a B in tree
INNER JOIN Foo AS [NodeB]
    ON (
        [NodeB].[Pid] = [Main].[Pid]            --Sibling
            OR [NodeB].[ParentId] = [Main].[Id] --Child
            OR [NodeB].[Id] = [Parent].[Id]     --Parent
    )
        AND [NodeB].[Type] = 'B'

--Must have a C or D in tree
INNER JOIN Foo AS [NodeCD]
    ON (
        [NodeCD].[Pid] = [Main].[Pid]            --Sibling
            OR [NodeCD].[ParentId] = [Main].[Id] --Child
            OR [NodeCD].[Id] = [Parent].[Id]     --Parent
    )
        AND [NodeCD].[Type] IN ('C', 'D')

WHERE [Main].[Type] = 'A'

从实际执行计划仅限于查看 650,000 行中的前 10,000 行


如果我从查询中删除 --Parent 行

OR [NodeB].[Id] = [Parent].[Id]  --Parent
OR [NodeCD].[Id] = [Parent].[Id] --Parent

然后执行变得几乎是瞬时的,但是它忽略了 A 是一个孩子并且只有一个兄弟姐妹的情况

Misses this:    Catches this:
B               B
├A              ├A
└C              ├B
                └C

我试图提出一个 CTE 来执行此操作,因为它在性能方面似乎更有希望,但我无法弄清楚如何排除那些不满足标准的树。

到目前为止的 CTE

WITH [Parent] AS 
(
SELECT  *
FROM    [Foo]
WHERE   [ParentId] IS NULL

UNION ALL
SELECT  [Child].*
FROM    Foo AS [Child]
JOIN    [Parent]
ON      [Child].[ParentId] = [Parent].Id
WHERE   [Child].[Type] = 'P'

UNION ALL
SELECT  [ChildCD].*
FROM    Foo AS [ChildCD]
JOIN    [Parent]
ON      [ChildCD].[ParentId] = [Parent].Id
WHERE   [ChildCD].[Type] IN ('C', 'D')
)

SELECT  *
FROM [Parent]
WHERE [Type] = 'I';

但是,如果我尝试添加 Sibling-Child-Parent OR 语句,则会达到最大递归级别 100。


SQL Fiddle with test data

【问题讨论】:

  • @ypercube 根据所需的结果(在 sqlfiddle 上),是的,它确实

标签: sql sql-server hierarchy


【解决方案1】:

用这组数据优化它并不容易,但不妨试试这个。 LEFT OUTER JOIN 似乎是多余的。此外,执行计划在内部循环中没有显示 96% 的命中率。

SELECT DISTINCT [Main].*
FROM Foo AS [Main]


--Must have a B in tree
INNER JOIN Foo AS [NodeB]
    ON (
        [NodeB].[ParentId] = [Main].[ParentId]            --Sibling
            OR [NodeB].[ParentId] = [Main].[Id] --Child
            OR [NodeB].[Id] = [Main].[ParentId]     --Parent
    )
        AND [NodeB].[Type] = 'B'

--Must have a C or D in tree
INNER JOIN Foo AS [NodeCD]
    ON (
        [NodeCD].[ParentId] = [Main].[ParentId]            --Sibling
            OR [NodeCD].[ParentId] = [Main].[Id] --Child
            OR [NodeCD].[Id] = [Main].[ParentId]     --Parent
    )
        AND [NodeCD].[Type] IN ('C', 'D')

WHERE [Main].[Type] = 'A'

请发布您的结果。希望这会有所帮助。

【讨论】:

  • 好吧,根据sqlfiddle,这确实可以正常工作。 +1
  • 不幸的是,这实际上性能更差。执行时间从 1:20 到 13:20,实际行数(如问题中的图片)从 ~2 亿到 ~30 亿。
  • 我应该澄清一下,这是为了在 650k 行表的 10k 行上运行测试,而不是在小提琴上运行。
【解决方案2】:

天啊,这比我想象的要长,肯定有更好的方法,但这是我的看法:

WITH CTE AS
(
    SELECT Id, ParentId FamilyId, [Type]
    FROM dbo.Foo
    UNION
    SELECT A.Id, B.Id, B.[Type]
    FROM dbo.Foo A
    INNER JOIN dbo.Foo B
        ON A.ParentId = B.Id
)
SELECT DISTINCT B.Id
FROM CTE A
INNER JOIN dbo.Foo B
    ON A.Id = B.Id
    OR A.FamilyId = B.Id
WHERE B.[Type] = 'A'
AND EXISTS( SELECT 1 FROM CTE
            WHERE FamilyId = A.FamilyId
            AND [Type] = 'B')
AND EXISTS( SELECT 1 FROM CTE
            WHERE FamilyId = A.FamilyId
            AND [Type] IN ('C','D'));

Here is the modifiedsqlfiddle.

【讨论】:

    【解决方案3】:

    这样的事情怎么样?

    select
        [F].[Id]
    from
        [Foo] [F]
    where
        [F].[Type] = 'A' and
        (
            (
                [F].[ParentId] is null and
                exists (select 1 from [Foo] [Child] where [F].[Id] = [Child].[ParentId] and [Child].[Type] = 'B') and
                exists (select 1 from [Foo] [Child] where [F].[Id] = [Child].[ParentId] and [Child].[Type] in ('C', 'D'))
            ) or
            (
                [F].[ParentId] is not null and
                exists (select 1 from [Foo] [ParentOrSibling] where [F].[ParentId] in ([ParentOrSibling].[Id], [ParentOrSibling].[ParentId]) and [ParentOrSibling].[Type] = 'B') and
                exists (select 1 from [Foo] [ParentOrSibling] where [F].[ParentId] in ([ParentOrSibling].[Id], [ParentOrSibling].[ParentId]) and [ParentOrSibling].[Type] in ('C', 'D'))
            )
        );
    

    【讨论】:

    • 哇!这立即运行!
    【解决方案4】:

    使用递归 CTE。这适用于任何多级层次结构:

    DECLARE @t TABLE
        (
          ID INT ,
          ParentID INT ,
          Type CHAR(1)
        )
    
    INSERT  INTO @t
    VALUES  ( 1, NULL, 'A' ),
            ( 2, 1, 'B' ),
            ( 3, NULL, 'C' ),
            ( 4, NULL, 'A' ),
            ( 5, 4, 'B' ),
            ( 6, 4, 'C' ),
            ( 7, NULL, 'A' ),
            ( 8, 7, 'B' ),
            ( 9, 8, 'D' ),
            ( 10, NULL, 'D' ),
            ( 11, 10, 'A' ),
            ( 12, 11, 'B' ),
            ( 13, 8, 'D' );
    
    WITH    cte1
              AS ( SELECT   ID ,
                            ParentID ,
                            Type ,
                            ID AS GroupID ,
                            0 AS B ,
                            0 AS CD
                   FROM     @t
                   WHERE    Type = 'A'
                   UNION ALL
                   SELECT   t.ID ,
                            t.ParentID ,
                            t.Type ,
                            c.GroupID ,
                            CASE WHEN t.Type = 'B' THEN 1
                                 ELSE 0
                            END ,
                            CASE WHEN t.Type IN ( 'C', 'D' ) THEN 1
                                 ELSE 0
                            END
                   FROM     @t t
                            JOIN cte1 c ON t.ParentID = c.ID
                 ),
            cte2
              AS ( SELECT   ID ,
                            ParentID ,
                            Type ,
                            ID AS GroupID ,
                            0 AS B ,
                            0 AS CD
                   FROM     @t
                   WHERE    Type = 'A'
                   UNION ALL
                   SELECT   t.ID ,
                            t.ParentID ,
                            t.Type ,
                            c.GroupID ,
                            CASE WHEN t.Type = 'B' THEN 1
                                 ELSE 0
                            END ,
                            CASE WHEN t.Type IN ( 'C', 'D' ) THEN 1
                                 ELSE 0
                            END
                   FROM     @t t
                            JOIN cte2 c ON t.ID = c.ParentID
                 ),
            filter
              AS ( SELECT   ID ,
                            Type ,
                            SUM(B) OVER ( PARTITION BY GroupID ) AS B ,
                            SUM(CD) OVER ( PARTITION BY GroupID ) AS CD
                   FROM     ( SELECT    *
                              FROM      cte1
                              UNION
                              SELECT    *
                              FROM      cte2
                            ) t
                 )
        SELECT  t.*
        FROM    filter f
                JOIN @t t ON t.ID = f.ID
        WHERE   f.Type = 'A'
                AND B > 0
                AND cd > 0
    

    输出:

    ID  ParentID    Type
    4   NULL        A
    7   NULL        A
    11  10          A
    

    【讨论】:

      【解决方案5】:

      正在检查的节点是根节点的情况与它是子节点的情况完全不同,您最好分别查询这两个节点并形成两个集合的UNION ALL。但是,您可以使用一个公用表表达式来简化,该表达式标识包含您所追求的节点的那些树。总的来说,这可能看起来像这样:

      WITH [TargetFamilies] AS (
          SELECT
            COALESCE(ParentId, Id) AS FamilyId
          FROM Foo
          GROUP BY COALESCE(ParentId, Id)
          HAVING 
            COUNT(CASE Type WHEN 'B' THEN 1 END) > 0
            AND COUNT(CASE Type WHEN 'C' THEN 1 WHEN 'D' THEN 1 END) > 0
      )
      
      -- root nodes
      SELECT [Main].*
      FROM
        Foo AS [Main]
        JOIN [TargetFamilies] ON [Main].Id = [TargetFamilies].FamilyId
      WHERE
        [Main].Type = 'A'
      
      UNION ALL
      
      -- child nodes
      SELECT 
        [Main].*
      FROM
        Foo AS [Main]
        JOIN [TargetFamilies] ON [Main].ParentId = [TargetFamilies].FamilyId
      WHERE
        [Main].Type = 'A'
      

      【讨论】:

      • 这非常快!它在 1.5 秒内完成了我的整个 650,000 行测试表。
      • 非常聪明!合并 id 以获得家庭 id 是我无法让我的大脑实现的部分。
      【解决方案6】:

      我无法预测效率,但这是另一种解决方案:

      SELECT *
      FROM Foo AS f
      WHERE Type = 'A'
        AND ParentId IS NULL
        AND EXISTS 
            ( SELECT *
              FROM Foo AS ch
              WHERE ch.ParentId = f.Id
                AND ch.Type = 'B'
            )
        AND EXISTS 
            ( SELECT *
              FROM Foo AS ch
              WHERE ch.ParentId = f.Id
                AND ch.Type IN ('C', 'D')
            ) 
      UNION ALL
      
      SELECT *
      FROM Foo AS f
      WHERE Type = 'A'
        AND ParentId IS NOT NULL
        AND EXISTS
          ( SELECT 1
            FROM
                ( SELECT *
                  FROM (VALUES ('B'), ('C'), ('D')) AS x (Type)
                EXCEPT
                  SELECT p.Type
                  FROM Foo AS p
                  WHERE f.ParentId = p.Id
                EXCEPT
                  SELECT sib.Type
                  FROM Foo AS sib
                  WHERE f.ParentId = sib.ParentId
                ) AS x
             HAVING MIN(Type) = MAX(Type) AND MIN(Type) <> 'B'
                 OR MIN(Type) IS NULL
          ) ; 
      

      SQLfiddle测试

      【讨论】:

      • 效率超高。在我的 650k 测试台上花了 2 秒。
      猜你喜欢
      • 1970-01-01
      • 2011-10-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多