Database
 sql >> Baza danych >  >> RDS >> Database

Niespodzianki dotyczące wydajności i założenia:STRING_SPLIT()

Ponad trzy lata temu opublikowałem trzyczęściową serię o dzieleniu strun:

  • Podziel struny we właściwy sposób – lub następny najlepszy sposób
  • Rozdzielanie ciągów:kontynuacja
  • Rozdzielanie ciągów:teraz z mniejszą ilością T-SQL

W styczniu podjąłem nieco bardziej skomplikowany problem:

  • Porównywanie metod dzielenia/konkatenacji ciągów

Przez cały czas mój wniosek był następujący:PRZESTAŃ ROBIĆ TO W T-SQL . Użyj CLR lub, jeszcze lepiej, przekaż ustrukturyzowane parametry, takie jak DataTables, z aplikacji do parametrów wycenianych w tabeli (TVP) w swoich procedurach, unikając całkowicie konstrukcji i dekonstrukcji ciągów — co jest tak naprawdę częścią rozwiązania, która powoduje problemy z wydajnością.

I wtedy pojawił się SQL Server 2016…

Po wydaniu RC0 nowa funkcja została udokumentowana bez większych fanfar:STRING_SPLIT . Szybki przykład:

SELECT * FROM STRING_SPLIT('a,b,cd', ',');
 
/* result:
 
    value
    --------
    a
    b
    cd
*/

Przyciągnęło to uwagę kilku kolegów, w tym Dave'a Ballantyne'a, który pisał o głównych funkcjach – ale był na tyle uprzejmy, że dał mi pierwszeństwo odmowy porównania wydajności.

Jest to głównie ćwiczenie akademickie, ponieważ przy dużym zestawie ograniczeń w pierwszej iteracji tej funkcji prawdopodobnie nie będzie to wykonalne dla dużej liczby przypadków użycia. Oto lista obserwacji dokonanych przeze mnie i Dave'a, z których niektóre mogą w pewnych sytuacjach złamać układ:

  • funkcja wymaga, aby baza danych miała poziom zgodności 130;
  • przyjmuje tylko ograniczniki jednoznakowe;
  • nie ma możliwości dodania kolumn wyjściowych (takich jak kolumna wskazująca pozycję porządkową w ciągu);
    • powiązane, nie ma możliwości kontrolowania sortowania – jedyne opcje to dowolne i alfabetyczne ORDER BY value;
  • do tej pory zawsze szacuje 50 wierszy wyjściowych;
  • używając go do DML, w wielu przypadkach otrzymasz szpulę tabeli (dla ochrony Hallowe'en);
  • NULL wejście prowadzi do pustego wyniku;
  • nie ma możliwości przesuwania predykatów, jak eliminowanie duplikatów lub pustych ciągów z powodu kolejnych ograniczników;
  • nie ma możliwości wykonania operacji na wartościach wyjściowych aż do momentu po fakcie (na przykład wiele funkcji dzielenia wykonuje LTRIM/RTRIM lub jawne konwersje dla Ciebie – STRING_SPLIT wypluwa wszystkie brzydkie, takie jak wiodące spacje).

Po ujawnieniu tych ograniczeń możemy przejść do testów wydajności. Biorąc pod uwagę historię firmy Microsoft z wbudowanymi funkcjami, które wykorzystują CLR pod przykrywkami (kaszel FORMAT() kaszel ), byłem sceptyczny, czy ta nowa funkcja może zbliżyć się do najszybszych metod, jakie testowałem do tej pory.

Użyjmy rozdzielaczy ciągów, aby oddzielić ciągi liczb oddzielone przecinkami, w ten sposób nasz nowy przyjaciel JSON również może przyjść i zagrać. I powiemy, że żadna lista nie może przekroczyć 8000 znaków, więc nie ma MAX typy są wymagane, a ponieważ są to liczby, nie mamy do czynienia z czymś egzotycznym, takim jak Unicode.

Najpierw stwórzmy nasze funkcje, z których kilka zaadaptowałem z pierwszego artykułu powyżej. Pominąłem parę, która według mnie nie będzie konkurować; Zostawię to jako ćwiczenie dla czytelnika, aby je przetestować.

    Tabela liczb

    Ten znowu wymaga trochę konfiguracji, ale może to być dość mały stół ze względu na sztuczne ograniczenia, które nakładamy:

    SET NOCOUNT ON;
     
    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 BETWEEN 1 AND @UpperLimit;
    GO
    CREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number);

    Następnie funkcja:

    CREATE FUNCTION dbo.SplitStrings_Numbers
    (
      @List       varchar(8000), 
      @Delimiter  char(1)
    )
    RETURNS TABLE WITH SCHEMABINDING
    AS
      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

    W oparciu o podejście ujawnione po raz pierwszy przez zespół silnika pamięci masowej, stworzyłem podobne opakowanie wokół OPENJSON , po prostu zauważ, że separatorem musi być w tym przypadku przecinek, lub musisz wykonać kilka ciężkich podstawień łańcucha przed przekazaniem wartości do funkcji natywnej:

    CREATE FUNCTION dbo.SplitStrings_JSON
    (
      @List       varchar(8000),
      @Delimiter  char(1) -- ignored but made automated testing easier
    )
    RETURNS TABLE WITH SCHEMABINDING
    AS
        RETURN (SELECT value FROM OPENJSON( CHAR(91) + @List + CHAR(93) ));

    Znaki CHAR(91)/CHAR(93) po prostu zastępują odpowiednio [ i ] z powodu problemów z formatowaniem.

    XML

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

    CLR

    Po raz kolejny pożyczyłem zaufany kod dzielący Adama Machanica sprzed prawie siedmiu lat, mimo że obsługuje Unicode, MAX typy i wieloznakowe ograniczniki (a właściwie, ponieważ nie chcę w ogóle zadzierać z kodem funkcji, ogranicza to nasze ciągi wejściowe do 4000 znaków zamiast 8000):

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

    STRING_SPLIT

    Dla zachowania spójności umieszczam otoczkę wokół STRING_SPLIT :

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

