Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

SQL Server v.Next:Wydajność STRING_AGG, część 2

W zeszłym tygodniu dokonałem kilku szybkich porównań wydajności, używając nowego STRING_AGG() funkcja w stosunku do tradycyjnego FOR XML PATH podejście, którego używałem od wieków. Przetestowałem zarówno niezdefiniowaną/arbitralną kolejność, jak i jawną kolejność oraz STRING_AGG() w obu przypadkach wypadł na szczycie:

    SQL Server v.Next:Wydajność STRING_AGG(), część 1

W przypadku tych testów pominąłem kilka rzeczy (nie wszystkie celowo):

  1. Mikael Eriksson i Grzegorz Łyp wskazali, że nie używałem absolutnie najbardziej wydajnego FOR XML PATH konstrukt (a żeby było jasne, nigdy nie miałem).
  2. Nie przeprowadzałem żadnych testów na Linuksie; tylko w systemie Windows. Nie spodziewam się, że będą one bardzo różne, ale ponieważ Grzegorz widział bardzo różne czasy trwania, warto to zbadać.
  3. Testowałem również tylko wtedy, gdy dane wyjściowe byłyby skończonym ciągiem niebędącym ciągiem znaków LOB – co moim zdaniem jest najczęstszym przypadkiem użycia (nie sądzę, że ludzie będą często łączyć każdy wiersz w tabeli w jeden oddzielony przecinkami string, ale właśnie dlatego poprosiłem w poprzednim poście o twoje przypadki użycia.
  4. W przypadku testów porządkowania nie stworzyłem indeksu, który mógłby być pomocny (ani nie próbowałem czegokolwiek, w którym wszystkie dane pochodzą z jednej tabeli).

W tym poście zajmę się kilkoma z tych elementów, ale nie wszystkimi.

DLA ŚCIEŻKI XML

Używałem następujących:

... FOR XML PATH, TYPE).value(N'.[1]', ...

Po tym komentarzu Mikaela zaktualizowałem swój kod, aby zamiast tego używał nieco innej konstrukcji:

... FOR XML PATH(''), TYPE).value(N'text()[1]', ...

Linux kontra Windows

Początkowo zawracałem sobie głowę tylko uruchamianiem testów w systemie Windows:

Microsoft SQL Server vNext (CTP1.1) - 14.0.100.187 (X64) 
	Dec 10 2016 02:51:11 
	Copyright (C) 2016 Microsoft Corporation. All rights reserved.
	Developer Edition (64-bit) on Windows Server 2016 Datacenter 6.3  (Build 14393: ) (Hypervisor)

Ale Grzegorz miał rację, że on (i prawdopodobnie wielu innych) miał dostęp tylko do wersji CTP 1.1 dla Linuksa. Więc dodałem Linuksa do mojej macierzy testowej:

Microsoft SQL Server vNext (CTP1.1) - 14.0.100.187 (X64) 
	Dec 10 2016 02:51:11 
	Copyright (C) 2016 Microsoft Corporation. All rights reserved.
	on Linux (Ubuntu 16.04.1 LTS)

Kilka interesujących, ale całkowicie stycznych obserwacji:

  • @@VERSION nie pokazuje edycji w tej kompilacji, ale SERVERPROPERTY('Edition') zwraca oczekiwany Developer Edition (64-bit) .
  • Na podstawie czasów kompilacji zakodowanych w plikach binarnych, wersje Windows i Linux wydają się być teraz kompilowane w tym samym czasie iz tego samego źródła. Albo to był jeden szalony zbieg okoliczności.

Testy nieuporządkowane

Zacząłem od przetestowania dowolnie uporządkowanego wyjścia (gdzie nie ma wyraźnie zdefiniowanego porządkowania połączonych wartości). Podążając za Grzegorzem, użyłem WideWorldImporters (Standard), ale wykonałem łączenie między Sales.Orders i Sales.OrderLines . Fikcyjnym wymaganiem jest tutaj wyświetlenie listy wszystkich zamówień, a wraz z każdym zamówieniem oddzielonej przecinkami listy każdego StockItemID .

Od StockItemID jest liczbą całkowitą, możemy użyć zdefiniowanego varchar , co oznacza, że ​​ciąg może mieć 8000 znaków, zanim będziemy musieli martwić się o MAX. Ponieważ int może mieć maksymalną długość 11 (naprawdę 10, jeśli nie ma znaku), plus przecinek, oznacza to, że zamówienie musiałoby obsługiwać około 8 000/12 (666) pozycji magazynowych w najgorszym przypadku (np. wszystkie wartości StockItemID mają 11 cyfr). W naszym przypadku najdłuższy identyfikator to 3 cyfry, więc dopóki dane nie zostaną dodane, potrzebowalibyśmy 8000/4 (2000) unikalnych pozycji magazynowych w jednym zamówieniu, aby uzasadnić MAX. W naszym przypadku jest w sumie tylko 227 pozycji magazynowych, więc MAX nie jest konieczny, ale warto mieć na to oko. Jeśli tak duży ciąg jest możliwy w twoim scenariuszu, będziesz musiał użyć varchar(max) zamiast domyślnego (STRING_AGG() zwraca nvarchar(max) , ale obcina do 8000 bajtów, chyba że dane wejściowe jest typu MAX).

Początkowe zapytania (aby pokazać przykładowe dane wyjściowe i obserwować czasy trwania dla pojedynczych wykonań):

SET STATISTICS TIME ON;
GO
 
SELECT o.OrderID, StockItemIDs = STRING_AGG(ol.StockItemID, ',')
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO
 
SELECT o.OrderID, 
  StockItemIDs = STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,'')
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO
 
SET STATISTICS TIME OFF;
 
/*
   Sample output:
 
       OrderID    StockItemIDs
       =======    ============
       1          67
       2          50,10
       3          114
       4          206,130,50
       5          128,121,155
 
   Important SET STATISTICS TIME metrics (SQL Server Execution Times):
 
      Windows:
        STRING_AGG:    CPU time =  217 ms,  elapsed time =  405 ms.
        FOR XML PATH:  CPU time = 1954 ms,  elapsed time = 2097 ms.
 
      Linux:
        STRING_AGG:    CPU time =  627 ms,  elapsed time =  472 ms.
        FOR XML PATH:  CPU time = 2188 ms,  elapsed time = 2223 ms.
*/

Całkowicie zignorowałem analizowanie i kompilację danych dotyczących czasu, ponieważ zawsze były one dokładnie zerowe lub wystarczająco bliskie, aby były nieistotne. Wystąpiły niewielkie różnice w czasach wykonania dla każdego uruchomienia, ale niewiele — powyższe komentarze odzwierciedlają typową różnicę w czasie wykonywania (STRING_AGG wydawało się, że trochę korzysta z równoległości, ale tylko w Linuksie, podczas gdy FOR XML PATH nie na żadnej platformie). Obie maszyny miały jedno gniazdo, czterordzeniowy procesor, 8 GB pamięci, gotową konfigurację i nie miały żadnej innej aktywności.

Następnie chciałem przetestować na dużą skalę (po prostu pojedynczą sesję wykonującą to samo zapytanie 500 razy). Nie chciałem zwracać wszystkich wyników, jak w powyższym zapytaniu, 500 razy, ponieważ przytłoczyłoby to SSMS – i mam nadzieję, że i tak nie reprezentuje rzeczywistych scenariuszy zapytań. Więc przypisałem wyjście do zmiennych i po prostu zmierzyłem całkowity czas dla każdej partii:

SELECT sysdatetime();
GO
 
DECLARE @i int, @x varchar(8000);
SELECT @i = o.OrderID, @x = STRING_AGG(ol.StockItemID, ',')
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();
GO
 
DECLARE @i int, @x varchar(8000);
SELECT @i = o.OrderID, 
    @x = STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,'')
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();

