Prestazioni di AGGIORNAMENTO in cui nessun dato cambia


31

Se ho una UPDATEdichiarazione che in realtà non modifica alcun dato (perché i dati sono già nello stato aggiornato). C'è qualche vantaggio in termini di prestazioni nel mettere un segno di spunta nella WHEREclausola per impedire l'aggiornamento?

Ad esempio, ci sarebbe una differenza nella velocità di esecuzione tra UPDATE 1 e UPDATE 2 nei seguenti:

CREATE TABLE MyTable (ID int PRIMARY KEY, Value int);
INSERT INTO MyTable (ID, Value)
VALUES
    (1, 1),
    (2, 2),
    (3, 3);

-- UPDATE 1
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2
    AND Value <> 2;
SELECT @@ROWCOUNT;

-- UPDATE 2
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2;
SELECT @@ROWCOUNT;

DROP TABLE MyTable;

Il motivo per cui chiedo è che ho bisogno che il conteggio delle righe includa la riga invariata, quindi so se effettuare un inserimento se l'ID non esiste. Come tale ho usato il modulo UPDATE 2. Se c'è un vantaggio in termini di prestazioni nell'uso del modulo UPDATE 1, è possibile ottenere il conteggio delle righe di cui ho bisogno in qualche modo?


Vedi sqlperformance.com/2012/10/t-sql-queries/conditional-updates (anche se non ho profilato il caso in cui nessun valore cambia).
Aaron Bertrand

Risposte:


24

Se ho un'istruzione UPDATE che in realtà non modifica alcun dato (perché i dati sono già nello stato aggiornato), c'è qualche vantaggio in termini di prestazioni nel mettere un segno di spunta nella clausola where per impedire l'aggiornamento?

