2008-09-29 7 views
7

J'ai récemment été chargé de déboguer un problème étrange dans une application de commerce électronique. Après la mise à niveau d'une application, le site a commencé à être suspendu de temps en temps et j'ai été envoyé en mode débogage. Après avoir vérifié le journal des événements, j'ai trouvé que le serveur SQL a écrit ~ 200 000 événements en quelques minutes avec le message disant qu'une contrainte avait échoué. Après beaucoup de débogage et quelques recherches, j'ai trouvé le coupable. J'ai enlevé un peu de code inutile et nettoyé un peu, mais essentiellement c'est ilWhile-clause dans T-SQL qui boucle pour toujours

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 

Faits:

  1. Il y a seulement un ou deux dossiers correspondant à la première sélection de la clause
  2. RowCount de l'instruction DELETE indique qu'il a été supprimé
  3. la boucle de volonté While clause pour toujours

La procédure a été réécrite pour sélectionner les lignes qui devraient être supprimées dans une table temporaire en mémoire à la place afin que le problème immédiat soit résolu mais cela a vraiment suscité ma curiosité.

Pourquoi boucle-t-il toujours?

Précision: La suppression ne manque pas (@@ rowcount est 1 après la suppression stmt lorsque débogué) Précision 2: Il ne faut pas d'importance si le TOP SELECT ... clause est commandé par n'importe quel champ spécifique puisque l'enregistrement avec l'identifiant retourné sera supprimé ainsi dans la prochaine boucle il devrait obtenir un autre enregistrement.

Mise à jour: Après avoir vérifié les journaux de subversion j'ai trouvé le commit coupable qui a rendu cette procédure stockée pour aller haywire. La seule vraie différence que je peux trouver est qu'il n'y avait auparavant aucune jointure dans l'instruction SELECT TOP 1, c'est-à-dire sans cette jointure, cela fonctionnait sans aucune déclaration de transaction entourant la suppression. Il semble être l'introduction de la jointure qui a rendu le serveur SQL plus pointilleux.

clarification de mise à jour: brien a fait remarquer qu'il n'y a pas besoin de le rejoindre, mais nous faisons réellement utiliser certains champs de la table GoodsForSale mais je les ai enlevés pour garder le code simplement pour que nous puissions concentrer sur le problème hand

+0

Quelle contrainte a échoué? Était-ce sur ShoppingCartItem ou GoodsForSale? – brien

+0

Voir ma réponse, cette question est toujours ouverte? –

Répondre

3

Fonctionnement explicite ou implicite transaction mode?Puisque vous êtes en mode explicite, je pense que vous devez entourer l'opération DELETE avec les instructions BEGIN TRANSACTION et 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 

Précision: La raison pour laquelle vous auriez besoin d'utiliser des transactions est que la suppression ne se produit pas réellement dans la base de données jusqu'à ce que vous faites une opération COMMIT. Ceci est généralement utilisé lorsque vous avez plusieurs opérations d'écriture dans une transaction atomique. Fondamentalement, vous voulez seulement que les changements arrivent à la base de données si toutes les opérations réussissent.

Dans votre cas, il n'y a qu'une seule opération, mais puisque vous êtes en mode de transaction explicite, vous devez dire à SQL Server à vraiment apporter les modifications.

+0

explicite, s'il vous plaît élaborer –

+0

qui semble plausible, pourriez-vous s'il vous plaît élaborer sur pourquoi je devrais l'entourer de déclarations de transaction. –

+0

s'il vous plaît ma mise à jour sur l'introduction de la jointure –

0

Évidemment, quelque chose n'est pas effacé ou modifié là où il le devrait. Si la condition est toujours la même à l'itération suivante, ça va continuer.

En outre, vous comparez @TmpShoppingCartItemID et non @PurchID. Je peux voir comment ils pourraient être différents, et vous pourriez supprimer une ligne différente de celle qui est vérifiée dans l'instruction while.

-1

Je ne sais pas si je comprends le problème, mais dans la clause select, il crée une jointure interne avec une autre table. Cette jointure peut entraîner l'absence d'enregistrements et la suppression échoue. Essayez d'utiliser une jointure à gauche.

+0

Il a dit que le nombre de lignes sur la suppression est 1, donc il supprime l'élément. – brien

1

Y at-il un enregistrement dans ShoppingCartItem avec cet @PurchID où le GFSID n'est pas dans la table GoodsForSale? Cela expliquerait pourquoi EXISTS renvoie true, mais il n'y a plus d'enregistrements à supprimer.

+0

Ce n'est pas le cas puisque la suppression n'échoue pas, @@ rowcount vaut 1 après la suppression. –

+0

Il a précisé que la suppression avait réussi, donc je ne pense pas que cela puisse l'expliquer. – brien

0

Si les commentaires ci-dessus ne vous ont pas jusqu'à présent, je propose d'ajouter/remplacer:

DECLARE [email protected] INT 

SET @OldShoppingCartItemID = 0 

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

SELECT TOP 1 WHERE ShoppingCartItemID > @OldShoppingCartItemID ORDER BY ShoppingCartItemID 

SET @OldShoppingCartItemID = @TmpShoppingCartItemID 
3
FROM 
    ShoppingCartItem 
    INNER JOIN 
    GoodsForSale 
    on ShoppingCartItem.GFSID = GoodsForSale.GFSID 

Oops, la jointure amène le jeu de résultats jusqu'à zéro lignes.

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

Oups, vous avez utilisé une affectation multiple pour un ensemble sans lignes. Cela fait que les variables restent inchangées (elles auront la même valeur qu'elles avaient la dernière fois dans la boucle). Les variables NE sont PAS affectées à null dans ce cas.

Si vous mettez ce code au début de la boucle, il (correctement) ne parviennent plus rapidement:

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

Si vous changez votre code pour récupérer une clé (sans se joindre), puis aller chercher les données connexes par clé dans une deuxième requête, vous allez gagner.

0

S'il y a des articles de panier qui n'existent pas dans la table GoodsForSale, alors cela tournera dans une boucle infinie.

Essayez de changer votre déclaration existe pour tenir compte de cette

(SELECT * FROM ShoppingCartItem WHERE JOIN GoodsForSale on ShoppingCartItem.GFSID = GoodsForSale.GFSID where ShoppingCartItem.PurchID = @PurchID) 

Ou mieux encore, la réécriture de cette façon il ne nécessite pas une boucle. Looping comme ceci est une boucle infinie qui attend de se produire. Vous devez remplacer par des opérations basées sur un ensemble et une transaction.

Questions connexes