Database
 sql >> Datenbank >  >> RDS >> Database

Leistungsüberraschungen und Annahmen:STRING_SPLIT()

Vor über drei Jahren habe ich eine dreiteilige Serie über das Teilen von Strings veröffentlicht:

  • Strings richtig aufteilen – oder auf die nächstbeste Art
  • Splitting Strings:Eine Fortsetzung
  • Strings aufteilen:Jetzt mit weniger T-SQL

Dann, im Januar, habe ich mich einem etwas komplizierteren Problem angenommen:

  • Vergleich von String-Aufteilungs-/Verkettungsmethoden

Meine Schlussfolgerung war durchgehend:HÖREN SIE AUF, DIES IN T-SQL ZU MACHEN . Verwenden Sie CLR oder, noch besser, übergeben Sie strukturierte Parameter wie DataTables von Ihrer Anwendung an Tabellenwertparameter (TVPs) in Ihren Prozeduren und vermeiden Sie die gesamte Zeichenfolgenkonstruktion und -dekonstruktion – das ist wirklich der Teil der Lösung, der Leistungsprobleme verursacht.

Und dann kam SQL Server 2016…

Als RC0 veröffentlicht wurde, wurde ohne viel Tamtam eine neue Funktion dokumentiert:STRING_SPLIT . Ein kurzes Beispiel:

SELECT * FROM STRING_SPLIT('a,b,cd', ','); /* Ergebnis:Wert -------- a b cd*/

Es erregte die Aufmerksamkeit einiger Kollegen, darunter Dave Ballantyne, der über die Hauptmerkmale schrieb – aber so freundlich war, mir ein Vorkaufsrecht auf einen Leistungsvergleich zu gewähren.

Dies ist hauptsächlich eine akademische Übung, da es bei einer Reihe von Einschränkungen in der ersten Iteration des Features für eine große Anzahl von Anwendungsfällen wahrscheinlich nicht machbar sein wird. Hier ist die Liste der Beobachtungen, die Dave und ich gemacht haben, von denen einige in bestimmten Szenarien den Deal brechen können:

  • Die Funktion erfordert, dass sich die Datenbank im Kompatibilitätsgrad 130 befindet;
  • es akzeptiert nur einzelne Trennzeichen;
  • Es gibt keine Möglichkeit, Ausgabespalten hinzuzufügen (wie eine Spalte, die die Ordinalposition innerhalb der Zeichenfolge angibt);
    • bezogen gibt es keine Möglichkeit, die Sortierung zu steuern – die einzigen Optionen sind willkürlich und alphabetischer ORDER BY value;
  • bis jetzt werden immer 50 Ausgabezeilen geschätzt;
  • wenn Sie es für DML verwenden, erhalten Sie in vielen Fällen eine Tabellenspule (für Halloween-Schutz);
  • NULL Eingabe führt zu einem leeren Ergebnis;
  • Es gibt keine Möglichkeit, Prädikate herunterzudrücken, wie etwa Duplikate oder leere Zeichenfolgen aufgrund aufeinanderfolgender Trennzeichen zu eliminieren;
  • Es gibt keine Möglichkeit, Operationen gegen die Ausgabewerte durchzuführen, bis die Tatsache erfolgt (zum Beispiel führen viele Aufteilungsfunktionen LTRIM/RTRIM aus oder explizite Conversions für Sie – STRING_SPLIT spuckt alles Hässliche aus, wie führende Leerzeichen).

Mit diesen offengelegten Einschränkungen können wir also zu einigen Leistungstests übergehen. Angesichts der Erfolgsbilanz von Microsoft mit integrierten Funktionen, die CLR unter der Decke nutzen (hust FORMAT() Husten ), war ich skeptisch, ob diese neue Funktion an die schnellsten Methoden heranreicht, die ich bisher getestet habe.

Lassen Sie uns String-Splitter verwenden, um durch Kommas getrennte Zahlenfolgen zu trennen, auf diese Weise kann unser neuer Freund JSON mitkommen und auch spielen. Und wir werden sagen, dass keine Liste 8.000 Zeichen überschreiten darf, also kein MAX Typen sind erforderlich, und da es sich um Zahlen handelt, müssen wir uns nicht mit etwas Exotischem wie Unicode herumschlagen.

