Мені не було відомо про це запитання, коли я відповів на відповідне запитання ( чи потрібні явні транзакції в цьому циклі? ), Але заради повноти я вирішу це питання тут, оскільки воно не було частиною моєї пропозиції в тій пов'язаній відповіді .
Оскільки я пропоную запланувати це за допомогою роботи агента SQL (зрештою, це 100 мільйонів рядків), я не думаю, що будь-яка форма надсилання повідомлень про стан клієнту (тобто SSMS) буде ідеальною (хоча якщо це колись виникає потреба в інших проектах, тоді я погоджуюся з Володимиром, що використання RAISERROR('', 10, 1) WITH NOWAIT;
- це шлях).
У цьому конкретному випадку я створив би таблицю статусу, яку можна оновити для кожного циклу, кількість оновлених до цього рядків. І не завадить кинутись у поточний час, щоб серце билося на цьому процесі.
Зважаючи на те, що ви хочете мати можливість скасувати та перезапустити процес, Мені стомлено обгортати ОНОВЛЕННЯ головної таблиці ОНОВЛЕННЯМ таблиці статусу в явній транзакції. Однак якщо ви відчуваєте, що таблиця стану не синхронізована через скасування, легко оновити поточне значення, просто оновивши його вручну за допомогою COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL
.і ОНОВЛЮЄТЬСЯ дві таблиці (тобто головна таблиця та таблиця статусу), ми повинні використовувати явну транзакцію для синхронізації цих двох таблиць, але ми не хочемо ризикувати осиротілою транзакцією, якщо ви скасуєте процес у пункт після того, як вона розпочала транзакцію, але не здійснила її. Це слід безпечно робити, доки ви не зупините роботу агента SQL.
Як можна зупинити процес, не зупинившись? Попросивши зупинити :-). Так. Відправляючи процесу "сигнал" (подібний до kill -3
Unix), ви можете вимагати, щоб він зупинився в наступний зручний момент (тобто, коли немає активної транзакції!) І дозволити йому очистити всі приємні та охайні.
Як можна спілкуватися з запущеним процесом в іншому сеансі? Використовуючи той самий механізм, який ми створили для нього, щоб повідомити вам про його поточний стан: таблицю стану. Нам просто потрібно додати стовпчик, який процес перевірятиме на початку кожного циклу, щоб він знав, чи слід продовжувати чи скасовувати. А оскільки наміром є запланувати це як завдання SQL Agent (запускати кожні 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 Agent або навіть у 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 мільйонів рядків.
І щоб це було зазначено так, щоб це не було неоднозначним, не повинно бути жодних причин додавати ПК до #FullSet
тимчасової таблиці, оскільки це просто проста таблиця черг, де порядок не має значення.
- У деяких випадках це допомагає додати відфільтрований індекс, щоб допомогти тим,
SELECT
хто подається в #FullSet
таблицю темпів. Ось кілька міркувань, пов’язаних із додаванням такого індексу:
- Умови WHERE повинні відповідати умові WHERE вашого запиту, отже
WHERE deleted is null or deletedDate is null
- На початку процесу більшість рядків відповідатиме вашому умові WHERE, тому індекс не так корисний. Можливо, ви захочете зачекати, поки десь біля позначки 50% перед додаванням цього. Звичайно, наскільки це допомагає і коли найкраще додати індекс, змінюється через декілька факторів, тому це трохи спроб та помилок.
- Можливо, вам доведеться вручну ОНОВЛЮВАТИ ДЕРЖАВИ та / або ВІДНОВИТИ індекс, щоб оновити його в актуальному стані, оскільки базові дані змінюються досить часто
- Не забудьте пам’ятати, що індекс, допомагаючи
SELECT
, завдає шкоди, UPDATE
оскільки це ще один об’єкт, який необхідно оновити під час цієї операції, отже, більше вводу / виводу. Це грає як з використанням відфільтрованого індексу (який скорочується, коли ви оновлюєте рядки, оскільки менше рядків відповідає фільтру), так і трохи чекаєте, щоб додати індекс (якщо він не буде дуже корисним на початку, то немає причин для виникнення сумнівів додатковий введення / виведення).