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

Fai questi errori quando usi SQL CURSOR?

Per alcune persone, è la domanda sbagliata. CURSORE SQL SI l'errore. Il diavolo è nei dettagli! Puoi leggere tutti i tipi di blasfemia nell'intera blogosfera SQL nel nome di SQL CURSOR.

Se la pensi allo stesso modo, cosa ti ha portato a questa conclusione?

Se è di un amico e collega fidato, non posso biasimarti. Succede. A volte molto. Ma se qualcuno ti ha convinto con delle prove, questa è un'altra storia.

Non ci siamo incontrati prima. Non mi conosci come amico. Ma spero di poterlo spiegare con esempi e convincerti che SQL CURSOR ha il suo posto. Non è molto, ma quel piccolo spazio nel nostro codice ha delle regole.

Ma prima, lascia che ti racconti la mia storia.

Ho iniziato a programmare con i database usando xBase. Questo è stato al college fino ai miei primi due anni di programmazione professionale. Te lo dico perché in passato elaboravamo i dati in sequenza, non in batch prestabiliti come SQL. Quando ho imparato l'SQL, è stato come un cambio di paradigma. Il motore di database decide per me con i suoi comandi basati su set che ho emesso. Quando ho appreso di SQL CURSOR, mi è sembrato di essere tornato con i metodi vecchi ma comodi.

Ma alcuni colleghi senior mi hanno avvertito:"Evita a tutti i costi SQL CURSOR!" Ho ricevuto alcune spiegazioni verbali, e basta.

SQL CURSOR può essere dannoso se lo usi per il lavoro sbagliato. Come usare un martello per tagliare il legno, è ridicolo. Certo, possono capitare degli errori, ed è qui che ci concentreremo.

1. L'utilizzo di SQL CURSOR quando si impostano i comandi basati su funzionerà

Non posso enfatizzarlo abbastanza, ma QUESTO è il cuore del problema. Quando ho appreso per la prima volta cos'era SQL CURSOR, si è accesa una lampadina. “Cicli! Lo so!" Tuttavia, non finché non mi ha dato mal di testa e i miei anziani mi hanno rimproverato.

Vedete, l'approccio di SQL è basato su set. Emetti un comando INSERT dai valori della tabella e farà il lavoro senza loop sul tuo codice. Come ho detto prima, è il lavoro del motore di database. Quindi, se forzi un ciclo per aggiungere record a una tabella, stai ignorando quell'autorità. Diventerà brutto.

Prima di provare un esempio ridicolo, prepariamo i dati:


SELECT TOP (500)
  val = ROW_NUMBER() OVER (ORDER BY sod.SalesOrderDetailID)
, modified = GETDATE()
, status = 'inserted'
INTO dbo.TestTable
FROM AdventureWorks.Sales.SalesOrderDetail sod
CROSS JOIN AdventureWorks.Sales.SalesOrderDetail sod2

SELECT
 tt.val
,GETDATE() AS modified
,'inserted' AS status
INTO dbo.TestTable2
FROM dbo.TestTable tt
WHERE CAST(val AS VARCHAR) LIKE '%2%'

La prima istruzione genererà 500 record di dati. Il secondo ne riceverà un sottoinsieme. Allora siamo pronti. Inseriamo i dati mancanti da TestTable in TestTable2 utilizzando SQL CURSOR. Vedi sotto:


DECLARE @val INT

DECLARE test_inserts CURSOR FOR 
	SELECT val FROM TestTable tt
	WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

OPEN test_inserts
FETCH NEXT FROM test_inserts INTO @val
WHILE @@fetch_status = 0
BEGIN
	INSERT INTO TestTable2
	(val, modified, status)
	VALUES
	(@val, GETDATE(),'inserted')

	FETCH NEXT FROM test_inserts INTO @val
END

CLOSE test_inserts
DEALLOCATE test_inserts

Ecco come eseguire il ciclo utilizzando SQL CURSOR per inserire un record mancante uno per uno. Abbastanza lungo, vero?

Ora, proviamo un modo migliore:l'alternativa basata sui set. Ecco:


INSERT INTO TestTable2
(val, modified, status)
SELECT val, GETDATE(), status
FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

È breve, pulito e veloce. Quanto velocemente? Vedere la Figura 1 di seguito:

Utilizzando xEvent Profiler in SQL Server Management Studio, ho confrontato le cifre del tempo della CPU, la durata e le letture logiche. Come puoi vedere nella Figura 1, l'utilizzo del comando set-based per INSERT record vince il test delle prestazioni. I numeri parlano da soli. L'utilizzo di SQL CURSOR consuma più risorse e tempo di elaborazione.

Quindi, prima di utilizzare SQL CURSOR, provare a scrivere prima un comando basato su set. Pagherà meglio a lungo termine.

Ma cosa succede se hai bisogno di SQL CURSOR per portare a termine il lavoro?

