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

STRING_SPLIT() w SQL Server 2016:kontynuacja nr 2

Wcześniej w tym tygodniu opublikowałem kontynuację mojego ostatniego posta na temat STRING_SPLIT() w SQL Server 2016, adresując kilka komentarzy pozostawionych we wpisie i/lub przesłanych bezpośrednio do mnie:

  • STRING_SPLIT() w SQL Server 2016:Kontynuacja nr 1

Po tym, jak ten post został napisany, pojawiło się ostatnie pytanie od Douga Ellnera:

Jak te funkcje wypadają w porównaniu z parametrami wycenianymi w tabeli?

Teraz testowanie TVP było już na mojej liście przyszłych projektów, po ostatniej wymianie na Twitterze z @Nick_Craver na Stack Overflow. Powiedział, że byli podekscytowani, że STRING_SPLIT() działały dobrze, ponieważ były niezadowolone z wydajności wysyłania ~7000 wartości przez parametr wyceniany w tabeli.

Moje testy

Do tych testów użyłem SQL Server 2016 RC3 (13.0.1400.361) na 8-rdzeniowej maszynie wirtualnej z systemem Windows 10, z pamięcią masową PCIe i 32 GB pamięci RAM.

Stworzyłem prostą tabelę, która naśladowała to, co robili (wybierając około 10 000 wartości z tabeli zawierającej ponad 3 miliony wierszy), ale dla moich testów ma znacznie mniej kolumn i mniej indeksów:

CREATE TABLE dbo.Posts_Regular( PostID int PRIMARY KEY, HitCount int NOT NULL DEFAULT 0); INSERT dbo.Posts_Regular(PostID) SELECT TOP (3000000) ROW_NUMBER() OVER (ORDER BY s1.[object_id]) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2;

Stworzyłem również wersję In-Memory, ponieważ byłem ciekaw, czy jakieś podejście działałoby tam inaczej:

CREATE TABLE dbo.Posts_InMemory( PostID int PRIMARY KEY NIEROZKLOROWANY HASH WITH (BUCKET_COUNT =4000000), HitCount int NOT NULL DEFAULT 0) WITH (MEMORY_OPTIMIZED =ON);

Teraz chciałem stworzyć aplikację C#, która przekaże 10 000 unikalnych wartości, jako ciąg oddzielony przecinkami (zbudowany przy użyciu StringBuilder) lub jako TVP (przekazywany z DataTable). Chodziłoby o pobranie lub zaktualizowanie wybranych wierszy na podstawie dopasowania do elementu utworzonego przez podział listy lub wyraźnej wartości w TVP. Tak więc kod został napisany tak, aby dołączać co 300. wartość do ciągu lub tabeli DataTable (kod C# znajduje się w załączniku poniżej). Wziąłem funkcje, które stworzyłem w oryginalnym poście, zmieniłem je do obsługi varchar(max) , a następnie dodał dwie funkcje, które zaakceptował TVP – jedna z nich to optymalizacja pamięci. Oto typy tabel (funkcje znajdują się w załączniku poniżej):

CREATE TYPE dbo.PostIDs_Regular AS TABLE(PostID int PRIMARY KEY);GO CREATE TYPE dbo.PostIDs_InMemory AS TABLE(PostID int NOT NULL PRIMARY KEY NIECLUSTROWANY HASH WITH (BUCKET_COUNT =1000000)) WITH (MEMORY_ON); 

Musiałem również powiększyć tabelę Numbers, aby obsłużyć ciągi> 8K i elementy> 8K (zrobiłem to 1MM wierszy). Następnie utworzyłem siedem procedur składowanych:pięć z nich pobiera varchar(max) i łączenie się z wyjściem funkcji w celu aktualizacji tabeli bazowej, a następnie dwa, aby zaakceptować TVP i dołączyć bezpośrednio przeciwko niemu. Kod C# wywołuje każdą z tych siedmiu procedur z listą 10 000 wpisów do wybrania lub zaktualizowania 1000 razy. Procedury te znajdują się również w załączniku poniżej. Podsumowując, testowane metody to:

  • Natywny (STRING_SPLIT() )
  • XML
  • CLR
  • Tabela liczb
  • JSON (z jawnym int wyjście)
  • Parametr wyceniany w tabeli
  • Parametr zoptymalizowany pod kątem pamięci, wyceniany w tabeli

