Почему мой курсор останавливается в середине цикла? - PullRequest
1 голос
/ 04 июня 2009

Код, размещенный здесь, является примером кода, это не рабочий код. Я сделал это, чтобы объяснить проблему, которую я объясняю, читабельной / краткой.


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

таблица содержит 100 строк, когда вставка выполняется после 50 строк, курсор останавливается, коснувшись только первых 50 строк. Когда вставка выполняется после 55, она останавливается после 55 и т. Д.

-- This code is an hypothetical example written to express
-- an problem seen in production

DECLARE @v1 int
DECLARE @v2 int

DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table

OPEN MyCursor

FETCH NEXT FROM MyCursor INTO @v1, @v2

WHILE(@@FETCH_STATUS=0)
BEGIN

  IF(@v1>10)
  BEGIN
    INSERT INTO table2(col1) VALUES (@v2)
  END

  FETCH NEXT FROM MyCursor INTO @v1, @v2

END

CLOSE MyCursor
DEALLOCATE MyCursor

Существует триггер AFTER INSERT на table2 , который используется для регистрации мутаций на table2 в третьей таблице с метким именем mutations Он содержит курсор, который вставляет для обработки вставки (мутации записываются для каждого столбца очень специфическим способом, для которого требуется курсор).

Немного предыстории: это существует на множестве небольших таблиц поддержки. Для проекта требуется, чтобы каждое изменение, внесенное в исходные данные, регистрировалось для целей аудита. Таблицы с регистрацией содержат такие вещи, как номера банковских счетов, на которые будут зачисляться огромные суммы денег. Существует максимум несколько тысяч записей, и они должны изменяться очень редко. Функция аудита предназначена для предотвращения мошенничества: мы записываем «что изменилось» с «кто это сделал».

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

Уф. Теперь вернемся к вопросу.

Упрощенная версия триггера (реальная версия выполняет вставку в столбец, а также вставляет старое значение):

--This cursor is an hypothetical cursor written to express
--an problem seen in production.

--On UPDATE a new record must be added to table Mutaties for
--every row in every column in the database.  This is required
--for auditing purposes.

--An set-based approach which stores the previous state of the row
--is expressly forbidden by the customer


DECLARE @col1 int
DECLARE @col2 int
DECLARE @col1_old int
DECLARE @col2_old int

--Loop through old values next to new values
DECLARE MyTriggerCursor CURSOR FAST_FORWARD FOR
SELECT i.col1, i.col2, d.col1 as col1_old, d.col2 as col2_old
FROM Inserted i
  INNER JOIN Deleted d ON i.id=d.id

OPEN MyTriggerCursor 

FETCH NEXT FROM MyTriggerCursor INTO @col1, @col2, @col1_old, @col2_old

--Loop through all rows which were updated
WHILE(@@FETCH_STATUS=0)
BEGIN

    --In production code a few more details are logged, such as userid, times etc etc

    --First column
    INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
    VALUES ('table2', 'col1', @col1, @col1_old)

    --Second column
    INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
    VALUES ('table2', 'col2', @col2, @col1_old)

    FETCH NEXT FROM MyTriggerCursor INTO @col1, @col2, @col1_old, @col2_old

END

CLOSE MyTriggerCursor
DEALLOCATE MyTriggerCursor

Почему код выходит из середины цикла?

Ответы [ 6 ]

9 голосов
/ 04 июня 2009

Ваша проблема в том, что вам вообще НЕ следует использовать курсор для этого! Это код для приведенного выше примера.

INSERT INTO table2(col1)
SELECT Col1 FROM table
where col1>10

Вы также никогда не должны использовать курсор в триггере, который снизит производительность. Если кто-то добавил 100 000 строк во вставку, это может занять минуты (или даже часы) вместо миллисекунд или секунд. Мы заменили один здесь (который предшествовал моему приходу на эту работу) и сократили импорт в эту таблицу с 40 минут до 45 секунд.

Любой производственный код, использующий курсор, должен быть проверен, чтобы заменить его на правильный код на основе набора. по моему опыту, 90 +% всех курсоров могут быть переписаны на основе набора.

4 голосов
/ 04 июня 2009

это простое неправильное понимание триггеров ... для этого вам вообще не нужен курсор

if UPDATE(Col1)
begin

    insert into mutaties
    (
        tablename, 
        columnname, 
        newvalue
    )
    select
    'table2',
    coalesce(d.Col1,''),
    coalesce(i.Col1,''),
    getdate()
    from inserted i
        join deleted d on i.ID=d.ID
            and coalesce(d.Col1,-666)<>coalesce(i.Col1,-666)

end

По сути, этот код проверяет, были ли обновлены данные этого столбца. если это так, он сравнивает новые и старые данные, а если он отличается, он вставляет в таблицу журналов.

ваш первый пример кода может быть легко заменен чем-то вроде этого

insert into table2 (col1)
select Col2
from table
where Col1>10
2 голосов
/ 04 июня 2009

Райан, ваша проблема в том, что @@ FETCH_STATUS является глобальным для всех курсоров в соединении.

Таким образом, курсор в триггере заканчивается @@ FETCH_STATUS -1. Когда управление возвращается к приведенному выше коду, последний @@ FETCH_STATUS был равен -1, поэтому курсор заканчивается.

