Chociaż SQL Server w systemie Linux ukradł prawie wszystkie nagłówki na temat v.Next, w kolejnej wersji naszej ulubionej platformy bazodanowej pojawi się kilka innych interesujących postępów. Na froncie T-SQL mamy wreszcie wbudowany sposób wykonywania zgrupowanych konkatenacji ciągów:STRING_AGG()
.
Załóżmy, że mamy następującą prostą strukturę tabeli:
CREATE TABLE dbo.Objects ( [object_id] int, [object_name] nvarchar(261), CONSTRAINT PK_Objects PRIMARY KEY([object_id]) ); CREATE TABLE dbo.Columns ( [object_id] int NOT NULL FOREIGN KEY REFERENCES dbo.Objects([object_id]), column_name sysname, CONSTRAINT PK_Columns PRIMARY KEY ([object_id],column_name) );
W przypadku testów wydajności wypełnimy to za pomocą sys.all_objects
i sys.all_columns
. Ale najpierw dla prostej demonstracji dodajmy następujące wiersze:
INSERT dbo.Objects([object_id],[object_name]) VALUES(1,N'Employees'),(2,N'Orders'); INSERT dbo.Columns([object_id],column_name) VALUES(1,N'EmployeeID'),(1,N'CurrentStatus'), (2,N'OrderID'),(2,N'OrderDate'),(2,N'CustomerID');
Jeśli fora są jakąkolwiek wskazówką, bardzo powszechnym wymogiem jest zwrócenie wiersza dla każdego obiektu wraz z rozdzieloną przecinkami listą nazw kolumn. (Ekstrapoluj to na dowolne typy jednostek, które modelujesz w ten sposób – nazwy produktów powiązane z zamówieniem, nazwy części biorących udział w montażu produktu, podwładni zgłaszający się do kierownika itp.) Tak więc na przykład z powyższymi danymi będziemy chcesz uzyskać taki wynik:
object columns --------- ---------------------------- Employees EmployeeID,CurrentStatus Orders OrderID,OrderDate,CustomerID
Sposób, w jaki moglibyśmy to osiągnąć w obecnych wersjach SQL Server, to prawdopodobnie użycie FOR XML PATH
, jak pokazałem, że jest najbardziej wydajny poza CLR w tym wcześniejszym poście. W tym przykładzie wyglądałoby to tak:
SELECT [object] = o.[object_name], [columns] = STUFF( (SELECT N',' + c.column_name FROM dbo.Columns AS c WHERE c.[object_id] = o.[object_id] FOR XML PATH, TYPE ).value(N'.[1]',N'nvarchar(max)'),1,1,N'') FROM dbo.Objects AS o;
Jak można się było spodziewać, otrzymujemy ten sam wynik, co pokazano powyżej. W SQL Server v.Next będziemy mogli wyrazić to prościej:
SELECT [object] = o.[object_name], [columns] = STRING_AGG(c.column_name, N',') FROM dbo.Objects AS o INNER JOIN dbo.Columns AS c ON o.[object_id] = c.[object_id] GROUP BY o.[object_name];
Ponownie daje to dokładnie takie same dane wyjściowe. Udało nam się to zrobić za pomocą funkcji natywnej, unikając kosztownego FOR XML PATH
rusztowanie i STUFF()
funkcja używana do usunięcia pierwszego przecinka (odbywa się to automatycznie).
A co z zamówieniem?
Jednym z problemów związanych z wieloma rozwiązaniami grupowych konkatenacji jest to, że kolejność listy oddzielonej przecinkami powinna być uważana za arbitralną i niedeterministyczną.
Dla XML PATH
rozwiązanie, które pokazałem w innym wcześniejszym poście, że dodanie ORDER BY
jest trywialne i gwarantowane. Tak więc w tym przykładzie możemy uporządkować listę kolumn według nazwy kolumny alfabetycznie, zamiast pozostawiać ją do sortowania SQL Server (lub nie):
SELECT [object] = [object_name], [columns] = STUFF( (SELECT N',' +c.column_name FROM dbo.Columns AS c WHERE c.[object_id] = o.[object_id] ORDER BY c.column_name -- only change FOR XML PATH, TYPE ).value(N'.[1]',N'nvarchar(max)'),1,1,N'') FROM dbo.Objects AS o;
Wyjście:
object columns --------- ---------------------------- Employees CurrentStatus,EmployeeID Order CustomerID,OrderDate,OrderID
CTP 1.1 dodaje WITHIN GROUP
do STRING_AGG()
, więc korzystając z nowego podejścia, możemy powiedzieć:
SELECT [object] = o.[object_name], [columns] = STRING_AGG(c.column_name, N',') WITHIN GROUP (ORDER BY c.column_name) -- only change FROM dbo.Objects AS o INNER JOIN dbo.Columns AS c ON o.[object_id] = c.[object_id] GROUP BY o.[object_name];
Teraz otrzymujemy te same wyniki. Zwróć uwagę, że tak jak w normalnym ORDER BY
klauzuli, możesz dodać wiele kolumn porządkowych lub wyrażeń wewnątrz WITHIN GROUP ()
.
W porządku, już wydajność!
Używając czterordzeniowych procesorów 2,6 GHz, 8 GB pamięci i SQL Server CTP1.1 (14.0.100.187), stworzyłem nową bazę danych, odtworzyłem te tabele i dodałem wiersze z sys.all_objects
i sys.all_columns
. Upewniłem się, że uwzględniam tylko obiekty, które mają co najmniej jedną kolumnę:
INSERT dbo.Objects([object_id], [object_name]) -- 656 rows SELECT [object_id], QUOTENAME(s.name) + N'.' + QUOTENAME(o.name) FROM sys.all_objects AS o INNER JOIN sys.schemas AS s ON o.[schema_id] = s.[schema_id] WHERE EXISTS ( SELECT 1 FROM sys.all_columns WHERE [object_id] = o.[object_id] ); INSERT dbo.Columns([object_id], column_name) -- 8,085 rows SELECT [object_id], name FROM sys.all_columns AS c WHERE EXISTS ( SELECT 1 FROM dbo.Objects WHERE [object_id] = c.[object_id] );
W moim systemie dało to 656 obiektów i 8085 kolumn (Twój system może dać nieco inne liczby).
Plany
Najpierw porównajmy plany i karty we/wy tabeli dla naszych dwóch nieuporządkowanych zapytań, używając Eksploratora planów. Oto ogólne dane dotyczące czasu działania:
Wskaźniki środowiska wykonawczego dla ścieżki XML PATH (na górze) i STRING_AGG() (na dole)
Plan graficzny i tabela I/O z FOR XML PATH
zapytanie:
I/O planu i tabeli dla ścieżki XML PATH, bez kolejności
I z STRING_AGG
wersja:
I/O planu i tabeli dla STRING_AGG, brak zamawiania
W przypadku tych ostatnich wyszukiwanie indeksu klastrowego wydaje mi się trochę kłopotliwe. Wydawało się, że to dobry przypadek do przetestowania rzadko używanego FORCESCAN
wskazówka (i nie, to z pewnością nie pomogłoby w FOR XML PATH
zapytanie):
SELECT [object] = o.[object_name], [columns] = STRING_AGG(c.column_name, N',') FROM dbo.Objects AS o INNER JOIN dbo.Columns AS c WITH (FORCESCAN) -- added hint ON o.[object_id] = c.[object_id] GROUP BY o.[object_name];
Teraz plan i karta I/O tabeli wyglądają dużo lepiej, przynajmniej na pierwszy rzut oka:
I/O planu i tabeli dla STRING_AGG(), bez porządkowania, z FORCESCAN
Uporządkowane wersje zapytań generują mniej więcej te same plany. Dla FOR XML PATH
wersji, dodaje się sortowanie:
Dodano sortowanie w wersji FOR XML PATH
Dla STRING_AGG()
, w tym przypadku wybierane jest skanowanie, nawet bez FORCESCAN
podpowiedź i nie jest wymagana dodatkowa operacja sortowania – więc plan wygląda identycznie jak FORCESCAN
wersja.
W skali
Spojrzenie na plan i jednorazowe metryki środowiska wykonawczego może dać nam pewne wyobrażenie o tym, czy STRING_AGG()
działa lepiej niż istniejący FOR XML PATH
rozwiązanie, ale większy test może mieć więcej sensu. Co się stanie, gdy wykonamy zgrupowaną konkatenację 5000 razy?
SELECT SYSDATETIME(); GO DECLARE @x nvarchar(max); SELECT @x = STRING_AGG(c.column_name, N',') FROM dbo.Objects AS o INNER JOIN dbo.Columns AS c ON o.[object_id] = c.[object_id] GROUP BY o.[object_name]; GO 5000 SELECT [string_agg, unordered] = SYSDATETIME(); GO DECLARE @x nvarchar(max); SELECT @x = STRING_AGG(c.column_name, N',') FROM dbo.Objects AS o INNER JOIN dbo.Columns AS c WITH (FORCESCAN) ON o.[object_id] = c.[object_id] GROUP BY o.[object_name]; GO 5000 SELECT [string_agg, unordered, forcescan] = SYSDATETIME(); GO DECLARE @x nvarchar(max); SELECT @x = STUFF((SELECT N',' +c.column_name FROM dbo.Columns AS c WHERE c.[object_id] = o.[object_id] FOR XML PATH, TYPE).value(N'.[1]',N'nvarchar(max)'),1,1,N'') FROM dbo.Objects AS o; GO 5000 SELECT [for xml path, unordered] = SYSDATETIME(); GO DECLARE @x nvarchar(max); SELECT @x = STRING_AGG(c.column_name, N',') WITHIN GROUP (ORDER BY c.column_name) FROM dbo.Objects AS o INNER JOIN dbo.Columns AS c ON o.[object_id] = c.[object_id] GROUP BY o.[object_name]; GO 5000 SELECT [string_agg, ordered] = SYSDATETIME(); GO DECLARE @x nvarchar(max); SELECT @x = STUFF((SELECT N',' +c.column_name FROM dbo.Columns AS c WHERE c.[object_id] = o.[object_id] ORDER BY c.column_name FOR XML PATH, TYPE).value(N'.[1]',N'nvarchar(max)'),1,1,N'') FROM dbo.Objects AS o ORDER BY o.[object_name]; GO 5000 SELECT [for xml path, ordered] = SYSDATETIME();
Po pięciokrotnym uruchomieniu tego skryptu uśredniłem liczby czasu trwania i oto wyniki:
Czas trwania (milisekundy) dla różnych metod łączenia grupowego
Widzimy, że nasz FORCESCAN
wskazówka naprawdę pogorszyła sprawę – podczas gdy przesunęliśmy koszt z wyszukiwania indeksu klastrowego, sortowanie było w rzeczywistości znacznie gorsze, mimo że szacunkowe koszty uznawały je za stosunkowo równoważne. Co ważniejsze, widzimy, że STRING_AGG()
oferuje korzyści związane z wydajnością, niezależnie od tego, czy połączone ciągi muszą być uporządkowane w określony sposób. Tak jak w przypadku STRING_SPLIT()
, który przeglądałem w marcu, jestem pod wrażeniem, że ta funkcja skaluje się dobrze przed wersją „v1”.
Mam zaplanowane dalsze testy, być może w przyszłym poście:
- Kiedy wszystkie dane pochodzą z jednej tabeli, z indeksem obsługującym porządkowanie lub bez niego
- Podobne testy wydajności w systemie Linux
W międzyczasie, jeśli masz konkretne przypadki użycia grupowej konkatenacji, udostępnij je poniżej (lub napisz do mnie na adres [email protected]). Zawsze jestem otwarty na upewnienie się, że moje testy są jak najbardziej rzeczywiste.