Casper-SC
@Casper-SC
Программист (.NET)

Опасно ли выполнять такой код в С# + T-SQL? Или каким способом можно написать по-другому?

Есть 3 таблицы. Основная SOME_GROUP и две дочерние, что видно из столбцов GROUP_ID.

У всех трёх таблиц первичный ключ:
CONSTRAINT PK_BLA_BLA PRIMARY KEY CLUSTERED (ID)

Так же у дочерних таблиц есть внешние ключи (GROUP_ID), которые указывают на ID в таблице SOME_GROUP.

Эти строки в запросе приведены для удобства и понимания, что как выполняется.
BEGIN TRANSACTION
SET TRANSACTION ISOLATION LEVEL READ COMMITTED -- REPEATABLE READ

В реальности же из C# кода создаётся транзакция и вызывается получение данных для 3-ёх таблиц. То есть к MSSQL серверу отправляется 3 запроса по очереди в одной транзакции.

Вопрос: Есть ли вероятность, что на одном из запросов строка...
SELECT TOP(@Quantity) ID FROM SOME_GROUP WITH(UPDLOCK) WHERE REPLICATED <> 1

...вернёт другой результат? По идее строки отсортированы по первичному ключу. Если другая транзакция залочит одну из необходимых записей, то мой даже первый запрос будет ждать, пока записи разлочатся, а далее сразу же залочит все записи, которые будут выбраны и в пределах одной транзакции все 3 запроса выполнятся и все 3 раза получение SELECT TOP(@Quantity) вернёт один и тот же результат. Верно?

BEGIN TRANSACTION
SET TRANSACTION ISOLATION LEVEL READ COMMITTED -- REPEATABLE READ

SET DEADLOCK_PRIORITY LOW;
DECLARE @Quantity INT = 5;

-- Первый запрос, вызываемый из C# кода в одной транзакции.
-- 1 ------------------------------------------------------------------
UPDATE SOME_GROUP SET REPLICATED = 2 
OUTPUT 
  INSERTED.ID
 ,INSERTED.NAME
WHERE ID IN(SELECT TOP(@Quantity) ID FROM SOME_GROUP WITH(UPDLOCK) WHERE REPLICATED <> 1)

-- Второй запрос, вызываемый из C# кода в одной транзакции.
-- 2------------------------------------------------------------------
UPDATE SOME_CHILD_ONE SET REPLICATED = 2 
OUTPUT 
  INSERTED.ID
 ,INSERTED.GROUP_ID
 ,INSERTED.UPDATED
WHERE GROUP_ID IN(SELECT TOP(@Quantity) ID FROM SOME_GROUP WITH(UPDLOCK) WHERE REPLICATED <> 1) AND REPLICATED <> 1

-- Третий запрос, вызываемый из C# кода в одной транзакции.
-- 3 ------------------------------------------------------------------
UPDATE SOME_CHILD_TWO SET REPLICATED = 2 
OUTPUT 
  INSERTED.ID
 ,INSERTED.GROUP_ID
 ,INSERTED.NAME
 ,INSERTED.CREATED
WHERE GROUP_ID IN(SELECT TOP(@Quantity) ID FROM SOME_GROUP WITH(UPDLOCK) WHERE REPLICATED <> 1) AND REPLICATED <> 1
-- 3 ------------------------------------------------------------------

COMMIT TRANSACTION
  • Вопрос задан
  • 1054 просмотра
Решения вопроса 2
@edb
SQL
Ваш код не будет работать всегда предсказуемым образом. Основная проблема в том, что SELECT TOP без ORDER BY не гарантирует ни в коем случае одни и те же данные. Да, в вашем случае они скорее всего приходят в порядке из сохранения в кластерном индексе. Но если изменится индекс или добавится новый, то ваш код может перестать работать как ожидается.
Другая проблема в том, что несмотря на то, что подзапросы в WHERE одинаковые, они могут возвращать разные данные из-за первого UPDATE. Если у вас было 10 записей с REPLICATED =-10 (минус!), то до первого обновления подзапрос выдаст записи с 1 по 5, а после него с 6 по 10.
Совет: сохраните обновляемые ID в отдельную временную таблицу и присоединяйте ее для обновления.
Либо настройте внешние ключи и каскадное обновление.
Ответ написан
Casper-SC
@Casper-SC Автор вопроса
Программист (.NET)
В итоге решил это дело так. На место {0} в C# коде подставляется сгенерированный GUID

BEGIN TRANSACTION
SET TRANSACTION ISOLATION LEVEL READ COMMITTED

SET DEADLOCK_PRIORITY LOW;
DECLARE @Quantity INT = 5;

IF OBJECT_ID('tempdb..##GroupIDs_{0}') IS NULL
BEGIN
	CREATE TABLE ##GroupIDs_{0} (
		ID INT NOT NULL,
		CONSTRAINT PK_GroupIDs_ID_{0} PRIMARY KEY(ID),
	);
END

INSERT INTO ##GroupIDs_{0} (ID) 
SELECT TOP(@Quantity) ID FROM SOME_GROUP WITH(UPDLOCK, READPAST) WHERE REPLICATED <> 1

-- Первый запрос, вызываемый из C# кода в одной транзакции.
-- 1 ------------------------------------------------------------------
UPDATE SOME_GROUP SET REPLICATED = 2 
OUTPUT 
  INSERTED.ID
 ,INSERTED.NAME
WHERE ID IN(SELECT ID FROM ##GroupIDs_{0})

-- Второй запрос, вызываемый из C# кода в одной транзакции.
-- 2------------------------------------------------------------------
UPDATE SOME_CHILD_ONE SET REPLICATED = 2 
OUTPUT 
  INSERTED.ID
 ,INSERTED.GROUP_ID
 ,INSERTED.UPDATED
WHERE GROUP_ID IN(SELECT ID FROM ##GroupIDs_{0}) AND REPLICATED <> 1

-- Третий запрос, вызываемый из C# кода в одной транзакции.
-- 3 ------------------------------------------------------------------
UPDATE SOME_CHILD_TWO SET REPLICATED = 2 
OUTPUT 
  INSERTED.ID
 ,INSERTED.GROUP_ID
 ,INSERTED.NAME
 ,INSERTED.CREATED
WHERE GROUP_ID IN(SELECT ID FROM ##GroupIDs_{0}) AND REPLICATED <> 1

DROP TABLE ##GroupIDs_{0};
-- 3 ------------------------------------------------------------------

COMMIT TRANSACTION
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы