sql >> Databáze >  >> RDS >> Database

Děláte tyto chyby při používání SQL CURSOR?

Pro některé lidi je to špatná otázka. SQL CURSOR JE chyba. Ďábel se skrývá v detailech! V celé SQL blogosféře si můžete přečíst nejrůznější rouhání jménem SQL CURSOR.

Pokud se cítíte stejně, co vás vedlo k tomuto závěru?

Pokud je to od důvěryhodného přítele a kolegy, nemohu vás vinit. Stalo se to. Někdy hodně. Ale pokud vás někdo přesvědčil důkazem, to je jiný příběh.

ještě jsme se nesetkali. neznáš mě jako přítele. Doufám ale, že to dokážu vysvětlit na příkladech a přesvědčit vás, že SQL CURSOR má své místo. Není to mnoho, ale to malé místo v našem kódu má svá pravidla.

Ale nejprve mi dovolte, abych vám řekl svůj příběh.

Začal jsem programovat s databázemi pomocí xBase. To bylo na vysoké škole až do mých prvních dvou let profesionálního programování. Říkám vám to, protože v minulosti jsme zpracovávali data postupně, ne v nastavených dávkách, jako je SQL. Když jsem se naučil SQL, bylo to jako změna paradigmatu. Databázový stroj za mě rozhoduje svými příkazy založenými na sadě, které jsem vydal. Když jsem se dozvěděl o SQL CURSOR, měl jsem pocit, jako bych se vrátil ke starým, ale pohodlným způsobům.

Ale někteří starší kolegové mě varovali:"Za každou cenu se vyhněte SQL CURSOR!" Dostal jsem pár slovních vysvětlení a bylo to.

SQL CURSOR může být špatný, pokud jej použijete pro nesprávnou úlohu. Jako používat kladivo k řezání dřeva, je to směšné. Samozřejmě se mohou stát chyby, a na to se zaměříme.

1. Použití SQL CURSOR, když se vykonají příkazy založené na nastavení

Nemohu to dostatečně zdůraznit, ale TOHLE je jádro problému. Když jsem se poprvé dozvěděl, co je SQL CURSOR, rozsvítila se žárovka. „Smyčky! Vím to!" Nicméně ne, dokud mě z toho bolela hlava a moji senioři mi nadávali.

Jak vidíte, přístup SQL je založen na množinách. Vydáte příkaz INSERT z hodnot tabulky a ten provede práci bez smyček ve vašem kódu. Jak jsem řekl dříve, je to práce databázového stroje. Pokud tedy vynutíte smyčku k přidání záznamů do tabulky, obcházíte toto oprávnění. Bude to ošklivé.

Než se pokusíme o směšný příklad, připravme si data:


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

První výpis vygeneruje 500 záznamů dat. Druhý získá jeho podmnožinu. Pak jsme připraveni. Chystáme se vložit chybějící data z TestTable do TestTable2 pomocí SQL CURSOR. Viz níže:


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

Takto smyčkovat pomocí SQL CURSOR k vložení chybějícího záznamu jeden po druhém. Docela dlouho, že?

Nyní zkusme lepší způsob – alternativu založenou na setu. Tady:


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 je krátké, přehledné a rychlé. Jak rychle? Viz obrázek 1 níže:

Pomocí xEvent Profiler v SQL Server Management Studio jsem porovnal hodnoty CPU, trvání a logická čtení. Jak můžete vidět na obrázku 1, použití příkazu set-based k INSERT záznamům vyhraje test výkonu. Čísla mluví sama za sebe. Použití SQL CURSOR spotřebovává více zdrojů a času na zpracování.

Než tedy použijete SQL CURSOR, zkuste nejprve napsat příkaz založený na sadě. Z dlouhodobého hlediska se to lépe vyplatí.

Ale co když potřebujete SQL CURSOR k dokončení práce?

2. Nepoužíváte vhodné možnosti SQL CURSOR

Další chybou, kterou jsem v minulosti udělal, bylo, že jsem nepoužil vhodné možnosti v DECLARE CURSOR. Existují možnosti pro rozsah, model, souběžnost a zda lze rolovat nebo ne. Tyto argumenty jsou volitelné a je snadné je ignorovat. Pokud je však SQL CURSOR jediným způsobem, jak úkol provést, musíte svůj záměr jasně vyjádřit.

