sql >> Databáze >  >> RDS >> Database

Překvapení a předpoklady výkonu:STRING_SPLIT()

Před více než třemi lety jsem zveřejnil třídílnou sérii o dělení strun:

  • Rozdělte řetězce správným způsobem – nebo dalším nejlepším způsobem
  • Rozdělení řetězců:Následná akce
  • Rozdělení řetězců:Nyní s méně T-SQL

V lednu jsem pak řešil trochu komplikovanější problém:

  • Porovnání metod dělení / zřetězení řetězců

Můj závěr byl:PŘESTAŇTE TO DĚLAT V T-SQL . Použijte CLR nebo, ještě lépe, předejte strukturované parametry, jako jsou DataTables, z vaší aplikace do parametrů s hodnotou tabulky (TVP) ve vašich procedurách, a vyhněte se tak veškeré konstrukci a dekonstrukci řetězců – což je skutečně ta část řešení, která způsobuje problémy s výkonem.

A pak přišel SQL Server 2016…

Když bylo vydáno RC0, byla zdokumentována nová funkce bez velkých fanfár:STRING_SPLIT . Rychlý příklad:

SELECT * FROM STRING_SPLIT('a,b,cd', ','); /* výsledek:hodnota -------- a b cd*/

Zaujalo to několik kolegů, včetně Davea Ballantyna, který psal o hlavních rysech – ale byl tak laskav, že mi nabídl první právo na odmítnutí srovnání výkonu.

