【问题标题】:SQL Recursive CTE Linked ChainsSQL 递归 CTE 链接链
【发布时间】:2017-01-02 10:37:39
【问题描述】:

我已包含一个用于复制/粘贴的简化数据集。将有一个链接组件表,以特定顺序链接。在生产中,该表将包含数十万条链,每条链包含 5-10 个组件。

输入也将是链,我将在数据库中搜索数据库链具有与输入链相同顺序的相同组件的任何实例。例如,如果数据库有链A,B,C,D,E,F,G和I输入链B和A,B,C和D,F,Z;结果会告诉我: 输入链 B 匹配 输入链 A、B、C 匹配 输入链 D、F、Z 不匹配

理想情况下,我希望能够输入数万条链来搜索数据库中的数十万条链。

我在 VB.Net 中有一个解决方案,它从数据库中获取与每个输入链的第一个组件匹配的所有链。然后它递归地向下导航数据库链,直到它到达末尾,或者它发现数据库链不匹配。它既不优雅也不高效,因为它一次只能查看一个输入链。我可以很容易地在 SQL 中编写一些使用 Cursor 达到相同效果的东西,但同样效率不高。

我正在尝试找到一种递归 CTE,它允许我一次运行数千个输入的整个查询。

这是一个简化的数据集:

DECLARE @Chains TABLE (ID int, Component varchar(15), ParentID int, DisplayOrder int, ChainID int)
DECLARE @SearchChains TABLE (Component varchar(15), DisplayOrder int, GroupID int)

INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (1, 'Head bone', 0, 0, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (2, 'Neck bone', 1, 1, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (3, 'Shoulder bone', 2, 2, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (4, 'Back bone', 3, 3, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (5, 'Hip bone', 4, 4, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (6, 'Head bone', 0, 0, 2)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (7, 'Shoulder bone', 6, 1, 2)

INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Back bone', 0, 1)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Hip bone', 1, 1)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Leg bone', 2, 1)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Head bone', 0, 2)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Neck bone', 1, 2)
;

WITH cteMatching (ID, Component, ParentID, DisplayOrder, ChainID, RecLevel)
AS
(
SELECT  C.ID, C.Component, C.ParentID, C.DisplayOrder, C.ChainID, 1 as RecLevel
FROM @Chains C
WHERE DisplayOrder = 0
UNION ALL
SELECT C.ID, C.Component, C.ParentID, C.DisplayOrder, C.ChainID, cte.RecLevel + 1
FROM @Chains C
INNER JOIN cteMatching cte ON C.ParentID = cte.ID
)

SELECT SC.Component, SC.DisplayOrder, SC.GroupID, cte.ID, cte.Component, cte.ParentID,
    ISNULL(cte.DisplayOrder, 2147483647) as cteDisplayOrder, cte.ChainID, cte.RecLevel,
    ISNULL((SELECT 1 WHERE SC.Component = cte.Component), 0) as IsMatch
FROM @SearchChains SC
LEFT OUTER JOIN cteMatching cte ON SC.Component = cte.Component
ORDER BY SC.GroupID ASC, cte.ChainID ASC, SC.DisplayOrder ASC, cteDisplayOrder ASC

结果是:

    Component       DisplayOrder GroupID     ID          Component       ParentID    cteDisplayOrder ChainID     RecLevel    IsMatch
--------------- ------------ ----------- ----------- --------------- ----------- --------------- ----------- ----------- -----------
Leg bone        2            1           NULL        NULL            NULL        2147483647      NULL        NULL        0
Back bone       0            1           4           Back bone       3           3               1           4           1
Hip bone        1            1           5           Hip bone        4           4               1           5           1
Head bone       0            2           1           Head bone       0           0               1           1           1
Neck bone       1            2           2           Neck bone       1           1               1           2           1
Head bone       0            2           6           Head bone       0           0               2           1           1

这里的问题是 SearchChain GroupID 2 (Neck bone-> Head bone) 应该显示与数据库 ChainID 1 的匹配(它确实如此)并且它还应该显示 GroupID 2 和 ChainID 2、Neck 之间的 Head 组件匹配与 ChainID 2 中的 Shoulder 不匹配。但是当我注释掉 ChainID 1 和 SearchChain GroupID 1 时,ChainID 2 和 SearchChain GroupID 2 的结果是正确的。这让我很困惑。

目标是能够同时通过多个数据库链运行多个搜索链,目前,我的尝试失败了。有人有什么建议吗?

-E

【问题讨论】:

  • 只是为了检查一下,如果我理解正确:两个表都导致独立的链。如果您在主链中的某处找到完整的链,则搜索表的链相当短并且它们是匹配的。正确的?您的链条是否有可能分裂、交叉或绕圈运行,或者它总是一条普通的链条?
  • 是的,没错。输入链的长度通常要短得多,并且链始终是线性的,从头到尾没有循环或交叉。
  • 我知道这不是你的真实数据,但结构有点奇怪。也就是说,您似乎有一个 Component 表、一个 Chain 表,然后您将搜索组件,而不管它们位于哪个链中。如果当前模式与您的问题空间匹配,那就足够了;我只是在观察。
  • 嗨 Elroy,可能想listen into this :-)
  • Shnugo - 你找到了我的数据源 :)

标签: sql sql-server recursion common-table-expression


【解决方案1】:

除此之外,您的链的设计有异味(见下文),我想为您提供这个解决方案:两个链都连接到一个路径,然后通过 LIKE 执行搜索

DECLARE @Chains TABLE (ID int, Component varchar(15), ParentID int, DisplayOrder int, ChainID int)
DECLARE @SearchChains TABLE (Component varchar(15), DisplayOrder int, GroupID int)

INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (1, 'Head bone', 0, 0, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (2, 'Neck bone', 1, 1, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (3, 'Shoulder bone', 2, 2, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (4, 'Back bone', 3, 3, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (5, 'Hip bone', 4, 4, 1)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (6, 'Head bone', 0, 0, 2)
INSERT INTO @Chains (ID, Component, ParentID, DisplayOrder, ChainID) VALUES (7, 'Shoulder bone', 6, 1, 2)

INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Back bone', 0, 1)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Hip bone', 1, 1)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Leg bone', 2, 1)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Head bone', 0, 2)
INSERT INTO @SearchChains(Component, DisplayOrder, GroupID) VALUES ('Neck bone', 1, 2);

--这是你的查询

WITH recChains AS
(
    SELECT c.ChainID,c.ID,c.ParentID,CAST('|' + c.Component AS NVARCHAR(MAX)) AS ChainAsPath,1 AS Step
    FROM @Chains AS c
    WHERE c.ParentID=0
    UNION ALL
    SELECT nxt.ChainID,nxt.ID,nxt.ParentID,prv.ChainAsPath + '|' + nxt.Component,prv.Step+1
    FROM recChains AS prv
    INNER JOIN @Chains AS nxt ON prv.ChainID=nxt.ChainID AND prv.ID=nxt.ParentID
)
,ChainsPath AS
(
    SELECT ChainID,ChainAsPath
    FROM
    (
        SELECT ChainAsPath
              ,ChainID
              ,ROW_NUMBER() OVER(PARTITION BY ChainID ORDER BY Step DESC) AS StepOrderRev
        FROM recChains
    ) AS tbl
    WHERE StepOrderRev=1
)
,SearchPath AS
(
    SELECT GroupID
          ,(
            SELECT CAST('|' + Component AS NVARCHAR(MAX)) 
            FROM @SearchChains AS p
            WHERE p.GroupID=sc.GroupID
            FOR XML PATH('')
           ) AS SearchPath
    FROM @SearchChains AS sc
    GROUP BY GroupID
)

SELECT *
      ,CASE WHEN ChainAsPath LIKE '%' + SearchPath + '%' THEN 'X' ELSE '' END AS Match 
FROM ChainsPath
CROSS JOIN SearchPath

我必须承认,我没有完全理解您的预期结果,希望这是您所需要的:

+---------+-------------------------------------------------------+---------+------------------------------+-------+
| ChainID | ChainAsPath                                           | GroupID | SearchPath                   | Match |
+---------+-------------------------------------------------------+---------+------------------------------+-------+
| 1       | |Head bone|Neck bone|Shoulder bone|Back bone|Hip bone | 1       | |Back bone|Hip bone|Leg bone |       |
+---------+-------------------------------------------------------+---------+------------------------------+-------+
| 2       | |Head bone|Shoulder bone                              | 1       | |Back bone|Hip bone|Leg bone |       |
+---------+-------------------------------------------------------+---------+------------------------------+-------+
| 1       | |Head bone|Neck bone|Shoulder bone|Back bone|Hip bone | 2       | |Head bone|Neck bone         | X     |
+---------+-------------------------------------------------------+---------+------------------------------+-------+
| 2       | |Head bone|Shoulder bone                              | 2       | |Head bone|Neck bone         |       |
+---------+-------------------------------------------------------+---------+------------------------------+-------+

设计

指向同一集合的另一行的 parentID 的模式(自引用)很笨拙...您为树和路径以及 - 通常 - 图形选择此模式。 必须无间隙普通链更容易配置为具有排名的列表。您可以将一列用于显示排名,另一列用于技术链。就像您使用搜索链一样...ORDER BY 比递归 CTE 更快、更干净(并且允许有间隙),后者是一个隐藏的 RBAR,因此相当慢...

设计 2

我建议您不要将单词本身用作路径,而应为组件使用目录表并根据它们的 ID 构建路径。这更清洁、更易于维护且速度更快。

【讨论】:

  • 感谢您提供潜在的解决方案和解释。我总是想知道为什么某些东西有效。让我深入研究一下,并确保我理解了这一切。我曾假设内联递归 CTE 不会是 RBAR,但我承认集合论不是我的专业领域。
  • 我早期解决此问题的尝试不包括 ParentID,我只是添加了它以尝试递归 CTE。我已经用您的解决方案编辑了我的原始问题,并删除了递归 CTE。这是您的想法还是您有更有效的方法?
【解决方案2】:

这是我采用新结构的最终解决方案:

DECLARE @Chains TABLE (ID int IDENTITY(1,1), ComponentID int, DisplayOrder int, ChainID int)
DECLARE @SearchChains TABLE (ComponentID int, DisplayOrder int, GroupID int)
DECLARE @Components TABLE (ID int IDENTITY(10,1), Component varchar(15))

INSERT INTO @Components (Component) VALUES ('Head bone')
INSERT INTO @Components (Component) VALUES ('Neck bone')
INSERT INTO @Components (Component) VALUES ('Shoulder bone')
INSERT INTO @Components (Component) VALUES ('Back bone')
INSERT INTO @Components (Component) VALUES ('Hip bone')
INSERT INTO @Components (Component) VALUES ('Thigh bone')
INSERT INTO @Components (Component) VALUES ('Knee bone')
INSERT INTO @Components (Component) VALUES ('Shin bone')
INSERT INTO @Components (Component) VALUES ('Ankle bone')
INSERT INTO @Components (Component) VALUES ('Heel bone')
INSERT INTO @Components (Component) VALUES ('Foot bone')
INSERT INTO @Components (Component) VALUES ('Toe bone')
INSERT INTO @Components (Component) VALUES ('Leg bone')

INSERT INTO @Chains (ComponentID, DisplayOrder, ChainID) VALUES (10, 0, 1)
INSERT INTO @Chains (ComponentID, DisplayOrder, ChainID) VALUES (11, 1, 1)
INSERT INTO @Chains (ComponentID, DisplayOrder, ChainID) VALUES (12, 2, 1)
INSERT INTO @Chains (ComponentID, DisplayOrder, ChainID) VALUES (13, 3, 1)
INSERT INTO @Chains (ComponentID, DisplayOrder, ChainID) VALUES (14, 4, 1)
INSERT INTO @Chains (ComponentID, DisplayOrder, ChainID) VALUES (10, 0, 2)
INSERT INTO @Chains (ComponentID, DisplayOrder, ChainID) VALUES (12, 1, 2)

INSERT INTO @SearchChains(ComponentID, DisplayOrder, GroupID) VALUES (13, 0, 1)
INSERT INTO @SearchChains(ComponentID, DisplayOrder, GroupID) VALUES (14, 1, 1)
INSERT INTO @SearchChains(ComponentID, DisplayOrder, GroupID) VALUES (22, 2, 1)
INSERT INTO @SearchChains(ComponentID, DisplayOrder, GroupID) VALUES (10, 0, 2)
INSERT INTO @SearchChains(ComponentID, DisplayOrder, GroupID) VALUES (11, 1, 2)
INSERT INTO @SearchChains(ComponentID, DisplayOrder, GroupID) VALUES (10, 0, 3)
;

WITH ChainPath AS
(
    SELECT ChainID
        ,(
            SELECT '|' + CAST(co.ID AS NVARCHAR(MAX))
            FROM @Chains AS ch
                INNER JOIN @Components co ON ch.ComponentID = co.ID
            WHERE ch.ChainID = cc.ChainID
            FOR XML PATH('')
        ) as ChainPath
    FROM @Chains cc
    GROUP BY ChainID
)
,SearchPath AS
(
    SELECT GroupID
          ,(
            SELECT '|' + CAST(c.ID AS NVARCHAR(MAX)) 
            FROM @SearchChains AS p
                INNER JOIN @Components c ON p.ComponentID = c.ID
            WHERE p.GroupID=sc.GroupID
            FOR XML PATH('')
           ) AS SearchPath
    FROM @SearchChains AS sc
    GROUP BY GroupID
)


SELECT SP.*, CP.*
FROM ChainPath CP
INNER JOIN SearchPath SP ON CP.ChainPath + '|' LIKE '%' + SP.SearchPath + '|' + '%'

【讨论】:

  • 我继续并更改了我的解决方案以在搜索路径中反映 ID。首先,正如您所说,它应该更快。其次,使用 ID 将防止管道字符出现任何问题 |在组件名称中使用。数据验证很好。永远不要让用户输入出现在 SQL 代码中会更好。另外,我添加了一个尾随 |到最终的搜索查询。没有它,就有可能出现糟糕的匹配。搜索 |12|34 可能会匹配 |12|345。现在|12|34|将不匹配 |12|345|。
猜你喜欢
  • 2014-07-18
  • 2018-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
相关资源
最近更新 更多