sql server: aggiornamento dei campi su un'enorme tabella in piccoli blocchi: come ottenere progressi / stato?


10

Abbiamo una tabella molto grande (100 milioni di righe) e dobbiamo aggiornare un paio di campi su di essa.

Per la spedizione di tronchi, ecc., Ovviamente, vogliamo anche mantenerlo in transazioni di dimensioni ridotte.

  • Il seguito farà il trucco?
  • E come possiamo farlo stampare qualche output, così possiamo vedere i progressi? (abbiamo provato ad aggiungere un'istruzione PRINT lì dentro, ma durante il ciclo while non è stato emesso nulla)

Il codice è:

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

Risposte:


12

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 -3Unix), è 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 ;-).

  1. 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ù.
  2. Aggiungi un PRIMARY KEY alla #CurrentSettabella 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 #FullSettabella temporanea in quanto è solo una semplice tabella di coda in cui l'ordine è irrilevante.

  3. In alcuni casi aiuta ad aggiungere un indice filtrato per aiutare quello SELECTche si inserisce nella #FullSettabella temporanea. Ecco alcune considerazioni relative all'aggiunta di un tale indice:
    1. La condizione WHERE dovrebbe corrispondere alla condizione WHERE della query, quindi WHERE deleted is null or deletedDate is null
    2. 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.
    3. Potrebbe essere necessario AGGIORNARE STATISTICHE manualmente e / o REVISIONARE l'indice per mantenerlo aggiornato poiché i dati di base cambiano abbastanza frequentemente
    4. Assicurati di tenere presente che l'indice, pur aiutando SELECT, danneggerà UPDATEpoiché è 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).

1
Questo è eccellente Lo sto eseguendo ora e fuma che possiamo eseguirlo online, durante il giorno. Grazie!
Jonesome ripristina Monica il

@samsmith Si prega di consultare la sezione AGGIORNAMENTO che ho appena aggiunto in quanto vi sono alcune idee per rendere potenzialmente il processo ancora più veloce.
Solomon Rutzky,

Senza i miglioramenti di UPDATE, stiamo ottenendo circa 8 milioni di aggiornamenti / ora ... con @BatchRows impostato su 10000000 (dieci milioni)
Jonesome Reinstate Monica,

@samsmith È fantastico :) giusto? Tenete a mente due cose: 1) Il processo si rallenta, come ci sono sempre meno le righe corrispondenti clausola WHERE, quindi, perché sarebbe un buon momento per aggiungere un indice filtrato, ma è già stato aggiunto un indice non filtrato al avviare quindi non sono sicuro se questo aiuto volontà o male, ma ancora mi aspetterei il throughput a diminuire in quanto si avvicina ad essere fatto, e 2) si potrebbe aumentare il throughput riducendo il WAITFOR DELAYa mezzo secondo o giù di lì, ma questo è un compromesso con la concorrenza e possibilmente quanto viene inviato tramite log shipping.
Solomon Rutzky,

Siamo contenti di 8 milioni di file / ora. Sì, possiamo vederlo rallentare. Siamo restii a creare altri indici (perché la tabella è bloccata per l'intera build). Quello che abbiamo fatto un paio di volte è fare un rimbalzo sull'indice esistente (perché è online).
Jonesome ripristina Monica il

4

Rispondere alla seconda parte: come stampare alcuni output durante il ciclo.

Ho alcune procedure di manutenzione a lungo termine che a volte l'amministratore di sistema deve eseguire.

Li eseguo da SSMS e ho anche notato che l' PRINTistruzione è mostrata in SSMS solo al termine dell'intera procedura.

Quindi, sto usando RAISERRORcon bassa gravità:

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

Sto usando SQL Server 2008 Standard e SSMS 2012 (11.0.3128.0). Ecco un esempio di lavoro completo da eseguire in 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

Quando commento RAISERRORe lascio solo PRINTi messaggi nella scheda Messaggi in SSMS compaiono solo dopo che l'intero batch è terminato, dopo 6 secondi.

Quando commento PRINTe utilizzo RAISERRORi messaggi nella scheda Messaggi in SSMS vengono visualizzati senza attendere 6 secondi, ma mentre il ciclo avanza.

È interessante notare che quando uso entrambi RAISERRORe PRINT, vedo entrambi i messaggi. Prima arriva il messaggio dal primo RAISERROR, quindi ritardare per 2 secondi, quindi primo PRINTe secondo RAISERROR, e così via.


In altri casi utilizzo una logtabella dedicata separata e inserisco semplicemente una riga nella tabella con alcune informazioni che descrivono lo stato corrente e il timestamp del processo di lunga durata.

Mentre il lungo processo viene eseguito periodicamente SELECTdalla logtabella per vedere cosa sta succedendo.

Questo ovviamente ha un certo sovraccarico, ma lascia un registro (o cronologia dei registri) che posso esaminare al mio ritmo in seguito.


Su SQL 2008/2014, non possiamo vedere i risultati di raiseerror .... cosa ci manca?
Jonesome ripristina Monica il

@samsmith, ho aggiunto un esempio completo. Provalo. Che comportamento hai in questo semplice esempio?
Vladimir Baranov,

2

Puoi monitorarlo da un'altra connessione con qualcosa del tipo:

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

per vedere quanto resta da fare. Questo potrebbe essere utile se un'applicazione sta chiamando il processo, anziché eseguirlo manualmente in SSMS o simili, e deve mostrare i progressi: esegui il processo principale in modo asincrono (o su un altro thread) e quindi esegui il ciclo chiamando "quanto è rimasto "controlla ogni tanto fino al completamento della chiamata asincrona (o thread).

Impostare il livello di isolamento il più rilassato possibile significa che questo dovrebbe tornare in tempi ragionevoli senza essere bloccato dietro la transazione principale a causa di problemi di blocco. Potrebbe significare che il valore restituito è un po 'impreciso ovviamente, ma come semplice indicatore di progresso questo non dovrebbe importare affatto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.