Jedná se většinou o akademické cvičení, protože s velkou sadou omezení v první iteraci funkce to pravděpodobně nebude proveditelné pro velký počet případů použití. Zde je seznam postřehů, které jsme s Davem učinili, z nichž některá mohou v určitých scénářích narušit dohodu:

  • funkce vyžaduje, aby databáze byla na úrovni kompatibility 130;
  • přijímá pouze jednoznakové oddělovače;
  • není žádný způsob, jak přidat výstupní sloupce (jako sloupec označující pořadovou pozici v řetězci);
    • související, neexistuje způsob, jak ovládat řazení – jediné možnosti jsou libovolné a abecední ORDER BY value;
  • zatím vždy odhaduje 50 výstupních řádků;
  • při použití pro DML v mnoha případech získáte stolní cívku (pro ochranu Hallowe'en);
  • NULL vstup vede k prázdnému výsledku;
  • neexistuje žádný způsob, jak potlačit predikáty, jako je eliminace duplikátů nebo prázdných řetězců kvůli po sobě jdoucím oddělovačům;
  • neexistuje žádný způsob, jak provádět operace s výstupními hodnotami až poté (například mnoho rozdělovacích funkcí provádí LTRIM/RTRIM nebo explicitní konverze pro vás – STRING_SPLIT vyplivne všechno ošklivé, jako jsou například úvodní mezery).

Takže když jsou tato omezení otevřená, můžeme přejít k testování výkonu. Vzhledem k dosavadním zkušenostem společnosti Microsoft s vestavěnými funkcemi, které využívají CLR pod pokličkou (kašel FORMAT() kašel ), byl jsem skeptický ohledně toho, zda se tato nová funkce může přiblížit nejrychlejším metodám, které jsem dosud testoval.

Použijme rozdělovače řetězců k oddělení řetězců čísel oddělených čárkami, tímto způsobem může přijít a hrát i náš nový přítel JSON. A řekneme, že žádný seznam nesmí přesáhnout 8 000 znaků, takže žádný MAX typy jsou povinné, a protože jsou to čísla, nemusíme se zabývat ničím exotickým, jako je Unicode.

Nejprve si vytvoříme naše funkce, z nichž několik jsem upravil z prvního článku výše. Vynechal jsem pár, o kterém jsem neměl pocit, že by soutěžil; Nechám to jako cvičení na čtenáři, aby je otestoval.

    Tabulka čísel

    Tento opět potřebuje nějaké nastavení, ale může to být docela malý stůl kvůli umělým omezením, která umísťujeme:

    SET NOCOUNT ON; DECLARE @UpperLimit INT =8000;;WITH n AS( SELECT x =ROW_NUMBER() NAD (ORDER BY s1.[object_id]) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2)SELECT Number =x INTO dbo.Numbers OD n WHERE x MEZI 1 AND @UpperLimit;GOCREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number);

    Potom funkce:

    CREATE FUNCTION dbo.SplitStrings_Numbers( @List varchar(8000), @Delimiter char(1))VRACÍ TABULKU SE SCHEMABINDINGAS RETURN ( SELECT [Hodnota] =PODŘETĚZEC(@Seznam, [Číslo], CHARINDEX(L@Delimiter) + @Oddělovač, [Číslo]) - [Číslo]) Z dbo.Čísla WHERE Číslo <=LEN(@Seznam) A PODŘETĚZEC(@Oddělovač + @Seznam, [Číslo], 1) =@Delimiter );

    JSON

    Na základě přístupu, který poprvé odhalil tým storage engine, jsem vytvořil podobný obal kolem OPENJSON , jen si všimněte, že v tomto případě musí být oddělovačem čárka, nebo musíte před předáním hodnoty do nativní funkce provést nějakou náročnou náhradu řetězce:

    CREATE FUNCTION dbo.SplitStrings_JSON( @List varchar(8000), @Delimiter char(1) -- ignorováno, ale usnadněno automatické testování)RETURNS TABLE WITH SCHEMABINDINGAS RETURN (VYBRAT hodnotu FROM OPENJSON( CHAR(91) + @List + CHAR(93) ));

    CHAR(91)/CHAR(93) právě nahrazují [ a ] ​​kvůli problémům s formátováním.

    XML

    CREATE FUNCTION dbo.SplitStrings_XML( @List varchar(8000), @Delimiter char(1))VRÁTÍ TABULU SE SCHEMABINDINGAS RETURN (SELECT [value] =y.i.value('(./text())[1]', 'varchar(8000)') FROM (SELECT x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').dotaz ('.') ) JAKO KŘÍŽOVÉ POUŽITÍ x.nodes('i') JAKO y(i));

    CLR

    Znovu jsem si vypůjčil důvěryhodný dělicí kód Adama Machanice z doby před téměř sedmi lety, i když podporuje Unicode, MAX typů a víceznakových oddělovačů (a vlastně, protože se mi vůbec nechce zasahovat do kódu funkce, omezuje to naše vstupní řetězce na 4 000 znaků místo 8 000):

    VYTVOŘIT FUNKCI dbo.SplitStrings_CLR( @List nvarchar(MAX), @Delimiter nvarchar(255))TABULKA VRÁCENÍ (hodnota nvarchar(4000) )EXTERNÍ NÁZEV CLRUtilities.UserDefinedFunctions.SplitString_Mult 

    STRING_SPLIT

    Kvůli konzistenci jsem kolem STRING_SPLIT vložil obal :

    VYTVOŘIT FUNKCI dbo.SplitStrings_Native( @List varchar(8000), @Delimiter char(1))VRÁTÍ TABULKU SE SCHEMABINDINGAS RETURN (VYBRAT hodnotu FROM STRING_SPLIT(@List, @Delimiter));

Zdrojová data a kontrola zdraví

Vytvořil jsem tuto tabulku, aby sloužila jako zdroj vstupních řetězců pro funkce:

CREATE TABLE dbo.SourceTable( RowNum int IDENTITY(1,1) PRIMÁRNÍ KLÍČ, StringValue varchar(8000));;WITH x AS ( SELECT TOP (60000) x =STUFF((SELECT TOP (ABS(o.[id_objektu] % 20)) ',' + CONVERT(varchar(12), c.[id_objektu]) FROM sys.all_columns AS c WHERE c.[id_objektu]  

Jen pro informaci, ověřte, že se do tabulky vešlo 50 000 řádků, a zkontrolujte průměrnou délku řetězce a průměrný počet prvků na řetězec:

SELECT [Values] =COUNT(*), AvgStringLength =AVG(1,0*LEN(StringValue)), AvgElementCount =AVG(1,0*LEN(StringValue)-LEN(REPLACE(StringValue, ',','')) ) Z dbo.SourceTable; /* výsledek:Hodnoty AvgStringLength AbgElementCount ------ --------------- ---------------- 50000 108,476380 8,911840*/ 

A nakonec se ujistěte, že každá funkce vrací správná data pro jakékoli dané RowNum , takže jen náhodně vybereme jednu a porovnáme hodnoty získané každou metodou. Vaše výsledky se budou samozřejmě lišit.

SELECT f.value FROM dbo.SourceTable AS s CROSS APPLY dbo.SplitStrings_/* metoda */(s.StringValue, ',') AS f WHERE s.RowNum =37219 ORDER BY f.value;

Všechny funkce samozřejmě fungují podle očekávání (třídění není numerické; nezapomeňte, že funkce vydávají řetězce):

Ukázková sada výstupu z každé z funkcí

Testování výkonu

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

Spustil jsem výše uvedený kód 10krát pro každou metodu a zprůměroval jsem časování pro každou z nich. A tady pro mě přišlo překvapení. Vzhledem k omezením v nativním STRING_SPLIT Předpokládal jsem, že se to dá rychle dohromady a že výkon tomu dodá věrohodnost. Boy byl výsledek jiný, než jsem očekával:

Průměrná doba trvání STRING_SPLIT ve srovnání s jinými metodami

Aktualizace 20. 3. 2016

Na základě níže uvedené otázky od Larse jsem znovu provedl testy s několika změnami:

  • Monitoroval jsem svou instanci pomocí SQL Sentry Performance Advisor, abych během testu zachytil profil CPU;
  • Zachytil jsem statistiky čekání na úrovni relace mezi každou dávkou;
  • Mezi dávky jsem vložil prodlevu, aby byla aktivita na panelu Performance Advisor vizuálně odlišná.

Vytvořil jsem novou tabulku pro zachycení informací o stavu čekání:

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

Poté se kód pro každý test změnil na tento:

WAITFOR DELAY '00:00:30'; DECLARE @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, bod, wait_type, wait_time_ms)SELECT @d, test =/* 'method' */, point ='Start', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats WHERE session_id =ARESPID;GO DECL @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable JAKO s CROSS APPLY dbo.SplitStrings_/* metoda */(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;

Spustil jsem test a poté spustil následující dotazy:

-- ověřte, že časování bylo ve stejném sledu jako předchozí testy SELECT test, DATEDIFF(SECOND, MIN(dt), MAX(dt)) FROM dbo. Časování S (NOLOCK)GROUP BY test ORDER BY 2 DESC; -- určit okno, které se použije na řídicí panel Performance AdvisorSELECT MIN(dt), MAX(dt) FROM dbo.Timings; -- získat statistiky čekání registrované pro každou relaci 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) OD dbo.Časování JAKO f LEFT OUTER JOIN dbo.Časování JAKO s ON s.test =f.test AND s.wait_type =f.wait_type AND s.point ='Start' WHERE f.point ='Konec') AS x WHERE delta> 0ORDER BY rn, delta DESC;

Od prvního dotazu zůstaly načasování konzistentní s předchozími testy (uvedl bych je znovu, ale to by neodhalilo nic nového).

Na základě druhého dotazu jsem byl schopen zvýraznit tento rozsah na řídicím panelu Performance Advisor a odtud bylo snadné identifikovat každou dávku:

Dávky zachycené v grafu CPU na řídicím panelu Performance Advisor

Je zřejmé, že všechny metody *kromě* STRING_SPLIT po dobu trvání testu fixováno jedno jádro (jedná se o čtyřjádrový stroj a CPU byl stabilně na 25 %). Je pravděpodobné, že Lars naznačoval pod tímto STRING_SPLIT je rychlejší za cenu zatloukání CPU, ale nezdá se, že by tomu tak bylo.

Konečně, ze třetího dotazu jsem byl schopen vidět následující statistiky čekání narůstající po každé dávce:

Čekání na relaci, v milisekundách

Čekání zachycená DMV plně nevysvětlují dobu trvání dotazů, ale slouží k zobrazení toho, kde jsou další čekání vznikají.

Závěr

I když vlastní CLR stále vykazuje obrovskou výhodu oproti tradičním přístupům T-SQL a použití JSON pro tuto funkci se zdá být ničím jiným než novinkou, STRING_SPLIT byl jasný vítěz – o míli. Pokud tedy potřebujete pouze rozdělit řetězec a dokážete se vypořádat se všemi jeho omezeními, vypadá to, že je to mnohem schůdnější možnost, než bych očekával. Doufejme, že se v budoucích sestaveních dočkáme dalších funkcí, jako je výstupní sloupec označující pořadovou pozici každého prvku, možnost odfiltrovat duplikáty a prázdné řetězce a víceznakové oddělovače.

Několik komentářů níže řeším ve dvou následných příspěvcích:

  • STRING_SPLIT() v SQL Server 2016:Následná akce č. 1
  • STRING_SPLIT() v SQL Server 2016:Následná akce č. 2

  1. Připojení Pythonu k databázi MySQL pomocí konektoru MySQL a příkladu PyMySQL

  2. Vytvořte spouštěč „místo“ na serveru SQL Server

  3. Generujte náhodná celá čísla bez kolizí

  4. Zobrazení seznamu PostgreSQL