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

Nummerseriegenerator-udfordringsløsninger – del 5

Dette er den femte og sidste del i serien, der dækker løsninger på nummerseriegeneratorudfordringen. I del 1, del 2, del 3 og del 4 dækkede jeg rene T-SQL-løsninger. Tidligt, da jeg lagde puslespillet, kommenterede flere personer, at den bedst ydende løsning sandsynligvis ville være en CLR-baseret. I denne artikel sætter vi denne intuitive antagelse på prøve. Specifikt vil jeg dække CLR-baserede løsninger indsendt af Kamil Kosno og Adam Machanic.

Mange tak til Alan Burstein, Joe Obbish, Adam Machanic, Christopher Ford, Jeff Moden, Charlie, NoamGr, Kamil Kosno, Dave Mason, John Nelson #2, Ed Wagner, Michael Burbea og Paul White for at dele dine ideer og kommentarer.

Jeg vil lave min test i en database kaldet testdb. Brug følgende kode til at oprette databasen, hvis den ikke findes, og til at aktivere I/O- og tidsstatistik:

-- DB og statsSET NOCOUNT ON;SET STATISTICS IO, TID PÅ;GO HVIS DB_ID('testdb') ER NULL OPRET DATABASE testdb;GO BRUG testdb;GO

For nemheds skyld deaktiverer jeg CLR streng sikkerhed og gør databasen troværdig ved hjælp af følgende kode:

-- Aktiver CLR, deaktiver CLR streng sikkerhed og gør db trustworthyEXEC sys.sp_configure 'vis avancerede indstillinger', 1;RECONFIGURE; EXEC sys.sp_configure 'clr aktiveret', 1;EXEC sys.sp_configure 'clr strict security', 0;RECONFIGURE; EXEC sys.sp_configure 'vis avancerede indstillinger', 0;RECONFIGURE; ALTER DATABASE testdb INDSTILLE TROLIGT TIL; GÅ

Tidligere løsninger

Før jeg dækker de CLR-baserede løsninger, lad os hurtigt gennemgå ydeevnen af ​​to af de bedst ydende T-SQL-løsninger.

Den bedst ydende T-SQL-løsning, der ikke brugte nogen vedvarende basistabeller (bortset fra den tomme kolonnelagertabel for at få batchbehandling), og derfor ikke involverede nogen I/O-operationer, var den, der blev implementeret i funktionen dbo.GetNumsAlanCharlieItzikBatch. Jeg dækkede denne løsning i del 1.

Her er koden til at oprette den tomme kolonnelagertabel, som funktionens forespørgsel bruger:

DROP TABEL HVIS FINDER dbo.BatchMe;GO OPRET TABEL dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);GO

Og her er koden med funktionens definition:

CREATE OR ALTER FUNCTION dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT =1, @high AS BIGINT) RETURNERER TABLEASRETURN MED L0 AS (VÆLG 1 AS c FRA (VÆRDIER(1),(1),(1),(1) ),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( VÆLG 1 AS c FRA L0 SOM A CROSS JOIN L0 AS B ), L2 AS ( VÆLG 1 AS c FRA L1 SOM A CROSS JOIN L1 AS B ), L3 AS ( VÆLG 1 AS c FRA L2 SOM ET KRYDS JOIN L2 AS B ), Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM L3 ) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums VENSTRE YDRE JOIN dbo.BatchMe ON 1 =0 BESTIL EFTER rownum;GO

Lad os først teste funktionen, der anmoder om en serie på 100 millioner tal, med MAX-aggregatet anvendt på kolonne n:

VÆLG MAX(n) AS mx FROM dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) OPTION(MAXDOP 1);

Husk, denne testteknik undgår at sende 100 millioner rækker til den, der ringer, og undgår også rækketilstandsindsatsen, der er involveret i variabeltildeling, når du bruger variabeltildelingsteknikken.

Her er tidsstatistikken, som jeg fik for denne test på min maskine:

CPU-tid =6719 ms, forløbet tid =6742 ms .

Udførelsen af ​​denne funktion producerer selvfølgelig ingen logiske læsninger.

Lad os derefter teste det med rækkefølge ved hjælp af variabeltildelingsteknikken:

ERKLÆR @n SOM STORT; VÆLG @n =n FRA dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) BESTIL EFTER n OPTION(MAXDOP 1);

Jeg fik følgende tidsstatistik for denne udførelse:

CPU-tid =9468 ms, forløbet tid =9531 ms .

