Non ero a conoscenza di questa domanda quando ho risposto alla domanda correlata ( sono necessarie transazioni esplicite in questo ciclo while? ), Ma per completezza, affronterò questo problema qui poiché non faceva parte del mio suggerimento in quella risposta collegata .
Dato che sto suggerendo di pianificare questo tramite un processo di SQL Agent (dopo 100 milioni di righe), non penso che qualsiasi forma di invio di messaggi di stato al client (ad esempio SSMS) sia l'ideale (anche se mai una necessità per altri progetti, quindi concordo con Vladimir che l'uso RAISERROR('', 10, 1) WITH NOWAIT;
è la strada da percorrere).
In questo caso particolare, creerei una tabella di stato che può essere aggiornata per ogni ciclo con il numero di righe aggiornato finora. E non fa male lanciare in questo momento per avere un battito cardiaco sul processo.
Dato che desideri essere in grado di annullare e riavviare il processo, Sono stanco di racchiudere l'UPDATE della tabella principale con l'UPDATE della tabella di stato in una transazione esplicita. Tuttavia, se ritieni che la tabella di stato non sia mai sincronizzata a causa dell'annullamento, è facile aggiornare il valore corrente semplicemente aggiornandolo manualmente con COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL
.e ci sono due tabelle da AGGIORNARE (ovvero la tabella principale e la tabella di stato), dovremmo usare una transazione esplicita per mantenere sincronizzate quelle due tabelle, ma non vogliamo rischiare di avere una transazione orfana se annulli il processo in un punto dopo aver avviato la transazione ma non l'ha impegnata. Questo dovrebbe essere sicuro finché non si interrompe il processo di SQL Agent.
Come puoi interrompere il processo senza, um, beh, fermarlo? Chiedendolo di smettere :-). Sì. Inviando al processo un "segnale" (simile a kill -3
Unix), è possibile richiedere che si interrompa al momento opportuno successivo (ovvero quando non vi è alcuna transazione attiva!) E che si ripulisca da solo.
Come è possibile comunicare con il processo in esecuzione in un'altra sessione? Usando lo stesso meccanismo che abbiamo creato per comunicarti il suo stato attuale: la tabella di stato. Dobbiamo solo aggiungere una colonna che il processo verificherà all'inizio di ogni ciclo in modo che sappia se procedere o interrompere. E poiché l'intento è quello di pianificare questo come un processo SQL Agent (eseguito ogni 10 o 20 minuti), dovremmo anche controllare all'inizio poiché non ha senso riempire una tabella temporanea con 1 milione di righe se il processo sta semplicemente andando per uscire un momento dopo e non utilizzare nessuno di questi dati.
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;
È quindi possibile controllare lo stato in qualsiasi momento utilizzando la seguente query:
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;
Vuoi mettere in pausa il processo, sia che sia in esecuzione in un processo di SQL Agent o persino in SSMS sul computer di qualcun altro? Corri:
UPDATE ht
SET ht.PauseProcess = 1
FROM dbo.HugeTable_TempStatus ts;
Vuoi che il processo sia in grado di riavviare di nuovo? Corri:
UPDATE ht
SET ht.PauseProcess = 0
FROM dbo.HugeTable_TempStatus ts;
AGGIORNARE:
Ecco alcune cose aggiuntive da provare che potrebbero migliorare le prestazioni di questa operazione. Nessuno è garantito per aiutare, ma probabilmente vale la pena testarlo. E con 100 milioni di righe da aggiornare, hai un sacco di tempo / opportunità per testare alcune variazioni ;-).
- Aggiungi
TOP (@UpdateRows)
alla query UPDATE in modo che appaia la riga superiore:
UPDATE TOP (@UpdateRows) ht
A volte aiuta l'ottimizzatore a sapere quante righe saranno interessate al massimo in modo da non perdere tempo a cercarne di più.
Aggiungi un PRIMARY KEY alla #CurrentSet
tabella temporanea. L'idea qui è di aiutare l'ottimizzatore con JOIN alla tabella da 100 milioni di righe.
E solo per averlo dichiarato in modo da non essere ambiguo, non dovrebbe esserci alcun motivo per aggiungere un PK alla #FullSet
tabella temporanea in quanto è solo una semplice tabella di coda in cui l'ordine è irrilevante.
- In alcuni casi aiuta ad aggiungere un indice filtrato per aiutare quello
SELECT
che si inserisce nella #FullSet
tabella temporanea. Ecco alcune considerazioni relative all'aggiunta di un tale indice:
- La condizione WHERE dovrebbe corrispondere alla condizione WHERE della query, quindi
WHERE deleted is null or deletedDate is null
- All'inizio del processo, la maggior parte delle righe corrisponderà alla condizione WHERE, quindi un indice non è molto utile. Potresti voler aspettare fino a qualche punto intorno al segno del 50% prima di aggiungere questo. Naturalmente, quanto aiuta e quando è meglio aggiungere l'indice varia a causa di diversi fattori, quindi è un po 'di tentativi ed errori.
- Potrebbe essere necessario AGGIORNARE STATISTICHE manualmente e / o REVISIONARE l'indice per mantenerlo aggiornato poiché i dati di base cambiano abbastanza frequentemente
- Assicurati di tenere presente che l'indice, pur aiutando
SELECT
, danneggerà UPDATE
poiché è un altro oggetto che deve essere aggiornato durante tale operazione, quindi più I / O. Questo si basa sia sull'utilizzo di un indice filtrato (che si restringe man mano che aggiorni le righe poiché meno righe corrispondono al filtro), sia sull'attesa un po 'di tempo per aggiungere l'indice (se all'inizio non sarà molto utile, quindi nessun motivo per incorrere l'I / O aggiuntivo).