Database
 sql >> Baza danych >  >> RDS >> Database

Czy popełniasz te błędy podczas korzystania z SQL CURSOR?

Dla niektórych to złe pytanie. KURSOR SQL JEST błąd, pomyłka. Diabeł tkwi w szczegółach! Możesz czytać wszelkiego rodzaju bluźnierstwa w całej blogosferze SQL w imieniu SQL CURSOR.

Jeśli czujesz to samo, co skłoniło Cię do takiego wniosku?

Jeśli pochodzi od zaufanego przyjaciela i współpracownika, nie mogę cię winić. Zdarza się. Czasami dużo. Ale jeśli ktoś przekonał Cię dowodami, to już inna historia.

Nie spotkaliśmy się wcześniej. Nie znasz mnie jako przyjaciela. Mam jednak nadzieję, że uda mi się to wyjaśnić przykładami i przekonać, że SQL CURSOR ma swoje miejsce. To niewiele, ale to małe miejsce w naszym kodzie ma zasady.

Ale najpierw pozwól, że opowiem ci moją historię.

Zacząłem programować z bazami danych używając xBase. To było w college'u, aż do pierwszych dwóch lat profesjonalnego programowania. Mówię ci o tym, ponieważ kiedyś przetwarzaliśmy dane sekwencyjnie, a nie w zestawach, jak SQL. Kiedy nauczyłem się SQL, było to jak zmiana paradygmatu. Decyduje za mnie silnik bazy danych za pomocą wydanych przeze mnie poleceń opartych na zestawach. Kiedy dowiedziałem się o SQL CURSOR, poczułem się, jakbym wrócił do starych, ale wygodnych sposobów.

Ale niektórzy starsi koledzy ostrzegali mnie:„Unikaj SQL CURSOR za wszelką cenę!” Dostałem kilka ustnych wyjaśnień i to wszystko.

SQL CURSOR może być zły, jeśli użyjesz go do niewłaściwego zadania. Jak używanie młotka do cięcia drewna, to śmieszne. Oczywiście mogą się zdarzyć błędy i na tym będziemy się skupiać.

1. Używanie KURSORA SQL, gdy wystarczą polecenia oparte na ustawieniach

Nie mogę tego wystarczająco podkreślić, ale TO jest sedno problemu. Kiedy po raz pierwszy dowiedziałem się, czym jest SQL CURSOR, zapaliła się żarówka. „Pętle! Wiem to!" Jednak dopiero wtedy, gdy zacząłem boleć głowy, a moi seniorzy mnie zbesztali.

Widzisz, podejście SQL jest oparte na zbiorach. Wydasz polecenie INSERT z wartości tabeli i wykona to zadanie bez pętli w kodzie. Jak powiedziałem wcześniej, to zadanie silnika bazy danych. Tak więc, jeśli zmusisz pętlę do dodania rekordów do tabeli, omijasz ten autorytet. Będzie brzydko.

Zanim spróbujemy absurdalnego przykładu, przygotujmy dane:


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%'

Pierwsze zestawienie wygeneruje 500 rekordów danych. Drugi otrzyma podzbiór tego. Wtedy jesteśmy gotowi. Wstawimy brakujące dane z TestTable do TestTable2 przy użyciu kursora SQL. Zobacz poniżej:


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

Oto jak zapętlić się za pomocą SQL CURSOR, aby wstawić brakujący rekord jeden po drugim. Dość długo, prawda?

Teraz wypróbujmy lepszy sposób – alternatywę opartą na zestawie. Oto idzie:


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)

To jest krótkie, zgrabne i szybkie. Jak szybko? Zobacz rysunek 1 poniżej:

Używając xEvent Profiler w SQL Server Management Studio, porównałem czas pracy procesora, czas trwania i odczyty logiczne. Jak widać na rysunku 1, użycie polecenia set-based do WSTAWIANIA rekordów wygrywa test wydajności. Liczby mówią same za siebie. Używanie SQL CURSOR zużywa więcej zasobów i czasu przetwarzania.

Dlatego przed użyciem SQL CURSOR spróbuj najpierw napisać polecenie oparte na zestawie. Na dłuższą metę lepiej się to opłaci.

Ale co, jeśli potrzebujesz SQL CURSOR, aby wykonać zadanie?

2. Nieużywanie odpowiednich opcji kursora SQL

Innym błędem, który nawet ja popełniłem w przeszłości, było nie używanie odpowiednich opcji w DECLARE CURSOR. Istnieją opcje dotyczące zakresu, modelu, współbieżności oraz przewijania lub nie. Te argumenty są opcjonalne i łatwo je zignorować. Jeśli jednak SQL CURSOR jest jedynym sposobem wykonania tego zadania, musisz jasno określić swoją intencję.

