sql >> Database teknologi >  >> RDS >> Database

Begår du disse fejl, når du bruger SQL CURSOR?

For nogle mennesker er det det forkerte spørgsmål. SQL CURSOR IS fejlen. Djævelen er i detaljerne! Du kan læse alle mulige former for blasfemi i hele SQL blogosfæren i navnet SQL CURSOR.

Hvis du har det på samme måde, hvad fik dig så til denne konklusion?

Hvis det er fra en betroet ven og kollega, kan jeg ikke bebrejde dig. Det sker. Nogle gange meget. Men hvis nogen overbeviste dig med beviser, er det en anden historie.

Vi har ikke mødt hinanden før. Du kender mig ikke som ven. Men jeg håber, at jeg kan forklare det med eksempler og overbevise dig om, at SQL CURSOR har sin plads. Det er ikke meget, men det lille sted i vores kode har regler.

Men først, lad mig fortælle dig min historie.

Jeg begyndte at programmere med databaser ved hjælp af xBase. Det var tilbage på college indtil mine første to år med professionel programmering. Jeg fortæller dig dette, fordi vi dengang plejede at behandle data sekventielt, ikke i sæt batches som SQL. Da jeg lærte SQL, var det som et paradigmeskifte. Databasemotoren bestemmer for mig med sine sæt-baserede kommandoer, som jeg udstedte. Da jeg lærte om SQL CURSOR, føltes det som om, jeg var tilbage med de gamle, men komfortable måder.

Men nogle seniorkolleger advarede mig:"Undgå SQL CURSOR for enhver pris!" Jeg fik et par verbale forklaringer, og det var det.

SQL CURSOR kan være dårlig, hvis du bruger den til det forkerte job. Ligesom at bruge en hammer til at skære træ, det er latterligt. Selvfølgelig kan der ske fejl, og det er der, vores fokus vil være.

1. Brug af SQL CURSOR, når indstillede baserede kommandoer vil fungere

Jeg kan ikke understrege dette nok, men DETTE er kernen i problemet. Da jeg først lærte, hvad SQL CURSOR var, tændte en pære. "Sløjfer! Jeg ved det!" Dog ikke før det gav mig hovedpine og mine ældre skældte mig ud.

Du kan se, tilgangen til SQL er sæt-baseret. Du udsteder en INSERT-kommando fra tabelværdier, og den vil gøre jobbet uden loops på din kode. Som jeg sagde tidligere, er det databasemotorens opgave. Så hvis du tvinger en loop til at tilføje poster til en tabel, omgår du denne autoritet. Det bliver grimt.

Før vi prøver et latterligt eksempel, lad os forberede dataene:


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

Den første erklæring vil generere 500 registreringer af data. Den anden får en delmængde af den. Så er vi klar. Vi vil indsætte de manglende data fra TestTable ind i TestTable2 ved hjælp af SQL CURSOR. Se nedenfor:


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 er sådan, man sløjfer ved hjælp af SQL CURSOR til at indsætte en manglende post én efter én. Ganske lang, ikke?

Lad os nu prøve en bedre måde - det sætbaserede alternativ. Her 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 er kort, pænt og hurtigt. Hvor hurtigt? Se figur 1 nedenfor:

Ved at bruge xEvent Profiler i SQL Server Management Studio sammenlignede jeg CPU-tidstal, varighed og logiske læsninger. Som du kan se i figur 1, vinder du præstationstesten ved at bruge den sæt-baserede kommando til INSERT records. Tallene taler for sig selv. Brug af SQL CURSOR bruger flere ressourcer og behandlingstid.

Derfor, før du bruger SQL CURSOR, prøv først at skrive en sæt-baseret kommando. Det vil bedre betale sig i det lange løb.

Men hvad hvis du har brug for SQL CURSOR for at få arbejdet gjort?

2. Bruger ikke de passende SQL CURSOR-indstillinger

En anden fejl, selv jeg lavede tidligere, var ikke at bruge passende muligheder i DECLARE CURSOR. Der er muligheder for omfang, model, samtidighed, og om det kan rulles eller ej. Disse argumenter er valgfrie, og det er nemt at ignorere dem. Men hvis SQL CURSOR er den eneste måde at udføre opgaven på, skal du være eksplicit med din hensigt.

