sql >> Databasteknik >  >> RDS >> Database

Prestandaöverraskningar och antaganden:STRING_SPLIT()

För över tre år sedan postade jag en serie i tre delar om att dela strängar:

  • Dela strängar på rätt sätt – eller näst bästa sätt
  • Dela strängar:En uppföljning
  • Dela strängar:Nu med mindre T-SQL

Sedan i januari tog jag mig an ett lite mer utarbetat problem:

  • Jämföra metoder för strängdelning/sammansättning

Genomgående har min slutsats varit:SLUTA ATT GÖRA DETTA I T-SQL . Använd CLR eller, ännu bättre, skicka strukturerade parametrar som DataTables från din applikation till tabellvärderade parametrar (TVP) i dina procedurer, undvik all strängkonstruktion och dekonstruktion helt och hållet – vilket egentligen är den del av lösningen som orsakar prestandaproblem.

Och sedan kom SQL Server 2016...

När RC0 släpptes dokumenterades en ny funktion utan mycket fanfar:STRING_SPLIT . Ett snabbt exempel:

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

Det fångade ögonen på några kollegor, inklusive Dave Ballantyne, som skrev om huvuddragen – men som var vänlig nog att ge mig första avslag vid en prestationsjämförelse.

Detta är mestadels en akademisk övning, för med en rejäl uppsättning begränsningar i den första iterationen av funktionen, kommer det förmodligen inte att vara genomförbart för ett stort antal användningsfall. Här är listan över de observationer som Dave och jag har gjort, av vilka några kan vara deal-breakers i vissa scenarier:

  • funktionen kräver att databasen är på kompatibilitetsnivå 130;
  • det accepterar bara en-teckenavgränsare;
  • det finns inget sätt att lägga till utdatakolumner (som en kolumn som anger ordningsposition inom strängen);
    • relaterat, det finns inget sätt att styra sortering – de enda alternativen är godtyckliga och alfabetiska ORDER BY value;
  • hittills uppskattar den alltid 50 utdatarader;
  • när du använder det för DML får du i många fall en bordsspole (för Halloween-skydd);
  • NULL input leder till ett tomt resultat;
  • det finns inget sätt att trycka ner predikat, som att eliminera dubbletter eller tomma strängar på grund av på varandra följande avgränsare;
  • det finns inget sätt att utföra operationer mot utgångsvärdena förrän i efterhand (till exempel utför många delningsfunktioner LTRIM/RTRIM eller explicita konverteringar för dig – STRING_SPLIT spottar tillbaka allt det fula, som att leda mellanslag).

Så med dessa begränsningar i det fria, kan vi gå vidare till vissa prestandatester. Med tanke på Microsofts meritlista med inbyggda funktioner som utnyttjar CLR under täcket (hosta FORMAT() hosta ), var jag skeptisk till om den här nya funktionen kunde komma i närheten av de snabbaste metoderna jag hittills testat.

Låt oss använda strängdelare för att separera kommaseparerade strängar av siffror, på så sätt kan vår nya vän JSON följa med och spela också. Och vi säger att ingen lista kan överstiga 8 000 tecken, så ingen MAX typer krävs, och eftersom de är siffror behöver vi inte hantera något exotiskt som Unicode.

