T-SQL中的while循环语句会一直循环下去,直到满足退出条件。

7

最近,我被委派调试一款电子商务应用程序中的一个奇怪问题。在应用程序升级后,该网站开始不时地挂起,因此我被派去进行调试。在检查事件日志后,我发现SQL服务器在几分钟内写入了大约 200,000 条事件,并显示失败了一个约束条件的消息。经过长时间的调试和一些追踪,我找到了罪魁祸首。我已删除了一些不必要的代码并做了一些清理,但基本上就是这样。

WHILE EXISTS (SELECT * FROM ShoppingCartItem WHERE ShoppingCartItem.PurchID = @PurchID)
BEGIN
    SELECT TOP 1 
        @TmpGFSID = ShoppingCartItem.GFSID, 
        @TmpQuantity = ShoppingCartItem.Quantity,
        @TmpShoppingCartItemID = ShoppingCartItem.ShoppingCartItemID,
    FROM
        ShoppingCartItem INNER JOIN GoodsForSale on ShoppingCartItem.GFSID = GoodsForSale.GFSID
    WHERE ShoppingCartItem.PurchID = @PurchID

    EXEC @ErrorCode = spGoodsForSale_ReverseReservations @TmpGFSID, @TmpQuantity
    IF @ErrorCode <> 0
    BEGIN
        Goto Cleanup    
    END

    DELETE FROM ShoppingCartItem WHERE ShoppingCartItem.ShoppingCartItemID = @TmpShoppingCartItemID
    -- @@ROWCOUNT is 1 after this
END

事实:

  1. 只有一个或两个记录与第一个选择子句匹配
  2. 从DELETE语句的RowCount指示已被删除
  3. WHILE子句将永远循环

该存储过程已被重写,将应该被删除的行选择到一个临时内存表中,因此立即问题得到了解决,但这确实激发了我的好奇心。

为什么它会一直循环?

澄清:删除没有失败(在调试后,delete stmt后的 @@rowcount为1) 澄清2:SELECT TOP ... 子句是否按任何特定字段排序都无关紧要,因为返回的id对应的记录将被删除,所以在下一次循环中应该获取另一个记录。

更新:经过检查,我找到了导致这个存储过程失控的罪犯提交。我唯一发现的真正区别是,在SELECT TOP 1语句中先前没有加入连接,也就是说,没有那个连接它可以在不使用任何事务语句的情况下运行。似乎引入连接使SQL服务器变得更加挑剔。

更新澄清brien 指出没有必要加入连接,但我们实际上确实使用了GoodsForSale表中的一些字段,但为了使代码简单,我已将它们删除,以便我们可以专注于手头的问题。


哪个约束条件失败了?是在ShoppingCartItem还是GoodsForSale上? - brien
请看我的回答,这个问题还没有解决吗? - Sam Saffron
7个回答

3

您是在明确模式还是隐式模式下进行交易

既然您处于明确模式,我认为您需要在DELETE操作周围加上BEGIN TRANSACTION和COMMIT TRANSACTION语句。

WHILE EXISTS (SELECT * FROM ShoppingCartItem WHERE ShoppingCartItem.PurchID = @PurchID)
BEGIN
    SELECT TOP 1 
            @TmpGFSID = ShoppingCartItem.GFSID, 
            @TmpQuantity = ShoppingCartItem.Quantity,
            @TmpShoppingCartItemID = ShoppingCartItem.ShoppingCartItemID,
    FROM
            ShoppingCartItem INNER JOIN GoodsForSale on ShoppingCartItem.GFSID = GoodsForSale.GFSID
    WHERE ShoppingCartItem.PurchID = @PurchID

    EXEC @ErrorCode = spGoodsForSale_ReverseReservations @TmpGFSID, @TmpQuantity
    IF @ErrorCode <> 0
    BEGIN
            Goto Cleanup    
    END

    BEGIN TRANSACTION delete

        DELETE FROM ShoppingCartItem WHERE ShoppingCartItem.ShoppingCartItemID = @TmpShoppingCartItemID
        -- @@ROWCOUNT is 1 after this

    COMMIT TRANSACTION delete
