sql >> Databasteknik >  >> RDS >> Database

Gör du dessa misstag när du använder SQL CURSOR?

För vissa människor är det fel fråga. SQL CURSOR ÄR misstaget. Djävulen är i detaljerna! Du kan läsa alla typer av hädelse i hela SQL-bloggosfären i namnet SQL CURSOR.

Om du känner likadant, vad fick dig att dra den här slutsatsen?

Om det kommer från en pålitlig vän och kollega kan jag inte klandra dig. Det händer. Ibland mycket. Men om någon övertygade dig med bevis är det en annan historia.

Vi har inte träffats tidigare. Du känner mig inte som vän. Men jag hoppas att jag kan förklara det med exempel och övertyga dig om att SQL CURSOR har sin plats. Det är inte mycket, men det lilla stället i vår kod har regler.

Men först, låt mig berätta min historia.

Jag började programmera med databaser med hjälp av xBase. Det var tillbaka på college fram till mina första två år av professionell programmering. Jag berättar detta för dig förr i tiden brukade vi behandla data sekventiellt, inte i uppsättningar som SQL. När jag lärde mig SQL var det som ett paradigmskifte. Databasmotorn bestämmer åt mig med sina set-baserade kommandon som jag utfärdade. När jag lärde mig om SQL CURSOR kändes det som att jag var tillbaka med de gamla men bekväma sätten.

Men några seniora kollegor varnade mig, "Undvik SQL CURSOR till varje pris!" Jag fick några verbala förklaringar, och det var det.

SQL CURSOR kan vara dåligt om du använder det för fel jobb. Som att använda en hammare för att kapa trä, det är löjligt. Självklart kan misstag hända, och det är där vårt fokus kommer att ligga.

1. Använda SQL CURSOR när uppsättningsbaserade kommandon fungerar

Jag kan inte betona detta nog, men DETTA är kärnan i problemet. När jag först fick reda på vad SQL CURSOR var tändes en glödlampa. "Slingor! Jag vet det!" Men inte förrän det gav mig huvudvärk och mina äldre skällde ut mig.

Du förstår, tillvägagångssättet för SQL är set-baserat. Du utfärdar ett INSERT-kommando från tabellvärden, och det kommer att göra jobbet utan loopar på din kod. Som jag sa tidigare, det är databasmotorns jobb. Så om du tvingar en loop att lägga till poster i en tabell, kringgår du den auktoriteten. Det kommer att bli fult.

Innan vi försöker ett löjligt exempel, låt oss förbereda 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%'

Det första uttalandet kommer att generera 500 dataposter. Den andra kommer att få en delmängd av den. Då är vi redo. Vi kommer att infoga de data som saknas från Testtabell till TestTable2 använder SQL CURSOR. Se nedan:


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

Det är hur man loopar med SQL CURSOR för att infoga en saknad post en efter en. Ganska lång, eller hur?

Låt oss nu prova ett bättre sätt – det setbaserade alternativet. Här kommer:


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)

Det är kort, snyggt och snabbt. Hur snabbt? Se figur 1 nedan:

Med hjälp av xEvent Profiler i SQL Server Management Studio jämförde jag CPU-tidssiffrorna, varaktigheten och logiska läsningar. Som du kan se i figur 1 vinner prestandatestet genom att använda det set-baserade kommandot för att INFOGA poster. Siffrorna talar för sig själva. Att använda SQL CURSOR förbrukar mer resurser och bearbetningstid.

Därför, innan du använder SQL CURSOR, försök att skriva ett uppsättningsbaserat kommando först. Det kommer att löna sig bättre i längden.

Men vad händer om du behöver SQL CURSOR för att få jobbet gjort?

2. Använder inte lämpliga SQL CURSOR-alternativ

Ett annat misstag även jag gjorde tidigare var att inte använda lämpliga alternativ i DECLARE CURSOR. Det finns alternativ för omfattning, modell, samtidighet och om det går att rulla eller inte. Dessa argument är valfria och det är lätt att ignorera dem. Men om SQL CURSOR är det enda sättet att utföra uppgiften, måste du vara tydlig med din avsikt.

