Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Aggiornamento delle tabelle di SQL Server con meno interruzioni utilizzando il cambio di partizione

Un requisito comune in ETL e in vari scenari di reporting consiste nel caricare silenziosamente una tabella di staging di SQL Server in background, in modo che gli utenti che eseguono query sui dati non siano interessati dalle scritture e viceversa. Il trucco sta nel come e quando indirizzi gli utenti alla nuova versione aggiornata dei dati.

Esempio semplificato di tabella di staging:analogia di un mercato agricolo

Quindi, cos'è una tabella di staging in SQL? Una tabella di staging può essere più facilmente compresa usando un esempio del mondo reale:supponiamo che tu abbia un tavolo pieno di verdure che stai vendendo al mercato contadino locale. Man mano che le tue verdure vendono e porti nuovo inventario:

  • Quando porti un carico di verdure nuove, ci vorranno 20 minuti per sgombrare il tavolo e sostituire il brodo rimanente con il prodotto più nuovo.
  • Non vuoi che i clienti si siedano lì e aspettino 20 minuti prima che avvenga il passaggio, dal momento che la maggior parte porterà le loro verdure altrove.

Ora, cosa accadrebbe se avessi un secondo tavolo vuoto dove carichi le nuove verdure e mentre lo fai, i clienti possono ancora acquistare le verdure più vecchie dal primo tavolo? (Fingiamo che non sia perché le verdure più vecchie sono andate a male o sono altrimenti meno desiderabili.)

Aggiornamento delle tabelle in SQL Server

Esistono diversi metodi per ricaricare intere tabelle mentre vengono interrogate attivamente; due decenni fa, ho approfittato sfrenato di sp_rename — Giocherei a un gioco di shell con una copia shadow vuota del tavolo, ricaricando felicemente la copia shadow e quindi eseguendo la ridenominazione solo all'interno di una transazione.

In SQL Server 2005, ho iniziato a utilizzare schemi per contenere copie shadow di tabelle che ho semplicemente trasferito utilizzando la stessa tecnica di gioco della shell, di cui ho scritto in questi due post:

  • Tick Shots:Schema Switch-a-Roo
  • Schema Switch-a-Roo, parte 2

L'unico vantaggio del trasferimento di oggetti tra schemi rispetto alla loro ridenominazione è che non ci sono messaggi di avviso sulla ridenominazione di un oggetto, il che non è nemmeno un problema, di per sé, tranne per il fatto che i messaggi di avviso riempiono i registri della cronologia degli agenti molto più velocemente.

Entrambi gli approcci richiedono ancora un blocco di modifica dello schema (Sch-M), quindi devono attendere che tutte le transazioni esistenti rilascino i propri blocchi. Una volta acquisito il blocco Sch-M, bloccano tutte le query successive che richiedono blocchi di stabilità dello schema (Sch-S)... che è quasi tutte le query. Può diventare rapidamente un incubo della catena di blocco, poiché tutte le nuove query che richiedono Sch-S devono mettersi in coda dietro lo Sch-M. (E no, non puoi aggirare questo problema usando RCSI o NOLOCK ovunque, poiché anche quelle query richiedono ancora Sch-S. Non puoi acquisire Sch-S con uno Sch-M in atto, poiché sono incompatibili:ne parla Michael J. Swart qui.)

Kendra Little mi ha davvero aperto gli occhi sui pericoli con il trasferimento dello schema nel suo post, "Staging Data:Locking Danger with ALTER SCHEMA TRANSFER". Lì mostra perché il trasferimento dello schema può essere peggio della ridenominazione. In seguito ha descritto in dettaglio un terzo modo molto meno efficace per scambiare le tabelle, che ora uso esclusivamente:il cambio di partizione. Questo metodo consente allo switch di attendere con una priorità inferiore, che non è nemmeno un'opzione con le tecniche di ridenominazione o trasferimento dello schema. Joe Sack ha approfondito questo miglioramento aggiunto in SQL Server 2014:"Esplorazione delle opzioni di attesa del blocco con priorità bassa in SQL Server 2014 CTP1".

Esempio di cambio di partizione di SQL Server

Diamo un'occhiata a un esempio di base, seguendo il succo completo di Kendra qui. Per prima cosa creeremo due nuovi database:

CREATE DATABASE NewWay;
CREATE DATABASE OldWay;
GO

Nel nuovo database creeremo un tavolo per contenere il nostro inventario di verdure e due copie del tavolo per il nostro gioco di conchiglie:

