Предложение while в T-SQL, которое зацикливается навсегда - PullRequest
7 голосов
/ 29 сентября 2008

Недавно мне было поручено отладить странную проблему в приложении электронной коммерции. После обновления приложения сайт начал время от времени зависать, и меня отправили на отладку. После проверки журнала событий я обнаружил, что 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. RowCount из оператора DELETE указывает, что он был удален
  3. Предложение WHILE будет зациклено навсегда

Процедура была переписана для выбора строк, которые должны быть удалены во временную таблицу в памяти, чтобы немедленная проблема была решена, но это действительно вызвало мое любопытство.

Почему он зацикливается навсегда?

Уточнение : удаление не завершается неудачно (@@ rowcount равно 1 после удаления stmt при отладке) Разъяснение 2 : не должно иметь значения, упорядочен ли оператор SELECT TOP ... каким-либо конкретным полем, поскольку запись с возвращенным идентификатором будет удалена, поэтому в следующем цикле она должна получить другую запись .

Обновление : После проверки журналов подрывной деятельности я обнаружил, что коммит-виновник, который сделал эту хранимую процедуру, вышел из строя. Единственное реальное различие, которое я могу найти, состоит в том, что ранее в операторе SELECT TOP 1 не было никакого соединения, то есть без того, чтобы это соединение работало без каких-либо операторов транзакций, окружающих удаление. Похоже, это было введение объединения, которое сделало SQL-сервер более разборчивым.

Обновление пояснений : brien указало, что нет необходимости в объединении, но мы действительно используем некоторые поля из таблицы GoodsForSale, но я удалил их, чтобы просто сохранить код так что мы можем сосредоточиться на проблеме под рукой

Ответы [ 7 ]

3 голосов
/ 30 сентября 2008
FROM
  ShoppingCartItem
    INNER JOIN
  GoodsForSale
    on ShoppingCartItem.GFSID = GoodsForSale.GFSID

Упс, ваше объединение сводит результирующий набор к нулю строк.

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

Упс, вы использовали множественное назначение для набора без строк. Это приводит к тому, что переменные остаются неизменными (они будут иметь то же значение, что и в прошлый раз в цикле). В этом случае переменным НЕ присваивается значение null.

Если вы поместите этот код в начало цикла, он будет (правильно) работать быстрее:

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

Если вы измените свой код для получения ключа (без объединения), а затем для извлечения связанных данных по ключу во втором запросе, вы выиграете.

3 голосов
/ 29 сентября 2008

Вы работаете в явном или неявном режиме транзакции ?

Поскольку вы находитесь в явном режиме, я думаю, что вам необходимо окружить операцию 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 действительно внести изменения.

1 голос
/ 29 сентября 2008

Есть ли в ShoppingCartItem запись с этим @PurchID, где GFSID отсутствует в таблице GoodsForSale? Это объясняет, почему EXISTS возвращает true, но больше нет записей для удаления.

0 голосов
/ 11 октября 2008

Если в таблице GoodsForSale есть какие-либо элементы корзины покупок, которых не существует, это приведет к бесконечному циклу.

Попробуйте изменить свое заявление о существовании, чтобы учесть это

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

Или, что еще лучше, переписать это так, чтобы оно не требовало цикла. Цикл как это бесконечный цикл, ожидающий, чтобы случиться. Вы должны заменить операции на основе набора и транзакцию.

0 голосов
/ 30 сентября 2008

Если приведенные выше комментарии не помогли вам, я предлагаю добавить / заменить:

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 голосов
/ 29 сентября 2008

Я не уверен, что понимаю проблему, но в предложении select происходит внутреннее соединение с другой таблицей. Это объединение может привести к тому, что записи не будут получены, и тогда удаление не удастся. Попробуйте использовать левое соединение.

0 голосов
/ 29 сентября 2008

Очевидно, что что-то не удаляется или изменяется, где это должно. Если на следующей итерации условие будет таким же, оно будет продолжаться.

Кроме того, вы сравниваете @TmpShoppingCartItemID, а не @PurchID. Я вижу, как они могут отличаться, и вы можете удалить строку, отличную от той, которая проверяется в операторе while.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...