Я не знал об этом вопросе, когда отвечал на связанный вопрос ( нужны ли явные транзакции в этом цикле 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 миллионов строк у вас будет достаточно времени / возможностей для тестирования некоторых вариантов ;-).
- Добавьте
TOP (@UpdateRows)к запросу UPDATE, чтобы верхняя строка выглядела следующим образом:
UPDATE TOP (@UpdateRows) ht
Иногда это помогает оптимизатору узнать, на какое число строк будет оказано влияние, чтобы не тратить время на поиск большего количества.
Добавьте ПЕРВИЧНЫЙ КЛЮЧ во #CurrentSetвременную таблицу. Идея состоит в том, чтобы помочь оптимизатору присоединиться к таблице 100 миллионов строк.
И просто чтобы это было заявлено, чтобы не быть двусмысленным, не должно быть никаких причин добавлять PK во #FullSetвременную таблицу, поскольку это просто простая таблица очередей, где порядок не имеет значения.
- В некоторых случаях это помогает добавить фильтрованный индекс, чтобы помочь тому,
SELECTчто подается во #FullSetвременную таблицу. Вот некоторые соображения, связанные с добавлением такого индекса:
- Условие WHERE должно соответствовать условию WHERE вашего запроса, поэтому
WHERE deleted is null or deletedDate is null
- В начале процесса большинство строк будут соответствовать вашему условию WHERE, поэтому индекс не так уж полезен. Возможно, вы захотите подождать где-нибудь около отметки 50%, прежде чем добавлять это. Конечно, насколько это помогает и когда лучше всего добавлять индекс, зависит от нескольких факторов, так что это немного проб и ошибок.
- Возможно, вам придется вручную обновить STATS и / или перестроить индекс, чтобы поддерживать его в актуальном состоянии, поскольку базовые данные меняются довольно часто
- Обязательно имейте в виду, что индекс, хотя и помогает
SELECT, повредит, UPDATEпоскольку это еще один объект, который должен быть обновлен во время этой операции, а следовательно, больше операций ввода-вывода. Это влияет как на использование фильтрованного индекса (который уменьшается при обновлении строк, так как фильтр соответствует меньшему количеству строк), так и на ожидание добавления индекса (если вначале это не очень полезно, то нет причин для этого дополнительный ввод / вывод).