sql >> Database >  >> RDS >> Database

Maakt u deze fouten bij het gebruik van SQL CURSOR?

Voor sommige mensen is het de verkeerde vraag. SQL-CURSOR IS de fout. De duivel is in de details! In de naam van SQL CURSOR kun je allerlei godslastering lezen in de hele SQL-blogosfeer.

Als u hetzelfde voelt, waarom bent u dan tot deze conclusie gekomen?

Als het van een vertrouwde vriend en collega is, kan ik het je niet kwalijk nemen. Het gebeurt. Soms veel. Maar als iemand je met bewijs heeft overtuigd, is dat een ander verhaal.

We hebben elkaar nog niet eerder ontmoet. Je kent me niet als vriend. Maar ik hoop dat ik het met voorbeelden kan uitleggen en je ervan kan overtuigen dat SQL CURSOR zijn plaats heeft. Het is niet veel, maar die kleine plaats in onze code heeft regels.

Maar laat me je eerst mijn verhaal vertellen.

Ik begon te programmeren met databases met behulp van xBase. Dat was op de universiteit tot mijn eerste twee jaar van professionele programmering. Ik vertel je dit omdat we vroeger gegevens opeenvolgend verwerkten, niet in vaste batches zoals SQL. Toen ik SQL leerde, was het als een paradigmaverschuiving. De database-engine beslist voor mij met zijn set-gebaseerde opdrachten die ik heb gegeven. Toen ik over SQL CURSOR hoorde, voelde het alsof ik terug was op de oude maar comfortabele manieren.

Maar sommige senior collega's waarschuwden me:"Vermijd SQL CURSOR ten koste van alles!" Ik kreeg een paar mondelinge uitleg, en dat was het dan.

SQL CURSOR kan slecht zijn als u het voor de verkeerde taak gebruikt. Alsof je een hamer gebruikt om hout te hakken, het is belachelijk. Natuurlijk kunnen er fouten gebeuren, en daar ligt onze focus.

1. SQL CURSOR gebruiken wanneer op ingestelde commando's voldoende zijn

Ik kan dit niet genoeg benadrukken, maar DIT is de kern van het probleem. Toen ik voor het eerst leerde wat SQL CURSOR was, ging er een lampje branden. “Loops! Ik weet dat!" Maar pas toen ik er hoofdpijn van kreeg en mijn senioren me uitschelden.

Zie je, de benadering van SQL is set-gebaseerd. U geeft een INSERT-opdracht uit tabelwaarden en het zal het werk doen zonder lussen in uw code. Zoals ik al eerder zei, het is de taak van de database-engine. Dus als u een lus forceert om records aan een tabel toe te voegen, omzeilt u die autoriteit. Het wordt lelijk.

Laten we, voordat we een belachelijk voorbeeld proberen, de gegevens voorbereiden:


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

De eerste instructie genereert 500 records met gegevens. De tweede krijgt er een subset van. Dan zijn we er klaar voor. We gaan de ontbrekende gegevens invoegen van TestTable in TestTable2 met behulp van SQL-CURSOR. Zie hieronder:


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

Zo kunt u een lus uitvoeren met SQL CURSOR om een ​​ontbrekend record één voor één in te voegen. Best lang, niet?

Laten we nu een betere manier proberen:het op sets gebaseerde alternatief. Hier gaat het:


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)

Dat is kort, netjes en snel. Hoe snel? Zie Afbeelding 1 hieronder:

Met xEvent Profiler in SQL Server Management Studio heb ik de CPU-tijdcijfers, duur en logische uitlezingen vergeleken. Zoals je in figuur 1 kunt zien, wint het gebruik van de set-gebaseerde opdracht om records in te voegen de prestatietest. De cijfers spreken voor zich. Het gebruik van SQL CURSOR kost meer bronnen en verwerkingstijd.

Probeer daarom, voordat u SQL CURSOR gebruikt, eerst een op een set gebaseerde opdracht te schrijven. Het zal op de lange termijn beter renderen.

Maar wat als u SQL CURSOR nodig heeft om de klus te klaren?

2. De juiste SQL CURSOR-opties niet gebruiken

Een andere fout die ik in het verleden heb gemaakt, was het niet gebruiken van de juiste opties in DECLARE CURSOR. Er zijn opties voor scope, model, concurrency en of scrollable is of niet. Deze argumenten zijn optioneel en het is gemakkelijk om ze te negeren. Als SQL CURSOR echter de enige manier is om de taak uit te voeren, moet u expliciet zijn met uw intentie.