Lassen Sie uns zuerst unsere Funktionen erstellen, von denen ich einige aus dem ersten Artikel oben übernommen habe. Ich habe ein paar ausgelassen, von denen ich nicht glaubte, dass sie antreten würden; Ich überlasse es dem Leser als Übung, diese zu testen.

    Zahlentabelle

    Auch hier muss etwas eingerichtet werden, aber es kann aufgrund der künstlichen Einschränkungen, die wir platzieren, eine ziemlich kleine Tabelle sein:

    SET NOCOUNT ON; DECLARE @UpperLimit INT =8000;;MIT 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 BETWEEN 1 AND @UpperLimit;GOCREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number);

    Dann die Funktion:

    CREATE FUNCTION dbo.SplitStrings_Numbers( @List varchar(8000), @Delimiter char(1))RETURN 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

    Basierend auf einem Ansatz, der zuerst vom Speicher-Engine-Team aufgedeckt wurde, habe ich einen ähnlichen Wrapper um OPENJSON erstellt , beachten Sie nur, dass das Trennzeichen in diesem Fall ein Komma sein muss, oder Sie müssen eine schwere String-Ersetzung durchführen, bevor Sie den Wert an die native Funktion übergeben:

    CREATE FUNCTION dbo.SplitStrings_JSON( @List varchar(8000), @Delimiter char(1) -- ignoriert, aber automatisiertes Testen einfacher gemacht)RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT value FROM OPENJSON( CHAR(91) + @List + CHAR(93) ));

    Die Zeichen CHAR(91)/CHAR(93) ersetzen nur [ und ] aufgrund von Formatierungsproblemen.

    XML

    CREATE FUNCTION dbo.SplitStrings_XML( @List varchar(8000), @Delimiter char(1))RETURN TABLE WITH SCHEMABINDINGAS RETURN (SELECT [value] =y.i.value('(./text())[1]', 'varchar(8000)') FROM (SELECT x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').query ('.') ) AS a CROSS APPLY x.nodes('i') AS y(i));

    CLR

    Ich habe mir wieder einmal Adam Machanics bewährten Splitting-Code von vor fast sieben Jahren ausgeliehen, obwohl er Unicode unterstützt, MAX -Typen und Trennzeichen mit mehreren Zeichen (und weil ich überhaupt nicht mit dem Funktionscode herumspielen möchte, begrenzt dies unsere Eingabezeichenfolgen auf 4.000 Zeichen statt 8.000):

    CREATE FUNCTION dbo.SplitStrings_CLR( @List nvarchar(MAX), @Delimiter nvarchar(255))RETURNS TABLE ( value nvarchar(4000) )EXTERNAL NAME CLRUtilities.UserDefinedFunctions.SplitString_Multi;

    STRING_SPLIT

    Aus Gründen der Konsistenz habe ich einen Wrapper um STRING_SPLIT gelegt :

    CREATE FUNCTION dbo.SplitStrings_Native( @List varchar(8000), @Delimiter char(1))RETURN TABLE WITH SCHEMABINDINGAS RETURN (SELECT value FROM STRING_SPLIT(@List, @Delimiter));

Quelldaten &Plausibilitätsprüfung

Ich habe diese Tabelle erstellt, um als Quelle für Eingabezeichenfolgen für die Funktionen zu dienen:

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

Lassen Sie uns nur als Referenz überprüfen, ob 50.000 Zeilen in die Tabelle gelangt sind, und die durchschnittliche Länge der Zeichenfolge und die durchschnittliche Anzahl von Elementen pro Zeichenfolge überprüfen:

SELECT [Werte] =COUNT(*), AvgStringLength =AVG(1.0*LEN(StringValue)), AvgElementCount =AVG(1.0*LEN(StringValue)-LEN(REPLACE(StringValue, ',','')) ) VON dbo.SourceTable; /* Ergebnis:Werte AvgStringLength AbgElementCount ------ --------------- --------------- 50000 108.476380 8.911840*/ 

Und schließlich wollen wir sicherstellen, dass jede Funktion die richtigen Daten für jede gegebene RowNum zurückgibt , also wählen wir einfach zufällig eine aus und vergleichen die Werte, die durch jede Methode erhalten wurden. Ihre Ergebnisse werden natürlich variieren.

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;

Sicher genug, alle Funktionen funktionieren wie erwartet (das Sortieren ist nicht numerisch; denken Sie daran, dass die Funktionen Strings ausgeben):

Beispielsatz der Ausgabe von jeder der Funktionen

Leistungstest

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

Ich habe den obigen Code 10 Mal für jede Methode ausgeführt und die Timings für jede Methode gemittelt. Und hier kam die Überraschung für mich ins Spiel. Angesichts der Einschränkungen im nativen STRING_SPLIT funktionieren, war meine Annahme, dass es schnell zusammengewürfelt war und dass die Leistung dem Glaubwürdigkeit verleihen würde. Boy war das Ergebnis anders als ich erwartet hatte:

Durchschnittliche Dauer von STRING_SPLIT im Vergleich zu anderen Methoden

Aktualisierung 2016-03-20

Basierend auf der folgenden Frage von Lars habe ich die Tests mit einigen Änderungen erneut durchgeführt:

  • Ich habe meine Instanz mit SQL Sentry Performance Advisor überwacht, um das CPU-Profil während des Tests zu erfassen;
  • Ich habe Wartestatistiken auf Sitzungsebene zwischen jedem Stapel erfasst;
  • Ich habe eine Verzögerung zwischen den Stapeln eingefügt, damit die Aktivität auf dem Performance Advisor-Dashboard sichtbar ist.

Ich habe eine neue Tabelle erstellt, um Wartestatusinformationen zu erfassen:

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

Dann änderte sich der Code für jeden Test wie folgt:

WAIT FOR 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 WO 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;

Ich habe den Test ausgeführt und dann die folgenden Abfragen ausgeführt:

-- validieren, dass die Timings im selben Bereich wie vorherige Tests lagenSELECT test, DATEDIFF(SECOND, MIN(dt), MAX(dt)) FROM dbo.Timings WITH (NOLOCK)GROUP BY test ORDER BY 2 DESC; -- Festlegen des Fensters, das auf das Performance Advisor-Dashboard angewendet werden sollSELECT MIN(dt), MAX(dt) FROM dbo.Timings; -- für jede Sitzung registrierte Wartestatistik abrufenSELECT 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) FROM dbo.Timings AS f LEFT OUTER JOIN dbo.Timings AS s ON s.test =f.test AND s.wait_type =f.wait_type AND s.point ='Start' WHERE f.point ='Ende') AS x WHERE delta> 0ORDER BY rn, delta DESC;