Låt oss först skapa våra funktioner, av vilka jag anpassade flera från den första artikeln ovan. Jag utelämnade ett par som jag inte kände skulle tävla; Jag lämnar det som en övning till läsaren att testa dem.

    Siffertabell

    Den här behöver återigen lite installation, men det kan vara ett ganska litet bord på grund av de konstgjorda begränsningarna vi placerar:

    STÄLL IN NOCOUNT PÅ; 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 MELLAN 1 OCH @UpperLimit;GOCREATE UNIKT CLUSTERED INDEX n PÅ dbo.Numbers(Number);

    Sedan funktionen:

    SKAPA FUNKTION dbo.SplitStrings_Numbers( @List varchar(8000), @Delimiter char(1))RETURNER TABELL MED 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

    Baserat på ett tillvägagångssätt som först avslöjades av teamet för lagringsmotorer skapade jag ett liknande omslag runt OPENJSON , tänk bara på att avgränsaren måste vara ett kommatecken i det här fallet, eller så måste du göra några tunga strängbyten innan du skickar värdet till den ursprungliga funktionen:

    SKAPA FUNKTION dbo.SplitStrings_JSON( @List varchar(8000), @Delimiter char(1) -- ignoreras men gjorde automatisk testning lättare)RETURNERAR TABELL MED SCHEMABINDINGAS RETURN (VÄLJ värde FRÅN OPENJSON(CHAR(91) + @List + CHAR(93) ));

    CHAR(91)/CHAR(93) ersätter bara [ och ] på grund av formateringsproblem.

    XML

    SKAPA FUNKTION dbo.SplitStrings_XML( @List varchar(8000), @Delimiter char(1))RETURNERAR TABELL MED SCHEMABINDINGAS RETURN (SELECT [value] =y.i.value('(./text())[1]', 'varchar(8000)') FROM (SELECT x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').query ('.') ) SOM KORS APPLY x.nodes('i') AS y(i));

    CLR

    Jag lånade ännu en gång Adam Machanics pålitliga splittringskod från nästan sju år sedan, även om den stöder Unicode, MAX typer och flerteckenavgränsare (och faktiskt, eftersom jag inte vill bråka med funktionskoden alls, begränsar detta våra inmatningssträngar till 4 000 tecken istället för 8 000):

    SKAPA FUNKTION dbo.SplitStrings_CLR( @List nvarchar(MAX), @Delimiter nvarchar(255))RETURNER TABELL ( värde nvarchar(4000) )EXTERNT NAMN CLRUtilities.UserDefinedFunctions.SplitString_Multi;
    Multi;

    STRING_SPLIT

    Bara för konsekvensen lägger jag ett omslag runt STRING_SPLIT :

    SKAPA FUNKTION dbo.SplitStrings_Native( @List varchar(8000), @Delimiter char(1))RETURNERAR TABELL MED SCHEMABINDINGAS RETURN (VÄLJ värde FRÅN STRING_SPLIT(@List, @Delimiter));

Källdata och hälsokontroll

Jag skapade den här tabellen för att fungera som källa för inmatningssträngar till funktionerna:

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

Bara för referens, låt oss validera att 50 000 rader kom in i tabellen och kontrollera strängens genomsnittliga längd och det genomsnittliga antalet element per sträng:

VÄLJ [Values] =COUNT(*), AvgStringLength =AVG(1,0*LEN(StringValue)), AvgElementCount =AVG(1,0*LEN(StringValue)-LEN(REPLACE(StringValue, ',','')) ) FRÅN dbo.SourceTable; /* resultat:Värden AvgStringLength AbgElementCount ------ --------------- --------------- 50000 108,476380 8,911840*/ 

Och slutligen, låt oss se till att varje funktion returnerar rätt data för en given RowNum , så vi väljer bara en slumpmässigt och jämför de värden som erhållits genom varje metod. Dina resultat kommer naturligtvis att variera.

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

Visst, alla funktioner fungerar som förväntat (sorteringen är inte numerisk; kom ihåg att funktionernas utmatningssträngar):

Exempeluppsättning av utdata från var och en av funktionerna

Prestandatest

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();

Jag körde ovanstående kod 10 gånger för varje metod och tog ett genomsnitt av tidpunkterna för varje metod. Och det var här överraskningen kom in för mig. Med tanke på begränsningarna i den ursprungliga STRING_SPLIT funktion, mitt antagande var att det slängdes ihop snabbt, och att prestanda skulle ge trovärdighet till det. Pojken var resultatet annorlunda än vad jag förväntade mig:

Genomsnittlig varaktighet för STRING_SPLIT jämfört med andra metoder

Uppdatering 2016-03-20

Baserat på frågan nedan från Lars körde jag testerna igen med några ändringar:

  • Jag övervakade min instans med SQL Sentry Performance Advisor för att fånga CPU-profilen under testet;
  • Jag samlade in väntestatistik på sessionsnivå mellan varje batch;
  • Jag infogade en fördröjning mellan batcherna så att aktiviteten skulle vara visuellt distinkt på Performance Advisor-instrumentpanelen.

Jag skapade en ny tabell för att fånga information om väntestatistik:

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

Sedan ändrades koden för varje test till detta:

WAITFOR DELAY '00:00:30'; DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, punkt, wait_type, wait_time_ms)SELECT @d, test =/* 'metod' */, 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, punkt, wait_type, wait_time_ms)SELECT @d, /* 'metod' */, 'End', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =@@SPID;