Przetestujemy pobieranie 10 000 wartości, 1000 razy, za pomocą DataReadera — ale bez iteracji przez DataReader, ponieważ to po prostu wydłużyłoby test i wymagałoby tyle samo pracy dla aplikacji C#, niezależnie od tego, w jaki sposób baza danych wyprodukował zestaw. Przetestujemy również aktualizację 10 000 wierszy, po 1000 razy każdy, za pomocą ExecuteNonQuery() . Przetestujemy zarówno zwykłą, jak i zoptymalizowaną pod kątem pamięci wersję tabeli Posts, którą możemy bardzo łatwo przełączać bez konieczności zmiany jakichkolwiek funkcji lub procedur, używając synonimu:

UTWÓRZ SYNONIM dbo.Posts DLA dbo.Posts_Regular; -- aby przetestować wersję zoptymalizowaną pod kątem pamięci:DROP SYNONYM dbo.Posts;CREATE SYNONYM dbo.Posts FOR dbo.Posts_InMemory; -- aby ponownie przetestować wersję dyskową:DROP SYNONYM dbo.Posts; CREATE SYNONYM dbo.Posts FOR dbo.Posts_Regular;

Uruchomiłem aplikację, uruchomiłem ją kilka razy dla każdej kombinacji, aby upewnić się, że kompilacja, buforowanie i inne czynniki nie były niesprawiedliwe w stosunku do partii wykonanej jako pierwsza, a następnie przeanalizowałem wyniki z tabeli rejestrowania (również wyrywkowo sprawdziłem sys. dm_exec_procedure_stats, aby upewnić się, że żadne z podejść nie miało znaczącego obciążenia związanego z aplikacją, a tak się nie stało).

Wyniki – Tabele na dyskach

Czasami zmagam się z wizualizacją danych – naprawdę próbowałem wymyślić sposób na przedstawienie tych danych na jednym wykresie, ale myślę, że było po prostu zbyt wiele punktów danych, aby wyróżniały się te najistotniejsze.

Możesz kliknąć, aby powiększyć którekolwiek z nich w nowej karcie/oknie, ale nawet jeśli masz małe okno, starałem się, aby zwycięzca był jasny poprzez użycie koloru (i zwycięzca był taki sam w każdym przypadku). A żeby było jasne, przez „średni czas trwania” rozumiem średni czas, jaki zajęło aplikacji wykonanie pętli 1000 operacji.

Średni czas trwania (milisekundy) dla opcji SELECT względem tabeli Posty z dysku

Średni czas trwania (milisekundy) aktualizacji w odniesieniu do tabeli Posty na dysku

Najbardziej interesującą rzeczą jest dla mnie to, jak słabo zoptymalizowany pod kątem pamięci TVP radził sobie podczas asystowania przy UPDATE . Okazuje się, że skanowanie równoległe jest obecnie zbyt agresywnie blokowane, gdy w grę wchodzi DML; Microsoft uznał to za lukę w funkcjach i ma nadzieję, że wkrótce ją naprawi. Zauważ, że skanowanie równoległe jest obecnie możliwe za pomocą SELECT ale jest teraz zablokowany dla DML. (Nie zostanie rozwiązany w SQL Server 2014, ponieważ te specyficzne operacje skanowania równoległego nie są tam dostępne dla żadnej operacji.) Kiedy to zostanie naprawione lub gdy twoje TVP są mniejsze i/lub równoległość i tak nie jest korzystna, powinieneś zobaczyć że TVP zoptymalizowane pod kątem pamięci będą działały lepiej (wzorzec po prostu nie działa dobrze w tym konkretnym przypadku użycia stosunkowo dużych TVP).

W tym konkretnym przypadku, oto plany dla SELECT (które mogłem zmusić do pracy równoległej) i UPDATE (czego nie mogłem):

Parallelism w planie SELECT łączącym tabelę dyskową z TVP w pamięci

Brak równoległości w planie UPDATE łączącym tabelę dyskową z pamięcią TVP

