【问题标题】:Query to fetch all referenced entities recursively查询以递归方式获取所有引用的实体
【发布时间】:2017-07-26 06:50:21
【问题描述】:

我有一个由“声明”组成的数据模型(为了让 stackoverflow 变得简单)只有一个 OpenAmount 字段。还有另外两个表,“ClaimCoupling”和“ClaimEntryReference”。

ClaimCoupling 表直接引用回 Claim 表,ClaimEntryReference 实际上是对可在多个索赔中登记的已收到金额的预订(请参阅 ClaimEntry_ID)。看这张图;

为简单起见,我删除了所有金额,因为这不是我目前正在努力解决的问题。

我想要的是一个查询,该查询将从@Claim 表开始,并获取所有 OpenAmount 为 0 的索赔。但是我希望能够打印出关于此 OpenAmount 是如何产生的准确报告,这意味着我还需要打印出与此声明相关的任何声明。为了使它更有趣,同样的事情也适用于预订,如果在索赔 X 和索赔 Y 上进行了预订,并且只有 X 有未结金额,我想同时获取 X 和 Y,这样我就可以显示已预订的付款作为一个整体。

我尝试使用递归 CTE 来执行此操作,但这(正确地)在循环引用上失败了。我想我会用一个简单的 where 语句来解决这个问题,我会说只递归地添加还不是 CTE 一部分的记录,但这是不允许的....

    WITH coupledClaims AS (
    --Get all unique combinations 
    SELECT cc.SubstractedFromClaim_ID AS Claim_ID,
           cc.AddedToClaim_ID AS Linked_Claim_ID FROM dbo.ClaimCoupling cc
    UNION
    SELECT cc.AddedToClaim_ID AS Claim_ID,
           cc.SubstractedFromClaim_ID AS Linked_Claim_ID FROM dbo.ClaimCoupling cc
),
MyClaims as
(
  SELECT * FROM Claim WHERE OpenAmount <> 0
  UNION ALL
  SELECT c.* FROM coupledClaims JOIN MyClaims mc ON coupledClaims.claim_id = mc.ID JOIN claim c ON c.ID = coupledClaims.linked_Claim_ID
  WHERE c.ID NOT IN (SELECT ID FROM MyClaims)
)
SELECT * FROM MyClaims

在摆弄了太久之后,我决定用一个实际的循环来做......@@Rowcount 并简单地将它们手动添加到一个表变量中,但是当我正在编写这个解决方案时(我正在确定我可以开始工作了)我想我会先在这里问,因为我不喜欢在 TSQL 中编写循环,因为我总是觉得它丑陋且效率低下。

有关数据模型和一些测试数据,请参见以下 sql Fiddle(我将递归部分注释掉,否则我不允许创建链接);

http://sqlfiddle.com/#!6/129ad5/7/0

我希望这里有人能很好地处理这个问题(可能我在递归 CTE 上做错了)。为了完成,这是在 MS SQL 2016 上完成的。

【问题讨论】:

  • 检测和处理数据循环的一种方法是here。当您递归遍历数据时,每一行都会跟踪为到达它而探索的路径。任何复制路径上已有元素的新行都会被忽略。
  • 老实说,这是处理循环的非常聪明的方法。我可能会花一些时间将其放入,然后检查这两种解决方案的性能,因为如果我没记错的话,它实际上会循环“一次”,然后再检测到它。
  • 好的,所以我按照您建议的方式重建查询,并发现这在性能方面比自己进行递归稍微差一些。这可能是由于将 ID 转换为 varchar 然后连接字符串。公平地说,由于它是许多不同的语句,因此分析起来很痛苦,而且我无法获得整个查询的 IO/CPU 统计信息(而不是每个语句的语句)

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


【解决方案1】:

这就是我到目前为止所学到和所做的。感谢habo 的评论,它指的是以下问题; Infinite loop in CTE when parsing self-referencing table

首先,我决定至少“解决”我的问题并编写了一些手动递归,这解决了我的问题,但不像我希望/认为更容易阅读和输出的 CTE 解决方案那样“漂亮”执行手动递归解决方案。

手动递归

/****************************/
/* CLAIMS AND PAYMENT LOGIC */
/****************************/
DECLARE @rows as INT = 0
DECLARE @relevantClaimIds as Table(
Debtor_ID INT,
Claim_ID int
)
SET NOCOUNT ON

--Get anchor condition
INSERT INTO @relevantClaimIds (Debtor_ID, Claim_ID)
select Debtor_ID, ID
from Claim c
WHERE OpenAmount <> 0

--Do recursion
WHILE @rows <> (SELECT COUNT(*) FROM @relevantClaimIds)
BEGIN
set @rows = (SELECT COUNT(*) FROM @relevantClaimIds)