Więc zadaj sobie pytanie:

  • Czy podczas przechodzenia przez pętlę będziesz przechodzić wierszami tylko do przodu, czy przechodzić do pierwszego, ostatniego, poprzedniego lub następnego wiersza? Musisz określić, czy CURSOR jest tylko do przodu, czy można go przewijać. To jest DECLARE CURSOR FORWARD_ONLY lub ZADEKLARUJ PRZEWIJANIE KURSOREM .
  • Czy zamierzasz zaktualizować kolumny w KURSORZE? Użyj READ_ONLY, jeśli nie można go zaktualizować.
  • Czy potrzebujesz najnowszych wartości podczas przechodzenia przez pętlę? Użyj STATIC, jeśli wartości nie będą miały znaczenia, jeśli są najnowsze, czy nie. Użyj opcji DYNAMIC, jeśli inne transakcje aktualizują kolumny lub usuwają wiersze, których używasz w CURSOR i potrzebujesz najnowszych wartości. Uwaga :DYNAMIC będzie drogi.
  • Czy kursor CURSOR jest globalny dla połączenia, czy lokalny dla partii lub procedury składowanej? Określ, czy jest LOKALNY, czy GLOBALNY.

Więcej informacji na temat tych argumentów można znaleźć w dokumentacji Microsoft Docs.

Przykład

Wypróbujmy przykład porównujący trzy kursory dla czasu procesora, odczytów logicznych i czasu trwania za pomocą xEvents Profiler. Pierwszy nie będzie miał odpowiednich opcji po ZADEKLAROWANIU KURSORA. Drugi to LOCAL STATIC FORWARD_ONLY READ_ONLY. Ostatni to LOtyuiCAL FAST_FORWARD.

Oto pierwszy:

-- 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

Oczywiście istnieje lepsza opcja niż powyższy kod. Jeśli celem jest tylko wygenerowanie skryptu z istniejących tabel użytkowników, zrobi to SELECT. Następnie wklej wynik do innego okna zapytania.

Ale jeśli potrzebujesz wygenerować skrypt i uruchomić go od razu, to już inna historia. Musisz ocenić skrypt wyjściowy, czy będzie opodatkowywał twój serwer, czy nie. Zobacz błąd nr 4 później.

Aby pokazać porównanie trzech kursorów z różnymi opcjami, wystarczy.

Teraz zajmijmy się podobnym kodem, ale z 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

Jak widać powyżej, jedyną różnicą w stosunku do poprzedniego kodu jest LOCAL STATIC FORWARD_ONLY READ_ONLY argumenty.

Trzeci będzie miał LOCAL FAST_FORWARD. Teraz, według Microsoft, FAST_FORWARD to FORWARD_ONLY, READ_ONLY CURSOR z włączonymi optymalizacjami. Zobaczymy, jak sobie poradzi z pierwszymi dwoma.

Jak się porównują? Zobacz rysunek 2:

Ten, który zajmuje mniej czasu i czasu trwania procesora, to LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Zauważ również, że SQL Server ma wartości domyślne, jeśli nie określisz argumentów, takich jak STATIC lub READ_ONLY. Ma to straszne konsekwencje, jak zobaczysz w następnej sekcji.

Co ujawniono sp_describe_cursor

sp_describe_cursor jest procedurą składowaną z głównego baza danych, której możesz użyć do uzyskania informacji z otwartego CURSOR. A oto, co ujawnił w pierwszej partii zapytań bez opcji CURSOR. Zobacz Rysunek 3, aby zobaczyć wynik sp_describe_cursor :

Przesadzić dużo? Obstawiasz. KURSOR z pierwszej partii zapytań to:

  • globalnie do istniejącego połączenia.
  • dynamiczny, co oznacza, że ​​śledzi zmiany w tabeli #commands pod kątem aktualizacji, usuwania i wstawiania.
  • optymistyczny, co oznacza, że ​​SQL Server dodał dodatkową kolumnę do tabeli tymczasowej o nazwie CWT. To jest kolumna sumy kontrolnej do śledzenia zmian w wartościach tabeli #commands.
  • przewijalny, co oznacza, że ​​możesz przejść do poprzedniego, następnego, górnego lub dolnego wiersza kursora.

Absurdalny? Zdecydowanie się zgadzam. Dlaczego potrzebujesz globalnego połączenia? Dlaczego musisz śledzić zmiany w tabeli tymczasowej #commands? Czy przewinęliśmy gdziekolwiek poza kolejnym rekordem w KURSORZE?

Ponieważ SQL Server określa to za nas, pętla CURSOR staje się strasznym błędem.

Teraz zdajesz sobie sprawę, dlaczego jawne określenie opcji SQL CURSOR jest tak ważne. Tak więc od teraz zawsze podawaj te argumenty KURSORA, jeśli chcesz użyć KURSORA.

Plan wykonania ujawnia więcej

Rzeczywisty plan wykonania ma coś więcej do powiedzenia na temat tego, co dzieje się za każdym razem, gdy wykonywane jest polecenie FETCH NEXT FROM command_builder INTO @command. Na rysunku 4 wiersz jest wstawiany w indeksie klastrowym CWT_PrimaryKey w tempdb tabela CWT :