Это объясняется в документации, которую можно найти на MSDN здесь .

Что вы можете сделать, это использовать локальную переменную для хранения @@ FETCH_STATUS и поместить эту локальную переменную в цикл. Таким образом, вы получите что-то вроде этого:

DECLARE @v1 int
DECLARE @v2 int
DECLARE @FetchStatus int

DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table

OPEN MyCursor

FETCH NEXT FROM MyCursor INTO @v1, @v2

SET @FetchStatus = @@FETCH_STATUS

WHILE(@FetchStatus=0)
BEGIN

  IF(@v1>10)
  BEGIN
    INSERT INTO table2(col1) VALUES (@v2)
  END

  FETCH NEXT FROM MyCursor INTO @v1, @v2

  SET @FetchStatus = @@FETCH_STATUS

END

CLOSE MyCursor
DEALLOCATE MyCursor

Стоит отметить, что это поведение не относится к вложенным курсорам. Я сделал быстрый пример, который на SqlServer 2008 возвращает ожидаемый результат (50).

USE AdventureWorks
GO

DECLARE @LocationId smallint
DECLARE @ProductId smallint

DECLARE @Counter int
SET @Counter=0

DECLARE MyFirstCursor CURSOR FOR 
SELECT TOP 10 LocationId
FROM Production.Location

OPEN MyFirstCursor

FETCH NEXT FROM MyFirstCursor INTO @LocationId

WHILE (@@FETCH_STATUS=0)
BEGIN

    DECLARE MySecondCursor CURSOR FOR
    SELECT TOP 5 ProductID
    FROM Production.Product

    OPEN MySecondCursor

    FETCH NEXT FROM MySecondCursor INTO @ProductId

    WHILE(@@FETCH_STATUS=0)
    BEGIN

        SET @Counter=@Counter+1

        FETCH NEXT FROM MySecondCursor INTO @ProductId  

    END

    CLOSE MySecondCursor
    DEALLOCATE MySecondCursor

    FETCH NEXT FROM MyFirstCursor INTO @LocationId

END

CLOSE MyFirstCursor
DEALLOCATE MyFirstCursor

--
--Against the initial version of AdventureWorks, counter should be 50.
--
IF(@Counter=50)
    PRINT 'All is good with the world'
ELSE
    PRINT 'Something''s wrong with the world today'
1 голос
/ 05 июня 2009

Вам не нужно использовать курсор для вставки каждого столбца в виде отдельной строки.

Вот пример:

INSERT LOG.DataChanges
SELECT
   SchemaName = 'Schemaname',
   TableName = 'TableName',
   ColumnName = CASE ColumnID WHEN 1 THEN 'Column1' WHEN 2 THEN 'Column2' WHEN 3 THEN 'Column3' WHEN 4 THEN 'Column4' END
   ID = Key1,
   ID2 = Key2,
   ID3 = Key3,
   DataBefore = CASE ColumnID WHEN 1 THEN I.Column1 WHEN 2 THEN I.Column2 WHEN 3 THEN I.Column3 WHEN 4 THEN I.Column4 END,
   DataAfter = CASE ColumnID WHEN 1 THEN D.Column1 WHEN 2 THEN D.Column2 WHEN 3 THEN D.Column3 WHEN 4 THEN D.Column4 END,
   DateChange = GETDATE(),
   USER = WhateverFunctionYouAreUsingForThis
FROM
   Inserted I
   FULL JOIN Deleted D ON I.Key1 = D.Key1 AND I.Key2 = D.Key2
   CROSS JOIN (
      SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
   ) X (ColumnID)

В таблице X вы могли бы кодировать дополнительное поведение со вторым столбцом, который специально описывает, как обрабатывать только этот столбец (допустим, вы хотели, чтобы одни публиковали все время, а другие только при изменении значения). Важно то, что это пример метода перекрестного соединения, который разбивает строки на каждый столбец, но можно сделать гораздо больше. Обратите внимание, что полное объединение позволяет работать со вставками и удаляет, а также с обновлениями.

Я также полностью согласен с тем, что хранение каждой строки лучше FAR. Подробнее об этом см. на этом форуме .

1 голос
/ 04 июня 2009

Этот код не извлекает какие-либо дополнительные значения из курсора и не увеличивает значения. Как таковой, нет никакой причины для реализации курсора здесь.

Весь ваш код может быть переписан как:

DECLARE @v1 int
DECLARE @v2 int

SELECT @v1 = Col1, @v2 = Col2
FROM table

IF(@v1>10)
    INSERT INTO table2(col1) VALUES (@v2)

Редактировать: Сообщение отредактировано для устранения проблемы, о которой я говорил.

0 голосов
/ 04 июня 2009

Как уже упоминалось, вы не получаете никаких других значений. Таким образом, @@ FETCH_STATUS получает свое значение от вашего курсора, содержащегося в вашем триггере AFTER INSERT.

Вы должны изменить свой код на

DECLARE @v1 int
DECLARE @v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table

OPEN MyCursor

FETCH NEXT FROM MyCursor INTO @v1, @v2

WHILE(@@FETCH_STATUS=0)
BEGIN
  IF(@v1>10)
  BEGIN
    INSERT INTO table2(col1) VALUES (@v2)
  END
  FETCH NEXT FROM MyCursor INTO @v1, @v2
END
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...