Przeprowadziłem te testy trzy razy i różnica była głęboka – prawie o rząd wielkości. Oto średni czas trwania trzech testów:

Średni czas trwania w milisekundach dla 500 wykonań przypisania zmiennej

W ten sposób testowałem wiele innych rzeczy, głównie po to, by upewnić się, że omawiam typy testów, które prowadził Grzegorz (bez części LOB).

  1. Wybieranie tylko długości wyjścia
  2. Uzyskiwanie maksymalnej długości wyjścia (dowolnego wiersza)
  3. Wybieranie wszystkich danych wyjściowych do nowej tabeli

Wybieranie tylko długości wyjścia

Ten kod po prostu przechodzi przez każde zamówienie, łączy wszystkie wartości StockItemID, a następnie zwraca tylko długość.

SET STATISTICS TIME ON;
GO
 
SELECT LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO
 
SELECT LEN(STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO
 
SET STATISTICS TIME OFF;
 
/*
  Windows:
    STRING_AGG:   CPU time =  142 ms,  elapsed time =  351 ms.
    FOR XML PATH: CPU time = 1984 ms,  elapsed time = 2120 ms.
 
  Linux:
    STRING_AGG:   CPU time =  310 ms,  elapsed time =  191 ms.
    FOR XML PATH: CPU time = 2149 ms,  elapsed time = 2167 ms.    
*/

W przypadku wersji wsadowej ponownie użyłem przypisania zmiennych, zamiast próbować zwrócić wiele zestawów wyników do SSMS. Przypisanie zmiennej skończyłoby się na dowolnym wierszu, ale nadal wymaga to pełnego skanowania, ponieważ dowolny wiersz nie jest zaznaczany jako pierwszy.

SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = LEN(STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO 500
 
SELECT sysdatetime();

Wskaźniki wydajności 500 wykonań:

500 wykonań przypisania LEN() do zmiennej

Ponownie widzimy FOR XML PATH jest znacznie wolniejszy, zarówno w systemie Windows, jak i Linux.

Wybieranie maksymalnej długości wyjścia

Niewielka odmiana poprzedniego testu, ten po prostu pobiera maksimum długość połączonego wyjścia:

SET STATISTICS TIME ON;
GO
 
SELECT MAX(s) FROM (SELECT s = LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID) AS x;
GO
 
SELECT MAX(s) FROM (SELECT s = LEN(STUFF(
    (SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),
	1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID) AS x;
GO
 
SET STATISTICS TIME OFF;
 
/*
  Windows:
    STRING_AGG:   CPU time =  188 ms,  elapsed time =  48 ms.
    FOR XML PATH: CPU time = 1891 ms,  elapsed time = 907 ms.
 
  Linux:
    STRING_AGG:   CPU time =  270 ms,  elapsed time =   83 ms.
    FOR XML PATH: CPU time = 2725 ms,  elapsed time = 1205 ms.
*/

I w skali, po prostu ponownie przypisujemy to wyjście do zmiennej:

SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = MAX(s) FROM (SELECT s = LEN(STRING_AGG(ol.StockItemID, ','))
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID) AS x;
GO 500
 
SELECT sysdatetime();
GO
 
DECLARE @i int;
SELECT @i = MAX(s) FROM (SELECT s = LEN(STUFF
  (
    (SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),
	1,1,''))
  FROM Sales.Orders AS o
  GROUP BY o.OrderID) AS x;
GO 500
 
SELECT sysdatetime();

Wyniki wydajności dla 500 wykonań, uśrednione dla trzech przebiegów:

500 wykonań przypisania MAX(LEN()) do zmiennej

Możesz zacząć zauważać wzorzec w tych testach — FOR XML PATH jest zawsze psem, nawet z ulepszeniami wydajności sugerowanymi w moim poprzednim poście.

WYBIERZ DO

Chciałem sprawdzić, czy metoda konkatenacji ma jakiś wpływ na pisanie dane z powrotem na dysk, tak jak w niektórych innych scenariuszach:

SET NOCOUNT ON;
GO
SET STATISTICS TIME ON;
GO
 
DROP TABLE IF EXISTS dbo.HoldingTank_AGG;
 
SELECT o.OrderID, x = STRING_AGG(ol.StockItemID, ',')
  INTO dbo.HoldingTank_AGG
  FROM Sales.Orders AS o
  INNER JOIN Sales.OrderLines AS ol
  ON o.OrderID = ol.OrderID
  GROUP BY o.OrderID;
GO
 
DROP TABLE IF EXISTS dbo.HoldingTank_XML;
 
SELECT o.OrderID, x = STUFF((SELECT ',' + CONVERT(varchar(11),ol.StockItemID)
       FROM Sales.OrderLines AS ol
       WHERE ol.OrderID = o.OrderID
       FOR XML PATH(''), TYPE).value(N'text()[1]',N'varchar(8000)'),1,1,'')
  INTO dbo.HoldingTank_XML
  FROM Sales.Orders AS o
  GROUP BY o.OrderID;
GO
 
SET STATISTICS TIME OFF;
 
/*
  Windows:
    STRING_AGG:   CPU time =  218 ms,  elapsed time =   90 ms.
    FOR XML PATH: CPU time = 4202 ms,  elapsed time = 1520 ms.
 
  Linux:
    STRING_AGG:   CPU time =  277 ms,  elapsed time =  108 ms.
    FOR XML PATH: CPU time = 4308 ms,  elapsed time = 1583 ms.
*/

W tym przypadku widzimy, że być może SELECT INTO był w stanie wykorzystać trochę paralelizmu, ale nadal widzimy FOR XML PATH walka, z czasem pracy o rząd wielkości dłuższym niż STRING_AGG .

Wersja wsadowa właśnie zamieniła polecenia SET STATISTICS dla SELECT sysdatetime(); i dodałem ten sam GO 500 po dwóch głównych partiach jak w poprzednich testach. Oto, jak to się potoczyło (ponownie, powiedz mi, czy słyszałeś to wcześniej):

500 wykonań SELECT INTO

Zamówione testy

Przeprowadziłem te same testy przy użyciu uporządkowanej składni, np.:

... STRING_AGG(ol.StockItemID, ',') 
    WITHIN GROUP (ORDER BY ol.StockItemID) ...
 
... WHERE ol.OrderID = o.OrderID
    ORDER BY ol.StockItemID
    FOR XML PATH('') ...

Miało to bardzo niewielki wpływ na wszystko — ten sam zestaw czterech platform testowych wykazał prawie identyczne metryki i wzorce na całej planszy.

Będę ciekaw, czy jest inaczej, gdy połączone wyjście jest w trybie innym niż LOB lub gdy konkatenacja wymaga uporządkowania ciągów (z indeksem pomocniczym lub bez).

Wniosek

Dla ciągów innych niż LOB , jasne jest dla mnie, że STRING_AGG ma zdecydowaną przewagę wydajności nad FOR XML PATH , zarówno w systemie Windows, jak i Linux. Zauważ, że aby uniknąć wymagania varchar(max) lub nvarchar(max) , nie użyłem niczego podobnego do testów przeprowadzonych przez Grzegorza, co oznaczałoby po prostu połączenie wszystkich wartości z kolumny, w całej tabeli, w jeden ciąg. W następnym poście przyjrzę się przypadkowi użycia, w którym dane wyjściowe połączonego ciągu mogą być większe niż 8000 bajtów, a zatem musiałyby zostać użyte typy LOB i konwersje.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak WYBRAĆ * ale bez Nazwy kolumn muszą być unikalne w każdym widoku

  2. JDBC SQLServerException:Ten sterownik nie jest skonfigurowany do zintegrowanego uwierzytelniania.

  3. nvarchar konkatenacja / index / nvarchar(max) niewytłumaczalne zachowanie

  4. 4 funkcje do sformatowania liczby do 2 miejsc dziesiętnych w SQL Server

  5. Jak NULLIF() działa w SQL Server