Нужны ли явные транзакции в этом цикле while?

11

SQL Server 2014:

У нас очень большая таблица (100 миллионов строк), и нам нужно обновить пару полей в ней.

Что касается доставки журналов и т. Д., Мы также, очевидно, хотим, чтобы они содержали транзакции размером с кусочек.

Если мы позволим приведенному ниже выполнить некоторое время, а затем отменим / завершим запрос, будет ли вся работа, выполненная до сих пор, завершена, или нам нужно добавить явные операторы BEGIN TRANSACTION / END TRANSACTION, чтобы мы могли отменить в любое время?

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END
Jonesome Восстановить Монику
источник

Ответы:

13

Отдельные утверждения - DML, DDL и т. Д. - сами по себе являются транзакциями. Так что да, после каждой итерации цикла (технически: после каждого оператора) все, что было UPDATEизменено, автоматически фиксируется.

Конечно, всегда есть исключение, верно? Можно включить неявные транзакции через SET IMPLICIT_TRANSACTIONS , и в этом случае первый UPDATEоператор будет запускать транзакцию, которую вы должны будете выполнить COMMITили ROLLBACKв конце. Это настройка уровня сеанса, которая по умолчанию выключена в большинстве случаев.

нам нужно добавить явные операторы BEGIN TRANSACTION / END TRANSACTION, чтобы мы могли отменить их в любое время?

Нет. На самом деле, учитывая, что вы хотите иметь возможность остановить процесс и перезапустить, добавление явной транзакции (или включение неявных транзакций) было бы плохой идеей, поскольку остановка процесса могла бы его перехватить до того, как он выполнит COMMIT. В этом случае вам нужно будет вручную выполнить COMMIT(если вы находитесь в SSMS), или если вы запускаете это из задания агента SQL, у вас нет такой возможности, и вы можете получить потерянную транзакцию.


Кроме того, вы можете установить @CHUNK_SIZEменьшее число. Повышение блокировок обычно происходит при 5000 блокировках, полученных на одном объекте. В зависимости от размера строк и от того, выполняет ли он блокировку строк по сравнению с блокировками страниц, вы можете превысить это ограничение. Если размер строки таков, что на каждую страницу умещается только 1 или 2 строки, то вы можете столкнуться с этим, даже если она блокирует страницу.

Если таблица секционирована, у вас есть возможность установить LOCK_ESCALATIONпараметр (введенный в SQL Server 2008) для таблицы, чтобы AUTOпри эскалации она блокировала только раздел, а не всю таблицу. Или, для любой таблицы, вы можете установить ту же самую опцию DISABLE, хотя вы должны быть очень осторожны с этим. См. ALTER TABLE для деталей.

Вот некоторая документация, в которой говорится о повышении блокировки и пороговых значениях: Повышение блокировки (оно относится к «SQL Server 2008 R2 и более поздним версиям»). А вот сообщение в блоге, посвященное обнаружению и устранению эскалации блокировок: Блокировка в Microsoft SQL Server (часть 12 - Эскалация блокировок) .