Så spørg dig selv:

  • Når du går gennem løkken, vil du så kun navigere rækkerne fremad eller flytte til den første, sidste, forrige eller næste række? Du skal angive, om CURSOR kun er fremadrettet eller kan rulles. Det er DECLARE CURSOR FORWARD_ONLY eller ERKLÆR CURSOR SCROLL .
  • Vil du opdatere kolonnerne i CURSOR'en? Brug READ_ONLY, hvis det ikke kan opdateres.
  • Har du brug for de seneste værdier, når du krydser løkken? Brug STATIC, hvis værdierne er ligegyldige, om de er nyeste eller ej. Brug DYNAMISK, hvis andre transaktioner opdaterer kolonner eller sletter rækker, du bruger i CURSOR, og du har brug for de seneste værdier. Bemærk :DYNAMISK bliver dyrt.
  • Er CURSOR global for forbindelsen eller lokal for batchen eller en lagret procedure? Angiv om LOCAL eller GLOBAL.

For mere information om disse argumenter, se referencen fra Microsoft Docs.

Eksempel

Lad os prøve et eksempel, der sammenligner tre CURSOR'er for CPU-tid, logiske læsninger og varighed ved hjælp af xEvents Profiler. Den første vil ikke have nogen passende muligheder efter DECLARE CURSOR. Den anden er LOKAL STATISK FORWARD_ONLY READ_ONLY. Den sidste er LOtyuiCAL FAST_FORWARD.

Her er den første:

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

Der er selvfølgelig en bedre mulighed end ovenstående kode. Hvis formålet blot er at generere et script fra eksisterende brugertabeller, vil SELECT gøre det. Indsæt derefter outputtet i et andet forespørgselsvindue.

Men hvis du har brug for at generere et script og køre det på én gang, er det en anden historie. Du skal evaluere output-scriptet, om det kommer til at belaste din server eller ej. Se fejl #4 senere.

For at vise dig sammenligningen af ​​tre CURSOR'er med forskellige muligheder, vil dette fungere.

Lad os nu have en lignende kode, 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 ovenfor, er den eneste forskel fra den forrige kode den LOCAL STATIC FORWARD_ONLY READ_ONLY argumenter.

Den tredje vil have en LOCAL FAST_FORWARD. Nu, ifølge Microsoft, er FAST_FORWARD en FORWARD_ONLY, READ_ONLY CURSOR med optimeringer aktiveret. Vi får se, hvordan det vil klare sig med de to første.

Hvordan sammenligner de sig? Se figur 2:

Den, der tager mindre CPU-tid og -varighed, er LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Bemærk også, at SQL Server har standardindstillinger, hvis du ikke angiver argumenter som STATIC eller READ_ONLY. Der er en frygtelig konsekvens af det, som du vil se i næste afsnit.

Hvad sp_describe_cursor afslørede

sp_describe_cursor er en lagret procedure fra master database, som du kan bruge til at få information fra den åbne CURSOR. Og her er, hvad det afslørede fra den første gruppe af forespørgsler uden CURSOR-indstillinger. Se figur 3 for resultatet af sp_describe_cursor :

Overkill meget? Det kan du tro. CURSOR fra den første gruppe af forespørgsler er:

  • globalt til den eksisterende forbindelse.
  • dynamisk, hvilket betyder, at den sporer ændringer i #commands-tabellen for opdateringer, sletninger og indsættelser.
  • optimistisk, hvilket betyder, at SQL Server tilføjede en ekstra kolonne til en midlertidig tabel kaldet CWT. Dette er en kontrolsum-kolonne til sporing af ændringer i værdierne i #commands-tabellen.
  • rulbar, hvilket betyder, at du kan gå til den forrige, næste, øverste eller nederste række i markøren.

Absurd? Jeg er meget enig. Hvorfor har du brug for en global forbindelse? Hvorfor skal du spore ændringer i den midlertidige tabel #commands? Scrollede vi andre steder end den næste post i CURSOR'en?

Da en SQL Server bestemmer dette for os, bliver CURSOR-løkken en frygtelig fejltagelse.

Nu indser du, hvorfor det er så afgørende at udtrykke SQL CURSOR-indstillinger. Så fra nu af skal du altid angive disse CURSOR-argumenter, hvis du skal bruge en CURSOR.

Udførelsesplanen afslører mere

Den faktiske udførelsesplan har noget mere at sige om, hvad der sker, hver gang en FETCH NEXT FROM command_builder INTO @kommando udføres. I figur 4 er en række indsat i Clustered Index CWT_PrimaryKey i tempdb tabel CWT :