Kontrola danych źródłowych i poprawności

Stworzyłem tę tabelę, aby służyła jako źródło ciągów wejściowych do funkcji:

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]) FROM sys.all_columns AS c
  WHERE c.[object_id] < o.[object_id] ORDER BY NEWID() FOR XML PATH(''), 
    TYPE).value(N'(./text())[1]', N'varchar(8000)'),1,1,'')
  FROM sys.all_objects AS o CROSS JOIN sys.all_objects AS o2
  ORDER BY NEWID()
) 
INSERT dbo.SourceTable(StringValue) 
  SELECT TOP (50000) x 
  FROM x WHERE x IS NOT NULL
  ORDER BY NEWID();

Dla porównania sprawdźmy, czy 50 000 wierszy znalazło się w tabeli i sprawdźmy średnią długość ciągu oraz średnią liczbę elementów w ciągu:

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

I na koniec upewnijmy się, że każda funkcja zwraca właściwe dane dla każdego podanego RowNum , więc po prostu wybierzemy losowo jedną i porównamy wartości uzyskane za pomocą każdej metody. Twoje wyniki będą się oczywiście różnić.

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;

Rzeczywiście, wszystkie funkcje działają zgodnie z oczekiwaniami (sortowanie nie jest numeryczne; pamiętaj, że funkcje wyprowadzają łańcuchy):

Przykładowy zestaw danych wyjściowych każdej z funkcji

Testowanie wydajności

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

Uruchomiłem powyższy kod 10 razy dla każdej metody i uśredniłem czasy dla każdej z nich. I tu właśnie pojawiła się dla mnie niespodzianka. Biorąc pod uwagę ograniczenia w natywnym STRING_SPLIT moim założeniem było to, że zostało to zmontowane szybko i że wydajność to uwiarygodnia. Chłopiec był rezultatem innym niż się spodziewałem:

Średni czas trwania STRING_SPLIT w porównaniu z innymi metodami

Aktualizacja 2016-03-20

