Ik heb verschillende keren geschreven over het gebruik van cursors en hoe het in de meeste gevallen efficiënter is om je cursors te herschrijven met behulp van set-gebaseerde logica.
Ik ben echter realistisch.
Ik weet dat er gevallen zijn waarin cursors "verplicht" zijn - u moet een andere opgeslagen procedure aanroepen of een e-mail sturen voor elke rij, u voert onderhoudstaken uit voor elke database, of u voert een eenmalige taak uit die eenvoudigweg is het niet waard om de tijd te investeren om te converteren naar set-based.
Hoe doe je het (waarschijnlijk) vandaag
Ongeacht de reden waarom u nog steeds cursors gebruikt, moet u op zijn minst oppassen dat u niet de vrij dure standaardopties gebruikt. De meeste mensen beginnen hun cursor als volgt:
DECLARE c CURSOR FOR SELECT whatever FROM ...
Nogmaals, voor ad-hoc, eenmalige taken is dit waarschijnlijk prima. Maar er zijn...
Andere manieren om het te doen
Ik wilde enkele tests uitvoeren met de standaardinstellingen en deze vergelijken met verschillende cursoropties zoals LOCAL
, STATIC
, READ_ONLY
en FAST_FORWARD
. (Er zijn een heleboel opties, maar dit zijn de meest gebruikte omdat ze van toepassing zijn op de meest voorkomende soorten cursorbewerkingen die mensen gebruiken.) Ik wilde niet alleen de onbewerkte snelheid van een paar verschillende combinaties testen, maar ook de impact op tempdb en geheugen, zowel na een koude herstart van de service als met een warme cache.
De query die ik besloot om naar de cursor te voeren, is een heel eenvoudige query tegen sys.objects
, in de voorbeelddatabase AdventureWorks2012. Dit geeft 318.500 rijen terug op mijn systeem (een zeer bescheiden 2-coresysteem met 4GB RAM):
SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2;
Vervolgens plaatste ik deze query in een cursor met verschillende opties (inclusief de standaardinstellingen) en voerde ik enkele tests uit, waarbij ik het totale servergeheugen meet, de pagina's toegewezen aan tempdb (volgens sys.dm_db_task_space_usage
en/of sys.dm_db_session_space_usage
), en de totale duur. Ik probeerde ook tempdb-conflicten te observeren met behulp van scripts van Glenn Berry en Robert Davis, maar op mijn armzalige systeem kon ik geen enkele controverse ontdekken. Natuurlijk gebruik ik ook SSD en er draait helemaal niets anders op het systeem, dus dit kunnen dingen zijn die je aan je eigen tests wilt toevoegen als tempdb eerder een knelpunt is.
Dus uiteindelijk zagen de zoekopdrachten er ongeveer zo uit, met diagnostische vragen op de juiste punten:
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;
Resultaten
Duur
Misschien wel de belangrijkste en meest voorkomende maatregel is:"Hoe lang heeft het geduurd?" Wel, het duurde bijna vijf keer zo lang om een cursor te laten lopen met de standaardopties (of met alleen LOCAL
gespecificeerd), vergeleken met het specificeren van STATIC
of FAST_FORWARD
:
Geheugen
Ik wilde ook het extra geheugen meten dat SQL Server zou vragen bij het vervullen van elk type cursor. Dus ik startte gewoon opnieuw voor elke koude cache-test, waarbij ik de prestatiemeter meet Total Server Memory (KB)
voor en na elke test. De beste combinatie hier was LOCAL FAST_FORWARD
:
tempdb-gebruik
Dit resultaat was verrassend voor mij. Aangezien de definitie van een statische cursor betekent dat het het volledige resultaat naar tempdb kopieert, en het wordt feitelijk uitgedrukt in sys.dm_exec_cursors
als SNAPSHOT
, Ik had verwacht dat de hit op tempdb-pagina's hoger zou zijn bij alle statische varianten van de cursor. Dit was niet het geval; opnieuw zien we een ongeveer 5X hit op tempdb-gebruik met de standaardcursor en degene met alleen LOCAL
gespecificeerd:
Conclusie
Jarenlang heb ik benadrukt dat de volgende optie altijd moet worden opgegeven voor uw cursors:
LOCAL STATIC READ_ONLY FORWARD_ONLY
Vanaf dit punt, totdat ik de kans heb om verdere permutaties te testen of gevallen te vinden waarin dit niet de snelste optie is, zal ik het volgende aanbevelen:
LOCAL FAST_FORWARD
(Terzijde, ik heb ook tests uitgevoerd zonder de LOCAL
optie, en de verschillen waren verwaarloosbaar.)
Dat gezegd hebbende, is dit niet noodzakelijk waar voor *alle* cursors. In dit geval heb ik het alleen over cursors waarbij u alleen gegevens van de cursor leest, alleen in voorwaartse richting, en u de onderliggende gegevens niet bijwerkt (met de toets of met behulp van WHERE CURRENT OF
). Dat zijn testen voor een andere dag.