Термин «предварительный просмотр» в большинстве случаев может вводить в заблуждение, в зависимости от характера обрабатываемых данных (который меняется от операции к операции). Что нужно для того, чтобы текущие данные были в том же состоянии между моментом сбора данных «предварительного просмотра» и когда пользователь возвращается через 15 минут - после того, как взял немного кофе, вышел на улицу, чтобы покурить, прогуляться вокруг блока, возвращаясь и проверяя что-то на eBay - и понимает, что они не нажимали кнопку «ОК», чтобы фактически выполнить операцию, и, наконец, нажимают кнопку?
У вас есть лимит времени на выполнение операции после создания предварительного просмотра? Или, возможно, способ определить, что данные находятся в том же состоянии во время изменения, что и в начальный SELECT
момент?
Это второстепенный момент, поскольку пример кода можно было сделать поспешно и не представлять истинный вариант использования, но почему для INSERT
операции должен быть «Предварительный просмотр» ? Это может иметь смысл при вставке нескольких строк с помощью чего-то подобного, INSERT...SELECT
и может быть вставлено переменное число строк, но это не имеет особого смысла для одноэтапной операции.
это нежелательно из-за ... относительно низкой степени уверенности в том, что данные предварительного просмотра на самом деле являются точным отражением того, что произойдет с обновлением.
Откуда именно эта «низкая степень доверия»? Хотя можно обновить иное количество строк, чем показано для, SELECT
когда объединены несколько таблиц и имеется дублирование строк в наборе результатов, это не должно быть проблемой здесь. Любые строки, на которые должен воздействовать объект, UPDATE
можно выбирать самостоятельно. Если есть несоответствие, то вы делаете запрос неправильно.
И те ситуации, в которых есть дублирование из-за таблицы JOINed, которая соответствует нескольким строкам в таблице, которая будет обновлена, не являются ситуациями, когда генерируется «Предварительный просмотр». И если есть случай, когда это имеет место, то пользователю необходимо объяснить, что он обновляет подмножество отчета, который повторяется в отчете, чтобы это не выглядело как ошибка, если кто-то только глядя на количество затронутых рядов.
Для полноты картины (хотя в других ответах упоминалось об этом) вы не используете TRY...CATCH
конструкцию, поэтому можете легко столкнуться с проблемами при вложении этих вызовов (даже если не использовать точки сохранения и даже если не используются транзакции). Пожалуйста, смотрите мой ответ на следующий вопрос, здесь, на DBA.SE, для шаблона, который обрабатывает транзакции через вложенные вызовы хранимых процедур:
Должны ли мы обрабатывать транзакции в коде C #, а также в хранимых процедурах
ДАЖЕ ЕСЛИ проблемы, о которых говорилось выше, были учтены, все еще существует критический недостаток: в течение короткого периода времени, когда выполняется операция (т. Е. До ROLLBACK
), любые грязные запросы (запросы с использованием WITH (NOLOCK)
или SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
) могут захватывать данные, которые нет там ни минуты спустя. В то время как любой, кто использует грязные запросы на чтение, должен уже знать об этом и принять такую возможность, такие операции, как это, значительно увеличивают вероятность введения аномалий данных, которые очень трудно отлаживать (то есть: сколько времени вы хотите потратить, пытаясь найти проблему, которая не имеет явной прямой причины?).
Подобный шаблон также снижает производительность системы, увеличивая блокировку, снимая больше блокировок и создавая больше активности в журнале транзакций. (Теперь я вижу, что @MartinSmith также упомянул эти 2 проблемы в комментарии к Вопросу.)
Кроме того, если в изменяемых таблицах есть триггеры, это может потребовать дополнительной обработки (ЦП и физическое / логическое чтение), которая не требуется. Триггеры также увеличивают вероятность аномалий данных в результате грязного чтения.
В связи с тем, что было отмечено выше - усиление блокировок - использование Транзакции увеличивает вероятность возникновения тупиковых ситуаций, особенно если задействованы триггеры.
Менее серьезная проблема, которая должна относиться только к менее вероятному сценарию INSERT
операций: данные «Предварительный просмотр» могут не совпадать с данными, вставляемыми в отношении значений столбцов, определенных DEFAULT
Ограничениями ( Sequences
/ NEWID()
/ NEWSEQUENTIALID()
) и IDENTITY
.
Нет необходимости в дополнительных затратах на запись содержимого переменной таблицы во временную таблицу. Это ROLLBACK
не повлияет на данные в табличной переменной (именно поэтому вы сказали, что в первую очередь используете табличные переменные), так что было бы более целесообразно просто SELECT FROM @output_to_return;
в конце, а потом даже не беспокоиться о создании временного Стол.
На всякий случай, если этот нюанс точек сохранения неизвестен (это трудно понять из примера кода, поскольку он показывает только одну хранимую процедуру): вам нужно использовать уникальные имена точек сохранения, чтобы ROLLBACK {save_point_name}
операция работала так, как вы ожидаете. Если вы повторно используете имена, ROLLBACK откатит самую последнюю точку сохранения этого имени, которая может быть не на том же уровне вложенности, откуда ROLLBACK
вызывается. Посмотрите первый пример блока кода в следующем ответе, чтобы увидеть это поведение в действии: Транзакция в хранимой процедуре
Выполнение «Предварительного просмотра» не имеет большого смысла для операций, ориентированных на пользователя. Я делаю это часто для операций обслуживания, чтобы я мог видеть, что будет удалено / Сборка мусора, если я продолжу операцию. Я добавляю необязательный параметр с именем @TestMode
и делаю IF
оператор, который либо делает, а SELECT
когда @TestMode = 1
это делает DELETE
. Иногда я добавляю @TestMode
параметр в хранимые процедуры, вызываемые приложением, чтобы я (и другие) могли выполнять простое тестирование, не влияя на состояние данных, но этот параметр никогда не используется приложением.
На всякий случай это было не понятно из верхнего раздела «Проблемы»:
Если вам нужен / нужен режим «Предварительный просмотр» / «Тест», чтобы увидеть, что должно быть затронуто, если должен выполняться оператор DML, НЕ используйте транзакции (то есть BEGIN TRAN...ROLLBACK
шаблон) для выполнения этого. Это шаблон, который в лучшем случае действительно работает только в однопользовательской системе, и в этой ситуации это даже не очень хорошая идея.
Повторение большей части запроса между двумя ветвями IF
оператора представляет потенциальную проблему необходимости обновления их обоих каждый раз, когда необходимо внести изменения. Тем не менее, различия между этими двумя запросами, как правило, достаточно просты, чтобы их можно было обнаружить в обзоре кода, и их легко исправить. С другой стороны, такие проблемы, как различия в состоянии и грязное чтение, найти и исправить гораздо сложнее. И проблему снижения производительности системы невозможно решить. Мы должны признать и принять, что SQL не является объектно-ориентированным языком, а инкапсуляция / сокращение дублированного кода не была целью разработки SQL, как это было со многими другими языками.
Если запрос достаточно длинный / сложный, вы можете инкапсулировать его во встроенную табличную функцию. Затем вы можете сделать простой SELECT * FROM dbo.MyTVF(params);
для режима «Предварительный просмотр» и присоединиться к значению (ям) ключа для режима «сделать это». Например:
UPDATE tab
SET tab.Col2 = tvf.ColB
...
FROM dbo.Table tab
INNER JOIN dbo.MyTVF(params) tvf
ON tvf.ColA = tab.Col1;
Если это сценарий отчета, как вы упомянули, это может быть, тогда запуск исходного отчета - «Предварительный просмотр». Если кто-то хочет изменить то, что он видит в отчете (возможно, статус), то это не требует дополнительного предварительного просмотра, поскольку ожидается изменение отображаемых в данный момент данных.
Если операция заключается в том, чтобы, возможно, изменить ставку на определенный процент или бизнес-правило, то это может быть выполнено на уровне представления (JavaScript?).
Если вам действительно необходимо выполнить «Предварительный просмотр» для операции , ориентированной на конечного пользователя , то вам необходимо сначала зафиксировать состояние данных (возможно, хеш всех полей в наборе результатов для UPDATE
операций или значения ключей для DELETE
операции), а затем, перед выполнением операции, сравните захваченную информацию о состоянии с текущей информацией - внутри транзакции, выполняющей HOLD
блокировку таблицы, чтобы ничего не изменилось после выполнения этого сравнения - и, если есть какая-либо разница, выведите ошибка и сделайте ROLLBACK
вместо того, чтобы продолжить UPDATE
или DELETE
.
Для обнаружения различий в UPDATE
операциях альтернативой вычислению хэша в соответствующих полях будет добавление столбца типа ROWVERSION . Значение типа ROWVERSION
данных автоматически изменяется каждый раз, когда происходит изменение в этой строке. Если бы у вас был такой столбец, вы бы добавили SELECT
его вместе с другими данными «Предварительный просмотр», а затем передали его на шаг «обязательно, продолжайте и обновите» вместе со значением (ями) ключа и значением (ями). изменить. Затем вы сравнили бы эти ROWVERSION
значения, переданные из «Предварительного просмотра», с текущими значениями (для каждого ключа) и продолжили UPDATE
бы только если ВСЕиз сопоставленных значений. Преимущество здесь в том, что вам не нужно вычислять хеш, который имеет потенциал, даже если он маловероятен, для ложноотрицательных результатов и занимает некоторое время каждый раз, когда вы делаете SELECT
. С другой стороны, ROWVERSION
значение увеличивается автоматически только при изменении, поэтому вам не о чем беспокоиться. Однако ROWVERSION
тип имеет 8 байтов, которые могут складываться при работе со многими таблицами и / или многими строками.
У каждого из этих двух методов есть свои плюсы и минусы, связанные с обнаружением несогласованного состояния, связанного с UPDATE
операциями, поэтому вам нужно будет определить, какой метод имеет больше «за», чем «против» для вашей системы. Но в любом случае вы можете избежать задержки между генерацией Preview и выполнением операции, которая может вызвать поведение, не соответствующее ожиданиям конечного пользователя.
Если вы работаете с режимом «Предварительный просмотр» для конечного пользователя, то в дополнение к регистрации состояния записей во время выбора, передаче и проверке во время модификации, добавьте DATETIME
for SelectTime
и заполните через GETDATE()
или что-то подобное. Передайте это вместе со слоем приложения, чтобы его можно было передать обратно в хранимую процедуру (чаще всего в виде одного входного параметра), чтобы ее можно было проверить в хранимой процедуре. Затем вы можете определить, что если операция не является режимом «Предварительный просмотр», тогда @SelectTime
значение должно быть не более, чем за X минут до текущего значения GETDATE()
. Может быть, 2 минуты? 5 минут? Скорее всего, не более 10 минут. Сгенерируйте ошибку, если значение DATEDIFF
IN MINUTES превышает этот порог.