Husk på, at denne funktion ikke resulterer i sortering, når der anmodes om data sorteret efter n; du får stort set samme plan, uanset om du anmoder om de bestilte data eller ej. Vi kan tilskrive det meste af den ekstra tid i denne test sammenlignet med den forrige til de 100M rækketilstandsbaserede variabeltildelinger.

Den bedst ydende T-SQL-løsning, der brugte en vedvarende basistabel og derfor resulterede i nogle I/O-operationer, selvom meget få, var Paul Whites løsning implementeret i funktionen dbo.GetNums_SQLkiwi. Jeg dækkede denne løsning i del 4.

Her er Pauls kode til at oprette både kolonnelagertabellen, der bruges af funktionen, og selve funktionen:

-- Helper columnstore tableDROP TABLE IF EXISTS dbo.CS; -- 64K rækker (nok til 4B rækker ved krydssammenføjning) -- kolonne 1 er altid nul -- kolonne 2 er (1...65536) SELECT -- skriv som heltal IKKE NULL -- (alt er normaliseret til 64 bit i kolonnelager/batch-tilstand alligevel) n1 =ISNULL(CONVERT(heltal, 0), 0), n2 =ISNULL(CONVERT(heltal, N.rn), 0)INTO dbo.CSFROM ( SELECT rn =ROW_NUMBER() OVER (ORDER BY @@SPID) FRA master.dbo.spt_values ​​AS SV1 CROSS JOIN master.dbo.spt_values ​​AS SV2 ORDER BY rn ASC OFFSET 0 ROWS FETCH NEXT 65536 ROWS ONLY) SOM N; -- Enkelt komprimeret rækkegruppe på 65.536 rækkerCREATE CLOSTERED COLUMNSTORE INDEX CCI ON dbo.CS WITH (MAXDOP =1);GO -- Funktionen CREATE OR ALTER FUNCTION dbo.GetNums_SQLkiwi( @low bigint =1) RETURNgh bigint ASRETURNS table ASRETURNS .rn, n =@low - 1 + N.rn, op =@high + 1 - N.rn FROM ( VÆLG -- Brug @@TRANCOUNT i stedet for @@SPID, hvis du kan lide alle dine forespørgsler seriel rn =ROW_NUMBER() OVER (ORDER BY @@SPID ASC) FRA dbo.CS AS N1 JOIN dbo.CS AS N2 -- Batch mode hash cross join -- Heltal ikke null datatype undgå hash probe residual -- Dette er altid 0 =0 PÅ N2. n1 =N1.n1 WHERE -- Prøv at undgå SQRT på negative tal og muliggør forenkling -- til en enkelt konstant scanning hvis @low> @high (med bogstaver) -- Ingen opstartsfiltre i batch-tilstand @high>=@low -- Groft filter:-- Begræns hver side af krydsforbindelsen til SQRT(mål antal rækker) -- IIF undgår SQRT på negative tal med parametre OG N1.n2 <=CONVERT(heltal, CEILING(SQRT(CONVERT(float, IIF(@high>=@low, @high) - @lav + 1, 0))))) OG N2.n2 <=CONVERT(heltal, CEILING(SQRT(CONVERT(float, IIF(@high>=@low, @high - @low + 1, 0)) ))) ) SOM N HVOR -- Præcis filter:-- Batch-tilstand filtrerer den begrænsede krydsforbindelse til det nøjagtige antal rækker, der er nødvendige - Undgår, at optimeringsværktøjet introducerer en række-tilstand Top med følgende række-tilstand beregne skalar @low - 2 + N.rn <@high;GO

Lad os først teste det uden bestilling ved hjælp af aggregatteknikken, hvilket resulterer i en all-batch-mode-plan:

VÆLG MAX(n) AS mx FRA dbo.GetNums_SQLkiwi(1, 100000000) OPTION(MAXDOP 1);

Jeg fik følgende tids- og I/O-statistik for denne udførelse:

CPU-tid =2922 ms, forløbet tid =2943 ms .

Tabel 'CS'. Scanningsantal 2, logisk læser 0, fysisk læser 0, sideserver læser 0, read-ahead læser 0, sideserver read-ahead læser 0, lob logisk læser 44 , lob fysisk læser 0, lob sideserver læser 0, lob read-ahead læser 0, lob sideserver read-ahead læser 0.

Tabel 'CS'. Segment viser 2, segment sprunget over 0.

Lad os teste funktionen med rækkefølge ved hjælp af variabeltildelingsteknikken:

ERKLÆR @n SOM STORT; VÆLG @n =n FRA dbo.GetNums_SQLkiwi(1, 100000000) BESTIL EFTER n OPTION(MAXDOP 1);

Ligesom med den tidligere løsning undgår også denne løsning eksplicit sortering i planen, og får derfor samme plan uanset om du beder om de bestilte data eller ej. Men igen, denne test medfører en ekstra straf, primært på grund af den variable tildelingsteknik, der bruges her, hvilket resulterer i, at den variable tildelingsdel i planen bliver behandlet i rækketilstand.

Her er den tid og I/O-statistik, som jeg fik for denne udførelse:

CPU-tid =6985 ms, forløbet tid =7033 ms .

Tabel 'CS'. Scanningsantal 2, logisk læser 0, fysisk læser 0, sideserver læser 0, read-ahead læser 0, sideserver read-ahead læser 0, lob logisk læser 44 , lob fysisk læser 0, lob sideserver læser 0, lob read-ahead læser 0, lob sideserver read-ahead læser 0.

Tabel 'CS'. Segment viser 2, segment sprunget over 0.

CLR-løsninger

Både Kamil Kosno og Adam Machanic leverede først en simpel CLR-only-løsning og kom senere med en mere sofistikeret CLR+T-SQL-kombination. Jeg starter med Kamils ​​løsninger og dækker derefter Adams løsninger.

Løsninger af Kamil Kosno

Her er CLR-koden, der blev brugt i Kamils ​​første løsning til at definere en funktion kaldet GetNums_KamilKosno1:

using System;using System.Data.SqlTypes;using System.Collections;public partial class GetNumsKamil1{ [Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName ="GetNums_KamilKosno1_Fill") GetNumsKamil1{ [Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName ="GetNums_KamilKosno1_Fill"), GetNumsKamil1. (SqlInt64 lav, SqlInt64 høj) { return (low.IsNull || high.IsNull) ? new GetNumsCS(0, 0) :new GetNumsCS(low.Value, high.Value); } offentlig statisk tomrum GetNums_KamilKosno1_Fill(Objekt o, ud SqlInt64 n) { n =(lang)o; } privat klasse GetNumsCS :IEnumerator { public GetNumsCS(long from, long to) { _lowrange =from; _current =_lavområde - 1; _highrange =til; } public bool MoveNext() { _current +=1; if (_current> _highrange) returner falsk; ellers returneres sandt; } public object Current { get { return _current; } } public void Reset() { _current =_lowrange - 1; } long _lowrange; lang _strøm; lang _highrange; }}

Funktionen accepterer to input kaldet lav og høj og returnerer en tabel med en BIGINT kolonne kaldet n. Funktionen er en streaming-type, der returnerer en række med det næste nummer i serien pr. række anmodning fra den kaldende forespørgsel. Som du kan se, valgte Kamil den mere formaliserede metode til implementering af IEnumerator-grænsefladen, som involverer implementering af metoderne MoveNext (fremfører tælleren for at få den næste række), Current (får rækken i den aktuelle tællerposition) og Nulstil (indstiller tælleren til sin udgangsposition, som er før den første række).

Variablen med det aktuelle tal i serien kaldes _current. Konstruktøren sætter _current til den lave grænse for det anmodede område minus 1, og det samme gælder for Reset-metoden. MoveNext-metoden rykker _current frem med 1. Så, hvis _current er større end den høje grænse for det anmodede område, returnerer metoden false, hvilket betyder, at den ikke bliver kaldt igen. Ellers vender den tilbage, hvilket betyder, at den bliver kaldt igen. Metoden Current returnerer naturligvis _current. Som du kan se, ret grundlæggende logik.

Jeg kaldte Visual Studio-projektet GetNumsKamil1 og brugte stien C:\Temp\ til det. Her er koden, jeg brugte til at implementere funktionen i testdb-databasen:

DROP FUNKTION HVIS FINDER dbo.GetNums_KamilKosno1; SLIP SAMLING, HVIS FINDER GetNumsKamil1;GO OPRET ASSEMBLY GetNumsKamil1 FRA 'C:\Temp\GetNumsKamil1\GetNumsKamil1\bin\Debug\GetNumsKamil1.dll';GO OPRET FUNKTION RETURN dbo.GetASNums1,BINT_GetNums1,BINTgh TABLE(n BIGINT) ORDER(n) SOM EKSTERNT NAVN GetNumsKamil1.GetNumsKamil1.GetNums_KamilKosno1;GO

Bemærk brugen af ​​ORDER-udtrykket i CREATE FUNCTION-sætningen. Funktionen udsender rækkerne i n-rækkefølge, så når rækkerne skal indsættes i planen i n-rækkefølge, ved SQL Server baseret på denne klausul, at den kan undgå en sortering i planen.

Lad os teste funktionen, først med aggregatteknikken, når bestilling ikke er nødvendig:

SELECT MAX(n) AS mx FROM dbo.GetNums_KamilKosno1(1, 100000000);

Jeg fik planen vist i figur 1.

Figur 1:Plan for dbo.GetNums_KamilKosno1-funktionen

Der er ikke meget at sige om denne plan, udover det faktum, at alle operatører bruger rækkeudførelsestilstand.

Jeg fik følgende tidsstatistik for denne udførelse:

CPU-tid =37375 ms, forløbet tid =37488 ms .

Og selvfølgelig var der ingen logiske læsninger involveret.

Lad os teste funktionen med rækkefølge ved hjælp af variabeltildelingsteknikken:

ERKLÆR @n SOM STORT; VÆLG @n =n FRA dbo.GetNums_KamilKosno1(1, 100000000) BESTIL EFTER n;

Jeg fik planen vist i figur 2 for denne udførelse.

Figur 2:Plan for dbo.GetNums_KamilKosno1-funktionen med ORDER BY

Bemærk, at der ikke er nogen sortering i planen, da funktionen blev oprettet med ORDER(n)-sætningen. Der er dog en indsats for at sikre, at rækkerne faktisk udsendes fra funktionen i den lovede rækkefølge. Dette gøres ved at bruge segment- og sekvensprojektoperatorerne, som bruges til at beregne rækkenumre, og Assert-operatoren, som afbryder forespørgselsudførelsen, hvis testen mislykkes. Dette arbejde har lineær skalering - i modsætning til den n log n skalering, du ville have fået, hvis en slags var påkrævet - men det er stadig ikke billigt. Jeg fik følgende tidsstatistik for denne test:

CPU-tid =51531 ms, forløbet tid =51905 ms .

Resultaterne kunne være overraskende for nogle - især dem, der intuitivt antog, at de CLR-baserede løsninger ville yde bedre end T-SQL-løsningerne. Som du kan se, er eksekveringstiderne en størrelsesorden længere end med vores bedst ydende T-SQL-løsning.

Kamils ​​anden løsning er en CLR-T-SQL hybrid. Ud over de lave og høje input tilføjer CLR-funktionen (GetNums_KamilKosno2) et trin-input og returnerer værdier mellem lav og høj, der er trinvis fra hinanden. Her er CLR-koden Kamil brugte i sin anden løsning:

using System;using System.Data.SqlTypes;using System.Collections; public partial class GetNumsKamil2{ [Microsoft.SqlServer.Server.SqlFunction(DataAccess =Microsoft.SqlServer.Server.DataAccessKind.None, IsDeterministic =true, IsPrecise =true, FillRowMethodName ="GetNums_Fill") ="BIGNums_Fill"), Tnic IEnumerator GetNums_KamilKosno2(SqlInt64 lav, SqlInt64 høj, SqlInt64 trin) { return (low.IsNull || high.IsNull) ? new GetNumsCS(0, 0, step.Value):new GetNumsCS(low.Value, high.Value, step.Value); } offentlig statisk tomrum GetNums_Fill(Objekt o, ud SqlInt64 n) { n =(lang)o; } privat klasse GetNumsCS :IEnumerator { public GetNumsCS(long from, long to, long step) { _lowrange =from; _trin =trin; _current =_lavområde - _trin; _highrange =til; } public bool MoveNext() { _current =_current + _step; if (_current> _highrange) returner falsk; ellers returneres sandt; } public object Current { get { return _current; } } public void Reset() { _current =_lowrange - _step; } long _lowrange; lang _strøm; lang _highrange; langt _trin; }}

Jeg navngav VS-projektet GetNumsKamil2, placerede det også i stien C:\Temp\ og brugte følgende kode til at implementere det i testdb-databasen:

-- Opret assembly og funktionDROP FUNKTION HVIS FINDER dbo.GetNums_KamilKosno2;DROP ASSEMBLY IF EXISTS GetNumsKamil2;GO OPRET ASSEMBLY GetNumsKamil2 FRA 'C:\Temp\GetNumsKamilKosno2\GetNumsKamilKosno2; .GetNums_KamilKosno2 (@low AS BIGINT =1, @high AS BIGINT, @step AS BIGINT) RETURTABEL(n BIGINT) ORDRE(n) SOM EKSTERNT NAVN GetNumsKamil2.GetNumsKamil2.GetNums_KamilKosno2; 

Som et eksempel på brug af funktionen er her en anmodning om at generere værdier mellem 5 og 59 med et trin på 10:

SELECT n FROM dbo.GetNums_KamilKosno2(5, 59, 10);

Denne kode genererer følgende output:

n---51525354555

Hvad angår T-SQL-delen, brugte Kamil en funktion kaldet dbo.GetNums_Hybrid_Kamil2 med følgende kode:

OPRET ELLER ÆNDRING FUNKTION dbo.GetNums_Hybrid_Kamil2(@low AS BIGINT, @high AS BIGINT) RETURER TABLEASRETURN SELECT TOP (@high - @low + 1) V.n FRA dbo.GetNums_KamilKosno2(@GN10) AS KRYDSANVEND (VÆRDIER(0+GN.n),(1+GN.n),(2+GN.n),(3+GN.n),(4+GN.n), (5+GN.n) ),(6+GN.n),(7+GN.n),(8+GN.n),(9+GN.n)) AS V(n);GO