Så fråga dig själv:

  • När du korsar slingan, kommer du att navigera bara raderna framåt eller flytta till första, sista, föregående eller nästa rad? Du måste ange om CURSOR endast är framåt eller rullbar. Det är DECLARE CURSOR FORWARD_ONLY eller DECLARE CURSOR SCROLL .
  • Ska du uppdatera kolumnerna i CURSOR? Använd READ_ONLY om det inte går att uppdatera.
  • Behöver du de senaste värdena när du korsar slingan? Använd STATIC om värdena inte spelar någon roll om de är senaste eller inte. Använd DYNAMISK om andra transaktioner uppdaterar kolumner eller tar bort rader som du använder i CURSOR, och du behöver de senaste värdena. Obs :DYNAMISK kommer att bli dyrt.
  • Är CURSOR global för anslutningen eller lokal för batchen eller en lagrad procedur? Ange om LOCAL eller GLOBAL.

För mer information om dessa argument, slå upp referensen från Microsoft Docs.

Exempel

Låt oss prova ett exempel som jämför tre CURSORs för CPU-tid, logiska avläsningar och varaktighet med hjälp av xEvents Profiler. Den första har inga lämpliga alternativ efter DECLARE CURSOR. Den andra är LOCAL STATIC FORWARD_ONLY READ_ONLY. Den sista är LOtyuiCAL FAST_FORWARD.

Här är den första:

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

Det finns ett bättre alternativ än ovanstående kod, naturligtvis. Om syftet bara är att generera ett skript från befintliga användartabeller, duger SELECT. Klistra sedan in utdata i ett annat frågefönster.

Men om du behöver skapa ett skript och köra det på en gång, är det en annan historia. Du måste utvärdera utdataskriptet om det kommer att belasta din server eller inte. Se misstag #4 senare.

För att visa dig jämförelsen av tre CURSORs med olika alternativ, fungerar detta.

Låt oss nu ha en liknande kod men med 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

Som du kan se ovan är den enda skillnaden från den tidigare koden LOKALA STATISKA FORWARD_ONLY READ_ONLY argument.

Den tredje kommer att ha en LOCAL FAST_FORWARD. Nu, enligt Microsoft, är FAST_FORWARD en FORWARD_ONLY, READ_ONLY CURSOR med optimeringar aktiverade. Vi får se hur det kommer att gå med de två första.

Hur jämför de? Se figur 2:

Den som tar mindre CPU-tid och varaktighet är LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Observera också att SQL Server har standardinställningar om du inte anger argument som STATIC eller READ_ONLY. Det är en fruktansvärd konsekvens av det som du kommer att se i nästa avsnitt.

Vad sp_describe_cursor avslöjade

sp_describe_cursor är en lagrad procedur från master databas som du kan använda för att få information från den öppna CURSOR. Och här är vad det avslöjade från den första satsen av frågor utan CURSOR-alternativ. Se figur 3 för resultatet av sp_describe_cursor :

Överdriven mycket? Det kan du ge dig på. MARKEREN från den första omgången av frågor är:

  • globalt till den befintliga anslutningen.
  • dynamisk, vilket innebär att den spårar ändringar i #commands-tabellen för uppdateringar, borttagningar och infogar.
  • optimistisk, vilket innebär att SQL Server lade till en extra kolumn i en temporär tabell som heter CWT. Detta är en kontrollsummakolumn för att spåra ändringar i värdena i #commands-tabellen.
  • rullningsbar, vilket innebär att du kan gå till föregående, nästa, övre eller nedre raden i markören.

Absurd? Jag håller starkt med. Varför behöver du en global anslutning? Varför behöver du spåra ändringar i den temporära tabellen #commands? Scrollade vi någon annanstans än nästa post i CURSOR?

Eftersom en SQL Server bestämmer detta för oss, blir CURSOR-slingan ett fruktansvärt misstag.

Nu inser du varför det är så viktigt att explicit specificera SQL CURSOR-alternativ. Så från och med nu, ange alltid dessa CURSOR-argument om du behöver använda en CURSOR.

Utförandeplanen avslöjar mer

Den faktiska exekveringsplanen har något mer att säga om vad som händer varje gång ett FETCH NEXT FROM command_builder INTO @kommando exekveras. I figur 4 infogas en rad i Clustered Index CWT_PrimaryKey i tempdb tabell CWT :

