SQL Server 2016 тупик, вызванный запросом? - PullRequest
0 голосов
/ 31 октября 2018

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

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

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

Вот мой запрос (с измененными именами для краткости):

BEGIN TRAN
IF EXISTS (SELECT BlahID FROM MyTable WITH (NOLOCK) WHERE BlahID = ?)
    BEGIN
        UPDATE MyTable SET
            Foo = ?,
            Bar = 1
        WHERE BlahID = ?
    END
ELSE
    BEGIN
        INSERT INTO MyTable (Foo, Bar)
        VALUES (1, ?,)
    END
COMMIT TRAN

Ответы [ 3 ]

0 голосов
/ 01 ноября 2018

Вам не нужно IF, чтобы проверить, существует ли уже запись. Предложение WHERE в операторе UPDATE делает это. Все, что вам нужно, это убедиться, что запись не существует до вставки новой записи, например:

UPDATE MyTable 
SET
    Foo = @foo,
    Bar = 1
WHERE BlahID = @id;

INSERT MyTable (Bar,Foo)
values (1,@foo)
where not exists (select BlahID 
                  from MyTable 
                  where BlahID=@id)

Используйте именованные параметры, если это возможно, поэтому вам нужно только передать 2 параметра вместо 4 и рискнуть перепутать порядок.

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

Это также позволяет избежать дублирования записей. Независимо от того, сколько блокировок вы берете, если вы используете предложение IF, две одновременные попытки с одним и тем же несуществующим идентификатором приведут к двум вставкам, потому что оба запроса найдут пропущенную строку, оба попытаются безоговорочно вставить.

Другой вариант - использовать MERGE, хотя в этом случае он не будет работать хорошо. Из документации MERGE

При простом обновлении одной таблицы на основе строк другой таблицы можно повысить производительность и масштабируемость с помощью базовых операторов INSERT, UPDATE и DELETE. Например:

INSERT tbl_A (col, col2)  
SELECT col, col2   
FROM tbl_B   
WHERE NOT EXISTS (SELECT col FROM tbl_A A2 WHERE A2.col = tbl_B.col);  

Текущий случай еще проще, задействована только одна таблица:

INSERT MyTable (Bar,Foo)
VALUES (1,@foo)
WHERE NOT EXISTS (SELECT BlahID FROM MyTable WHERE BlahID=@id);

Почему тупик?

Сервер должен блокировать строки, чтобы обеспечить возможность повторения транзакции. При выборе сервер принимает блокировки SHARED (S) для извлеченных или отсканированных строк. Вот почему индекс приводит к меньшему количеству блокировок - сервер может немедленно найти нужную ему строку. Эти общие блокировки останутся на время транзакции. Если нет явной транзакции, в зависимости от режима изоляции общие блокировки могут быть сохранены на время соединения. Вот что происходит с REPEATABLE READ.

Когда вы пытаетесь обновить строку, сервер попытается выполнить блокировку UPDATE. Если строка имеет SHARED-блокировку сервера, операция обновления будет заблокирована. Если транзакция уже содержит блокировку SHARED в строке, она попытается обновить ее до блокировки UPGRADE. Если у кого-то еще есть S-блокировка в ряду, сделка будет заблокирована. Чтобы чтение было повторяемым , сервер должен заблокировать строки, к которым он прикоснулся.

Хуже, если сервер не может найти одну строку из-за отсутствия индексов.

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

Вот как в этом случае происходит разлочка:

  1. Два соединения выполняют IF(SELECT) и получают блокировки SHARED в ряду, S1 и S2.
  2. Соединение 1 пытается обновить блокировку до UPGRADE, но находит на ней блокировку S2 и блокирует ее ожидание.
  3. Соединение 2 пытается перейти на U, но находит S1 и блоки. Соединение не может быть установлено, что приводит к тупику.

Подробнее о блокировке, типах блокировок, совместимости и области действия можно узнать в разделе Блокировка в компоненте Database Engine в Руководстве по блокировке транзакций SQL Server и управлению версиями строк

Изоляция моментального снимка

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

0 голосов
/ 01 ноября 2018

В конечном итоге я добавил «уникальное ограничение» в поле «BlahID», так как кажется, что он выполнял полную блокировку на уровне TABLE для моего первого оператора UPDATE. После того, как я добавил это ограничение, я считаю, что он правильно выполнил только блокировку на уровне строк, и это решило проблему взаимоблокировки для меня.

Я также удалил формат «IF / ELSE» для своего ОБНОВЛЕНИЯ и просто сделал:

UPDATE MyTable SET
    Foo = ?
WHERE BlahID = ?
IF @@ROWCOUNT=0
    INSERT INTO MyTable (Foo)
    VALUES (1)

Я многое узнал о «табличных подсказках» и блокировке с другими предоставленными ответами, поэтому их стоит прочитать, если вы своенравный Гуглер!

0 голосов
/ 31 октября 2018

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

Правильный шаблон здесь:

BEGIN TRAN
IF EXISTS (SELECT BlahID FROM MyTable WITH (UPDLOCK,HOLDLOCK) WHERE BlahID = ?)
    BEGIN
        UPDATE MyTable SET
            Foo = ?,
            Bar = 1
        WHERE BlahID = ?
    END
ELSE
    BEGIN
        INSERT INTO MyTable (Foo, Bar)
        VALUES (1, ?,)
    END
COMMIT TRAN

SELECT блокирует строку, если она существует, и получает блокировку диапазона обновления для диапазона клавиш, если строка не существует. В обоих случаях второй сеанс блокирует проверку существования, пока первый сеанс не завершит вставку или обновление.

Если вы не читаете с подсказками блокировки (либо в SELECT, UPDATE, INSERT или MERGE), то, если строка не существует, блокировки не будут приняты, и несколько сеансов могут попытаться выполнить INSERT.

...