Ho scritto più volte sull'uso dei cursori e su come, nella maggior parte dei casi, è più efficiente riscrivere i cursori usando la logica basata su insiemi.
Sono realista, però.
So che ci sono casi in cui i cursori sono "richiesti":è necessario chiamare un'altra stored procedure o inviare un'e-mail per ogni riga, si eseguono attività di manutenzione su ciascun database o si esegue un'attività una tantum che semplicemente non vale la pena investire il tempo per convertire in set-based.
Come lo stai (probabilmente) oggi
Indipendentemente dal motivo per cui stai ancora utilizzando i cursori, dovresti almeno stare attento a non utilizzare le opzioni predefinite piuttosto costose. La maggior parte delle persone avvia i propri cursori in questo modo:
DECLARE c CURSOR FOR SELECT whatever FROM ...
Ora, di nuovo, per attività ad hoc, una tantum, probabilmente va bene. Ma ci sono...
Altri modi per farlo
Volevo eseguire alcuni test utilizzando i valori predefiniti e confrontarli con diverse opzioni del cursore come LOCAL
, STATIC
, READ_ONLY
e FAST_FORWARD
. (Ci sono un sacco di opzioni, ma queste sono quelle più comunemente utilizzate in quanto sono applicabili ai tipi più comuni di operazioni con il cursore che le persone usano.) Non solo volevo testare la velocità grezza di alcune combinazioni diverse, ma anche l'impatto su tempdb e memoria, sia dopo un riavvio del servizio a freddo che con una cache a caldo.
La query che ho deciso di inviare al cursore è una query molto semplice su sys.objects
, nel database di esempio AdventureWorks2012. Ciò restituisce 318.500 righe sul mio sistema (un sistema a 2 core molto modesto con 4 GB di RAM):
SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2;
Quindi ho racchiuso questa query in un cursore con varie opzioni (incluse le impostazioni predefinite) ed ho eseguito alcuni test, misurando la memoria totale del server, le pagine allocate a tempdb (secondo sys.dm_db_task_space_usage
e/o sys.dm_db_session_space_usage
) e durata totale. Ho anche provato a osservare la contesa di tempdb usando gli script di Glenn Berry e Robert Davis, ma sul mio misero sistema non sono riuscito a rilevare alcuna contesa. Ovviamente sono anche su SSD e nient'altro è in esecuzione sul sistema, quindi queste potrebbero essere cose che vuoi aggiungere ai tuoi test se è più probabile che tempdb sia un collo di bottiglia.
Quindi alla fine le query erano simili a questa, con query diagnostiche sparse nei punti appropriati:
DECLARE @i INT = 1; DECLARE c CURSOR -- LOCAL -- LOCAL STATIC -- LOCAL FAST_FORWARD -- LOCAL STATIC READ_ONLY FORWARD_ONLY FOR SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2 ORDER BY c1.[object_id]; OPEN c; FETCH c INTO @i; WHILE (@@FETCH_STATUS = 0) BEGIN SET @i += 1; -- meaningless operation FETCH c INTO @i; END CLOSE c; DEALLOCATE c;
Risultati
Durata
Probabilmente la misura più importante e comune è "quanto tempo ci è voluto?" Bene, ci è voluto quasi cinque volte più tempo per eseguire un cursore con le opzioni predefinite (o solo con LOCAL
specificato), rispetto alla specifica di STATIC
o FAST_FORWARD
:
Memoria
Volevo anche misurare la memoria aggiuntiva richiesta da SQL Server per soddisfare ogni tipo di cursore. Quindi ho semplicemente riavviato prima di ogni test della cache a freddo, misurando il contatore delle prestazioni Total Server Memory (KB)
prima e dopo ogni prova. La migliore combinazione qui era LOCAL FAST_FORWARD
:
Utilizzo tempdb
Questo risultato è stato sorprendente per me. Poiché la definizione di un cursore statico significa che copia l'intero risultato in tempdb, ed è effettivamente espresso in sys.dm_exec_cursors
come SNAPSHOT
, mi aspettavo che l'hit sulle pagine tempdb fosse maggiore con tutte le varianti statiche del cursore. Questo non era il caso; di nuovo vediamo un successo di circa 5 volte sull'utilizzo di tempdb con il cursore predefinito e quello con solo LOCAL
specificato:
Conclusione
Per anni ho sottolineato che la seguente opzione dovrebbe essere sempre specificata per i tuoi cursori:
LOCAL STATIC READ_ONLY FORWARD_ONLY
Da questo momento in poi, finché non avrò la possibilità di testare ulteriori permutazioni o trovare eventuali casi in cui non è l'opzione più veloce, consiglierò quanto segue:
LOCAL FAST_FORWARD
(Per inciso, ho anche eseguito dei test omettendo il LOCAL
opzione e le differenze erano trascurabili.)
Detto questo, questo non è necessariamente vero per *tutti* i cursori. In questo caso, sto parlando esclusivamente di cursori in cui stai solo leggendo i dati dal cursore, solo in una direzione in avanti, e non stai aggiornando i dati sottostanti (né con il tasto né usando WHERE CURRENT OF
). Quelli sono test per un altro giorno.