Potrebbe esserci sicuramente una leggera differenza di prestazioni dovuta all'aggiornamento 1 :

  • non aggiornando effettivamente alcuna riga (quindi nulla da scrivere sul disco, nemmeno un'attività di registro minima), e
  • eliminando blocchi meno restrittivi di quelli necessari per eseguire l'aggiornamento effettivo (quindi meglio per la concorrenza) ( consultare la sezione Aggiornamento verso la fine )

Tuttavia, quanta differenza c'è dovrebbe essere misurata da te sul tuo sistema con il tuo schema, i dati e il carico del sistema. Esistono diversi fattori che influiscono sull'impatto di un AGGIORNAMENTO senza aggiornamento:

  • la quantità di contesa sulla tabella in fase di aggiornamento
  • il numero di righe da aggiornare
  • se ci sono trigger UPDATE sulla tabella in fase di aggiornamento (come notato da Mark in un commento sulla domanda). Se si esegue UPDATE TableName SET Field1 = Field1, verrà attivato un trigger di aggiornamento che indicherà che il campo è stato aggiornato (se si controlla utilizzando le funzioni UPDATE () o COLUMNS_UPDATED ) e che il campo in entrambe INSERTEDe nelle DELETEDtabelle ha lo stesso valore.

Inoltre, la seguente sezione di riepilogo si trova nell'articolo di Paul White, L'impatto degli aggiornamenti non di aggiornamento (come notato da @spaghettidba in un commento sulla sua risposta):

SQL Server contiene una serie di ottimizzazioni per evitare la registrazione non necessaria o lo svuotamento della pagina durante l'elaborazione di un'operazione di AGGIORNAMENTO che non comporterà alcuna modifica al database permanente.

  • Gli aggiornamenti non aggiornati a una tabella cluster generalmente evitano ulteriori registrazioni e svuotamento delle pagine, a meno che l'operazione di aggiornamento non influisca su una colonna che forma (parte) della chiave del cluster.
  • Se una parte della chiave del cluster viene "aggiornata" con lo stesso valore, l'operazione viene registrata come se i dati fossero stati modificati e le pagine interessate fossero contrassegnate come sporche nel pool di buffer. Questa è una conseguenza della conversione di UPDATE in un'operazione di cancellazione-inserimento-inserimento.
  • Le tabelle di heap si comportano allo stesso modo delle tabelle di cluster, tranne per il fatto che non dispongono di una chiave di cluster per causare ulteriori registrazioni o svuotamento della pagina. Questo rimane il caso anche quando esiste una chiave primaria non cluster sull'heap. Pertanto, gli aggiornamenti non aggiornati a un heap generalmente evitano il log e lo svuotamento extra (ma vedi sotto).
  • Sia le heap sia le tabelle cluster subiranno il log e lo svuotamento extra per qualsiasi riga in cui una colonna LOB contenente più di 8000 byte di dati viene aggiornata allo stesso valore utilizzando qualsiasi sintassi diversa da "SET nome_colonna = nome_colonna".
  • La semplice abilitazione di entrambi i tipi di livello di isolamento del controllo delle versioni di riga su un database causa sempre la registrazione e lo svuotamento aggiuntivi. Ciò si verifica indipendentemente dal livello di isolamento in vigore per la transazione di aggiornamento.

Tieni presente (soprattutto se non segui il link per vedere l'articolo completo di Paul), i seguenti due elementi:

  1. Gli aggiornamenti non aggiornati presentano ancora alcune attività di registro, a dimostrazione del fatto che una transazione sta iniziando e terminando. È solo che non si verifica alcuna modifica dei dati (che è ancora un buon risparmio).

  2. Come ho detto sopra, devi testare sul tuo sistema. Usa le stesse query di ricerca utilizzate da Paul e vedi se ottieni gli stessi risultati. Sto vedendo risultati leggermente diversi sul mio sistema rispetto a quelli mostrati nell'articolo. Ancora nessuna pagina sporca da scrivere, ma un po 'più attività di registro.


... Ho bisogno che il conteggio delle righe includa la riga invariata, quindi so se effettuare un inserimento se l'ID non esiste. ... è possibile ottenere il conteggio delle righe di cui ho bisogno in qualche modo?

Semplicisticamente, se hai a che fare con una sola riga, puoi fare quanto segue:

UPDATE MyTable
SET    Value = 2
WHERE  ID = 2
AND Value <> 2;

IF (@@ROWCOUNT = 0)
BEGIN
  IF (NOT EXISTS(
                 SELECT *
                 FROM   MyTable
                 WHERE  ID = 2 -- or Value = 2 depending on the scenario
                )
     )
  BEGIN
     INSERT INTO MyTable (ID, Value) -- or leave out ID if it is an IDENTITY
     VALUES (2, 2);
  END;
END;

Per più righe, è possibile ottenere le informazioni necessarie per prendere tale decisione utilizzando la OUTPUTclausola. Catturando esattamente quali righe sono state aggiornate, è possibile restringere gli elementi per cercare la differenza tra non aggiornare le righe che non esistono invece di non aggiornare le righe esistenti ma non è necessario l'aggiornamento.

Mostro l'implementazione di base nella seguente risposta:

Come evitare l'utilizzo della query Unisci quando si esegue l'uperting di più dati utilizzando il parametro xml?

Il metodo mostrato in quella risposta non filtra le righe esistenti ma non è necessario aggiornarle. Quella parte potrebbe essere aggiunta, ma prima devi mostrare esattamente dove stai ottenendo il tuo set di dati in cui ti stai unendo MyTable. Vengono da un tavolo temporaneo? Un parametro con valori di tabella (TVP)?


AGGIORNAMENTO 1:

Finalmente sono stato in grado di fare alcuni test ed ecco cosa ho trovato per quanto riguarda il registro delle transazioni e il blocco. Innanzitutto, lo schema per la tabella:

CREATE TABLE [dbo].[Test]
(
  [ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
  [StringField] [varchar](500) NULL
);

Successivamente, il test aggiorna il campo al valore che ha già:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117

risultati:

-- Transaction Log (2 entries):
Operation
----------------------------
LOP_BEGIN_XACT
LOP_COMMIT_XACT


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
8 - IX          6 - PAGE
5 - X           7 - KEY

Infine, il test che filtra l'aggiornamento a causa del valore che non cambia:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
AND    rt.StringField <> '04CF508B-B78E-4264-B9EE-E87DC4AD237A';

risultati:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
7 - IU          6 - PAGE
4 - U           7 - KEY

Come puoi vedere, nulla viene scritto nel registro delle transazioni quando si filtra la riga, al contrario delle due voci che segnano l'inizio e la fine della transazione. E mentre è vero che quelle due voci non sono quasi nulla, sono comunque qualcosa.

Inoltre, il blocco delle risorse PAGE e KEY è meno restrittivo quando si filtrano le righe che non sono state modificate. Se nessun altro processo interagisce con questa tabella, probabilmente si tratta di un problema (ma quanto è probabile, davvero?). Tieni presente che i test mostrati in uno qualsiasi dei blog collegati (e persino i miei test) presuppongono implicitamente che non ci siano contese sul tavolo poiché non fanno mai parte dei test. Dire che gli aggiornamenti senza aggiornamento sono così leggeri che non paga per fare il filtraggio deve essere preso con un granello di sale poiché i test sono stati eseguiti, più o meno, nel vuoto. Ma in produzione, molto probabilmente questa tabella non è isolata. Certo, potrebbe benissimo essere che un po 'di registrazione e blocchi più restrittivi non si traducano in meno efficienza. Quindi la fonte di informazioni più affidabile per rispondere a questa domanda? Server SQL. In particolare:il tuo SQL Server. Ti mostrerà quale metodo è migliore per il tuo sistema :-).


AGGIORNAMENTO 2:

Se le operazioni in cui il nuovo valore è uguale al valore corrente (ovvero nessun aggiornamento) numera le operazioni in cui il nuovo valore è diverso e l'aggiornamento è necessario, il modello seguente potrebbe rivelarsi ancora migliore, specialmente se c'è molta contesa sul tavolo. L'idea è di fare un semplice SELECTprimo per ottenere il valore corrente. Se non ottieni un valore, hai la tua risposta per quanto riguarda il INSERT. Se hai un valore, puoi fare un semplice IFed emettere l' UPDATE unico se è necessario.

DECLARE @CurrentValue VARCHAR(500) = NULL,
        @NewValue VARCHAR(500) = '04CF508B-B78E-4264-B9EE-E87DC4AD237A',
        @ID INT = 4082117;

SELECT @CurrentValue = rt.StringField
FROM   dbo.Test rt
WHERE  rt.ID = @ID;

IF (@CurrentValue IS NULL) -- if NULL is valid, use @@ROWCOUNT = 0
BEGIN
  -- row does not exist
  INSERT INTO dbo.Test (ID, StringField)
  VALUES (@ID, @NewValue);
END;
ELSE
BEGIN
  -- row exists, so check value to see if it is different
  IF (@CurrentValue <> @NewValue)
  BEGIN
    -- value is different, so do the update
    UPDATE rt
    SET    rt.StringField = @NewValue
    FROM   dbo.Test rt
    WHERE  rt.ID = @ID;
  END;
END;

risultati:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (2 Lock:Acquired events):
Mode            Type
--------------------------------------
6 - IS          5 - OBJECT
6 - IS          6 - PAGE

Pertanto, vengono acquisiti solo 2 blocchi anziché 3, ed entrambi questi blocchi sono Intent Shared, non Intent eXclusive o Intent Update ( Compatibilità dei blocchi ). Tenendo presente che anche ogni blocco acquisito verrà rilasciato, ogni blocco è in realtà 2 operazioni, quindi questo nuovo metodo è un totale di 4 operazioni anziché le 6 operazioni nel metodo proposto originariamente. Considerando che questa operazione viene eseguita una volta ogni 15 ms (approssimativamente, come indicato dall'OP), ovvero circa 66 volte al secondo. Quindi la proposta originale ammonta a 396 operazioni di blocco / sblocco al secondo, mentre questo nuovo metodo ammonta a solo 264 operazioni di blocco / sblocco al secondo di blocchi anche più leggeri. Questa non è una garanzia di prestazioni eccezionali, ma sicuramente vale la pena testarla :-).


14

Rimpicciolisci un po 'e pensa al quadro più grande. Nel mondo reale, la tua dichiarazione di aggiornamento sarà davvero simile a questa:

UPDATE MyTable
  SET Value = 2
WHERE
     ID = 2
     AND Value <> 2;

O sarà più simile a questo:

UPDATE Customers
  SET AddressLine1 = '123 Main St',
      AddressLine2 = 'Apt 24',
      City = 'Chicago',
      State = 'IL',
      (and a couple dozen more fields)
WHERE
     ID = 2
     AND (AddressLine1 <> '123 Main St'
     OR AddressLine2 <> 'Apt 24'
     OR City <> 'Chicago'
     OR State <> 'IL'
      (and a couple dozen more fields))

Perché nel mondo reale, le tabelle hanno molte colonne. Ciò significa che dovrai generare molte complesse logiche di app dinamiche per creare stringhe dinamiche, O dovrai specificare i contenuti prima e dopo di ogni campo, ogni volta.

Se si creano queste istruzioni di aggiornamento in modo dinamico per ogni tabella, passando solo nei campi che vengono aggiornati, è possibile imbattersi rapidamente in un problema di inquinamento della cache del piano simile al problema delle dimensioni dei parametri di NHibernate da qualche anno fa. Ancora peggio, se si creano le istruzioni di aggiornamento in SQL Server (come nelle stored procedure), si bruciano preziosi cicli della CPU perché SQL Server non è terribilmente efficiente nel concatenare stringhe insieme su larga scala.

A causa di queste complessità, di solito non ha senso fare questo tipo di confronto riga per riga, campo per campo mentre si eseguono gli aggiornamenti. Pensa invece a operazioni basate su set.


1
Il mio esempio nel mondo reale è semplice ma viene chiamato molto. La mia stima è una volta ogni 15 ms nelle ore di punta. Mi chiedevo se SQL Server è abbastanza mannaia da non scrivere su disco quando non è necessario.
Martin Brown,

3

È possibile notare un aumento delle prestazioni saltando le righe che non devono essere aggiornate solo quando il numero di righe è elevato (meno log, meno pagine sporche da scrivere su disco).

Quando si tratta di aggiornamenti a riga singola come nel proprio caso, la differenza di prestazioni è completamente trascurabile. Se l'aggiornamento delle righe in tutti i casi ti rende più semplice, fallo.

Per ulteriori informazioni sull'argomento, consultare Aggiornamenti non di aggiornamento di Paul White


3

È possibile combinare l'aggiornamento e inserirlo in un'istruzione. Su SQL Server, è possibile utilizzare un'istruzione MERGE per eseguire sia l'aggiornamento che l'inserimento se non trovato. Per MySQL, è possibile utilizzare INSERT ON DUPLICATE KEY UPDATE .


1

Invece di controllare i valori di tutti i campi, non riesci a ottenere un valore di hash utilizzando le colonne che ti interessano, quindi confrontalo con l'hash memorizzato rispetto alla riga nella tabella?

IF EXISTS (Select 1 from Table where ID =@ID AND HashValue=Sha256(column1+column2))
GOTO EXIT
ELSE
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.