Som du kan se, aktiverer T-SQL-funktionen CLR-funktionen med de samme @lav og @høj input, som den får, og bruger i dette eksempel en trinstørrelse på 10. Forespørgslen bruger CROSS APPLY mellem CLR-funktionens resultat og en tabel -værdi konstruktør, der genererer de endelige tal ved at tilføje værdier i området 0 til 9 til begyndelsen af ​​trinnet. TOP-filteret bruges til at sikre, at du ikke får mere end det antal numre, du har anmodet om.

Vigtigt: Jeg skal understrege, at Kamil her gør en antagelse om, at TOP-filteret anvendes baseret på resultatnummerrækkefølgen, hvilket ikke rigtig er garanteret, da forespørgslen ikke har en ORDER BY-klausul. Hvis du enten tilføjer en ORDER BY-klausul for at understøtte TOP, eller erstatter TOP med et WHERE-filter for at garantere et deterministisk filter, kan dette fuldstændig ændre løsningens ydeevneprofil.

Lad os i hvert fald først teste funktionen uden rækkefølge ved hjælp af aggregatteknikken:

VÆLG MAX(n) AS mx FROM dbo.GetNums_Hybrid_Kamil2(1, 100000000);

Jeg fik planen vist i figur 3 for denne udførelse.

Figur 3:Plan for dbo.GetNums_Hybrid_Kamil2-funktionen

Igen bruger alle operatører i planen rækkeudførelsestilstand.

Jeg fik følgende tidsstatistik for denne udførelse:

CPU-tid =13985 ms, forløbet tid =14069 ms .

Og naturligvis ingen logiske læsninger.

Lad os teste funktionen med rækkefølge:

ERKLÆR @n SOM STORT; VÆLG @n =n FRA dbo.GetNums_Hybrid_Kamil2(1, 100000000) BESTIL EFTER n;

Jeg fik planen vist i figur 4.

Figur 4:Plan for dbo.GetNums_Hybrid_Kamil2-funktionen med ORDER BY

Da resultattallene er resultatet af manipulation af den lave grænse for trinnet returneret af CLR-funktionen og deltaet tilføjet i tabelværdi-konstruktøren, stoler optimeringsværktøjet ikke på, at resultattallene genereres i den anmodede rækkefølge, og tilføjer eksplicit sortering til planen.

Jeg fik følgende tidsstatistik for denne udførelse:

CPU-tid =68703 ms, forløbet tid =84538 ms .

Så det ser ud til, at når der ikke er behov for en ordre, klarer Kamils ​​anden løsning sig bedre end hans første. Men når der er behov for orden, er det omvendt. Uanset hvad er T-SQL-løsningerne hurtigere. Personligt ville jeg stole på rigtigheden af ​​den første løsning, men ikke den anden.

Løsninger af Adam Machanic

Adams første løsning er også en grundlæggende CLR-funktion, der bliver ved med at øge en tæller. Kun i stedet for at bruge den mere involverede formaliserede tilgang, som Kamil gjorde, brugte Adam en enklere tilgang, der påkalder sig udbyttekommandoen pr. række, der skal returneres.

