Повторяющиеся записи возвращаются из таблицы без дубликатов

8

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

Упрощенная версия запроса:

INSERT INTO #TempWorkIDs (WorkID)
SELECT
        W.WorkID

    FROM
        dbo.WorkTable W

    WHERE
        (@bool_param = 0 AND
        ((W.InProgress = 0
         AND ISNULL(W.UserID, -1) != @userid_param
         AND (@bool_filtered = 0
              OR W.TypeID IN (SELECT TypeID FROM #Types AS t)))
         OR 
         (@bool_param = 1
          AND W.InProgress = 1
          AND W.UserID != @userid_param)
        OR
        (@Auto_Param = 0
         AND W.UserID = @userid_param)))
         OR
         (@bool_param = 1 AND W.UserID = @userid_param)
    OPTION
        (RECOMPILE)

#TypesТаблица заполняется в начале процедуры.

Как я уже сказал, WorkTableон занят, и иногда, когда этот запрос выполняется, я ПОДТВЕРЖДАЮ, что одна из записей перемещается из одного набора фильтров в WHEREдругой. В частности, это происходит, когда кто-то начинает работать с элементом, и W.InProgressизменяется с 0 на 1. Когда это происходит, я получаю нарушение дубликата ключа, когда пытаюсь добавить первичный ключ во временную таблицу, в которую этот запрос вставляется.

В плане запроса, сгенерированном при возникновении ошибки, я подтвердил, что параллелизма нет, уровень изоляции равен READ COMMITTED, а в исходной таблице нет повторяющихся записей. Вы также можете увидеть, что здесь нет JOINs или другого способа получить декартовы произведения.

Это анонимный план запроса:

введите описание изображения здесь

Вопрос в том, что вызывает дубликаты и как их остановить?

Я думаю, что READ COMMITTEDздесь должно работать, мне нужна блокировка. Я почти уверен, что дуплексы происходят, когда InProgressбит записи меняется, когда я запрашиваю. Я знаю это, потому что в таблице хранится время этого изменения, и оно находится в пределах миллисекунд с момента, когда я запрашиваю и получаю ошибку.

JNK
источник

Ответы:

9

Есть несколько хитрых сценариев, которые могут привести к тому, что одна и та же строка будет прочитана дважды из индекса, даже на READ COMMITTEDуровне изоляции .

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

Для вашей таблицы у вас есть InProgressпервый столбец кластерного ключа. Вероятно, вы получаете блокировки строк или страниц при сканировании таблицы. Если вы прочтете строку в начале сканирования, снимите блокировку с нее, эта строка обновится так, что InProgressизменится с 0 на 1, а затем строка снова будет считана на другой странице, после чего вы сможете увидеть повторяющиеся WorkIDзначения из вашего запроса. ,

Есть много обходных путей. Вы можете вставить в кучу и просто удалить дубликаты значений. Вы можете добавить DISTINCTк запросу. Можно также включить уровень изоляции управления версиями строк, чтобы обеспечить стабильное представление зафиксированного состояния базы данных, как в начале транзакции ( изоляция моментального снимка ), так и в начале оператора ( чтение изолированной фиксации моментального снимка) ).

Возможно, целесообразно добавить подсказки по блокировке или изменить структуру таблицы. Для довольно забавного решения (вероятно, не подходящего для производства) вы можете попробовать прочитать индекс в обратном направлении. Это может быть сделано с лишним TOPвместе с ORDER BY. Ниже приведена очень простая демонстрация, иллюстрирующая ситуацию:

CREATE TABLE #WorkTable (
    InProgress TINYINT NOT NULL,
    WorkID INT NOT NULL
    , PRIMARY KEY (InProgress, WorkID)
);

INSERT INTO #WorkTable WITH (TABLOCK)
SELECT (RN - 1) / 5000, RN
FROM
(
    SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t
OPTION (MAXDOP 1);

Следующий запрос имеет свойство Ordered: false, но он все равно будет считывать данные в порядке кластерных ключей:

SELECT WorkId
FROM #WorkTable;

Однако следующий запрос будет считывать данные в обратном кластерном порядке:

SELECT TOP (9223372036854775807) WorkId
FROM #WorkTable
ORDER BY InProgress DESC, WorkId DESC;

Мы можем увидеть это, посмотрев на свойства сканирования:

сканирование назад

Для вашей таблицы это означает, что если строка обновляется так, что InProgressизменяется с 0 на 1, вероятность того, что она появится дважды, будет гораздо меньше. Это может вообще не отображаться, что может быть другой проблемой.

Джо Оббиш
источник