Dus, vraag jezelf af:

  • Ga je bij het doorlopen van de lus alleen vooruit door de rijen of ga je naar de eerste, laatste, vorige of volgende rij? U moet specificeren of de CURSOR alleen voorwaarts of schuifbaar is. Dat is DECLARE CURSOR ALLEEN FORWARD_ of DECLARE CURSOR SCROLL .
  • Ga je de kolommen in de CURSOR bijwerken? Gebruik READ_ONLY als het niet kan worden bijgewerkt.
  • Heeft u de laatste waarden nodig als u de lus doorloopt? Gebruik STATIC als de waarden er niet toe doen, of ze nu de laatste zijn of niet. Gebruik DYNAMIC als andere transacties kolommen bijwerken of rijen verwijderen die u in de CURSOR gebruikt en u de nieuwste waarden nodig hebt. Opmerking :DYNAMIC zal duur zijn.
  • Is de CURSOR globaal voor de verbinding of lokaal voor de batch of een opgeslagen procedure? Specificeer of LOKAAL of GLOBAAL.

Zoek voor meer informatie over deze argumenten de referentie op in Microsoft Docs.

Voorbeeld

Laten we een voorbeeld proberen waarin drie CURSOR's worden vergeleken voor de CPU-tijd, logische uitlezingen en duur met behulp van xEvents Profiler. De eerste heeft geen geschikte opties na DECLARE CURSOR. De tweede is LOCAL STATIC FORWARD_ONLY READ_ONLY. De laatste is LOtyuiCAL FAST_FORWARD.

Dit is de eerste:

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

Er is natuurlijk een betere optie dan de bovenstaande code. Als het alleen de bedoeling is om een ​​script te genereren uit bestaande gebruikerstabellen, is SELECT voldoende. Plak de uitvoer vervolgens in een ander zoekvenster.

Maar als u een script moet genereren en in één keer moet uitvoeren, is dat een ander verhaal. U moet het uitvoerscript evalueren of het uw server belast of niet. Zie fout #4 later.

Dit is voldoende om u de vergelijking te laten zien van drie CURSOR's met verschillende opties.

Laten we nu een vergelijkbare code gebruiken, maar met 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

Zoals je hierboven kunt zien, is het enige verschil met de vorige code de LOCAL STATIC FORWARD_ONLY READ_ONLY argumenten.

De derde heeft een LOCAL FAST_FORWARD. Volgens Microsoft is FAST_FORWARD nu een FORWARD_ONLY, READ_ONLY CURSOR met optimalisaties ingeschakeld. We zullen zien hoe dit zal aflopen met de eerste twee.

Hoe vergelijken ze? Zie afbeelding 2:

Degene die minder CPU-tijd en duur kost, is de LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Merk ook op dat SQL Server standaardwaarden heeft als u geen argumenten zoals STATIC of READ_ONLY opgeeft. Dat heeft een verschrikkelijke consequentie, zoals je in het volgende gedeelte zult zien.

Wat sp_describe_cursor heeft onthuld

sp_describe_cursor is een opgeslagen procedure van de master database die u kunt gebruiken om informatie uit de open CURSOR te halen. En dit is wat het onthulde uit de eerste reeks zoekopdrachten zonder CURSOR-opties. Zie figuur 3 voor het resultaat van sp_describe_cursor :

Veel te veel? Zeker weten. De CURSOR van de eerste reeks zoekopdrachten is:

  • algemeen naar de bestaande verbinding.
  • dynamisch, wat betekent dat het wijzigingen in de #commands-tabel bijhoudt voor updates, verwijderingen en invoegingen.
  • optimistisch, wat betekent dat SQL Server een extra kolom heeft toegevoegd aan een tijdelijke tabel met de naam CWT. Dit is een checksum-kolom voor het bijhouden van wijzigingen in de waarden van de #commands-tabel.
  • scrollbaar, wat betekent dat u naar de vorige, volgende, bovenste of onderste rij in de cursor kunt gaan.

Absurd? Ik ben het daar zeer mee eens. Waarom heb je een wereldwijde verbinding nodig? Waarom moet u wijzigingen in de tijdelijke tabel #commands bijhouden? Zijn we ergens anders gescrold dan het volgende record in de CURSOR?

Omdat een SQL Server dit voor ons bepaalt, wordt de CURSOR-lus een vreselijke fout.

Nu realiseert u zich waarom het expliciet specificeren van SQL CURSOR-opties zo cruciaal is. Geef vanaf nu dus altijd deze CURSOR-argumenten op als u een CURSOR moet gebruiken.

Het uitvoeringsplan onthult meer

Het werkelijke uitvoeringsplan heeft iets meer te zeggen over wat er gebeurt elke keer dat een FETCH NEXT FROM command_builder INTO @command wordt uitgevoerd. In Afbeelding 4 is een rij ingevoegd in de geclusterde index CWT_PrimaryKey in de tempdb tabel CWT :