Wyniki – Tabele zoptymalizowane pod kątem pamięci

Tutaj trochę więcej spójności – cztery metody po prawej stronie są stosunkowo równe, podczas gdy trzy po lewej wydają się bardzo niepożądane. Zwróć również szczególną uwagę na skalę bezwzględną w porównaniu z tabelami opartymi na dyskach – w większości przypadków przy użyciu tych samych metod, a nawet bez równoległości, uzyskujesz znacznie szybsze operacje na tabelach zoptymalizowanych pod kątem pamięci, co prowadzi do niższego ogólnego wykorzystania procesora.

Średni czas trwania (milisekundy) dla opcji SELECT względem tabeli Posty zoptymalizowanej pod kątem pamięci

Średni czas trwania (milisekundy) aktualizacji w tabeli Posty zoptymalizowanej pod kątem pamięci

Wniosek

W tym konkretnym teście, z określonym rozmiarem danych, dystrybucją i liczbą parametrów oraz na moim konkretnym sprzęcie, JSON był konsekwentnym zwycięzcą (choć marginalnie). Jednak w przypadku niektórych innych testów w poprzednich postach inne podejścia wypadły lepiej. Tylko przykład tego, jak to, co robisz i gdzie to robisz, może mieć dramatyczny wpływ na względną skuteczność różnych technik. Oto rzeczy, które przetestowałem w tej krótkiej serii, wraz z moim podsumowaniem, którą technikę zastosować. użyj w takim przypadku i którego należy użyć jako drugiego lub trzeciego wyboru (na przykład, jeśli nie możesz zaimplementować środowiska CLR ze względu na zasady firmy lub ponieważ używasz Azure SQL Database lub nie możesz użyć JSON lub STRING_SPLIT() ponieważ nie korzystasz jeszcze z SQL Server 2016). Zauważ, że nie wróciłem i nie przetestowałem ponownie przypisania zmiennej i SELECT INTO skrypty wykorzystujące TVP – te testy zostały skonfigurowane przy założeniu, że masz już istniejące dane w formacie CSV, które i tak musiałyby zostać najpierw podzielone. Ogólnie rzecz biorąc, jeśli możesz tego uniknąć, nie ugniataj swoich zestawów w ciągi oddzielone przecinkami, IMHO.

Cel Pierwszy wybór Drugi wybór (i trzeci, w stosownych przypadkach)
Proste przypisanie zmiennych

STRING_SPLIT()

CLR, jeśli <2016
XML, jeśli nie ma CLR i <2016
WYBIERZ DO CLR

XML, jeśli nie ma CLR
WYBIERZ DO (bez buforowania)

CLR

Tabela liczb, jeśli nie ma CLR
WYBIERZ (bez szpuli + MAXDOP 1)

STRING_SPLIT()

CLR, jeśli <2016
Tabela liczb, jeśli nie ma CLR i <2016
SELECT łączenie dużej listy (opartej na dysku) JSON (int) TVP, jeśli <2016
SELECT łączenie dużej listy (zoptymalizowanej pod kątem pamięci) JSON (int) TVP, jeśli <2016
AKTUALIZUJ łączenie dużej listy (opartej na dysku) JSON (int) TVP, jeśli <2016
AKTUALIZUJ łączenie dużej listy (zoptymalizowanej pod kątem pamięci) JSON (int) TVP, jeśli <2016

Konkretne pytanie Douga:JSON, STRING_SPLIT() , a programy TVP działały średnio w tych testach dość podobnie — na tyle blisko, że programy TVP są oczywistym wyborem, jeśli nie korzystasz z SQL Server 2016. Jeśli masz różne przypadki użycia, te wyniki mogą się różnić. Świetnie .

Co prowadzi nas do morału tego historia:Ja i inni możemy przeprowadzić bardzo szczegółowe testy wydajności, obracając się wokół dowolnej funkcji lub podejścia, i dojść do wniosku, które podejście jest najszybsze. Ale jest tak wiele zmiennych, że nigdy nie będę miał pewności, by powiedzieć „to podejście jest zawsze najszybszy”. W tym scenariuszu bardzo starałem się kontrolować większość czynników, które przyczyniają się do tego, i chociaż JSON wygrał we wszystkich czterech przypadkach, możesz zobaczyć, jak te różne czynniki wpłynęły na czas wykonania (i drastycznie w przypadku niektórych podejść). zawsze warto konstruować własne testy i mam nadzieję, że pomogłem zilustrować, jak zabieram się do tego typu rzeczy.