USE NewWay;
GO
 
CREATE TABLE dbo.Vegetables_NewWay
(
  VegetableID int,
  Name        sysname,
  WhenPicked  datetime,
  BackStory   nvarchar(max)
);
GO
 
-- we need to create two extra copies of the table.
 
CREATE TABLE dbo.Vegetables_NewWay_prev
(
  VegetableID int,
  Name        sysname,
  WhenPicked  datetime,
  BackStory   nvarchar(max)
);
GO
 
CREATE TABLE dbo.Vegetables_NewWay_hold
(
  VegetableID int,
  Name        sysname,
  WhenPicked  datetime,
  BackStory   nvarchar(max)
);
GO

Creiamo una procedura che carica la copia di staging della tabella, quindi utilizza una transazione per trasferire la copia corrente.

CREATE PROCEDURE dbo.DoTheVeggieSwap_NewWay
AS
BEGIN
  SET NOCOUNT ON;
 
  TRUNCATE TABLE dbo.Vegetables_NewWay_prev;
 
  INSERT dbo.Vegetables_NewWay_prev
    SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, 
      LEFT(m.definition, 500)
    FROM sys.dm_exec_sessions AS s
    CROSS JOIN model.sys.all_objects AS o
    INNER JOIN model.sys.all_sql_modules AS m
    ON o.[object_id] = m.[object_id];
 
  -- need to take Sch-M locks here:
 
  BEGIN TRANSACTION;
    ALTER TABLE dbo.Vegetables_NewWay 
      SWITCH TO dbo.Vegetables_NewWay_hold
      WITH (WAIT_AT_LOW_PRIORITY 
            (MAX_DURATION = 1 MINUTES,
             ABORT_AFTER_WAIT = BLOCKERS));
 
    ALTER TABLE dbo.Vegetables_NewWay_prev
      SWITCH TO dbo.Vegetables_NewWay;
  COMMIT TRANSACTION;
 
  -- and now users will query the new data in dbo
  -- can switch the old copy back and truncate it 
  -- without interfering with other queries
 
  ALTER TABLE dbo.Vegetables_NewWay_hold
	SWITCH TO dbo.Vegetables_NewWay_prev;
 
  TRUNCATE TABLE dbo.Vegetables_NewWay_prev;
END
GO

La bellezza di WAIT_AT_LOW_PRIORITY è che puoi controllare completamente il comportamento con ABORT_AFTER_WAIT opzione:

ABORT_AFTER_WAIT
impostazione
Descrizione/sintomi
Ciò significa che lo switch si arrende dopo n minuti.

Per la sessione che tenta di eseguire il passaggio, verrà visualizzato il messaggio di errore:

Il periodo di timeout della richiesta di blocco è stato superato.
BLOCCANTI Questo indica che lo switch attenderà fino a n minuti, quindi portarsi in prima linea uccidendo tutti i bloccanti davanti a sé .

Le sessioni che tentano di interagire con la tabella che vengono urtate dall'operazione di cambio vedranno una combinazione di questi messaggi di errore:

La tua sessione è stata disconnessa a causa di un'operazione DDL ad alta priorità.

Impossibile continuare l'esecuzione perché la sessione è in stato di interruzione.

Si è verificato un errore grave nel comando corrente. I risultati, se presenti, dovrebbero essere eliminati.

NESSUNO Questo dice che lo switch aspetterà felicemente fino al suo turno, indipendentemente da MAX_DURATION .

Questo è lo stesso comportamento che avresti con la ridenominazione, il trasferimento dello schema o il cambio di partizione senza WAIT_AT_LOW_PRIORITY .

I BLOCKERS l'opzione non è il modo più amichevole per gestire le cose, dal momento che stai già dicendo che va bene attraverso questa operazione di staging/cambiamento per consentire agli utenti di vedere dati un po 'obsoleti. Probabilmente preferirei usare SELF e far riprovare l'operazione nei casi in cui non è stato possibile ottenere i blocchi richiesti nel tempo assegnato. Tuttavia, terrei traccia della frequenza con cui si verifica un errore, in particolare di quelli consecutivi, perché vuoi assicurarti che i dati non diventino mai troppo obsoleti.

Rispetto al vecchio modo di passare da uno schema all'altro

Ecco come avrei gestito il passaggio prima:

USE OldWay;
GO
 
-- create two schemas and two copies of the table
 
CREATE SCHEMA prev AUTHORIZATION dbo;
GO
 
CREATE SCHEMA hold AUTHORIZATION dbo;
GO
 