Zapisy zdarzają się w tempdb przy każdym FETCH NEXT. Poza tym jest więcej. Pamiętasz, że KURSOR jest OPTYMISTYCZNY na Rysunku 3? Właściwości klastrowego skanowania indeksu w skrajnej prawej części planu ujawniają dodatkową nieznaną kolumnę o nazwie Chk1002 :

Czy to może być kolumna Suma kontrolna? XML planu potwierdza, że ​​tak jest w rzeczywistości:

Teraz porównaj rzeczywisty plan wykonania FETCH NEXT, gdy KURSOR to LOCAL STATIC FORWARD_ONLY READ_ONLY:

Używa tempdb też, ale to znacznie prostsze. Tymczasem Rysunek 8 przedstawia plan wykonania, gdy używany jest tryb LOCAL FAST_FORWARD:

Na wynos

Jednym z odpowiednich zastosowań SQL CURSOR jest generowanie skryptów lub uruchamianie niektórych poleceń administracyjnych w stosunku do grupy obiektów bazy danych. Nawet jeśli istnieją niewielkie zastosowania, pierwszą opcją jest użycie LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR lub LOCAL FAST_FORWARD. Ten z lepszym planem i logicznymi odczytami wygra.

Następnie wymień dowolne z nich na odpowiednie, zgodnie z potrzebami. Ale wiesz co? Z mojego osobistego doświadczenia korzystałem tylko z lokalnego kursora tylko do odczytu z przechodzeniem tylko do przodu. Nigdy nie musiałem tworzyć CURSORa globalnego i aktualizowalnego.

Oprócz używania tych argumentów, czas egzekucji ma znaczenie.

3. Używanie SQL CURSOR do codziennych transakcji

Nie jestem administratorem. Ale mam wyobrażenie o tym, jak wygląda zajęty serwer z narzędzi DBA (lub z tego, ile decybeli krzyczą użytkownicy). Czy w tych okolicznościach chcesz zwiększyć dodatkowe obciążenie?

Jeśli próbujesz stworzyć swój kod za pomocą CURSOR do codziennych transakcji, pomyśl jeszcze raz. CURSORs są dobre dla jednorazowych uruchomień na mniej obciążonym serwerze z małymi zestawami danych. Jednak w typowy pracowity dzień CURSOR może:

  • Zablokuj wiersze, zwłaszcza jeśli wyraźnie określono argument współbieżności SCROLL_LOCKS.
  • Powodują wysokie zużycie procesora.
  • Użyj tempdb szeroko.

Wyobraź sobie, że kilka z nich działa jednocześnie w typowy dzień.

Niedługo skończymy, ale jest jeszcze jeden błąd, o którym musimy porozmawiać.

4. Brak oceny wpływu, jaki przynosi CURSOR SQL

Wiesz, że opcje CURSOR są dobre. Czy uważasz, że ich określenie wystarczy? Widziałeś już powyższe wyniki. Bez narzędzi nie wyciągnęlibyśmy właściwych wniosków.

Ponadto w KURSORZE znajduje się kod . W zależności od tego, co robi, dodaje więcej do zużywanych zasobów. Mogły one być dostępne dla innych procesów. Cała Twoja infrastruktura, sprzęt i konfiguracja SQL Server dodadzą więcej do historii.

A co z ilością danych ? Użyłem SQL CURSOR tylko na kilkuset rekordach. Dla ciebie może być inaczej. Pierwszy przykład obejmował tylko 500 rekordów, ponieważ była to liczba, na którą zgodziłbym się poczekać. 10 000, a nawet 1000 nie wystarczyło. Osiągnęli złe wyniki.

Ostatecznie, bez względu na to, jak mniej lub więcej, na przykład sprawdzenie odczytów logicznych może mieć znaczenie.

Co się stanie, jeśli nie sprawdzisz planu wykonania, odczytów logicznych lub czasu, który upłynął? Jakie straszne rzeczy mogą się wydarzyć poza zawieszaniem się programu SQL Server? Możemy sobie tylko wyobrazić wszelkiego rodzaju scenariusze końca świata. Masz rację.

Wniosek

SQL CURSOR działa poprzez przetwarzanie danych wiersz po wierszu. Ma swoje miejsce, ale może być źle, jeśli nie będziesz ostrożny. To jak narzędzie, które rzadko wychodzi z zestawu narzędzi.

Więc najpierw spróbuj rozwiązać problem za pomocą poleceń opartych na zestawach. Odpowiada na większość naszych potrzeb SQL. A jeśli kiedykolwiek użyjesz SQL CURSOR, użyj go z odpowiednimi opcjami. Oszacuj wpływ za pomocą Planu wykonania, STATISTICS IO i xEvent Profiler. Następnie wybierz odpowiedni czas na wykonanie.

Wszystko to sprawi, że korzystanie z SQL CURSOR będzie nieco lepsze.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Uzupełnianie SQL. Historie sukcesu i porażki

  2. Tworzenie kopii zapasowych i przywracanie bazy danych z obsługą FILESTREAM

  3. Wprowadzenie do SQL

  4. 10 najlepszych startupów w chmurze – 2018

  5. Minimalne logowanie za pomocą INSERT…SELECT do tabel sterty