Dodatek A:Kod aplikacji konsoli

Proszę nie czepiać się tego kodu; zostało to dosłownie połączone jako bardzo prosty sposób na uruchomienie tych procedur przechowywanych 1000 razy z prawdziwymi listami i tabelami DataTable złożonymi w C# oraz aby rejestrować czas trwania każdej pętli w tabeli (aby uwzględnić wszelkie koszty związane z aplikacją z obsługą albo duży ciąg lub kolekcję). Mógłbym dodać obsługę błędów, pętlę w inny sposób (np. konstruować listy wewnątrz pętli zamiast ponownego użycia pojedynczej jednostki pracy) i tak dalej.

używanie System;używanie System.Text;używanie System.Configuration;używanie System.Data;używanie System.Data.SqlClient; namespace SplitTesting{ class Program { static void Main(string[] args) { string operation ="Update"; if (args[0].ToString() =="-Wybierz") { operacja ="Wybierz"; } var csv =new StringBuilder(); Elementy DataTable =new DataTable(); elements.Columns.Add("wartość", typeof(int)); for (int i =1; i <=10000; i++) { csv.Append((i*300).ToString()); if (i <10000) { csv.Append(","); } elementy.Rows.Add(i*300); } string[] method ={ "Native", "CLR", "XML", "Numbers", "JSON", "TVP", "TVP_InMemory" }; using (SqlConnection con =new SqlConnection()) { con.ConnectionString =ConfigurationManager.ConnectionStrings["podstawowy"].ToString(); con.Open(); SqlParametr p; foreach (metoda łańcuchowa w metodach) { SqlCommand cmd =new SqlCommand("dbo." + operacja + "Posts_" + metoda, con); cmd.CommandType =CommandType.StoredProcedure; if (metoda =="TVP" || metoda =="TVP_InMemory") { cmd.Parameters.Add("@PostList", SqlDbType.Structured).Value =elementy; } else { cmd.Parameters.Add("@PostList", SqlDbType.VarChar, -1).Value =csv.ToString(); } var timer =System.Diagnostics.Stopwatch.StartNew(); for (int x =1; x <=1000; x++) { if (operacja =="Aktualizuj") { cmd.ExecuteNonQuery(); } else { SqlDataReader rdr =cmd.ExecuteReader(); rdr.Zamknij(); } } timer.Stop(); long this_time =timer.ElapsedMilliseconds; // czas logowania - procedura logowania dodaje czas zegarowy i // zapisy na podstawie pamięci/dysku (określane przez synonim) SqlCommand log =new SqlCommand("dbo.LogBatchTime", con); log.CommandType =CommandType.StoredProcedure; log.Parameters.Add("@Operation", SqlDbType.VarChar, 32).Value =operacja; log.Parameters.Add("@Method", SqlDbType.VarChar, 32).Value =metoda; log.Parameters.Add("@Timing", SqlDbType.Int).Value =this_time; log.ExecuteNonQuery(); Console.WriteLine(metoda + " :" + this_time.ToString()); } } } }}

Przykładowe użycie:

SplitTesting.exe -Wybierz
SplitTesting.exe -Aktualizuj

Dodatek B:Funkcje, procedury i tabela rejestrowania

Oto funkcje edytowane w celu obsługi varchar(max) (funkcja CLR już zaakceptowana nvarchar(max) i nadal nie chciałem próbować to zmienić):

CREATE FUNCTION dbo.SplitStrings_Native( @List varchar(max), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT [wartość] FROM STRING_SPLIT(@List, @Delimiter));GO CREATE FUNCTION dbo.SplitStrings_XML ( @List varchar(max), @Delimiter char(1))TABELA ZWROTÓW Z POWIĄZANIAMI SCHEMATÓW RETURN (SELECT [wartość] =y.i.value('(./text())[1]', 'varchar(max)') FROM (SELECT x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').query('.')) JAKO CROSS APPLY x.nodes('i') AS y(i));GO CREATE FUNCTION dbo.SplitStrings_Numbers( @List varchar(max), @Delimiter char(1))RETURNS TABLE WITH SCHEMABINDINGAS RETURN (SELECT [wartość] =SUBSTRING (@List, Number, CHARINDEX(@Delimiter, @List + @Delimiter, Number) - Number) FROM dbo.Numbers WHERE Number <=CONVERT(INT, LEN(@List)) AND SUBSTRING(@Delimiter + @List, Number , LEN(@Delimiter)) =@Delimiter );GO CREATE FUNCTION dbo.SplitStrings_JSON( @List varchar(max), @Delimiter char(1))TABELA ZWROTÓW Z SCH EMABINDINGAS RETURN (SELECT [wartość] FROM OPENJSON(CHAR(91) + @List + CHAR(93)) WITH (wartość int '$'));GO

A procedury składowane wyglądały tak:

CREATE PROCEDURE dbo.UpdatePosts_Native @PostList varchar(max)ASBEGIN UPDATE p SET HitCount +=1 FROM dbo.Posts AS p INNER JOIN dbo.SplitStrings_Native(@PostList, ',') AS s ON p.PostID =s [wartość];ENDGOCREATE PROCEDURE dbo.SelectPosts_Native @PostList varchar(max)ASBEGIN SELECT p.PostID, p.HitCount FROM dbo.Posts AS p INNER JOIN dbo.SplitStrings_Native(@PostList, ',') AS s ON p. s.[wartość];ENDGO-- powtórz dla 4 innych metod opartych na varchar(max) CREATE PROCEDURE dbo.UpdatePosts_TVP @PostList dbo.PostIDs_Regular READONLY -- przełącz _Regular na _InMemoryASBEGIN SET NOCOUNT ON; UPDATE p SET HitCount +=1 FROM dbo.Posts AS p INNER JOIN @PostList AS s ON p.PostID =s.PostID;ENDGOCREATE PROCEDURE dbo.SelectPosts_TVP @PostList dbo.PostIDs_Regular READONLY -- przełącz _Regular na SET_COUNTIn SELECT p.PostID, p.HitCount FROM dbo.Posts AS p INNER JOIN @PostList AS s ON p.PostID =s.PostID;ENDGO-- powtórz dla pamięci

I na koniec tabela i procedura logowania:

CREATE TABLE dbo.SplitLog( LogID int IDENTITY(1,1) PRIMARY KEY, ClockTime datetime NOT NULL DEFAULT GETDATE(), OperatingTable nvarchar(513) NOT NULL, -- Posts_InMemory lub Posts_Regular Operation varchar(32) NOT NULL DEFAULT „Aktualizuj” — lub wybierz metodę varchar(32) NOT NULL DEFAULT „Native” — lub TVP, JSON itp. Timing int NOT NULL DEFAULT 0);GO CREATE PROCEDURE dbo.LogBatchTime @Operation varchar(32), @Method varchar(32), @Timing intASBEGIN SET NOCOUNT ON; INSERT dbo.SplitLog(OperatingTable, Operation, Method, Timing) SELECT base_object_name, @Operation, @Method, @Timing FROM sys.synonyms WHERE name =N'Posts';ENDGO -- oraz zapytanie do wygenerowania wykresów:;WITH x AS( SELECT tabela operacyjna, operacja, metoda, czas, czas od przebycia =ROW_NUMBER() OVER (PARTYCJA BY tabela operacyjna, operacja, metoda ORDER BY ClockTime DESC) FROM dbo.SplitLog)SELECT tabela operacyjna, operacja, metoda, średni czas trwania =AVG (1,0*czas) FROM x WHERE Aktualność <=3GROUP WG tabeli operacyjnej, operacji, metody;

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Łączenie wartości kolumn w listę rozdzielaną przecinkami

  2. SQL Server 2016:sys.dm_exec_function_stats

  3. Automatyzacja defragmentacji indeksów w bazie danych MS SQL Server

  4. Sprawdź typ parametru funkcji partycji w SQL Server (T-SQL)

  5. Transponuj zestaw wierszy jako kolumny w SQL Server 2000