Von der ersten Abfrage an blieben die Timings konsistent mit früheren Tests (ich würde sie erneut aufzeichnen, aber das würde nichts Neues enthüllen).

Ab der zweiten Abfrage konnte ich diesen Bereich auf dem Performance Advisor-Dashboard hervorheben, und von dort aus war es einfach, jeden Stapel zu identifizieren:

Batches, die im CPU-Diagramm auf dem Performance Advisor-Dashboard erfasst wurden

Natürlich alle Methoden *außer* STRING_SPLIT für die Dauer des Tests an einen einzelnen Kern gebunden (dies ist eine Quad-Core-Maschine, und die CPU war konstant bei 25 %). Es ist wahrscheinlich, dass Lars unter diesem STRING_SPLIT angedeutet hat ist schneller auf Kosten der CPU, aber es scheint nicht so zu sein.

Schließlich konnte ich bei der dritten Abfrage die folgenden Wartestatistiken sehen, die nach jedem Batch auflaufen:

Wartezeiten pro Sitzung, in Millisekunden

Die vom DMV erfassten Wartezeiten erklären die Dauer der Abfragen nicht vollständig, aber sie dienen dazu, zu zeigen, wo zusätzliche Wartezeiten entstehen.

Schlussfolgerung

Obwohl die benutzerdefinierte CLR immer noch einen großen Vorteil gegenüber herkömmlichen T-SQL-Ansätzen aufweist und die Verwendung von JSON für diese Funktionalität nichts weiter als eine Neuheit zu sein scheint, STRING_SPLIT war der klare Sieger – mit einer Meile. Wenn Sie also nur eine Zeichenfolge teilen müssen und mit all ihren Einschränkungen umgehen können, sieht es so aus, als wäre dies eine viel praktikablere Option, als ich erwartet hätte. Hoffentlich werden wir in zukünftigen Builds zusätzliche Funktionen sehen, wie eine Ausgabespalte, die die Ordinalposition jedes Elements angibt, die Möglichkeit, Duplikate und leere Zeichenfolgen herauszufiltern, und Trennzeichen mit mehreren Zeichen.

Ich gehe auf mehrere Kommentare unten in zwei Folgebeiträgen ein:

  • STRING_SPLIT() in SQL Server 2016:Follow-up Nr. 1
  • STRING_SPLIT() in SQL Server 2016:Follow-up Nr. 2