2. Non utilizzare le opzioni SQL CURSOR appropriate

Un altro errore che anch'io ho commesso in passato è stato non utilizzare le opzioni appropriate in DECLARE CURSOR. Sono disponibili opzioni per ambito, modello, concorrenza e se è scorrevole o meno. Questi argomenti sono facoltativi ed è facile ignorarli. Tuttavia, se SQL CURSOR è l'unico modo per eseguire l'attività, devi essere esplicito con le tue intenzioni.

Quindi, chiediti:

  • Quando attraversi il ciclo, navigherai tra le righe solo in avanti o ti sposterai alla prima, all'ultima, precedente o successiva? È necessario specificare se il CURSORE è solo in avanti o scorrevole. È DECLARE CURSOR FORWARD_ONLY oppure DICHIARA SCORRIMENTO CURSORE .
  • Aggiornerai le colonne nel CURSOR? Usa READ_ONLY se non è aggiornabile.
  • Hai bisogno degli ultimi valori mentre attraversi il ciclo? Usa STATIC se i valori non contano se più recenti o meno. Usa DYNAMIC se altre transazioni aggiornano le colonne o eliminano le righe che usi nel CURSOR e hai bisogno dei valori più recenti. Nota :DYNAMIC sarà costoso.
  • Il CURSOR è globale per la connessione o locale per il batch o una stored procedure? Specificare se LOCALE o GLOBALE.

Per ulteriori informazioni su questi argomenti, cercare il riferimento in Microsoft Docs.

Esempio

Proviamo un esempio confrontando tre CURSOR per il tempo della CPU, le letture logiche e la durata utilizzando xEvents Profiler. Il primo non avrà opzioni appropriate dopo DECLARE CURSOR. Il secondo è LOCAL STATIC FORWARD_ONLY READ_ONLY. L'ultimo è LOtyuiCAL FAST_FORWARD.

Ecco il primo:

-- NOTE: Don't just COPY and PASTE this code then run in your machine. Read and assess.

-- DECLARE CURSOR with no options
SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR FOR 
  SELECT
	Command
  FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

C'è un'opzione migliore rispetto al codice sopra, ovviamente. Se lo scopo è solo quello di generare uno script da tabelle utente esistenti, SELECT lo farà. Quindi, incolla l'output in un'altra finestra di query.

Ma se devi generare uno script ed eseguirlo subito, questa è un'altra storia. Devi valutare lo script di output se metterà a dura prova il tuo server o meno. Vedi Errore n. 4 più avanti.

Per mostrarti il ​​confronto di tre CURSORI con diverse opzioni, questo sarà sufficiente.

Ora, abbiamo un codice simile ma con LOCAL STATIC FORWARD_ONLY READ_ONLY.

--- STATIC LOCAL FORWARD_ONLY READ_ONLY

SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR SELECT
	Command
FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

Come puoi vedere sopra, l'unica differenza rispetto al codice precedente è LOCAL STATIC FORWARD_ONLY READ_ONLY argomenti.

Il terzo avrà un LOCAL FAST_FORWARD. Ora, secondo Microsoft, FAST_FORWARD è un FORWARD_ONLY, READ_ONLY CURSOR con le ottimizzazioni abilitate. Vedremo come andrà a finire con i primi due.

Come si confrontano? Vedi figura 2:

Quello che richiede meno tempo e durata della CPU è il LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Si noti inoltre che SQL Server ha valori predefiniti se non si specificano argomenti come STATIC o READ_ONLY. C'è una terribile conseguenza, come vedrai nella prossima sezione.

Cosa ha rivelato sp_describe_cursor

sp_describe_cursor è una procedura memorizzata dal master database che puoi utilizzare per ottenere informazioni dal CURSOR aperto. Ed ecco cosa ha rivelato dal primo batch di query senza opzioni CURSOR. Vedere la Figura 3 per il risultato di sp_describe_cursor :

esagerare molto? Scommetti. Il CURSORE del primo batch di query è:

  • globale alla connessione esistente.
  • dinamico, il che significa che tiene traccia delle modifiche nella tabella dei #comandi per aggiornamenti, eliminazioni e inserimenti.
  • ottimista, il che significa che SQL Server ha aggiunto una colonna aggiuntiva a una tabella temporanea denominata CWT. Questa è una colonna di checksum per tenere traccia delle modifiche nei valori della tabella #commands.
  • scorrevole, il che significa che puoi passare alla riga precedente, successiva, superiore o inferiore del cursore.

Assurdo? Sono fortemente d'accordo. Perché hai bisogno di una connessione globale? Perché devi tenere traccia delle modifiche alla tabella temporanea #commands? Abbiamo fatto scorrere in un punto diverso dal record successivo nel CURSOR?

Poiché un SQL Server determina questo per noi, il ciclo CURSOR diventa un terribile errore.