Jag körde testet och körde sedan följande frågor:

-- validera att tidtagningar var i samma bollplank som tidigare tester. VÄLJ test, DATUMDIFF(SEKUND, MIN(dt), MAX(dt)) FRÅN dbo.Timingar MED (NOLOCK)GROUP BY test ORDER BY 2 DESC; -- bestäm fönster som ska tillämpas på Performance Advisor-instrumentpanelen VÄLJ MIN(dt), MAX(dt) FRÅN dbo.Timings; -- få väntestatistik registrerad för varje 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) FRÅN dbo.Timings AS f VÄNSTER YTTRE JOIN dbo.Timings AS s ON s.test =f.test OCH s.wait_type =f.wait_type AND s.point ='Start' VAR f.point ='Slut') AS x WHERE delta> 0ORDNING EFTER rn, delta DESC;

Från den första frågan förblev tidpunkterna i linje med tidigare tester (jag skulle kartlägga dem igen men det skulle inte avslöja något nytt).

Från den andra frågan kunde jag markera detta intervall på Performance Advisor-instrumentpanelen, och därifrån var det lätt att identifiera varje batch:

Batcher som fångats på CPU-diagrammet på Performance Advisor-instrumentpanelen

Helt klart, alla metoder *förutom* STRING_SPLIT kopplade en enstaka kärna under testets varaktighet (detta är en fyrkärnig maskin, och CPU:n låg stadigt på 25%). Det är troligt att Lars insinuerade under den STRING_SPLIT är snabbare till priset av att hamra på processorn, men det verkar inte som om så är fallet.

Slutligen, från den tredje frågan, kunde jag se följande väntestatistik som samlades in efter varje batch:

Väntar per session, i millisekunder

Väntetiderna som fångas upp av DMV förklarar inte till fullo varaktigheten av frågorna, men de tjänar till att visa var ytterligare väntan uppstår.

Slutsats

Även om anpassad CLR fortfarande visar en enorm fördel jämfört med traditionella T-SQL-metoder, och att använda JSON för denna funktion verkar inte vara något annat än en nyhet, STRING_SPLIT var den klara vinnaren – med en mil. Så om du bara behöver dela en sträng och kan hantera alla dess begränsningar, ser det ut som att detta är ett mycket mer genomförbart alternativ än jag hade förväntat mig. Förhoppningsvis kommer vi i framtida konstruktioner att se ytterligare funktionalitet, såsom en utdatakolumn som anger ordningspositionen för varje element, möjligheten att filtrera bort dubbletter och tomma strängar och avgränsare med flera tecken.

Jag tar upp flera kommentarer nedan i två uppföljningsinlägg:

  • STRING_SPLIT() i SQL Server 2016:Uppföljning #1
  • STRING_SPLIT() i SQL Server 2016:Uppföljning #2

  1. SQL Server pivot kontra multipel koppling

  2. PostgreSQL återkallar behörigheter från pg_catalog-tabeller

  3. Återställ dump på fjärrmaskinen

  4. Hur man kommer åt den inbyggda CRM-mallen i Microsoft Access