Преди повече от три години публикувах серия от три части за разделяне на струни:
- Разделяйте низовете по правилния начин – или по следващия най-добър начин
- Разделяне на струни:последващо действие
- Разделяне на низове:Вече с по-малко T-SQL
Тогава през януари се заех с малко по-сложен проблем:
- Сравняване на методите за разделяне/конкатенация на низове
През цялото време заключението ми беше:СПРЕТЕ ДА ПРАВИТЕ ТОВА В T-SQL . Използвайте CLR или, още по-добре, предайте структурирани параметри като DataTables от вашето приложение към параметри с таблично стойности (TVPs) във вашите процедури, като избягвате изцяло конструкцията и деконструкцията на низовете – което всъщност е частта от решението, която причинява проблеми с производителността.
И тогава се появи SQL Server 2016...
Когато RC0 беше пуснат, нова функция беше документирана без много фанфари:STRING_SPLIT
. Бърз пример:
SELECT * FROM STRING_SPLIT('a,b,cd', ','); /* резултат:стойност -------- a b cd*/
Това привлече погледите на няколко колеги, включително Дейв Балантайн, който пише за основните характеристики – но беше достатъчно любезен да ми предложи първо право на отказ при сравнение на производителността.
Това е предимно академично упражнение, тъй като с огромен набор от ограничения в първата итерация на функцията, вероятно няма да е осъществимо за голям брой случаи на употреба. Ето списъка с наблюденията, които Дейв и аз направихме, някои от които може да са нарушители на сделки в определени сценарии:
- функцията изисква базата данни да е на ниво съвместимост 130;
- приема само разделители от един знак;
- няма начин за добавяне на изходни колони (като колона, указваща редна позиция в низа);
- свързано, няма начин да се контролира сортирането – единствените опции са произволни и азбучни
ORDER BY value
;
- свързано, няма начин да се контролира сортирането – единствените опции са произволни и азбучни
- досега винаги оценява 50 изходни реда;
- когато го използвате за DML, в много случаи ще получите шпула за маса (за защита на Хелоуин);
NULL
въвеждането води до празен резултат;- няма начин да натискате предикати надолу, като елиминиране на дубликати или празни низове поради последователни разделители;
- няма начин да се извършват операции срещу изходните стойности преди това (например много функции за разделяне изпълняват
LTRIM/RTRIM
или изрични реализации за вас –STRING_SPLIT
изплюва обратно всичко грозно, като водещи интервали).
Така че с тези ограничения на открито, можем да преминем към някои тестове на производителността. Като се има предвид опитът на Microsoft с вградени функции, които използват CLR под завивките (кашлица FORMAT()
кашлица ), бях скептичен относно това дали тази нова функция може да се доближи до най-бързите методи, които съм тествал досега.
Нека използваме разделители на низове, за да разделим низове от числа, разделени със запетая, по този начин нашият нов приятел JSON може да дойде и да играе също. И ще кажем, че нито един списък не може да надвишава 8000 знака, така че няма MAX
типове са задължителни и тъй като те са числа, не е нужно да се занимаваме с нещо екзотично като Unicode.
Първо, нека създадем нашите функции, някои от които адаптирах от първата статия по-горе. Изпуснах една двойка, която не смятах, че ще се състезава; Ще го оставя като упражнение на читателя да ги тества.
Таблица с числа
Тази отново се нуждае от настройка, но може да бъде доста малка маса поради изкуствените ограничения, които поставяме:
ЗАДАДЕТЕ NOCOUNT ON; ДЕКЛАРИРАНЕ @UpperLimit INT =8000;;WITH n AS( SELECT x =ROW_NUMBER() НАД (РЕД s1.[object_id]) FROM sys.all_objects КАТО s1 КРЪСТО ПРИСЪЕДИНЕТЕ sys.all_objects AS s2)SELECT Number =x INTO dbo.Числа ОТ n, КЪДЕ x МЕЖДУ 1 И И. @UpperLimit;GOCREATE УНИКАЛЕН КЛУСТРИРАН ИНДЕКС n НА dbo.Numbers(Number);
След това функцията:
СЪЗДАВАНЕ НА ФУНКЦИЯ dbo.SplitStrings_Numbers( @List varchar(8000), @Delimiter char(1))ВЪЗРАЩА ТАБЛИЦА С ВЪЗРАЩАНЕ НА SCHEMABINDINGAS (ИЗБЕРЕТЕ [Стойност] =SUBSTRING(@List, [Number], CHARINDEX, @Listmimi + @Delimiter, [Number]) - [Number]) FROM dbo.Numbers WHERE Номер <=LEN(@List) AND SUBSTRING(@Delimiter + @List, [Number], 1) =@Delimiter );
JSON
Въз основа на подход, разкрит за първи път от екипа на механизма за съхранение, създадох подобна обвивка около OPENJSON
, просто имайте предвид, че разделителят трябва да бъде запетая в този случай или трябва да направите някаква тежка подмяна на низове, преди да предадете стойността в естествената функция:
СЪЗДАВАНЕ НА ФУНКЦИЯ dbo.SplitStrings_JSON( @List varchar(8000), @Delimiter char(1) -- игнорирано, но прави автоматичното тестване по-лесно) ВРЪЩА ТАБЛИЦА СЪС SCHEMABINDINGAS ВРЪЩАНЕ (ИЗБЕРЕТЕ стойност ОТ OPENJSON( CHAR(91) + @List + CHAR(93) ));
CHAR(91)/CHAR(93) просто заменят съответно [ и ] поради проблеми с форматирането.
XML
СЪЗДАВАНЕ НА ФУНКЦИЯ dbo.SplitStrings_XML( @List varchar(8000), @Delimiter char(1))ВЪРНА ТАБЛИЦА С ВЪЗРАЩАНЕ НА SCHEMABINDINGAS (SELECT [value] =y.i.value('(./text())[1]', 'varchar(8000)') ОТ (SELECT x =CONVERT(XML, '' + REPLACE(@List, @Delimiter, '') + '').заявка ('.') ) КАТО КРЪСТО ПРИЛОЖИ x.nodes('i') КАТО y(i));
CLR
Отново взех назаем надеждния код за разделяне на Адам Мачаник от преди почти седем години, въпреки че поддържа Unicode, MAX
типове и многозначни разделители (и всъщност, тъй като изобщо не искам да се забърквам с кода на функцията, това ограничава нашите входни низове до 4000 знака вместо 8000):
СЪЗДАВАНЕ НА ФУНКЦИЯ dbo.SplitStrings_CLR( @List nvarchar(MAX), @Delimiter nvarchar(255))ВЪЗРАЩАВА ТАБЛИЦА (стойност nvarchar(4000) )ВЪНШНО ИМЕ CLRUtilities.UserDefinedFunctions.SplitString_Multi>STRING_SPLIT
Само за последователност поставих обвивка около
STRING_SPLIT
:СЪЗДАВАНЕ НА ФУНКЦИЯ dbo.SplitStrings_Native( @List varchar(8000), @Delimiter char(1))ВЪРНА ТАБЛИЦА С ВЪЗРАЩАНЕ НА SCHEMABINDINGAS (ИЗБЕРЕТЕ стойност ОТ STRING_SPLIT(@List, @Delimiter));Изходни данни и проверка за здравина
Създадох тази таблица, за да служи като източник на входни низове към функциите:
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. КАТО c КЪДЕ c.[object_id]Само за справка, нека потвърдим, че 50 000 реда са попаднали в таблицата и да проверим средната дължина на низа и средния брой елементи на низ:
SELECT [Стойности] =COUNT(*), AvgStringLength =AVG(1.0*LEN(StringValue)), AvgElementCount =AVG(1.0*LEN(StringValue)-LEN(REPLACE(StringValue, ',','')) ) ОТ dbo.SourceTable; /* резултат:Стойности AvgStringLength AbgElementCount ------ --------------- --------------- 50000 108.476380 8.911840*/предварително>И накрая, нека се уверим, че всяка функция връща правилните данни за даден
RowNum
, така че просто ще изберем един произволно и ще сравним стойностите, получени чрез всеки метод. Резултатите ви ще варират, разбира се.ИЗБЕРЕТЕ f.value ОТ dbo.SourceTable AS s КРЪСТО ПРИЛОЖИ dbo.SplitStrings_/* метод */(s.StringValue, ',') КАТО f WHERE s.RowNum =37219 ORDER BY f.value;Разбира се, всички функции работят според очакванията (сортирането не е числово; запомнете, функциите извеждат низове):
Примерен набор от изходни данни от всяка от функциите
Тестване на производителността
SELECT SYSDATETIME();GODECLARE @x VARCHAR(8000);SELECT @x =f.value FROM dbo.SourceTable AS s КРЪСТО ПРИЛОЖИ dbo.SplitStrings_/* метод */(s.StringValue,',') AS f;GO 100SELECT SYSDATETIME();Изпълних горния код 10 пъти за всеки метод и осредних времето за всеки. И тук дойде изненадата за мен. Предвид ограниченията в родния
STRING_SPLIT
функция, моето предположение беше, че тя беше събрана бързо и че производителността ще даде достоверност на това. Момче резултатът беше различен от това, което очаквах:Средна продължителност на STRING_SPLIT в сравнение с други методи
Актуализация 20.03.2016
Въз основа на въпроса по-долу от Ларс, проведох тестовете отново с няколко промени:
- Наблюдавах моя екземпляр със SQL Sentry Performance Advisor, за да заснема профил на процесора по време на теста;
- Записах статистика на изчакване на ниво сесия между всяка партида;
- Вмъкнах забавяне между партидите, така че дейността да се различава визуално на таблото за управление на Performance Advisor.
Създадох нова таблица за събиране на информация за изчакване:
СЪЗДАВАНЕ НА ТАБЛИЦА dbo.Timings(dt datetime, test varchar(64), point varchar(64), session_id smallint, wait_type nvarchar(60), wait_time_ms bigint,);След това кодът за всеки тест се промени на това:
ИЗЧАКВАНЕ НА ЗАБАВАНЕ '00:00:30'; ДЕКЛАРИРАНЕ @d DATETIME =SYSDATETIME(); INSERT dbo.Timings(dt, test, point, wait_type, wait_time_ms)SELECT @d, test =/* 'method' */, point ='Start', wait_type, wait_time_msFROM sys.dm_exec_session_wait_stats КЪДЕ session_id =@@SPID; ОБЯВЯВАЙТЕ @x VARCHAR(8000);SELECT @x =f.value ОТ dbo.SourceTable AS s КРЪСТО ПРИЛОЖИ dbo.SplitStrings_/* метод */(s.StringValue, ',') КАТО fGO 100 ДЕКЛАРИРАНЕ @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;Изпълних теста и след това изпълних следните заявки:
-- потвърдете, че таймингата е била на същия принцип като предишните тестове SELECT test, DATEDIFF(SECOND, MIN(dt), MAX(dt)) ОТ dbo.Timings WITH (NOLOCK)GROUP BY test ORDER BY 2 DESC; -- определяне на прозореца, който да се приложи към таблото за управление на Performance Advisor ИЗБЕРЕТЕ MIN(dt), MAX(dt) ОТ dbo.Timings; -- получавате регистрирани статистически данни за чакане за всеки тест sessionSELECT, 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. ='Край') AS x WHERE delta> 0ORDER BY rn, delta DESC;От първото запитване времето остана в съответствие с предишните тестове (бих ги начертил отново, но това няма да разкрие нищо ново).
От втората заявка успях да подчертая този диапазон на таблото за управление на Performance Advisor и от там беше лесно да идентифицирам всяка партида:
Партии, заснети в диаграмата на процесора на таблото за управление на Performance Advisor
Ясно е, че всички методи *освен*
STRING_SPLIT
фиксира едно ядро за времетраенето на теста (това е четириядрена машина и процесорът беше стабилно на 25%). Вероятно Ларс намеква под тозиSTRING_SPLIT
е по-бързо с цената на удряне на процесора, но не изглежда, че това е така.И накрая, от третата заявка, успях да видя следната статистика за чакане, натрупваща се след всяка партида:
Изчакване на сесия, в милисекунди
Изчакванията, заснети от DMV, не обясняват напълно продължителността на заявките, но служат, за да покажат къде допълнителни възникват изчаквания.
Заключение
Докато персонализираният CLR все още показва огромно предимство пред традиционните подходи на T-SQL и използването на JSON за тази функционалност изглежда не е нищо повече от новост,
STRING_SPLIT
беше категоричен победител – с една миля. Така че, ако просто трябва да разделите низ и можете да се справите с всичките му ограничения, изглежда, че това е много по-жизнеспособен вариант, отколкото бих очаквал. Надяваме се, че в бъдещи компилации ще видим допълнителна функционалност, като изходна колона, указваща порядковата позиция на всеки елемент, възможност за филтриране на дублирани и празни низове и разделители от няколко знака.Отправям няколко коментара по-долу в две последващи публикации:
- STRING_SPLIT() в SQL Server 2016 :Продължение №1
- STRING_SPLIT() в SQL Server 2016 :Продължение №2