Her er Adams CLR-kode til hans første løsning, der definerer en streamingfunktion kaldet GetNums_AdamMachanic1:

brug af System.Data.SqlTypes;brug af System.Collections; public partial class GetNumsAdam1{ [Microsoft.SqlServer.Server.SqlFunction( FillRowMethodName ="GetNums_AdamMachanic1_fill", TableDefinition ="n BIGINT")] public static IEnumerable GetNums_AdamMachanic1(SqlInt. var max_int =max.Value; for (; min_int <=max_int; min_int++) { yield return (min_int); } } offentlig statisk tomrum GetNums_AdamMachanic1_fill(objekt o, ud lang i) { i =(lang)o; }};

Løsningen er så elegant i sin enkelthed. Som du kan se, accepterer funktionen to input kaldet min og max, der repræsenterer de lave og høje grænsepunkter for det anmodede område, og returnerer en tabel med en BIGINT kolonne kaldet n. Funktionen initialiserer variabler kaldet min_int og max_int med den respektive funktions inputparameterværdier. Funktionen kører derefter en løkke så lang som min_int <=max_int, der i hver iteration giver en række med den aktuelle værdi af min_int og øger min_int med 1. Det er det.

Jeg navngav projektet GetNumsAdam1 i VS, placerede det i C:\Temp\ og brugte følgende kode til at implementere det:

-- Opret assembly og funktionDROP FUNCTION HVIS FINDER dbo.GetNums_AdamMachanic1;DROP ASSEMBLY IF EXISTS GetNumsAdam1;GO OPRET ASSEMBLY GetNumsAdam1 FRA 'C:\Temp\GetNumsAdam1\GetNumGOgNumGOgNumGOgNumsAdam1C:\Temp\GetNumsAdam1\GetNumGOgNumGO .GetNums_AdamMachanic1(@low AS BIGINT =1, @high AS BIGINT) RETURTABEL(n BIGINT) ORDRE(n) SOM EKSTERNT NAVN GetNumsAdam1.GetNumsAdam1.GetNums_AdamMachanic1;GO

Jeg brugte følgende kode til at teste den med aggregatteknikken i tilfælde, hvor rækkefølgen ikke betyder noget:

VÆLG MAX(n) AS mx FRA dbo.GetNums_AdamMachanic1(1, 100000000);

Jeg fik planen vist i figur 5 for denne udførelse.

Figur 5:Plan for dbo.GetNums_AdamMachanic1-funktionen

Planen ligner meget den plan, du så tidligere for Kamils ​​første løsning, og det samme gælder dens ydeevne. Jeg fik følgende tidsstatistik for denne udførelse:

CPU-tid =36687 ms, forløbet tid =36952 ms .

Og der var selvfølgelig ingen logiske læsninger nødvendige.

Lad os teste funktionen med rækkefølge ved hjælp af variabeltildelingsteknikken:

ERKLÆR @n SOM STORT; VÆLG @n =n FRA dbo.GetNums_AdamMachanic1(1, 100000000) BESTIL EFTER n;

Jeg fik planen vist i figur 6 for denne udførelse.

Figur 6:Plan for dbo.GetNums_AdamMachanic1-funktionen med ORDER BY

Igen ligner planen den, du så tidligere for Kamils ​​første løsning. Der var ikke behov for eksplicit sortering, da funktionen blev oprettet med ORDER-klausulen, men planen inkluderer noget arbejde for at verificere, at rækkerne faktisk returneres bestilt som lovet.

Jeg fik følgende tidsstatistik for denne udførelse:

CPU-tid =55047 ms, forløbet tid =55498 ms .

I sin anden løsning kombinerede Adam også en CLR-del og en T-SQL-del. Her er Adams beskrivelse af den logik, han brugte i sin løsning:

"Jeg prøvede at tænke på, hvordan man kan løse problemet med SQLCLR-chatness, og også den centrale udfordring ved denne talgenerator i T-SQL, som er det faktum, at vi ikke bare kan magiske rækker til at eksistere.

CLR er et godt svar til anden del, men er naturligvis hæmmet af den første udgave. Så som et kompromis oprettede jeg en T-SQL TVF [kaldet GetNums_AdamMachanic2_8192] hårdkodet med værdierne 1 til 8192. (Temmelig vilkårligt valg, men for stort, og QO begynder at kvæle lidt i det.) Dernæst ændrede jeg min CLR-funktion [ opkaldt GetNums_AdamMachanic2_8192_base] til at udskrive to kolonner, "max_base" og "base_add", og havde det output rækker som:

    max_base, base_add
    ——————
    8191, 1
    8192, 8192
    8192, 16384

    8192, 99991552
    257, 99999744