Не связанный с конкретным вопросом, но связанный с запросом в вопросе, здесь можно сделать несколько улучшений (или, по крайней мере, так кажется, просто взглянув на него):

  1. Для вашего цикла это WHILE (@@ROWCOUNT = @CHUNK_SIZE)немного лучше, поскольку, если количество строк, обновленных на последней итерации, меньше, чем количество, запрошенное для UPDATE, то работы больше не остается.

  2. Если deletedполе является BITтипом данных, то это значение не определяется тем, deletedDateявляется ли оно 2000-01-01? Зачем вам оба?

  3. Если эти два поля являются новыми, и вы добавили их как NULLтаковые, это может быть оперативная / неблокирующая операция, и теперь вы хотите обновить их до значения по умолчанию, тогда в этом не было необходимости. Начиная с SQL Server 2012 (только для Enterprise Edition), добавление NOT NULLстолбцов с ограничением DEFAULT является неблокируемой операцией, если значение DEFAULT является константой. Поэтому, если вы еще не используете поля, просто удалите и повторно добавьте как NOT NULLи с ограничением DEFAULT.

  4. Если никакой другой процесс не обновляет эти поля, пока вы выполняете это ОБНОВЛЕНИЕ, то будет быстрее, если вы поставите в очередь записи, которые вы хотите обновить, а затем просто отработаете эту очередь. В текущем методе наблюдается снижение производительности, так как вам приходится каждый раз повторять запрос к таблице, чтобы получить набор, который необходимо изменить. Вместо этого вы могли бы выполнить следующее, которое сканирует таблицу только один раз в этих двух полях, а затем выдает только очень целенаправленные операторы UPDATE. Также нет штрафов за остановку процесса в любое время и запуск его позже, так как начальное заполнение очереди просто найдет записи, оставшиеся для обновления.

    1. Создайте временную таблицу (#FullSet), в которой есть только ключевые поля из кластерного индекса.
    2. Создайте вторую временную таблицу (#CurrentSet) той же структуры.
    3. вставить в #FullSet через SELECT TOP(n) KeyField1, KeyField2 FROM [huge-table] where deleted is null or deletedDate is null;

      Это TOP(n)там из-за размера таблицы. Имея 100 миллионов строк в таблице, вам не нужно заполнять таблицу очередей всем этим набором ключей, особенно если вы планируете останавливать процесс время от времени и перезапускать его позже. Так что, возможно, установите n1 миллион и дайте этому закончиться. Вы всегда можете запланировать это в задании агента SQL, которое запускает набор 1 миллион (или, может быть, даже меньше), а затем ждет следующего запланированного времени, чтобы забрать снова. Затем вы можете запланировать запуск каждые 20 минут, так что между наборами будет некоторое пространство для принудительного дыхания n, но оно все равно завершит весь процесс без присмотра. Тогда просто попросите удалить саму работу, когда больше нечего делать :-).

    4. в цикле выполните:
      1. Заполните текущий пакет через что-то вроде DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. Сделайте ОБНОВЛЕНИЕ, используя что-то вроде: UPDATE ht SET ht.deleted = 0, ht.deletedDate='2000-01-01' FROM [huge-table] ht INNER JOIN #CurrentSet cs ON cs.KeyField = ht.KeyField;
      4. Очистить текущий набор: TRUNCATE TABLE #CurrentSet;
  5. В некоторых случаях это помогает добавить фильтрованный индекс, чтобы помочь тому, SELECTчто подается во #FullSetвременную таблицу. Вот некоторые соображения, связанные с добавлением такого индекса:
    1. Условие WHERE должно соответствовать условию WHERE вашего запроса, поэтому WHERE deleted is null or deletedDate is null
    2. В начале процесса большинство строк будут соответствовать вашему условию WHERE, поэтому индекс не так уж полезен. Возможно, вы захотите подождать где-нибудь около отметки 50%, прежде чем добавлять это. Конечно, насколько это помогает и когда лучше всего добавлять индекс, зависит от нескольких факторов, так что это немного проб и ошибок.
    3. Возможно, вам придется вручную обновить STATS и / или перестроить индекс, чтобы поддерживать его в актуальном состоянии, поскольку базовые данные меняются довольно часто
    4. Обязательно имейте в виду, что индекс, хотя и помогает SELECT, повредит, UPDATEпоскольку это еще один объект, который должен быть обновлен во время этой операции, а следовательно, больше операций ввода-вывода. Это влияет как на использование фильтрованного индекса (который уменьшается при обновлении строк, так как фильтр соответствует меньшему количеству строк), так и на ожидание добавления индекса (если вначале это не очень полезно, то нет причин для этого дополнительный ввод / вывод).

ОБНОВЛЕНИЕ: Пожалуйста, смотрите мой ответ на вопрос, который связан с этим вопросом, для полной реализации того, что предложено выше, включая механизм для отслеживания состояния и чистой отмены: сервер sql: обновление полей в огромной таблице небольшими порциями: как получить статус прогресса?

Соломон Руцкий
источник
Ваши предложения в # 4 могут быть быстрее в некоторых случаях, но это кажется значительной сложностью кода для добавления. Я бы предпочел начать с простого, а затем, если это не отвечает вашим потребностям, подумайте об альтернативах.
Бекон Биты
@BaconBits Договорились о том, чтобы начать просто. Чтобы быть справедливым, эти предложения не предназначены для применения ко всем сценариям. Речь идет о работе с очень большой (более 100 миллионов строк) таблицей.
Соломон Руцки