Zeptejte se sami sebe:

  • Budete při procházení smyčkou procházet řádky pouze dopředu nebo se přesunete na první, poslední, předchozí nebo další řádek? Musíte určit, zda je KURZOR pouze dopředný nebo rolovací. To je DECLARE CURSOR FORWARD_ONLY nebo DECLARE CURSOR SCROLL .
  • Chystáte se aktualizovat sloupce v KURZORU? Pokud to nelze aktualizovat, použijte READ_ONLY.
  • Potřebujete nejnovější hodnoty, když procházíte smyčkou? Použijte STATIC, pokud na hodnotách nezáleží, zda jsou nejnovější nebo ne. Použijte DYNAMIC, pokud jiné transakce aktualizují sloupce nebo odstraňují řádky, které používáte v KURZORU, a potřebujete nejnovější hodnoty. Poznámka :DYNAMIC bude drahý.
  • Je CURSOR globální pro připojení nebo lokální pro dávku nebo uloženou proceduru? Zadejte, zda LOCAL nebo GLOBAL.

Další informace o těchto argumentech naleznete v odkazu z Microsoft Docs.

Příklad

Zkusme si příklad porovnat tři CURSORy pro čas CPU, logická čtení a trvání pomocí xEvents Profiler. První nebude mít žádné vhodné možnosti po DECLARE CURSOR. Druhý je LOCAL STATIC FORWARD_ONLY READ_ONLY. Poslední je LOtyuiCAL FAST_FORWARD.

Tady je první:

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

Samozřejmě existuje lepší možnost než výše uvedený kód. Pokud je účelem pouze vygenerovat skript z existujících uživatelských tabulek, postačí SELECT. Potom vložte výstup do jiného okna dotazu.

Ale pokud potřebujete vygenerovat skript a spustit jej najednou, to je jiný příběh. Musíte vyhodnotit výstupní skript, zda bude zdanit váš server nebo ne. Viz Mistake #4 později.

Abychom vám ukázali srovnání tří KURZORů s různými možnostmi, bude stačit.

Nyní mějme podobný kód, ale s 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 můžete vidět výše, jediný rozdíl od předchozího kódu je LOCAL STATIC FORWARD_ONLY READ_ONLY argumenty.

Třetí bude mít LOCAL FAST_FORWARD. Nyní je podle společnosti Microsoft FAST_FORWARD CURSOR FORWARD_ONLY, READ_ONLY s povolenými optimalizacemi. Uvidíme, jak to dopadne s prvními dvěma.

Jak se srovnávají? Viz obrázek 2:

Ten, který zabere méně času CPU a kratší dobu, je LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Všimněte si také, že SQL Server má výchozí hodnoty, pokud nezadáte argumenty jako STATIC nebo READ_ONLY. Jak uvidíte v další části, má to hrozný následek.

Co odhalil sp_describe_cursor

sp_describe_cursor je uložená procedura z master databázi, kterou můžete použít k získání informací z otevřeného CURSORu. A tady je to, co odhalila první várka dotazů bez možnosti KURZOR. Výsledek sp_describe_cursor naleznete na obrázku 3 :

Přehnaně moc? To se vsaď. KURZOR z první dávky dotazů je:

  • globálně ke stávajícímu připojení.
  • dynamický, což znamená, že sleduje změny v tabulce #commands pro aktualizace, mazání a vkládání.
  • optimistický, což znamená, že SQL Server přidal další sloupec do dočasné tabulky s názvem CWT. Toto je sloupec kontrolního součtu pro sledování změn hodnot v tabulce #commands.
  • Posouvací, což znamená, že můžete přejít na předchozí, další, horní nebo spodní řádek kurzoru.

Absurdní? plně souhlasím. Proč potřebujete globální připojení? Proč potřebujete sledovat změny v dočasné tabulce #commands? Posunuli jsme se někam jinam než na další záznam v KURZORU?

Protože to za nás určuje SQL Server, stává se smyčka CURSOR hroznou chybou.

Nyní si uvědomujete, proč je explicitní zadání možností SQL CURSOR tak zásadní. Takže od této chvíle vždy uvádějte tyto argumenty CURSOR, pokud potřebujete použít CURSOR.

Prováděcí plán odhaluje více

Skutečný plán provádění má ještě něco co říci k tomu, co se stane pokaždé, když je spuštěn příkaz FETCH NEXT FROM command_builder DO @command. Na obrázku 4 je do Clustered Index CWT_PrimaryKey vložen řádek v tempdb tabulka CWT :