CREATE TABLE dbo.Vegetables_OldWay
(
  VegetableID int,
  Name sysname,
  WhenPicked datetime,
  BackStory nvarchar(max)
);
GO
 
CREATE TABLE prev.Vegetables_OldWay
(
  VegetableID int,
  Name sysname,
  WhenPicked datetime,
  BackStory nvarchar(max)
);
GO
 
CREATE PROCEDURE dbo.DoTheVeggieSwap_OldWay
AS
BEGIN
  SET NOCOUNT ON;
 
  TRUNCATE TABLE prev.Vegetables_OldWay;
 
  INSERT prev.Vegetables_OldWay
    SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, 
      LEFT(m.definition, 500)
    FROM sys.dm_exec_sessions AS s
    CROSS JOIN model.sys.all_objects AS o
    INNER JOIN model.sys.all_sql_modules AS m
    ON o.[object_id] = m.[object_id];
 
  -- need to take Sch-M locks here:
  BEGIN TRANSACTION;
    ALTER SCHEMA hold TRANSFER dbo.Vegetables_OldWay;
    ALTER SCHEMA dbo  TRANSFER prev.Vegetables_OldWay;
  COMMIT TRANSACTION;
 
  -- and now users will query the new data in dbo
  -- can transfer the old copy back and truncate it without 
  -- interfering with other queries:
 
  ALTER SCHEMA prev TRANSFER hold.Vegetables_OldWay;
  TRUNCATE TABLE prev.Vegetables_OldWay;
END
GO

Ho eseguito test di concorrenza utilizzando due finestre di SQLQueryStress di Erik Ejlskov Jensen:una per ripetere una chiamata alla procedura ogni minuto e l'altra per eseguire 16 thread in questo modo, migliaia di volte:

BEGIN TRANSACTION;
 
UPDATE TOP (1) dbo.<table> SET name += 'x';
SELECT TOP (10) name FROM dbo.<table> ORDER BY NEWID();
WAITFOR DELAY '00:00:02';
 
COMMIT TRANSACTION;

Puoi guardare l'output di SQLQueryStress o sys.dm_exec_query_stats o Query Store e vedrai qualcosa sulla falsariga dei seguenti risultati (ma consiglio vivamente di utilizzare uno strumento di monitoraggio delle prestazioni di SQL Server di qualità se sei seriamente intenzionato ottimizzazione proattiva degli ambienti di database):

Durata e tassi di errore Trasferimento schema ABORT_AFTER_WAIT:
SELF
ABORT_AFTER_WAIT:
BLOCCANTI
Durata media – Trasferimento/Cambia 96,4 secondi 68,4 secondi 20,8 secondi
Durata media – DML 18,7 secondi 2,7 secondi 2,9 secondi
Eccezioni – Trasferimento/Cambia 0 0,5/minuto 0
Eccezioni – DML 0 0 25,5/minuto

Nota che le durate e il conteggio delle eccezioni dipenderanno fortemente dalle specifiche del tuo server e da cos'altro sta succedendo nel tuo ambiente. Si noti inoltre che, sebbene non vi fossero eccezioni per i test di trasferimento dello schema quando si usa SQLQueryStress, è possibile che si verifichino timeout più rigidi a seconda dell'applicazione che utilizza. Ed è stato in media molto più lento, perché il blocco si è accumulato in modo molto più aggressivo. Nessuno vuole mai eccezioni, ma quando c'è un compromesso come questo, potresti preferire alcune eccezioni qua e là (a seconda della frequenza dell'operazione di aggiornamento) rispetto a tutti che aspettano sempre più a lungo.

Cambio di partizione e trasferimento di ridenominazione/schema per aggiornare le tabelle di SQL Server

Il cambio di partizione ti consente di scegliere quale parte del tuo processo sostiene il costo della concorrenza. Puoi dare la preferenza al processo di commutazione, in modo che i dati siano aggiornati in modo più affidabile, ma ciò significa che alcune delle tue query avranno esito negativo. Al contrario, puoi dare la priorità alle query, al costo di un processo di aggiornamento più lento (e dell'occasionale errore lì). L'obiettivo principale è che il cambio di partizione di SQL Server è un metodo superiore per aggiornare le tabelle di SQL Server rispetto alle precedenti tecniche di trasferimento di ridenominazione/schema su quasi tutti i punti e puoi utilizzare una logica di ripetizione più robusta o sperimentare tolleranze di durata per atterrare al punto ottimale per il tuo carico di lavoro.