sql server: обновление полей на огромной таблице небольшими порциями: как получить прогресс / статус?

10

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

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

  • Подойдет ли нижеприведенное?
  • И как мы можем заставить его напечатать какой-то вывод, чтобы мы могли видеть прогресс? (мы попытались добавить туда оператор PRINT, но во время цикла while ничего не выводилось)

Код является:

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 Восстановить Монику
источник

Ответы:

12

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

Поскольку я предлагаю запланировать это с помощью задания агента SQL (в конце концов, это 100 миллионов строк), я не думаю, что любая форма отправки сообщений о состоянии клиенту (например, SSMS) будет идеальной (хотя, если это так Если когда-нибудь понадобятся другие проекты, то я согласен с Владимиром, что использование RAISERROR('', 10, 1) WITH NOWAIT;- это путь).

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

Учитывая, что вы хотите иметь возможность отменить и перезапустить процесс, Я устал от обертывания UPDATE основной таблицы с UPDATE таблицы состояния в явной транзакции. Однако, если вы чувствуете, что таблица состояния не синхронизирована из-за отмены, легко обновить текущее значение, просто обновив его вручную с помощью COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL.и есть две таблицы для ОБНОВЛЕНИЯ (то есть основная таблица и таблица состояния), мы должны использовать явную транзакцию, чтобы синхронизировать эти две таблицы, но мы не хотим рисковать потерянной транзакцией, если вы отмените процесс в точка после того, как он начал транзакцию, но не совершил ее. Это должно быть безопасно, если вы не остановите задание агента SQL.

Как вы можете остановить процесс, не прекращая его? Прося это остановить :-). Ага. Отправив процессу «сигнал» (аналогично kill -3Unix), вы можете запросить его остановку в следующий удобный момент (т. Е. Когда нет активной транзакции!) И заставить его очистить себя от всего приятного и аккуратного.

Как вы можете общаться с запущенным процессом в другой сессии? Используя тот же механизм, который мы создали для него, чтобы сообщить вам его текущий статус: таблицу состояния. Нам просто нужно добавить столбец, который процесс будет проверять в начале каждого цикла, чтобы он знал, продолжить или прервать. А поскольку цель состоит в том, чтобы запланировать это как задание агента SQL (запускать каждые 10 или 20 минут), мы также должны проверить в самом начале, так как нет смысла заполнять временную таблицу 1 миллионом строк, если процесс только идет выйти через мгновение и не использовать эти данные.

DECLARE @BatchRows INT = 1000000,
        @UpdateRows INT = 4995;

IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
  CREATE TABLE dbo.HugeTable_TempStatus
  (
    RowsUpdated INT NOT NULL, -- updated by the process
    LastUpdatedOn DATETIME NOT NULL, -- updated by the process
    PauseProcess BIT NOT NULL -- read by the process
  );

  INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
  VALUES (0, GETDATE(), 0);
END;

-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
  PRINT 'Process is paused. No need to start.';
  RETURN;
END;

CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);

INSERT INTO #FullSet (KeyField1, KeyField2)
  SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
  FROM   dbo.HugeTable ht
  WHERE  ht.deleted IS NULL
  OR     ht.deletedDate IS NULL

WHILE (1 = 1)
BEGIN
  -- Check if process is paused. If yes, just exit cleanly.
  IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
  BEGIN
    PRINT 'Process is paused. Exiting.';
    BREAK;
  END;

  -- grab a set of rows to update
  DELETE TOP (@UpdateRows)
  FROM   #FullSet
  OUTPUT Deleted.KeyField1, Deleted.KeyField2
  INTO   #CurrentSet (KeyField1, KeyField2);

  IF (@@ROWCOUNT = 0)
  BEGIN
    RAISERROR(N'All rows have been updated!!', 16, 1);
    BREAK;
  END;

  BEGIN TRY
    BEGIN TRAN;

    -- do the update of the main table
    UPDATE ht
    SET    ht.deleted = 0,
           ht.deletedDate = '2000-01-01'
    FROM   dbo.HugeTable ht
    INNER JOIN #CurrentSet cs
            ON cs.KeyField1 = ht.KeyField1
           AND cs.KeyField2 = ht.KeyField2;

    -- update the current status
    UPDATE ts
    SET    ts.RowsUpdated += @@ROWCOUNT,
           ts.LastUpdatedOn = GETDATE()
    FROM   dbo.HugeTable_TempStatus ts;

    COMMIT TRAN;
  END TRY
  BEGIN CATCH
    IF (@@TRANCOUNT > 0)
    BEGIN
      ROLLBACK TRAN;
    END;

    THROW; -- raise the error and terminate the process
  END CATCH;

  -- clear out rows to update for next iteration
  TRUNCATE TABLE #CurrentSet;

  WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;

-- clean up temp tables when testing
-- DROP TABLE #FullSet; 
-- DROP TABLE #CurrentSet; 

Затем вы можете проверить статус в любое время, используя следующий запрос:

SELECT sp.[rows] AS [TotalRowsInTable],
       ts.RowsUpdated,
       (sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
       ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE  sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND    sp.[index_id] < 2;

Хотите приостановить процесс, выполняется ли он в задании агента SQL или даже в SSMS на чужом компьютере? Просто беги:

UPDATE ht
SET    ht.PauseProcess = 1
FROM   dbo.HugeTable_TempStatus ts;

Хотите, чтобы процесс мог начать заново? Просто беги:

UPDATE ht
SET    ht.PauseProcess = 0
FROM   dbo.HugeTable_TempStatus ts;

ОБНОВИТЬ:

Вот некоторые дополнительные действия, которые могут улучшить производительность этой операции. Никто не гарантированно поможет, но, вероятно, стоит попробовать. А благодаря обновлению 100 миллионов строк у вас будет достаточно времени / возможностей для тестирования некоторых вариантов ;-).

  1. Добавьте TOP (@UpdateRows)к запросу UPDATE, чтобы верхняя строка выглядела следующим образом:
    UPDATE TOP (@UpdateRows) ht
    Иногда это помогает оптимизатору узнать, на какое число строк будет оказано влияние, чтобы не тратить время на поиск большего количества.
  2. Добавьте ПЕРВИЧНЫЙ КЛЮЧ во #CurrentSetвременную таблицу. Идея состоит в том, чтобы помочь оптимизатору присоединиться к таблице 100 миллионов строк.

    И просто чтобы это было заявлено, чтобы не быть двусмысленным, не должно быть никаких причин добавлять PK во #FullSetвременную таблицу, поскольку это просто простая таблица очередей, где порядок не имеет значения.

  3. В некоторых случаях это помогает добавить фильтрованный индекс, чтобы помочь тому, SELECTчто подается во #FullSetвременную таблицу. Вот некоторые соображения, связанные с добавлением такого индекса:
    1. Условие WHERE должно соответствовать условию WHERE вашего запроса, поэтому WHERE deleted is null or deletedDate is null
    2. В начале процесса большинство строк будут соответствовать вашему условию WHERE, поэтому индекс не так уж полезен. Возможно, вы захотите подождать где-нибудь около отметки 50%, прежде чем добавлять это. Конечно, насколько это помогает и когда лучше всего добавлять индекс, зависит от нескольких факторов, так что это немного проб и ошибок.
    3. Возможно, вам придется вручную обновить STATS и / или перестроить индекс, чтобы поддерживать его в актуальном состоянии, поскольку базовые данные меняются довольно часто
    4. Обязательно имейте в виду, что индекс, хотя и помогает SELECT, повредит, UPDATEпоскольку это еще один объект, который должен быть обновлен во время этой операции, а следовательно, больше операций ввода-вывода. Это влияет как на использование фильтрованного индекса (который уменьшается при обновлении строк, так как фильтр соответствует меньшему количеству строк), так и на ожидание добавления индекса (если вначале это не очень полезно, то нет причин для этого дополнительный ввод / вывод).
Соломон Руцкий
источник
1
Это отлично. Я запускаю его сейчас, и он курит, что мы можем запустить его на линии в течение дня. Спасибо!
Jonesome Восстановить Монику
@samsmith Пожалуйста, посмотрите раздел ОБНОВЛЕНИЯ, который я только что добавил, так как есть некоторые идеи, которые помогут сделать процесс еще быстрее.
Соломон Руцкий
Без улучшений UPDATE мы получаем около 8 миллионов обновлений в час ... с @BatchRows, установленным на 10000000 (десять миллионов)
Jonesome Reinstate Monica
@samsmith Это здорово :) верно? Имейте в виду две вещи: 1) Процесс будет замедляться, поскольку все меньше и меньше строк соответствуют предложению WHERE, поэтому лучше было бы добавить фильтрованный индекс, но вы уже добавили нефильтрованный индекс в начать, так что я не уверен, поможет ли это или навредит, но все же я ожидал бы, что пропускная способность будет уменьшаться по мере приближения к завершению, и 2) вы можете увеличить пропускную способность, уменьшив значение WAITFOR DELAYдо половины секунды или около того, но это компромисс с параллелизмом и, возможно, сколько пересылается через доставку журналов.
Соломон Руцки
Мы довольны 8 миллионами строк / час. Да, мы видим, что это замедляется. Мы не решаемся создавать еще какие-либо индексы (потому что таблица заблокирована для всей сборки). То, что мы сделали пару раз, - это сделаем переоценку существующего индекса (потому что он в сети).
Jonesome Reinstate Моника
4

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

У меня есть несколько длительных процедур обслуживания, которые иногда должен запускать sys admin.

Я запустил их из SSMS и также заметил, что PRINTоператор отображается в SSMS только после завершения всей процедуры.

Итак, я использую RAISERRORс низкой степенью серьезности:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

Я использую SQL Server 2008 Standard и SSMS 2012 (11.0.3128.0). Вот полный рабочий пример для запуска в SSMS:

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

Когда я закомментирую RAISERRORи оставлю только PRINTсообщения на вкладке «Сообщения» в SSMS, они появятся только после завершения всего пакета, через 6 секунд.

Когда я закомментирую PRINTи использую RAISERRORсообщения на вкладке «Сообщения» в SSMS, они появляются без ожидания в течение 6 секунд, но по мере прохождения цикла.

Интересно, что когда я использую оба RAISERRORи PRINT, я вижу оба сообщения. Сначала приходит сообщение сначала RAISERROR, затем задержка на 2 секунды, затем сначала PRINTи второе RAISERROR, и так далее.


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

Пока идет долгий процесс, я периодически SELECTиз logтаблицы вижу, что происходит.

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

Владимир Баранов
источник
В SQL 2008/2014 мы не можем увидеть результаты файла повышение ошибок .... чего нам не хватает?
Jonesome Восстановить Монику
@samsmith, я добавил полный пример. Попытайся. Какое поведение вы получаете в этом простом примере?
Владимир Баранов
2

Вы можете отслеживать это из другого соединения с помощью чего-то вроде:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

чтобы увидеть, сколько еще осталось сделать. Это может быть полезно, если приложение вызывает процесс, а не запускает его вручную в SSMS или подобном, и ему необходимо показать прогресс: запустить основной процесс асинхронно (или в другом потоке), а затем выполнить цикл с вызовом «сколько осталось» msgstr "проверять каждый раз, пока асинхронный вызов (или поток) не завершится.

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

Дэвид Спиллетт
источник