Skrivninger sker med tempdb på hver FETCH NEXT. Desuden er der mere. Husk at MARKøren er OPTIMISTISK i figur 3? Egenskaberne for Clustered Index Scan i den yderste højre del af planen afslører den ekstra ukendte kolonne kaldet Chk1002 :

Kan dette være kolonnen Checksum? Plan XML bekræfter, at dette faktisk er tilfældet:

Sammenlign nu den faktiske udførelsesplan for FETCH NEXT, når CURSOREN er LOKAL STATIC FORWARD_ONLY READ_ONLY:

Den bruger tempdb også, men det er meget enklere. I mellemtiden viser figur 8 udførelsesplanen, når LOCAL FAST_FORWARD bruges:

Takeaways

En af de passende anvendelser af SQL CURSOR er at generere scripts eller køre nogle administrative kommandoer mod en gruppe af databaseobjekter. Selvom der er mindre anvendelser af det, er din første mulighed at bruge LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR eller LOCAL FAST_FORWARD. Den med en bedre plan og logisk læsning vil vinde.

Udskift derefter nogen af ​​disse med den passende, som behovet dikterer. Men ved du hvad? I min personlige erfaring brugte jeg kun en lokal skrivebeskyttet CURSOR med kun fremadgående traversering. Jeg har aldrig behøvet at gøre CURSOR global og opdaterbar.

Bortset fra at bruge disse argumenter, har timingen af ​​udførelsen betydning.

3. Brug af SQL CURSOR på daglige transaktioner

Jeg er ikke administrator. Men jeg har en idé om, hvordan en travl server ser ud fra DBA’s værktøjer (eller fra hvor mange decibel brugere skriger). Vil du under disse omstændigheder tilføje yderligere byrder?

Hvis du forsøger at lave din kode med en CURSOR til daglige transaktioner, så tro om igen. CURSOR'er er fine til engangskørsel på en mindre travl server med små datasæt. På en typisk travl dag kan en CURSOR dog:

  • Lås rækker, især hvis SCROLL_LOCKS samtidighedsargumentet er eksplicit angivet.
  • Forårsager høj CPU-brug.
  • Brug tempdb omfattende.

Forestil dig, at du har flere af disse kørende samtidigt på en typisk dag.

Vi er ved at slutte, men der er endnu en fejl, vi skal tale om.

4. Ikke vurdering af virkningen SQL CURSOR bringer

Du ved, at CURSOR muligheder er gode. Synes du, at det er nok at specificere dem? Du har allerede set resultaterne ovenfor. Uden værktøjerne ville vi ikke komme med den rigtige konklusion.

Desuden er der kode inde i CURSOREN . Afhængigt af hvad det gør, tilføjer det mere til de forbrugte ressourcer. Disse kan have været tilgængelige for andre processer. Hele din infrastruktur, din hardware og SQL Server-konfiguration vil tilføje mere til historien.

Hvad med datamængden ? Jeg brugte kun SQL CURSOR på et par hundrede poster. Det kan være anderledes for dig. Det første eksempel tog kun 500 poster, fordi det var det antal, jeg ville gå med til at vente på. 10.000 eller endda 1000 skar det ikke. De klarede sig dårligt.

Til sidst, uanset hvor mindre eller mere, det kan gøre en forskel at kontrollere f.eks. de logiske aflæsninger.

Hvad hvis du ikke tjekker udførelsesplanen, de logiske læsninger eller den forløbne tid? Hvilke forfærdelige ting kan ske udover at SQL Server fryser? Vi kan kun forestille os alle mulige dommedagsscenarier. Du forstår pointen.

Konklusion

SQL CURSOR fungerer ved at behandle data række for række. Det har sin plads, men det kan være dårligt, hvis du ikke er forsigtig. Det er som et værktøj, der sjældent kommer ud af værktøjskassen.

Så prøv først at løse problemet ved at bruge sæt-baserede kommandoer. Det besvarer de fleste af vores SQL-behov. Og hvis du nogensinde bruger SQL CURSOR, så brug det med de rigtige muligheder. Estimer virkningen med udførelsesplanen, STATISTICS IO og xEvent Profiler. Vælg derefter det rigtige tidspunkt at udføre.

Alt dette vil gøre din brug af SQL CURSOR en smule bedre.


  1. Hvordan STRCMP() virker i MariaDB

  2. Indstil databasesortering i Entity Framework Code-First Initializer

  3. Postgres:vælg summen af ​​værdier, og summer dette igen

  4. Sådan viser du rækker, der ikke er til stede i en anden tabel i MySQL