Skriver händer med tempdb på varje FETCH NEXT. Dessutom finns det mer. Kommer du ihåg att MARKEREN är OPTIMISTISK i figur 3? Egenskaperna för Clustered Index Scan längst till höger i planen avslöjar den extra okända kolumnen som heter Chk1002 :

Kan detta vara kolumnen Checksum? Plan XML bekräftar att detta verkligen är fallet:

Jämför nu den faktiska exekveringsplanen för FETCH NEXT när CURSOR är LOCAL STATIC FORWARD_ONLY READ_ONLY:

Den använder tempdb också, men det är mycket enklare. Samtidigt visar figur 8 exekveringsplanen när LOCAL FAST_FORWARD används:

Hämtmat

En av de lämpliga användningarna av SQL CURSOR är att generera skript eller köra några administrativa kommandon mot en grupp av databasobjekt. Även om det finns mindre användningsområden, är ditt första alternativ att använda LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR eller LOCAL FAST_FORWARD. Den med en bättre plan och logisk läsning kommer att vinna.

Byt sedan ut någon av dessa med den lämpliga efter behov. Men vet du vad? Enligt min personliga erfarenhet använde jag bara en lokal skrivskyddad CURSOR med endast framåtriktad traversering. Jag behövde aldrig göra CURSOR global och uppdateringsbar.

Bortsett från att använda dessa argument spelar timingen för utförandet av betydelse.

3. Använda SQL CURSOR på dagliga transaktioner

Jag är inte administratör. Men jag har en uppfattning om hur en upptagen server ser ut från DBA:s verktyg (eller från hur många decibel användare skriker). Under dessa omständigheter, vill du lägga till ytterligare börda?

Om du försöker skapa din kod med en CURSOR för dagliga transaktioner, tänk om. CURSORs är bra för engångskörningar på en mindre upptagen server med små datamängder. Men på en typisk hektisk dag kan en CURSOR:

  • Lås rader, särskilt om samtidighetsargumentet SCROLL_LOCKS är explicit specificerat.
  • Orsaka hög CPU-användning.
  • Använd tempdb omfattande.

Föreställ dig att du har flera av dessa igång samtidigt en vanlig dag.

Vi håller på att ta slut men det finns ytterligare ett misstag vi måste prata om.

4. Att inte bedöma effekten SQL CURSOR ger

Du vet att CURSOR-alternativ är bra. Tycker du att det räcker med att specificera dem? Du har redan sett resultaten ovan. Utan verktygen skulle vi inte komma med rätt slutsats.

Dessutom finns det kod inuti CURSOR . Beroende på vad den gör, tillför den mer till de resurser som förbrukas. Dessa kan ha varit tillgängliga för andra processer. Hela din infrastruktur, din hårdvara och SQL Server-konfiguration kommer att lägga till mer till historien.

Vad sägs om datavolymen ? Jag använde bara SQL CURSOR på några hundra poster. Det kan vara annorlunda för dig. Det första exemplet tog bara 500 poster eftersom det var siffran som jag skulle gå med på att vänta på. 10 000 eller till och med 1000 skar inte det. De presterade dåligt.

Så småningom, oavsett hur färre eller mer, det kan göra skillnad att till exempel kontrollera de logiska läsningarna.

Vad händer om du inte kontrollerar exekveringsplanen, de logiska läsningarna eller den förflutna tiden? Vilka hemska saker kan hända förutom att SQL Server fryser? Vi kan bara föreställa oss alla möjliga domedagsscenarier. Du förstår poängen.

Slutsats

SQL CURSOR fungerar genom att bearbeta data rad för rad. Det har sin plats, men det kan vara dåligt om du inte är försiktig. Det är som ett verktyg som sällan kommer ut ur verktygslådan.

Så, först, försök att lösa problemet genom att använda uppsättningsbaserade kommandon. Den svarar på de flesta av våra SQL-behov. Och om du någonsin använder SQL CURSOR, använd den med rätt alternativ. Uppskatta effekten med exekveringsplanen, STATISTICS IO och xEvent Profiler. Välj sedan rätt tid att köra.

Allt detta kommer att göra din användning av SQL CURSOR lite bättre.


  1. Beräkna percentil från senaste i MySQL

  2. Hur man lägger till en ny kolumn i en befintlig tabell i SQL Server (T-SQL)

  3. CASE kontra DECODE

  4. Hur RTRIM_ORACLE() fungerar i MariaDB