Ora capisci perché specificare esplicitamente le opzioni SQL CURSOR è così cruciale. Quindi, d'ora in poi, specifica sempre questi argomenti CURSOR se devi usare un CURSOR.

Il piano di esecuzione rivela di più

Il piano di esecuzione effettivo ha qualcosa in più da dire su ciò che accade ogni volta che viene eseguito un FETCH NEXT FROM command_builder INTO @command. Nella figura 4, viene inserita una riga nell'indice cluster CWT_PrimaryKey nel tempdb tabella CWT :

Le scritture avvengono su tempdb su ogni FETCH NEXT. Inoltre, c'è di più. Ricordate che il CURSORE è OTTIMISTICO nella Figura 3? Le proprietà della scansione dell'indice cluster nella parte più a destra del piano rivelano la colonna sconosciuta extra chiamata Chk1002 :

Potrebbe essere questa la colonna Checksum? Il Plan XML conferma che questo è effettivamente il caso:

Ora, confronta il piano di esecuzione effettivo di FETCH NEXT quando il CURSOR è LOCAL STATIC FORWARD_ONLY READ_ONLY:

Utilizza tempdb anche, ma è molto più semplice. Nel frattempo, la Figura 8 mostra il piano di esecuzione quando viene utilizzato LOCAL FAST_FORWARD:

Da asporto

Uno degli usi appropriati di SQL CURSOR è la generazione di script o l'esecuzione di alcuni comandi amministrativi verso un gruppo di oggetti di database. Anche se ci sono usi minori, la tua prima opzione è usare LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR o LOCAL FAST_FORWARD. Vincerà quello con un piano migliore e letture logiche.

Quindi, sostituisci uno di questi con quello appropriato secondo la necessità. Ma sai una cosa? Nella mia esperienza personale, ho usato solo un CURSORE locale di sola lettura con attraversamento di sola lettura. Non ho mai avuto bisogno di rendere il CURSOR globale e aggiornabile.

Oltre a usare questi argomenti, la tempistica dell'esecuzione è importante.

3. Utilizzo di SQL CURSOR nelle transazioni giornaliere

Non sono un amministratore. Ma ho un'idea di come appare un server occupato dagli strumenti del DBA (o da quanti decibel urlano gli utenti). In queste circostanze, vorrai aggiungere ulteriore onere?

Se stai cercando di creare il tuo codice con un CURSORE per le transazioni quotidiane, ripensaci. I CURSOR vanno bene per le esecuzioni una tantum su un server meno occupato con piccoli set di dati. Tuttavia, in una tipica giornata intensa, un CURSORE può:

  • Blocca le righe, specialmente se l'argomento di concorrenza SCROLL_LOCKS è specificato in modo esplicito.
  • Causa un utilizzo elevato della CPU.
  • Utilizza tempdb ampiamente.

Immagina di avere molti di questi in esecuzione contemporaneamente in una giornata tipo.

Stiamo per finire ma c'è un altro errore di cui dobbiamo parlare.

4. Non valutare l'impatto che porta SQL CURSOR

Sai che le opzioni CURSOR sono buone. Pensi che basti specificarli? Hai già visto i risultati sopra. Senza gli strumenti, non arriveremmo alla conclusione giusta.

Inoltre, c'è un codice all'interno del CURSOR . A seconda di ciò che fa, aggiunge di più alle risorse consumate. Questi potrebbero essere stati disponibili per altri processi. L'intera infrastruttura, l'hardware e la configurazione di SQL Server aggiungeranno altro alla storia.

Che ne dici del volume di dati ? Ho usato SQL CURSOR solo su poche centinaia di record. Può essere diverso per te. Il primo esempio ha richiesto solo 500 record perché quello era il numero che avrei accettato di aspettare. 10.000 o anche 1000 non l'hanno tagliato. Si sono comportati male.

Alla fine, non importa quanto meno o più, controllare le letture logiche, ad esempio, può fare la differenza.

Cosa succede se non controlli il piano di esecuzione, le letture logiche o il tempo trascorso? Quali cose terribili possono accadere oltre al blocco di SQL Server? Possiamo solo immaginare tutti i tipi di scenari apocalittici. Hai capito.

Conclusione

SQL CURSOR funziona elaborando i dati riga per riga. Ha il suo posto, ma può essere brutto se non stai attento. È come uno strumento che esce raramente dalla cassetta degli attrezzi.

Quindi, per prima cosa, prova a risolvere il problema usando i comandi basati su set. Risponde alla maggior parte delle nostre esigenze SQL. E se mai usi SQL CURSOR, usalo con le giuste opzioni. Stimare l'impatto con il piano di esecuzione, STATISTICS IO e xEvent Profiler. Quindi, scegli il momento giusto per l'esecuzione.

Tutto ciò renderà un po' migliore il tuo uso di SQL CURSOR.