Nu er det en simpel løkke. CLR-outputtet sendes til T-SQL TVF, som er sat op til kun at returnere op til "max_base" rækker af dets hårdkodede sæt. Og for hver række tilføjer den "base_add" til værdien og genererer derved de nødvendige tal. Nøglen her, tror jeg, er, at vi kan generere N rækker med kun en enkelt logisk krydsforbindelse, og CLR-funktionen skal kun returnere 1/8192 så mange rækker, så den er hurtig nok til at fungere som basisgenerator."

Logikken virker ret ligetil.

Her er koden, der bruges til at definere CLR-funktionen kaldet GetNums_AdamMachanic2_8192_base:

brug af System.Data.SqlTypes;brug af System.Collections; public partial class GetNumsAdam2{ private struct row { public long max_base; offentlig lang base_add; } [Microsoft.SqlServer.Server.SqlFunction( FillRowMethodName ="GetNums_AdamMachanic2_8192_base_fill", TableDefinition ="max_base int, base_add int")] public static IEnumerable GetNums_AdamMachanic2_q4Smax.I minal var max_int =max.Value; var min_group =min_int / 8192; var max_group =max_int / 8192; for (; min_group <=max_group; min_group++) { if (min_int> max_int) yield break; var max_base =8192 - (min_int % 8192); hvis (min_gruppe ==max_group &&max_int <(((max_int / 8192) + 1) * 8192) - 1) max_base =max_int - min_int + 1; yield return (ny række() { max_base =max_base, base_add =min_int } ); min_int =(min_gruppe + 1) * 8192; } } offentlig statisk tomrum GetNums_AdamMachanic2_8192_base_fill(objekt o, ud lang max_base, ud lang base_add) { var r =(række)o; max_base =r.max_base; base_add =r.base_add; }};

Jeg navngav VS-projektet GetNumsAdam2 og placerede i stien C:\Temp\ ligesom med de andre projekter. Her er koden, jeg brugte til at implementere funktionen i testdb-databasen:

-- Opret assembly og funktionDROP FUNKTION HVIS FINDER dbo.GetNums_AdamMachanic2_8192_base;DROP ASSEMBLY IF EXISTS GetNumsAdam2;GO OPRET ASSEMBLY GetNumsAdam2 FRA 'C:\Temp\GetNumAdamsAdam C:\Temp\GetNumsAdams\DefNumsAdamC:\Temp\Temp\GetNumsAdam C:\Temp\GetNumsAdamc\DefNumsAdamc .GetNums_AdamMachanic2_8192_base(@max_base AS BIGINT, @add_base AS BIGINT) RETURTABEL(max_base BIGINT, base_add BIGINT) ORDER(base_add) AS EXTERNAL NAME GetNumsAdam2.GetNum_2.GetNums2.GetNumgO 

Her er et eksempel på brug af GetNums_AdamMachanic2_8192_base med intervallet 1 til 100M:

VÆLG * FRA dbo.GetNums_AdamMachanic2_8192_base(1, 100000000);

Denne kode genererer følgende output, vist her i forkortet form:

max_base base_add------------------------- --------------------8191 18192 81928192 163848192 245768192 32768...8192 999669768192 999751688192 999833608192 99991552257 99999744(12208 rækker påvirket)

Her er koden med definitionen af ​​T-SQL-funktionen GetNums_AdamMachanic2_8192 (forkortet):

OPRET ELLER ÆNDRING FUNKTION dbo.GetNums_AdamMachanic2_8192(@max_base AS BIGINT, @add_base AS BIGINT) RETURER TABLEASRETURN SELECT TOP (@max_base) V.i + @add_base AS val FROM ( VALUES ), (2), (3), (4), ... (8187), (8188), (8189), (8190), (8191) ) AS V(i);GO

Vigtigt: Også her skal jeg understrege, at i lighed med det, jeg sagde om Kamils ​​anden løsning, antager Adam her, at TOP-filteret vil udtrække de øverste rækker baseret på rækkens udseende i tabelværdi-konstruktøren, hvilket ikke rigtig er garanteret. Hvis du tilføjer en ORDER BY-klausul for at understøtte TOP eller ændrer filteret til et WHERE-filter, får du et deterministisk filter, men dette kan fuldstændig ændre løsningens ydeevneprofil.