W oparciu o poniższe pytanie od Larsa, przeprowadziłem testy ponownie z kilkoma zmianami:

  • Monitorowałem moją instancję za pomocą SQL Sentry Performance Advisor, aby przechwycić profil procesora podczas testu;
  • Zrobiłem statystyki oczekiwania na poziomie sesji pomiędzy każdą partią;
  • Wstawiłem opóźnienie między partiami, aby aktywność była wizualnie odmienna w panelu Performance Advisor.

Utworzyłem nową tabelę do przechwytywania informacji o czekaniu:

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

Następnie kod dla każdego testu zmienił się na następujący:

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_ms
FROM 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 f
GO 100
 
DECLARE @d DATETIME = SYSDATETIME();
 
INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)
SELECT @d, /* 'method' */, 'End', wait_type, wait_time_ms
FROM sys.dm_exec_session_wait_stats WHERE session_id = @@SPID;

Przeprowadziłem test, a następnie uruchomiłem następujące zapytania:

-- validate that timings were in same ballpark as previous tests
SELECT test, DATEDIFF(SECOND, MIN(dt), MAX(dt)) 
FROM dbo.Timings WITH (NOLOCK)
GROUP BY test ORDER BY 2 DESC;
 
-- determine window to apply to Performance Advisor dashboard
SELECT MIN(dt), MAX(dt) FROM dbo.Timings;
 
-- get wait stats registered for each session
SELECT 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 = 'End'
) AS x 
WHERE delta > 0
ORDER BY rn, delta DESC;

Od pierwszego zapytania czasy pozostały zgodne z poprzednimi testami (wykreśliłbym je ponownie, ale to nie ujawniłoby niczego nowego).

W drugim zapytaniu udało mi się wyróżnić ten zakres na pulpicie nawigacyjnym Performance Advisor, a stamtąd łatwo było zidentyfikować każdą partię:

Pakiety przechwycone na wykresie procesora w panelu Performance Advisor

Oczywiście wszystkie metody *oprócz* STRING_SPLIT Ustalono pojedynczy rdzeń na czas trwania testu (jest to maszyna czterordzeniowa, a procesor ma stale 25%). Jest prawdopodobne, że Lars sugerował poniżej, że STRING_SPLIT jest szybszy kosztem bicia procesora, ale wydaje się, że tak nie jest.

Wreszcie, z trzeciego zapytania, mogłem zobaczyć następujące statystyki oczekiwania naliczane po każdej partii:

Oczekiwanie na sesję, w milisekundach

Oczekiwania przechwycone przez DMV nie wyjaśniają w pełni czasu trwania zapytań, ale służą do pokazania, gdzie dodatkowe ponoszone są oczekiwania.

Wniosek

Podczas gdy niestandardowy CLR nadal wykazuje ogromną przewagę nad tradycyjnymi podejściami T-SQL, a użycie JSON do tej funkcji wydaje się niczym więcej niż nowością, STRING_SPLIT był wyraźnym zwycięzcą – o milę. Tak więc, jeśli potrzebujesz tylko podzielić łańcuch i możesz poradzić sobie ze wszystkimi jego ograniczeniami, wygląda na to, że jest to znacznie bardziej opłacalna opcja, niż bym się spodziewał. Mamy nadzieję, że w przyszłych kompilacjach zobaczymy dodatkowe funkcje, takie jak kolumna wyjściowa wskazująca pozycję porządkową każdego elementu, możliwość filtrowania duplikatów i pustych ciągów oraz wieloznakowe ograniczniki.

W dwóch kolejnych postach zwracam się do wielu komentarzy poniżej:

  • STRING_SPLIT() w SQL Server 2016:kontynuacja nr 1
  • STRING_SPLIT() w SQL Server 2016:kontynuacja nr 2

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Wprowadzenie do SQL

  2. Blockchain:co to jest, jak działa i co oznacza dla Big Data

  3. 11 typowych instrukcji SQL z podstawowymi przykładami

  4. Wyzwalacz w SQL

  5. Co to jest baza danych Greenplum? Wprowadzenie do bazy danych Big Data