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

Ydeevne overraskelser og antagelser:STRING_SPLIT()

For over tre år siden nu postede jeg en serie i tre dele om at splitte strenge:

  • Opdel strenge på den rigtige måde – eller den næstbedste måde
  • Opdeling af strenge:En opfølgning
  • Opdeling af strenge:Nu med mindre T-SQL

Så tilbage i januar påtog jeg mig et lidt mere omfattende problem:

  • Sammenligning af strengopdelings-/sammenkædningsmetoder

Hele vejen igennem har min konklusion været:STOP MED AT GØRE DETTE I T-SQL . Brug CLR eller, endnu bedre, overfør strukturerede parametre som DataTables fra din applikation til tabelværdiparametre (TVP'er) i dine procedurer, og undgå al strengkonstruktionen og dekonstruktionen helt – som i virkeligheden er den del af løsningen, der forårsager ydeevneproblemer.

Og så kom SQL Server 2016...

Da RC0 blev frigivet, blev en ny funktion dokumenteret uden megen fanfare:STRING_SPLIT . Et hurtigt eksempel:

SELECT * FROM STRING_SPLIT('a,b,cd', ','); /* resultat:værdi -------- a b cd*/

Det fangede nogle få kollegers øjne, inklusiv Dave Ballantyne, som skrev om hovedtrækkene – men som var venlig nok til at give mig førsteret til afslag på en præstationssammenligning.

Dette er for det meste en akademisk øvelse, for med et stort sæt begrænsninger i den første iteration af funktionen, vil det sandsynligvis ikke være muligt for et stort antal brugssager. Her er listen over de observationer, som Dave og jeg har lavet, hvoraf nogle kan være deal-breakers i visse scenarier:

  • funktionen kræver, at databasen er på kompatibilitetsniveau 130;
  • den accepterer kun enkelttegns afgrænsninger;
  • der er ingen måde at tilføje outputkolonner (som en kolonne, der angiver ordensposition i strengen);
    • relateret, der er ingen måde at kontrollere sortering på – de eneste muligheder er vilkårlige og alfabetiske ORDER BY value;
  • indtil videre estimerer den altid 50 outputrækker;
  • når du bruger det til DML, vil du i mange tilfælde få en bordspole (til Halloween-beskyttelse);
  • NULL input fører til et tomt resultat;
  • der er ingen måde at skubbe prædikater ned, som at eliminere dubletter eller tomme strenge på grund af fortløbende afgrænser;
  • der er ingen måde at udføre operationer mod outputværdierne før efter kendsgerningen (f.eks. udfører mange opdelingsfunktioner LTRIM/RTRIM eller eksplicitte konverteringer til dig – STRING_SPLIT spytter alt det grimme tilbage, såsom førende mellemrum).

Så med disse begrænsninger ude i det fri, kan vi gå videre til nogle præstationstests. Givet Microsofts track record med indbyggede funktioner, der udnytter CLR under dynen (hoste FORMAT() hoste ), var jeg skeptisk over for, om denne nye funktion kunne komme tæt på de hurtigste metoder, jeg til dato havde testet.

Lad os bruge strengsplittere til at adskille kommaseparerede strenge af tal, på denne måde kan vores nye ven JSON også komme med og spille. Og vi vil sige, at ingen liste kan overstige 8.000 tegn, så ingen MAX typer er påkrævet, og da de er tal, skal vi ikke beskæftige os med noget eksotisk som Unicode.

Lad os først oprette vores funktioner, hvoraf flere jeg tilpassede fra den første artikel ovenfor. Jeg udelod et par, som jeg ikke følte ville konkurrere; Jeg vil overlade det som en øvelse til læseren at teste dem.

    Tabel med tal

    Denne har igen brug for noget opsætning, men det kan være et ret lille bord på grund af de kunstige begrænsninger, vi placerer:

    INDSTIL ANTAL TIL; DECLARE @UpperLimit INT =8000;;WITH n AS( SELECT x =ROW_NUMBER() OVER (ORDER BY s1.[object_id]) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2)SELECT Number =x INTO dbo.Numbers FROM n WHERE x MELLEM 1 OG @UpperLimit;GOCREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number);

    Derefter funktionen:

    CREATE FUNCTION dbo.SplitStrings_Numbers( @List varchar(8000), @Delimiter char(1))RETURNER TABLE WITH SCHEMABINDINGAS RETURN ( SELECT [Value] =SUBSTRING(@List, [Number], CHARINDEX(@Delimiter, @List + @Delimiter, [Number]) - [Number]) FROM dbo.Numbers WHERE Number <=LEN(@List) AND SUBSTRING(@Delimiter + @List, [Number], 1) =@Delimiter );

    JSON

    Baseret på en tilgang, som først blev afsløret af storage engine-teamet, lavede jeg en lignende indpakning omkring OPENJSON , bemærk blot, at afgrænsningstegnet skal være et komma i dette tilfælde, ellers skal du foretage en streng erstatning af strengen, før du overfører værdien til den oprindelige funktion:

    CREATE FUNCTION dbo.SplitStrings_JSON( @List varchar(8000), @Delimiter char(1) -- ignoreret, men gjort automatiseret test lettere)RETURNERER TABEL MED SCHEMABINDINGAS RETURN (VÆLG værdi FRA OPENJSON( CHAR(91) + @List + CHAR(93) ));

    CHAR(91)/CHAR(93) erstatter kun henholdsvis [ og ] på grund af formateringsproblemer.

    XML

    CREATE FUNCTION dbo.SplitStrings_XML( @List varchar(8000), @Delimiter char(1))RETURNER TABLE WITH SCHEMABINDINGAS RETURN (SELECT [value] =y.i.value('(./text())[1]', 'varchar(8000)') FROM (SELECT x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').forespørgsel ('.') ) SOM KRYDS GØR x.nodes('i') AS y(i));

    CLR

    Jeg lånte endnu en gang Adam Machanics troværdige opdelingskode fra næsten syv år siden, selvom den understøtter Unicode, MAX typer og afgrænsere med flere tegn (og faktisk, fordi jeg slet ikke vil rode med funktionskoden, begrænser dette vores inputstrenge til 4.000 tegn i stedet for 8.000):

    CREATE FUNCTION dbo.SplitStrings_CLR( @List nvarchar(MAX), @Delimiter nvarchar(255))RETURNER TABLE ( værdi nvarchar(4000) )EXTERNAL NAME CLRUtilities.UserDefinedFunctions.SplitString_Multi;
    Multi;

    STRING_SPLIT

    For at sikre konsistensen har jeg lagt en indpakning omkring STRING_SPLIT :

    OPRET FUNKTION dbo.SplitStrings_Native( @List varchar(8000), @Delimiter char(1))RETURNERER TABEL MED SCHEMABINDINGAS RETURN (SELECT værdi FRA STRING_SPLIT(@List, @Delimiter));

Kildedata og fornuftstjek

Jeg oprettede denne tabel for at tjene som kilden til inputstrenge til funktionerne:

CREATE TABLE dbo.SourceTable( RowNum int IDENTITY(1,1) PRIMARY KEY, StringValue varchar(8000));;WITH x AS ( SELECT TOP (60000) x =TOP((SELECT TOP (ABS(o.[object_id] % 20)) ',' + CONVERT(varchar(12), c.[object_id]) FROM sys.all_columns AS c WHERE c.[object_id]  

Bare for reference, lad os validere, at 50.000 rækker kom ind i tabellen, og tjekke den gennemsnitlige længde af strengen og det gennemsnitlige antal elementer pr. streng:

VÆLG [Values] =COUNT(*), AvgStringLength =AVG(1,0*LEN(StringValue)), AvgElementCount =AVG(1,0*LEN(StringValue)-LEN(REPLACE(StringValue, ',','')) ) FRA dbo.SourceTable; /* resultat:Værdier AvgStringLength AbgElementCount ------ -------------- --------------- 50000 108,476380 8,911840*/ 

Og endelig, lad os sørge for, at hver funktion returnerer de rigtige data for en given RowNum , så vi vælger bare en tilfældigt og sammenligner værdierne opnået gennem hver metode. Dine resultater vil naturligvis variere.

SELECT f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/*-metoden */(s.StringValue, ',') AS f WHERE s.RowNum =37219 ORDER BY f.value;

Sikkert nok fungerer alle funktioner som forventet (sorteringen er ikke numerisk; husk, funktionernes outputstrenge):

Eksempelsæt af output fra hver af funktionerne

Performancetest

SELECT SYSDATETIME();GODECLARE @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* method */(s.StringValue,',') AS f;GO 100SELECT SYSDATETIME();

Jeg kørte ovenstående kode 10 gange for hver metode og tog gennemsnittet af timingen for hver. Og det var her, overraskelsen kom ind for mig. I betragtning af begrænsningerne i den oprindelige STRING_SPLIT funktion, var min antagelse, at det blev smidt sammen hurtigt, og at ydeevnen ville give troværdighed til det. Dreng var resultatet anderledes end hvad jeg forventede:

Gennemsnitlig varighed af STRING_SPLIT sammenlignet med andre metoder

Opdatering 2016-03-20

På baggrund af nedenstående spørgsmål fra Lars kørte jeg testene igen med et par ændringer:

  • Jeg overvågede min instans med SQL Sentry Performance Advisor for at fange CPU-profilen under testen;
  • Jeg fangede ventestatistikker på sessionsniveau mellem hver batch;
  • Jeg indsatte en forsinkelse mellem batches, så aktiviteten ville være visuelt tydelig på Performance Advisor-dashboardet.

Jeg oprettede en ny tabel for at fange ventestatsoplysninger:

CREATE TABLE dbo.Timings( dt datetime, test varchar(64), point varchar(64), session_id smallint, wait_type nvarchar(60), wait_time_ms bigint,);

Så blev koden for hver test ændret til denne:

WAITFOR DELAY '00:00:30'; DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)SELECT @d, test =/* 'method' */, point ='Start', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =@@SPID;GO DECLARE @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* method */(s.StringValue, ',') AS fGO 100 DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)SELECT @d, /* 'method' */, 'End', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =@@SPID;

Jeg kørte testen og kørte derefter følgende forespørgsler:

-- valider, at timing var i samme boldbane som tidligere test. Vælg test, DATODIFF(SECOND, MIN(dt), MAX(dt)) FRA dbo.Timings MED (NOLOCK)GROUP BY test ORDER BY 2 DESC; -- bestemme vinduet, der skal gælde for Performance Advisor-dashboard.VÆLG MIN(dt), MAX(dt) FRA dbo.Timings; -- få ventestatistik registreret for hver sessionSELECT-test, wait_type, delta FROM(SELECT f.test, rn =RANK() OVER (PARTITION BY f.point ORDER BY f.dt), f.wait_type, delta =f.wait_time_ms - COALESCE(s.wait_time_ms, 0) FRA dbo.Timings AS f VENSTRE YDRE JOIN dbo.Timings AS s ON s.test =f.test OG s.wait_type =f.wait_type OG s.point ='Start' HVOR f.point ='End') AS x WHERE delta> 0ORDER BY rn, delta DESC;

Fra den første forespørgsel forblev timingerne i overensstemmelse med tidligere tests (jeg ville kortlægge dem igen, men det ville ikke afsløre noget nyt).

Fra den anden forespørgsel var jeg i stand til at fremhæve dette område på Performance Advisor-dashboardet, og derfra var det nemt at identificere hver batch:

Batches fanget på CPU-diagrammet på Performance Advisor-dashboardet

Det er klart, alle metoderne *undtagen* STRING_SPLIT knyttet en enkelt kerne under testens varighed (dette er en quad-core maskine, og CPU'en var konstant på 25%). Det er sandsynligt, at Lars insinuerede under den STRING_SPLIT er hurtigere på bekostning af at hamre CPU'en, men det ser ikke ud til, at det er tilfældet.

Til sidst, fra den tredje forespørgsel, var jeg i stand til at se følgende ventestatistikker, der opstod efter hver batch:

Venter pr. session i millisekunder

De ventetider, der fanges af DMV, forklarer ikke fuldstændigt varigheden af ​​forespørgslerne, men de tjener til at vise, hvor yderligere ventetider opstår.

Konklusion

Selvom tilpasset CLR stadig viser en enorm fordel i forhold til traditionelle T-SQL-tilgange, og brugen af ​​JSON til denne funktionalitet ser ud til at være intet andet end en nyhed, STRING_SPLIT var den klare vinder - med en mile. Så hvis du bare har brug for at opdele en streng og kan håndtere alle dens begrænsninger, ser det ud til, at dette er en meget mere levedygtig mulighed, end jeg ville have forventet. Forhåbentlig vil vi i fremtidige builds se yderligere funktionalitet, såsom en outputkolonne, der angiver ordenspositionen for hvert element, evnen til at bortfiltrere dubletter og tomme strenge og adskillere med flere tegn.

Jeg adresserer flere kommentarer nedenfor i to opfølgende indlæg:

  • STRING_SPLIT() i SQL Server 2016:Opfølgning #1
  • STRING_SPLIT() i SQL Server 2016:Opfølgning #2

  1. Sådan rettes "Serveren er ikke konfigureret til DATAADGANG" i SQL Server

  2. Brug af indekser i SQL Server-hukommelsesoptimerede tabeller

  3. Sådan konverteres antal minutter til tt:mm-format i TSQL?

  4. Automatiser implementering af din MySQL- eller Postgres-klynge fra backup