Zápisy probíhají do tempdb při každém FETCH NEXT. Kromě toho je toho víc. Pamatujete si, že KURZOR je na obrázku 3 OPTIMISTICKÝ? Vlastnosti Clustered Index Scan v pravé části plánu odhalují další neznámý sloupec nazvaný Chk1002 :

Mohl by to být sloupec Kontrolní součet? Plán XML potvrzuje, že tomu tak skutečně je:

Nyní porovnejte skutečný plán provádění funkce FETCH NEXT, když je CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY:

Používá tempdb taky, ale je to mnohem jednodušší. Mezitím Obrázek 8 ukazuje plán provádění při použití LOCAL FAST_FORWARD:

Takové věci

Jedním z vhodných použití SQL CURSORu je generování skriptů nebo spouštění některých administrativních příkazů vůči skupině databázových objektů. I když existují menší použití, vaší první možností je použít LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR nebo LOCAL FAST_FORWARD. Ten s lepším plánem a logickým čtením vyhraje.

Poté vyměňte kterýkoli z nich za vhodný podle potřeby. Ale víš co? Podle mé osobní zkušenosti jsem používal pouze místní KURZOR pouze pro čtení s procházením pouze vpřed. Nikdy jsem nepotřeboval, aby byl CURSOR globální a aktualizovatelný.

Kromě použití těchto argumentů záleží na načasování popravy.

3. Použití SQL CURSOR u denních transakcí

nejsem správce. Ale mám představu o tom, jak vypadá zaneprázdněný server z nástrojů DBA (nebo z toho, kolik decibelů uživatelé křičí). Budete za těchto okolností chtít přidat další zátěž?

Pokud se pokoušíte vytvořit svůj kód pomocí KURZORU pro každodenní transakce, zamyslete se znovu. CURSORy jsou vhodné pro jednorázové spuštění na méně vytíženém serveru s malými datovými sadami. V typickém rušném dni však KURZOR může:

  • Zamknout řádky, zejména pokud je výslovně uveden argument souběžnosti SCROLL_LOCKS.
  • Způsobit vysoké využití CPU.
  • Použijte tempdb značně.

Představte si, že jich v typický den běží několik souběžně.

Už se blíží konec, ale je tu ještě jedna chyba, o které si musíme promluvit.

4. Nevyhodnocení dopadu SQL CURSOR přináší

Víte, že možnosti KURZOR jsou dobré. Myslíte si, že stačí je specifikovat? Výsledky jste již viděli výše. Bez nástrojů bychom nedošli ke správnému závěru.

Navíc je kód uvnitř CURSORu . V závislosti na tom, co dělá, přidává více ke spotřebovaným zdrojům. Ty mohly být dostupné pro jiné procesy. Celá vaše infrastruktura, váš hardware a konfigurace SQL Server přidají další příběh.

Jak je to s objemem dat ? SQL CURSOR jsem použil pouze na pár stovkách záznamů. U vás to může být jinak. První příklad trval pouze 500 záznamů, protože to bylo číslo, na které bych souhlasil, že počkám. 10 000 nebo dokonce 1 000 to neškrtlo. Předvedli špatný výkon.

Nakonec, bez ohledu na to, jak méně nebo více, například kontrola logických čtení může znamenat rozdíl.

Co když nezkontrolujete prováděcí plán, logická čtení nebo uplynulý čas? Jaké strašné věci se mohou stát kromě zamrznutí SQL Serveru? Můžeme si jen představovat všemožné scénáře soudného dne. Chápete pointu.

Závěr

SQL CURSOR funguje tak, že zpracovává data řádek po řádku. Má své místo, ale může být špatné, pokud si nedáte pozor. Je to jako nástroj, který jen zřídkakdy vypadne ze sady nástrojů.

Nejprve tedy zkuste problém vyřešit pomocí příkazů založených na sadě. Odpovídá většině našich SQL potřeb. A pokud někdy použijete SQL CURSOR, použijte jej se správnými možnostmi. Odhadněte dopad pomocí plánu provádění, STATISTICS IO a xEvent Profiler. Poté vyberte správný čas pro provedení.

To vše vám umožní používat SQL CURSOR o něco lépe.


  1. Konfigurace posluchače v databázi Oracle (edice 12c, 18c a 19c)

  2. Nasazení LocalDB na klientském PC

  3. MariaDB a externí data

  4. Jak RPAD() funguje v MariaDB