Her er endelig den yderste T-SQL-funktion, dbo.GetNums_AdamMachanic2, som slutbrugeren ringer til for at få nummerserien:

OPRET ELLER ÆNDRING FUNKTION dbo.GetNums_AdamMachanic2(@low AS BIGINT =1, @high AS BIGINT) RETUR TABLEASRETURN SELECT Y.val AS n FROM ( SELECT max_base, base_add FROM dbo.GetNums_AdamMachanic @baseMachanic) X CROSS APPLY dbo.GetNums_AdamMachanic2_8192(X.max_base, X.base_add) AS YGO

Denne funktion bruger CROSS APPLY-operatoren til at anvende den indre T-SQL-funktion dbo.GetNums_AdamMachanic2_8192 pr. række, der returneres af den indre CLR-funktion dbo.GetNums_AdamMachanic2_8192_base.

Lad os først teste denne løsning ved hjælp af aggregatteknikken, når rækkefølgen ikke betyder noget:

VÆLG MAX(n) AS mx FRA dbo.GetNums_AdamMachanic2(1, 100000000);

Jeg fik planen vist i figur 7 for denne udførelse.

Figur 7:Plan for dbo.GetNums_AdamMachanic2-funktionen

Jeg fik følgende tidsstatistik for denne test:

SQL Server parse og kompileringstid :CPU-tid =313 ms, forløbet tid =339 ms .
SQL Server udførelsestid :CPU-tid =8859 ms, forløbet tid =8849 ms .

Ingen logiske aflæsninger var nødvendige.

Udførelsestiden er ikke dårlig, men bemærk den høje kompileringstid på grund af den store tabelværdi-konstruktør, der bruges. Du ville betale så høj kompileringstid uanset rækkevidden, du anmoder om, så dette er især vanskeligt, når du bruger funktionen med meget små områder. Og denne løsning er stadig langsommere end T-SQL.

Lad os teste funktionen med rækkefølge:

ERKLÆR @n SOM STORT; VÆLG @n =n FRA dbo.GetNums_AdamMachanic2(1, 100000000) BESTIL EFTER n;

Jeg fik planen vist i figur 8 for denne udførelse.

Figur 8:Plan for dbo.GetNums_AdamMachanic2-funktionen med ORDER BY

Ligesom med Kamils ​​anden løsning er der behov for en eksplicit sortering i planen, hvilket medfører en betydelig præstationsstraf. Her er de tidsstatistikker, jeg fik for denne test:

Udførelsestid:CPU-tid =54891 ms, forløbet tid =60981 ms .

Derudover er der stadig den høje kompileringstidsstraf på omkring en tredjedel af et sekund.

Konklusion

Det var interessant at teste CLR-baserede løsninger til nummerserieudfordringen, fordi mange mennesker oprindeligt antog, at den bedst ydende løsning sandsynligvis vil være en CLR-baseret. Kamil og Adam brugte lignende tilgange, hvor det første forsøg brugte en simpel løkke, der øger en tæller og giver en række med den næste værdi per iteration, og det mere sofistikerede andet forsøg, der kombinerer CLR- og T-SQL-dele. Personligt føler jeg mig ikke tryg ved, at de i både Kamils ​​og Adams anden løsninger stolede på et ikke-deterministisk TOP-filter, og da jeg konverterede det til et deterministisk i min egen test, havde det en negativ indvirkning på løsningens ydeevne. . Either way, our two T-SQL solutions perform better than the CLR ones, and do not result in explicit sorting in the plan when you need the rows ordered. So I don’t really see the value in pursuing the CLR route any further. Figure 9 has a performance summary of the solutions that I presented in this article.

Figure 9:Time performance comparison

To me, GetNums_AlanCharlieItzikBatch should be the solution of choice when you require absolutely no I/O footprint, and GetNums_SQKWiki should be preferred when you don’t mind a small I/O footprint. Of course, we can always hope that one day Microsoft will add this critically useful tool as a built-in one, and hopefully if/when they do, it will be a performant solution that supports batch processing and parallelism. So don’t forget to vote for this feature improvement request, and maybe even add your comments for why it’s important for you.

I really enjoyed working on this series. I learned a lot during the process, and hope that you did too.


  1. Hvordan trunkerer man en tabel med begrænsning af fremmednøgler?

  2. Multi-Statement TVF'er i Dynamics CRM

  3. Introduktion til PostgreSQL

  4. Slå advarsler og fejl fra på PHP og MySQL