--Subtracted
INSERT @relevantClaimIds (Debtor_ID, Claim_ID)
SELECT DISTINCT c.Debtor_ID, c.id
FROM claim c
inner join claimcoupling cc on cc.SubstractedFromClaim_ID = c.ID
JOIN @relevantClaimIds rci on rci.Claim_ID = cc.AddedToClaim_ID
--might be multiple paths to this recursion so eliminate duplicates
left join @relevantClaimIds dup on dup.Claim_ID = c.id
WHERE dup.Claim_ID is null

--Added
INSERT @relevantClaimIds (Debtor_ID, Claim_ID)
SELECT DISTINCT c.Debtor_ID, c.id
FROM claim c
inner join claimcoupling cc on cc.AddedToClaim_ID = c.ID
JOIN @relevantClaimIds rci on rci.Claim_ID = cc.SubstractedFromClaim_ID
--might be multiple paths to this recursion so eliminate duplicates
left join @relevantClaimIds dup on dup.Claim_ID = c.id
WHERE dup.Claim_ID is null

--Payments
INSERT @relevantClaimIds (Debtor_ID, Claim_ID)
SELECT DISTINCT c.Debtor_ID, c.id
FROM @relevantClaimIds f
join ClaimEntryReference cer on f.Claim_ID = cer.Claim_ID
JOIN ClaimEntryReference cer_linked on cer.ClaimEntry_ID = cer_linked.ClaimEntry_ID AND cer.ID <> cer_linked.ID
JOIN Claim c on c.ID = cer_linked.Claim_ID
--might be multiple paths to this recursion so eliminate duplicates
left join @relevantClaimIds dup on dup.Claim_ID = c.id
WHERE dup.Claim_ID is null
END

然后在我收到并阅读评论后,我决定尝试看起来像这样的 CTE 解决方案;

CTE 递归

with Tree as
        (
        select Debtor_ID, ID AS Claim_ID, CAST(ID AS VARCHAR(MAX)) AS levels
        from Claim c
        WHERE OpenAmount <> 0

        UNION ALL
        SELECT c.Debtor_ID, c.id, t.levels + ',' + CAST(c.ID AS VARCHAR(MAX)) AS levels
        FROM claim c
        inner join claimcoupling cc on cc.SubstractedFromClaim_ID = c.ID
        JOIN Tree t on t.Claim_ID = cc.AddedToClaim_ID
        WHERE (','+T.levels+',' not like '%,'+cast(c.ID as varchar(max))+',%')

        UNION ALL
        SELECT c.Debtor_ID, c.id, t.levels + ',' + CAST(c.ID AS VARCHAR(MAX)) AS levels
        FROM claim c
        inner join claimcoupling cc on cc.AddedToClaim_ID = c.ID
        JOIN Tree t on t.Claim_ID = cc.SubstractedFromClaim_ID
        WHERE (','+T.levels+',' not like '%,'+cast(c.ID as varchar(max))+',%')

        UNION ALL
        SELECT c.Debtor_ID, c.id, t.levels + ',' + CAST(c.ID AS VARCHAR(MAX)) AS levels
        FROM Tree t
        join ClaimEntryReference cer on t.Claim_ID = cer.Claim_ID
        JOIN ClaimEntryReference cer_linked on cer.ClaimEntry_ID = cer_linked.ClaimEntry_ID AND cer.ID <> cer_linked.ID
        JOIN Claim c on c.ID = cer_linked.Claim_ID
        WHERE (','+T.levels+',' not like '%,'+cast(c.ID as varchar(max))+',%')
        )
select  DISTINCT Tree.Debtor_ID, Tree.Claim_ID
from Tree

这个解决方案确实“更短”了很多,而且更容易看,但它实际上表现更好吗?

性能差异

手动; CPU 16,读取 1793,持续时间 13

CTE; CPU 47,读取 4001,持续时间 48

结论

不确定这是由于 CTE 解决方案中所需的 varchar 强制转换,还是它必须在完成递归之前进行一次额外的迭代,但实际上它在所有方面都需要比手动递归更多的资源。

最后,CTE 是可能的,但看起来并不是一切(感谢上帝 ;-))在性能方面坚持手动递归似乎是一条更好的路线。

【讨论】:

  • 对于咯咯笑,可能值得使用VarChar(8000)(或其他合适的大小)而不是VarChar(max)再次运行基准测试。数据库引擎对它们的处理方式完全不同。
  • 对于我所做的 lulz,遗憾的是,8000 是唯一合适的尺寸。如果我将其设置为更低,您会收到以下消息; '递归查询的“级别”列中的锚点和递归部分之间的类型不匹配。'。当设置为 8000 时,尽管性能指标完全相同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-05
  • 2022-10-12
  • 2017-05-23
  • 2014-12-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多