Schrijfacties gebeuren met tempdb op elke FETCH NEXT. Trouwens, er is meer. Weet je nog dat de CURSOR OPTIMISTISCH is in figuur 3? De eigenschappen van de Clustered Index Scan aan de rechterkant van het plan onthullen de extra onbekende kolom genaamd Chk1002 :

Zou dit de Checksum-kolom kunnen zijn? De Plan XML bevestigt dat dit inderdaad het geval is:

Vergelijk nu het werkelijke uitvoeringsplan van de FETCH NEXT wanneer de CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY is:

Het gebruikt tempdb ook, maar het is veel eenvoudiger. Ondertussen toont figuur 8 het uitvoeringsplan wanneer LOCAL FAST_FORWARD wordt gebruikt:

Afhaalmaaltijden

Een van de juiste toepassingen van SQL CURSOR is het genereren van scripts of het uitvoeren van enkele administratieve opdrachten naar een groep database-objecten. Zelfs als er weinig gebruik van is, is uw eerste optie om de LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR of LOCAL FAST_FORWARD te gebruiken. Degene met een beter plan en logische leest zal winnen.

Vervang vervolgens een van deze door de juiste, afhankelijk van de behoefte. Maar weet je wat? In mijn persoonlijke ervaring heb ik alleen een lokale alleen-lezen CURSOR gebruikt met alleen voorwaartse verplaatsing. Ik hoefde de CURSOR nooit globaal en updatebaar te maken.

Afgezien van het gebruik van deze argumenten, is de timing van de uitvoering van belang.

3. SQL CURSOR gebruiken voor dagelijkse transacties

Ik ben geen beheerder. Maar ik heb een idee van hoe een drukke server eruitziet door de tools van de DBA (of uit hoeveel decibel gebruikers schreeuwen). Wilt u onder deze omstandigheden nog meer lasten toevoegen?

Als u uw code probeert te maken met een CURSOR voor dagelijkse transacties, denk dan nog eens goed na. CURSOR's zijn prima voor eenmalige uitvoeringen op een minder drukke server met kleine datasets. Op een typische drukke dag kan een CURSOR echter:

  • Vergrendel rijen, vooral als het gelijktijdigheidsargument SCROLL_LOCKS expliciet is opgegeven.
  • Veroorzaak een hoog CPU-gebruik.
  • Gebruik tempdb uitgebreid.

Stel je voor dat je er meerdere tegelijk hebt op een normale dag.

We staan ​​op het punt te eindigen, maar er is nog een fout waar we over moeten praten.

4. De impact die SQL CURSOR met zich meebrengt niet beoordelen

U weet dat de CURSOR-opties goed zijn. Denk je dat het voldoende is om ze te specificeren? Je hebt de resultaten hierboven al gezien. Zonder de tools zouden we niet tot de juiste conclusie komen.

Verder zit er code in de CURSOR . Afhankelijk van wat het doet, voegt het meer toe aan de verbruikte middelen. Deze waren mogelijk beschikbaar voor andere processen. Uw volledige infrastructuur, uw hardware en SQL Server-configuratie zullen meer aan het verhaal toevoegen.

Hoe zit het met de volume aan gegevens ? Ik heb alleen SQL CURSOR gebruikt voor een paar honderd records. Voor jou kan het anders zijn. Het eerste voorbeeld nam slechts 500 records in beslag, want dat was het aantal waarop ik zou wachten. 10.000 of zelfs 1000 haalden het niet. Ze presteerden slecht.

Uiteindelijk, hoe minder of meer, kan bijvoorbeeld het controleren van de logische uitlezingen een verschil maken.

Wat als u het Uitvoeringsplan, de logische uitlezingen of de verstreken tijd niet controleert? Welke vreselijke dingen kunnen er gebeuren behalve dat SQL Server vastloopt? We kunnen ons alleen maar allerlei doemscenario's voorstellen. Je snapt het punt.

Conclusie

SQL CURSOR werkt door gegevens rij voor rij te verwerken. Het heeft zijn plaats, maar het kan slecht zijn als je niet oppast. Het is als een gereedschap dat zelden uit de gereedschapskist komt.

Probeer dus eerst het probleem op te lossen met behulp van set-gebaseerde opdrachten. Het beantwoordt aan de meeste van onze SQL-behoeften. En als je ooit SQL CURSOR gebruikt, gebruik het dan met de juiste opties. Schat de impact in met het Uitvoeringsplan, STATISTICS IO en xEvent Profiler. Kies vervolgens het juiste moment om uit te voeren.

Dit alles zal uw gebruik van SQL CURSOR een beetje beter maken.


  1. Voer een query uit met een LIMIT/OFFSET en verkrijg ook het totale aantal rijen

  2. Voordelen en nadelen van het gebruik van opgeslagen procedures

  3. gegevens opslaan in een database met behulp van tekst bewerken en knop

  4. DefType-statements in VBA:de donkere kant van achterwaartse compatibiliteit