END
澄清:你需要使用事务的原因是删除操作直到执行提交(COMMIT)操作时才会在数据库中真正发生。通常,当你有多个写操作在一个原子事务中时,你会使用它。基本上,如果所有操作都成功,你只想让更改发生在数据库中。

在你的情况下,只有1个操作,但由于你处于显式事务模式,你需要告诉SQL Server< strong>确实进行更改。


听起来很有道理,您能否详细说明一下为什么我应该将其包含在事务语句中。 - Markus Olsson
我正在尝试使用更新语句做类似的事情,但即使我在脚本中包含了事务,它仍然无法正常工作并进入永久循环。我的脚本是:declare AtPart varchar(20)(我在这里放置了At而不是猴子符号,仅因为写注释的规则)while exists ((select top 1 * from partiidev p where isnull(brojRacuna,'')='') begin set AtPart=(select top 1 partija from partiidev p where isnull(brojRacuna,'')='') begin transaction t1 update partiidev set BrojRacuna= (select dbo.dev_brojracuna (AtPart)) where partija like AtPart commit transaction t1end - Nemanja Vujacic

3
FROM
  ShoppingCartItem
    INNER JOIN
  GoodsForSale
    on ShoppingCartItem.GFSID = GoodsForSale.GFSID

糟糕,您的连接导致结果集变为空行。

 SELECT TOP 1
    @TmpGFSID = ShoppingCartItem.GFSID,
    @TmpQuantity = ShoppingCartItem.Quantity,
    @TmpShoppingCartItemID =
      ShoppingCartItem.ShoppingCartItemID

哎呀,你对一个没有行的集合使用了多重赋值。这会导致变量保持不变(它们将具有它们上一次通过循环时的相同值)。在这种情况下,变量不会被赋值为空。

如果你将这段代码放在循环的开头,它会更快地失败:

 SELECT
    @TmpGFSID = null,
    @TmpQuantity = null,
    @TmpShoppingCartItemID = null

如果你更改代码,先获取键(不进行连接),然后在第二个查询中通过键获取相关数据,那么你就能获胜。


1

在ShoppingCartItem表中是否存在一个@PurchID的记录,其中GFSID不在GoodsForSale表中?这可能解释了为什么EXISTS返回true,但没有更多记录可供删除。


这还不够,因为删除操作没有失败,@@rowcount 在删除后的值为1。 - Markus Olsson
他澄清了删除成功,所以我认为这不是问题的原因。 - brien

0

显然,某些东西没有在应该删除或修改的地方被删除或修改。如果下一次迭代条件仍然相同,它将继续进行。

此外,您正在比较@TmpShoppingCartItemID而不是@PurchID。我可以看出这两者可能不同,并且您可能会删除与while语句中检查的行不同的行。


0
如果以上的评论到目前为止没有帮助到您,我建议添加/替换以下内容:
DECLARE Old@ShoppingCartItemID INT

SET @OldShoppingCartItemID = 0

WHILE EXISTS (SELECT ... WHERE ShoppingCartItemID > @ShoppingCartItemID)

SELECT TOP 1 WHERE ShoppingCartItemID > @OldShoppingCartItemID ORDER BY ShoppingCartItemID 

SET @OldShoppingCartItemID = @TmpShoppingCartItemID

0
如果购物车中存在GoodsForSale表中不存在的商品,则会陷入无限循环。
尝试修改您的exists语句以考虑这一点。
(
  SELECT 
    * 
  FROM 
    ShoppingCartItem 
  WHERE 
    JOIN GoodsForSale on ShoppingCartItem.GFSID = GoodsForSale.GFSID 
  where 
    ShoppingCartItem.PurchID = @PurchID
)


或者更好的是,重新编写这段代码,使其不需要循环。像这样循环是一个无限循环等待发生的情况。你应该使用基于集合的操作和事务来替代。

-1

我不确定是否理解了问题,但在选择子句中,它正在与另一个表进行内部连接。该连接可能导致未获取任何记录,然后删除操作失败。尝试使用左连接。


他说删除操作的行数是1,因此它正在删除该项。 - brien

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接