Kilka razy pisałem o używaniu kursorów i o tym, jak w większości przypadków bardziej wydajne jest przepisywanie kursorów przy użyciu logiki opartej na zbiorach.
Ale jestem realistą.
Wiem, że zdarzają się przypadki, w których kursory są „wymagane” – musisz wywołać inną procedurę składowaną lub wysłać e-mail dla każdego wiersza, wykonujesz zadania konserwacyjne dla każdej bazy danych lub uruchamiasz jednorazowe zadanie, które po prostu nie warto inwestować czasu na konwersję na zestaw.
Jak (prawdopodobnie) to dzisiaj robisz
Bez względu na powód, dla którego nadal używasz kursorów, powinieneś przynajmniej uważać, aby nie używać dość drogich opcji domyślnych. Większość ludzi zaczyna swoje kursory w ten sposób:
DECLARE c CURSOR FOR SELECT whatever FROM ...
Teraz znowu, w przypadku doraźnych, jednorazowych zadań jest to prawdopodobnie w porządku. Ale są…
Inne sposoby na zrobienie tego
Chciałem uruchomić kilka testów przy użyciu ustawień domyślnych i porównać je z różnymi opcjami kursora, takimi jak LOKALNIE
, STATYCZNE
, TYLKO DO ODCZYTU
i FAST_FORWARD
. (Istnieje mnóstwo opcji, ale są to te najczęściej używane, ponieważ mają zastosowanie do najczęstszych typów operacji kursora, których ludzie używają.) Nie tylko chciałem przetestować surową prędkość kilku różnych kombinacji, ale również wpływ na tempdb i pamięć, zarówno po zimnym ponownym uruchomieniu usługi, jak i z ciepłą pamięcią podręczną.
Zapytanie, które postanowiłem podać do kursora, jest bardzo prostym zapytaniem przeciwko sys.objects
, w przykładowej bazie danych AdventureWorks2012. Zwraca to 318 500 wierszy w moim systemie (bardzo skromny system 2-rdzeniowy z 4 GB pamięci RAM):
SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2;
Następnie umieściłem to zapytanie w kursorze z różnymi opcjami (w tym wartościami domyślnymi) i przeprowadziłem kilka testów, mierząc całkowitą pamięć serwera, strony przydzielone do tempdb (zgodnie z sys.dm_db_task_space_usage
i/lub sys.dm_db_session_space_usage
) oraz całkowity czas trwania. Próbowałem również obserwować rywalizację tempdb za pomocą skryptów Glenna Berry'ego i Roberta Davisa, ale w moim marnym systemie nie mogłem wykryć żadnej rywalizacji. Oczywiście jestem również na SSD i absolutnie nic innego nie działa w systemie, więc mogą to być rzeczy, które chcesz dodać do własnych testów, jeśli tempdb jest bardziej wąskim gardłem.
Ostatecznie więc zapytania wyglądały mniej więcej tak, z zapytaniami diagnostycznymi wstawionymi w odpowiednich punktach:
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;
Wyniki
Czas trwania
Całkiem prawdopodobnie najważniejszą i najczęstszą miarą jest „jak długo to trwało?” Cóż, uruchomienie kursora z domyślnymi opcjami (lub tylko z LOCAL
zajęło prawie pięć razy więcej czasu określony), w porównaniu do określenia STATIC
lub FAST_FORWARD
:
Pamięć
Chciałem również zmierzyć dodatkową pamięć, której SQL Server zażądałby podczas wypełniania każdego typu kursora. Dlatego po prostu uruchamiałem ponownie przed każdym testem zimnej pamięci podręcznej, mierząc licznik wydajności Całkowita pamięć serwera (KB)
przed i po każdym teście. Najlepsza kombinacja tutaj to LOCAL FAST_FORWARD
:
użycie tempdb
Ten wynik był dla mnie zaskakujący. Ponieważ definicja kursora statycznego oznacza, że kopiuje on cały wynik do tempdb i jest on faktycznie wyrażony w sys.dm_exec_cursors
jako ZDJĘCIE
, spodziewałem się, że trafienie na stronach tempdb będzie wyższe przy wszystkich statycznych wariantach kursora. Tak nie było; ponownie widzimy około 5X trafienie na użycie tempdb z domyślnym kursorem i tym z tylko LOKALNY
określono:
Wniosek
Od lat podkreślam, że dla twoich kursorów zawsze powinna być określona następująca opcja:
LOCAL STATIC READ_ONLY FORWARD_ONLY
Od tego momentu, dopóki nie będę miał szansy przetestować dalszych permutacji lub znaleźć przypadków, w których nie jest to najszybsza opcja, będę polecać następujące:
LOCAL FAST_FORWARD
(Na marginesie przeprowadziłem również testy z pominięciem LOKALNE
opcja, a różnice były znikome.)
To powiedziawszy, niekoniecznie dotyczy to *wszystkich* kursorów. W tym przypadku mówię wyłącznie o kursorach, w których tylko odczytujesz dane z kursora, tylko w kierunku do przodu i nie aktualizujesz danych bazowych (albo za pomocą klucza, albo za pomocą WHERE CURRENT OF ). To są testy na kolejny dzień.