Това е петата и последна част от поредицата, обхващаща решения на предизвикателството за генериране на серии от числа. В част 1, част 2, част 3 и част 4 обхванах чисти T-SQL решения. Още в началото, когато публикувах пъзела, няколко души коментираха, че най-ефективното решение вероятно ще бъде базирано на CLR. В тази статия ще изпробваме това интуитивно предположение. По-конкретно, ще разгледам базирани на CLR решения, публикувани от Kamil Kosno и Adam Machanic.
Много благодаря на Алън Бърщайн, Джо Оббиш, Адам Мачаник, Кристофър Форд, Джеф Модън, Чарли, НоамГр, Камил Косно, Дейв Мейсън, Джон Нелсън #2, Ед Вагнер, Майкъл Бърбеа и Пол Уайт за споделянето на вашите идеи и коментари.
Ще направя тестването си в база данни, наречена testdb. Използвайте следния код, за да създадете базата данни, ако тя не съществува, и да активирате I/O и статистика за времето:
-- DB и statsSET NOCOUNT ON;SET STATISTICS IO, TIME ON;GO IF DB_ID('testdb') Е НУЛВ СЪЗДАВАНЕ НА БАЗА ДАННИ testdb;ИЗПОЛЗВАЙТЕ ИЗПОЛЗВАЙТЕ testdb;GO
За простота ще деактивирам строгата сигурност на CLR и ще направя базата данни надеждна, като използвам следния код:
-- Активирайте CLR, деактивирайте строгата сигурност на CLR и направете db trustworthyEXEC sys.sp_configure 'показване на разширени настройки', 1;RECONFIGURE; EXEC sys.sp_configure 'clr активиран', 1;EXEC sys.sp_configure 'clr строга сигурност', 0;RECONFIGURE; EXEC sys.sp_configure 'покажи разширени настройки', 0;RECONFIGURE; ПРОМЕНИ БАЗА ДАННИ testdb ВКЛЮЧИ НАДЕРЖЕН; ОТПРАВИ
По-ранни решения
Преди да разгледам базираните на CLR решения, нека бързо да прегледаме производителността на две от най-добре работещите T-SQL решения.
Най-добре представящото се решение на T-SQL, което не използваше никакви постоянни базови таблици (освен фиктивната празна таблица на columnstore за получаване на пакетна обработка) и следователно не включваше I/O операции, беше това, внедрено във функцията dbo.GetNumsAlanCharlieItzikBatch. Разгледах това решение в част 1.
Ето кода за създаване на фиктивната празна таблица за columnstore, която използва заявката на функцията:
ПРОСТЪПНЕТЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА dbo.BatchMe;GO СЪЗДАЙТЕ ТАБЛИЦА dbo.BatchMe(col1 INT NOT NULL, INDEX idx_cs CLUSTERED COLUMNSTORE);GO
А ето и кода с дефиницията на функцията:
СЪЗДАВАНЕ ИЛИ ПРОМЕНЯНЕ НА ФУНКЦИЯ dbo.GetNumsAlanCharlieItzikBatch(@low AS BIGINT =1, @high AS BIGINT) ВРЪЩА ТАБЛИЦИ ВРЪЩАНЕ С L0 AS (ИЗБЕРЕТЕ 1 КАТО c ОТ (СТОЙНОСТИ(1),(1),(1),(1) ),(1),(1),(1),(1), (1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ), L1 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L0 КАТО КРЪСТО СЪЕДИНЕНИЕ L0 AS B ), L2 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L1 КАТО КРЪСТО СОЕДИНЕНИЕ L1 КАТО B ), L3 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L2 КАТО КРЪСТО ПРИСЪЕДИНЕТЕ L2 AS B ), Nums AS ( ИЗБЕРЕТЕ ROW_NUMBER() НАД (ПОРЪЧАЙТЕ ОТ (ИЗБЕРЕТЕ NULL)) КАТО rownum ОТ L3 ) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n ОТ Nums LEFT OUTER JOIN dbo.BatchMe ON 1 =0 ORDER BY rownum;GO
Нека първо да тестваме функцията, изискваща серия от 100 милиона числа, с MAX агрегат, приложен към колона n:
ИЗБЕРЕТЕ MAX(n) КАТО mx ОТ dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ОПЦИЯ(MAXDOP 1);
Припомнете си, че тази техника за тестване избягва предаването на 100 милиона реда към обаждащия се, а също така избягва усилията в режим на ред, свързани с присвояването на променлива, когато се използва техниката за присвояване на променлива.
Ето статистическите данни за времето, които получих за този тест на моята машина:
Време на процесора =6719 мс, изминало време =6742 мс .Изпълнението на тази функция, разбира се, не произвежда никакви логически четения.
След това нека го тестваме с ред, като използваме техниката за присвояване на променлива:
ДЕКЛАРИРАНЕ @n КАТО ГОЛЯМО; ИЗБЕРЕТЕ @n =n ОТ dbo.GetNumsAlanCharlieItzikBatch(1, 100000000) ПОРЪЧАЙТЕ ПО n ОПЦИЯ(MAXDOP 1);
Получих следната статистика за времето за това изпълнение:
Време на процесора =9468 мс, изминало време =9531 мс .Припомнете си, че тази функция не води до сортиране при поискване на данните, подредени по n; по принцип получавате същия план, независимо дали поискате поръчаните данни или не. Можем да припишем по-голямата част от допълнителното време в този тест в сравнение с предишния на присвояването на променливи, базирани на 100M редов режим.
Най-добре представящото се решение на T-SQL, което използваше постоянна базова таблица и следователно доведе до някои I/O операции, макар и много малко, беше решението на Пол Уайт, внедрено във функцията dbo.GetNums_SQLkiwi. Разгледах това решение в част 4.
Ето кода на Пол за създаване както на таблицата columnstore, използвана от функцията, така и на самата функция:
-- Helper columnstore tableDROP TABLE IF EXISTS dbo.CS; -- 64K реда (достатъчно за 4B реда при кръстосано съединение)-- колона 1 винаги е нула-- колона 2 е (1...65536)SELECT -- въведете като цяло число НЕ NULL -- (всичко е нормализирано до 64 бита в columnstore/batch режим все пак) n1 =ISNULL(CONVERT(цяло число, 0), 0), n2 =ISNULL(CONVERT(integer, N.rn), 0)INTO dbo.CSFROM ( SELECT rn =ROW_NUMBER() НАД (ПОРЪЧАЙТЕ ПО @@SPID) ОТ master.dbo.spt_values КАТО SV1 CROSS JOIN master.dbo.spt_values КАТО SV2 ПОРЪЧАЙ ПО rn ASC ОТМЕСТВАНЕ 0 РЕДОВЕ ИЗВЛЕЧВАНЕ СЛЕДВАЩО 65536 РЕДА САМО) КАТО N; -- Единична компресирана група редове от 65 536 реда СЪЗДАЙТЕ КЛУСТРИРАН ИНДЕКС НА COLUMNSTORE CCI НА dbo.CS С (MAXDOP =1);GO -- Функцията СЪЗДАВАНЕ ИЛИ ПРОМЕНЯВА ФУНКЦИЯ dbo.GetNums_SQLkiwi( @low bigint =1), @high RETURNS ИЗБИРАНЕ на таблицата .rn, n =@low - 1 + N.rn, op =@high + 1 - N.rn FROM ( SELECT -- Използвайте @@TRANCOUNT вместо @@SPID, ако харесвате всичките си заявки serial rn =ROW_NUMBER() НАД (ПОРЪЧКА ОТ @@SPID ASC) ОТ dbo.CS КАТО N1 ПРИСЪЕДИНЕТЕ dbo.CS КАТО N2 -- Хеш кръстосано свързване в пакетен режим -- Цело число, а не нулев тип данни, избягване на остатъчния хеш сонда -- Това винаги е 0 =0 НА N2. n1 =N1.n1 КЪДЕ -- Опитайте се да избегнете SQRT при отрицателни числа и активирайте опростяването -- към единично константно сканиране, ако @low> @high (с литерали) -- Няма филтри за стартиране в пакетен режим @high>=@low -- Груб филтър:-- Ограничете всяка страна на кръстосаното присъединяване към SQRT(целев брой редове) -- IIF избягва SQRT при отрицателни числа с параметри AND N1.n2 <=CONVERT(цяло число, CEILING(SQRT(CONVERT(float, IIF(@high>=@low, @high) - @low + 1, 0))))) И N2.n2 <=CONVERT(цяло число, CEILING(SQRT(CONVERT(float, IIF(@high>=@low, @high - @low + 1, 0)) ))) ) КАТО Н КЪДЕ -- Прецизен филтър:-- Пакетен режим филтриране на ограниченото кръстосано свързване до точния брой необходими редове -- Избягва оптимизатора да въвежда режим на ред отгоре със следния режим на ред изчислява скалар @low - 2 + N.rn <@high;GO
Нека първо го тестваме без поръчка, използвайки агрегатната техника, което води до план за всички партидни режими:
ИЗБЕРЕТЕ MAX(n) КАТО mx ОТ dbo.GetNums_SQLkiwi(1, 100000000) ОПЦИЯ(MAXDOP 1);
Получих следното време и I/O статистика за това изпълнение:
Време на процесора =2922 мс, изминало време =2943 мс .Таблица „CS“. Брой на сканиране 2, логически четения 0, физически четения 0, сървър на страници чете 0, четене напред 0, сървър за четене напред чете 0, лоб логически четения 44 , лобно физическо четене 0, сървърът на лоб страница чете 0, лобно четене напред чете 0, сървърът на lob страница чете напред 0.
Таблица 'CS'. Сегментът чете 2, сегментът е пропуснат 0.
Нека тестваме функцията с ред, използвайки техниката за присвояване на променлива:
ДЕКЛАРИРАНЕ @n КАТО ГОЛЯМО; ИЗБЕРЕТЕ @n =n ОТ dbo.GetNums_SQLkiwi(1, 100000000) ПОРЪЧАЙТЕ ПО n ОПЦИЯ(MAXDOP 1);
Подобно на предишното решение, и това решение избягва изричното сортиране в плана и следователно получава същия план, независимо дали поискате подредените данни или не. Но отново, този тест носи допълнително наказание, главно поради използваната тук техника за присвояване на променливи, което води до обработка на частта за присвояване на променлива в плана в режим на ред.
Ето времето и I/O статистиката, която получих за това изпълнение:
Време на процесора =6985 мс, изминало време =7033 мс .Таблица „CS“. Брой на сканирането 2, логически четения 0, физически четения 0, сървър на страници чете 0, четене напред 0, сървър за четене напред чете 0, лоб логически четения 44 , лобно физическо четене 0, сървърът на лоб страница чете 0, лобно четене напред чете 0, сървърът на lob страница чете напред 0.
Таблица 'CS'. Сегментът чете 2, сегментът е пропуснат 0.
Решения за CLR
И Камил Косно, и Адам Мачаник първо предоставиха просто решение само за CLR, а по-късно излязоха с по-сложна комбинация CLR+T-SQL. Ще започна с решенията на Камил и след това ще разгледам решенията на Адам.
Решения от Камил Косно
Ето CLR кода, използван в първото решение на Kamil за дефиниране на функция, наречена GetNums_KamilKosno1:
използване на System;използване на System.Data.SqlTypes;използване на System.Collections;публичен частичен клас GetNumsKamil1{ [Microsoft.SqlServer.Server.SqlFunction(FillRowMethodName ="GetNums_KamilKosno1_Fill") "IenKamlKosnoator stamil", "IenKamlKosnoator IGN", "IenKamlKosnoiGt" (SqlInt64 ниско, SqlInt64 високо) { return (low.IsNull || high.IsNull) ? new GetNumsCS(0, 0) :нов GetNumsCS(ниска.стойност, висока.стойност); } public static void GetNums_KamilKosno1_Fill(Object o, out SqlInt64 n) { n =(long)o; } частен клас GetNumsCS :IEnumerator { public GetNumsCS(дълго от, дълго до) { _lowrange =от; _ток =_нисък диапазон - 1; _висок диапазон =до; } public bool MoveNext() { _current +=1; if (_current> _highrange) върне false; else върне true; } публичен обект Current { get { return _current; } } public void Reset() { _current =_lowrange - 1; } дълги _ниски обхвати; дълъг _ток; дълъг _висок обхват; }}
Функцията приема два входа, наречени low и high и връща таблица с колона BIGINT, наречена n. Функцията е поточно предаване, връщайки ред със следващото число в поредицата на заявка за ред от заявката за извикване. Както можете да видите, Камил избра по-формализирания метод за внедряване на интерфейса на IEnumerator, който включва внедряване на методите MoveNext (предвижва преброителя, за да получи следващия ред), Current (получава реда в текущата позиция на преброителя) и Reset (задава преброителя до началната му позиция, която е преди първия ред).
Променливата, съдържаща текущия номер в поредицата, се нарича _current. Конструкторът задава _current на долната граница на искания диапазон минус 1, както и за метода Reset. Методът MoveNext увеличава _current с 1. След това, ако _current е по-голям от горната граница на заявения диапазон, методът връща false, което означава, че няма да бъде извикан отново. В противен случай той връща true, което означава, че ще бъде извикан отново. Методът Current естествено връща _current. Както можете да видите, доста елементарна логика.
Извиках проекта на Visual Studio GetNumsKamil1 и използвах пътя C:\Temp\ за него. Ето кода, който използвах за внедряване на функцията в базата данни testdb:
ИЗПУСКАНЕ ФУНКЦИЯТА, АКО СЪЩЕСТВУВА dbo.GetNums_KamilKosno1; ИЗПУСКАНЕ НА АСЕМБЛИЯ, АКО СЪЩЕСТВУВА GetNumsKamil1;GO СЪЗДАЙТЕ АСЕМБЛИЯ GetNumsKamil1 ОТ 'C:\Temp\GetNumsKamil1\GetNumsKamil1\bin\Debug\GetNumsKamil1.dll';GO CREATE FUNCTION @ints_KamilSKamil1.dll ПОВТОРНО СЪЗДАДЕТЕ ФУНКЦИЯ @INTs_KamilSigNh INT TABLE(n BIGINT) ORDER(n) КАТО ВЪНШНО ИМЕ GetNumsKamil1.GetNumsKamil1.GetNums_KamilKosno1;GO
Забележете използването на клаузата ORDER в израза CREATE FUNCTION. Функцията излъчва редовете в n ред, така че когато редовете трябва да бъдат погълнати в плана в n ред, въз основа на тази клауза SQL Server знае, че може да избегне сортиране в плана.
Нека тестваме функцията, първо с агрегатната техника, когато поръчката не е необходима:
ИЗБЕРЕТЕ MAX(n) КАТО mx ОТ dbo.GetNums_KamilKosno1(1, 100000000);
Получих плана, показан на фигура 1.
Фигура 1:План за функция dbo.GetNums_KamilKosno1
Няма какво да се каже за този план, освен факта, че всички оператори използват режим на изпълнение на редове.
Получих следната статистика за времето за това изпълнение:
Време на процесора =37375 мс, изминало време =37488 мс .И разбира се, не бяха включени логически четения.
Нека тестваме функцията с ред, като използваме техниката за присвояване на променлива:
ДЕКЛАРИРАНЕ @n КАТО ГОЛЯМО; ИЗБЕРЕТЕ @n =n ОТ dbo.GetNums_KamilKosno1(1, 100000000) ПОРЪЧАЙТЕ ПО n;
Получих плана, показан на фигура 2 за това изпълнение.
Фигура 2:План за функция dbo.GetNums_KamilKosno1 с ORDER BY
Забележете, че в плана няма сортиране, тъй като функцията е създадена с клаузата ORDER(n). Въпреки това има известни усилия да се гарантира, че редовете наистина се излъчват от функцията в обещания ред. Това се прави с помощта на операторите Segment и Sequence Project, които се използват за изчисляване на номера на редове, и оператора Assert, който прекратява изпълнението на заявката, ако тестът се провали. Тази работа има линейно мащабиране – за разлика от мащабирането n log n, което бихте получили, ако се изискваше сортиране – но все още не е евтино. Получих следните статистически данни за времето за този тест:
Време на процесора =51531 мс, изминало време =51905 мс .Резултатите биха могли да бъдат изненадващи за някои – особено тези, които интуитивно предполагаха, че базираните на CLR решения ще работят по-добре от T-SQL. Както можете да видите, времето за изпълнение е с порядък по-дълго, отколкото при нашето най-ефективно T-SQL решение.
Второто решение на Kamil е CLR-T-SQL хибрид. Отвъд ниските и високите входове, функцията CLR (GetNums_KamilKosno2) добавя стъпков вход и връща стойности между ниски и високи, които са на стъпка една от друга. Ето CLR кода, който Камил използва във второто си решение:
използване на System;използване на System.Data.SqlTypes;използване на System.Collections; публичен частичен клас GetNumsKamil2{ [Microsoft.SqlServer.Server.SqlFunction(DataAccess =Microsoft.SqlServer.Server.DataAccessKind.None, IsDeterministic =true, IsPrecise =true, FillRowMethodName ="GetNuml"] public IEnumerator GetNums_KamilKosno2(SqlInt64 low, SqlInt64 high, SqlInt64 step) { return (low.IsNull || high.IsNull) ? new GetNumsCS(0, 0, step.Value) :new GetNumsCS(low.Value, high.Value, step.Value); } public static void GetNums_Fill(Object o, out SqlInt64 n) { n =(long)o; } частен клас GetNumsCS :IEnumerator { public GetNumsCS(дълго от, дълга до, дълга стъпка) { _lowrange =от; _стъпка =стъпка; _current =_lowrange - _step; _висок диапазон =до; } public bool MoveNext() { _current =_current + _step; if (_current> _highrange) върне false; else върне true; } публичен обект Current { get { return _current; } } public void Reset() { _current =_lowrange - _step; } дълги _ниски обхвати; дълъг _ток; дълъг _висок обхват; дълга _стъпка; }}
Нарекох VS проекта GetNumsKamil2, поставих го също в пътя C:\Temp\ и използвах следния код, за да го разгърна в базата данни testdb:
-- Създаване на асембли и функции ИЗПУСКАНЕ НА ФУНКЦИЯ, АКО СЪЩЕСТВУВА dbo.GetNums_KamilKosno2; ИЗПУСКАНЕ НА АСЕМБЛИЯ, АКО СЪЩЕСТВУВА GetNumsKamil2;GO СЪЗДАВАНЕ НА СБОР GetNumsKamil2 ОТ 'C:\Temp\GetNumsKamil2\GetNums2Debull' C:\Temp\GetNumsKamil2\GetNums2Debull' .GetNums_KamilKosno2 (@low AS BIGINT =1, @high КАТО BIGINT, @step AS BIGINT) ВРЪЩА ТАБЛИЦА(n BIGINT) ORDER(n) КАТО ВЪНШНО ИМЕ GetNumsKamil2.GetNumsKamil2.GetNums_KamilKosno2;GOGO;
Като пример за използване на функцията, ето заявка за генериране на стойности между 5 и 59, със стъпка от 10:
ИЗБЕРЕТЕ n ОТ dbo.GetNums_KamilKosno2(5, 59, 10);
Този код генерира следния изход:
n---51525354555
Що се отнася до частта T-SQL, Kamil използва функция, наречена dbo.GetNums_Hybrid_Kamil2, със следния код:
СЪЗДАВАНЕ ИЛИ ПРОМЕНЯНЕ НА ФУНКЦИЯ dbo.GetNums_Hybrid_Kamil2(@low AS BIGINT, @high AS BIGINT) ВРЪЩА TABLEASRETURN SELECT TOP (@high - @low + 1) V.n ОТ dbo.GetNums_KamilKosno2(@low, @h AS BIGINT) КРЪСТНО ПРИЛАГАНЕ (СТОЙНОСТИ(0+GN.n),(1+GN.n),(2+GN.n),(3+GN.n),(4+GN.n), (5+GN.n) ),(6+GN.n),(7+GN.n),(8+GN.n),(9+GN.n)) AS V(n);GO
Както можете да видите, функцията T-SQL извиква функцията CLR със същите @low и @high входове, които получава, и в този пример използва размер на стъпка от 10. Заявката използва CROSS APPLY между резултата на функцията CLR и конструктор на стойност на таблица, който генерира крайните числа чрез добавяне на стойности в диапазона от 0 до 9 към началото на стъпката. Филтърът TOP се използва, за да се гарантира, че няма да получите повече от заявения брой номера.
Важно: Трябва да подчертая, че Камил прави предположение тук за прилагането на TOP филтъра въз основа на подреждането на номерата на резултата, което всъщност не е гарантирано, тъй като заявката няма клауза ORDER BY. Ако или добавите клауза ORDER BY, за да поддържате TOP, или замените TOP с филтър WHERE, за да гарантирате детерминиран филтър, това може напълно да промени профила на производителност на решението.
Във всеки случай, нека първо тестваме функцията без ред, използвайки агрегатната техника:
ИЗБЕРЕТЕ MAX(n) КАТО mx ОТ dbo.GetNums_Hybrid_Kamil2(1, 100000000);
Получих плана, показан на фигура 3 за това изпълнение.
Фигура 3:План за функция dbo.GetNums_Hybrid_Kamil2
Отново всички оператори в плана използват режим на изпълнение на редове.
Получих следната статистика за времето за това изпълнение:
Време на процесора =13985 мс, изминало време =14069 мс .И естествено няма логично четене.
Нека тестваме функцията с ред:
ДЕКЛАРИРАНЕ @n КАТО ГОЛЯМО; ИЗБЕРЕТЕ @n =n ОТ dbo.GetNums_Hybrid_Kamil2(1, 100000000) ПОРЪЧАЙТЕ ПО n;
Получих плана, показан на фигура 4.
Фигура 4:План за функция dbo.GetNums_Hybrid_Kamil2 с ORDER BY
Тъй като числата на резултатите са резултат от манипулирането на долната граница на стъпката, върната от функцията CLR и делтата, добавена в конструктора на стойност на таблицата, оптимизаторът не вярва, че числата на резултатите са генерирани в заявения ред и добавя изрично сортиране към плана.
Получих следната статистика за времето за това изпълнение:
Време на процесора =68703 мс, изминало време =84538 мс .Така че изглежда, че когато не е необходима поръчка, второто решение на Камил се справя по-добре от първото. Но когато е необходима поръчка, е обратното. Така или иначе, решенията на T-SQL са по-бързи. Лично аз бих се доверил на правилността на първото решение, но не и на второто.
Решения от Адам Мачаник
Първото решение на Адам също е основна CLR функция, която непрекъснато увеличава брояча. Само вместо да използва по-ангажирания формализиран подход като Камил, Адам използва по-опростен подход, който извиква командата yield на ред, която трябва да бъде върната.
Ето CLR кода на Адам за първото му решение, дефиниращо функция за поточно предаване, наречена GetNums_AdamMachanic1:
използване на System.Data.SqlTypes;използване на System.Collections; публичен частичен клас GetNumsAdam1{ [Microsoft.SqlServer.Server.SqlFunction( FillRowMethodName ="GetNums_AdamMachanic1_fill", TableDefinition ="n BIGINT")] публичен статичен IEnumerable GetNums_Adams_Adammachanic16. var max_int =макс. стойност; for (; min_int <=max_int; min_int++) { връщане на доходност (min_int); } } public static void GetNums_AdamMachanic1_fill(object o, out long i) { i =(long)o; }};
Решението е толкова елегантно в своята простота. Както можете да видите, функцията приема два входа, наречени min и max, представляващи ниската и високата гранични точки на заявения диапазон и връща таблица с BIGINT колона, наречена n. Функцията инициализира променливи, наречени min_int и max_int със стойностите на входните параметри на съответната функция. След това функцията изпълнява цикъл, докато min_int <=max_int, който при всяка итерация дава ред с текущата стойност на min_int и увеличава min_int с 1. Това е всичко.
Нарекох проекта GetNumsAdam1 в VS, поставих го в C:\Temp\ и използвах следния код, за да го разгърна:
- Създаване на сглобяване и функция на функция, ако съществува dbo.getnums_adammachanic1; капка монтаж, ако съществува getNumsadam1; отидете да създадете сглобяване getNumsadam1 от 'c:\ temp \ getNumsadam1 \ getNumsadam1 \ bin \ debug \ getNumsadam1.dll'; Go създаване на функция Do .GetNums_AdamMachanic1(@low AS BIGINT =1, @high AS BIGINT) ВРЪЩА ТАБЛИЦА(n BIGINT) ORDER(n) КАТО ВЪНШНО ИМЕ GetNumsAdam1.GetNumsAdam1.GetNums_AdamMachanic1;GO
Използвах следния код, за да го тествам с агрегатната техника, за случаи, когато редът няма значение:
ИЗБЕРЕТЕ MAX(n) КАТО mx ОТ dbo.GetNums_AdamMachanic1(1, 100000000);
Получих плана, показан на фигура 5 за това изпълнение.
Фигура 5:План за функция dbo.GetNums_AdamMachanic1
Планът е много подобен на плана, който видяхте по-рано за първото решение на Kamil, и същото важи и за неговото представяне. Получих следната статистика за времето за това изпълнение:
Време на процесора =36687 мс, изминало време =36952 мс .И разбира се, не бяха необходими логически четения.
Нека тестваме функцията с ред, като използваме техниката за присвояване на променлива:
ДЕКЛАРИРАНЕ @n КАТО ГОЛЯМО; ИЗБЕРЕТЕ @n =n ОТ dbo.GetNums_AdamMachanic1(1, 100000000) ПОРЪЧАЙТЕ ПО n;
Получих плана, показан на фигура 6 за това изпълнение.
Фигура 6:План за функция dbo.GetNums_AdamMachanic1 с ORDER BY
Отново планът изглежда подобен на този, който видяхте по-рано за първото решение на Камил. Нямаше нужда от изрично сортиране, тъй като функцията беше създадена с клаузата ORDER, но планът включва известна работа, за да се провери дали редовете наистина се връщат подредени, както е обещано.
Получих следната статистика за времето за това изпълнение:
Време на процесора =55047 мс, изминало време =55498 мс .Във второто си решение Адам също комбинира CLR част и T-SQL част. Ето описанието на Адам за логиката, която използва в своето решение:
„Опитвах се да помисля как да заобикаля проблема с чатовостта на SQLCLR, а също и основното предизвикателство на този генератор на числа в T-SQL, което е фактът, че не можем просто да създаваме магически редове.
CLR е добър отговор за втората част, но разбира се е възпрепятстван от първия проблем. Така че като компромис създадох T-SQL TVF [наречен GetNums_AdamMachanic2_8192] твърдо кодиран със стойности от 1 до 8192. (Доста произволен избор, но твърде голям и QO започва малко да се задавя.) След това модифицирах моята CLR функция [ с име GetNums_AdamMachanic2_8192_base], за да изведе две колони, "max_base" и "base_add", и да изведе редове като:
- max_base, base_add
——————
8191, 1
8192, 8192
8192, 16384
…
8192, 99991552
257, 99999744
Сега това е обикновен цикъл. CLR изходът се изпраща към T-SQL TVF, който е настроен да връща само до "max_base" редове от своя твърдо кодиран набор. И за всеки ред добавя "base_add" към стойността, като по този начин генерира необходимите числа. Ключът тук, според мен, е, че можем да генерираме N реда само с едно логическо кръстосано свързване, а функцията CLR трябва да върне само 1/8192 толкова редове, така че е достатъчно бърза, за да действа като основен генератор.“
Логиката изглежда доста ясна.
Ето кода, използван за дефиниране на функцията CLR, наречена GetNums_AdamMachanic2_8192_base:
използване на System.Data.SqlTypes;използване на System.Collections; публичен частичен клас GetNumsAdam2{ private struct row { public long max_base; публична дълга база_добавяне; } [Microsoft.SqlServer.Server.SqlFunction( FillRowMethodName ="GetNums_AdamMachanic2_8192_base_fill", TableDefinition ="max_base int, base_add int")] public static IEnumerable GetNums_Adammachanic2_8192_base_fill, TableDefinition ="max_base int, base_add int")] public static IEnumerable GetNums. var max_int =макс. стойност; var min_group =min_int / 8192; var max_group =max_int / 8192; for (; min_group <=max_group; min_group++) { if (min_int> max_int) прекъсване на доходността; var max_base =8192 - (min_int % 8192); if (min_group ==max_group &&max_int <(((max_int / 8192) + 1) * 8192) - 1) max_base =max_int - min_int + 1; връщане на доходност ( нов ред() { max_base =max_base, base_add =min_int }); min_int =(min_group + 1) * 8192; } } public static void GetNums_AdamMachanic2_8192_base_fill(object o, out long max_base, out long base_add) { var r =(row)o; max_base =r.max_base; base_add =r.base_add; }};
Нарекох VS проекта GetNumsAdam2 и го поставих в пътя C:\Temp\ както при другите проекти. Ето кода, който използвах за внедряване на функцията в базата данни testdb:
-- Създаване на сборка и функция. ИЗПУСКАНЕ НА ФУНКЦИЯ, АКО СЪЩЕСТВУВА dbo.GetNums_AdamMachanic2_8192_base;ИЗПУСКАНЕ НА АСЕМБЛИЯ, АКО СЪЩЕСТВУВА GetNumsAdam2;GO СЪЗДАДЕТЕ АСЕМБЛИЯ GetNumsAdam2 ОТ 'C:\Temp\GetNumsAdam2 FROM 'C:\Temp\GetNumsGetsAdam2FROM 'C:\Temp\GetNumsGetsAdam2FROM'C:\Temp\GetNumsGetsAdam2FROM'C:\Temp\GetNumsGetAmsAmbC\Temp\GetNumsGetSA\DamsAdam2Gum\GetNumsAdam2GET .GetNums_AdamMachanic2_8192_base(@max_base КАТО BIGINT, @add_base КАТО BIGINT) ВРЪЩА ТАБЛИЦА(max_base BIGINT, base_add BIGINT) ORDER(base_add) КАТО ВЪНШНО ИМЕ GetNumsAdams1damsMachamnic_8.Ето пример за използване на GetNums_AdamMachanic2_8192_base с диапазона от 1 до 100M:
ИЗБЕРЕТЕ * ОТ dbo.GetNums_AdamMachanic2_8192_base(1, 100000000);Този код генерира следния изход, показан тук в съкратена форма:
max_base base_add-------------------- --------------------8191 18192 81928192 163848192 245768192 32768...8192 999669768192 999751688192 999833608192 99991552257 99999744 (засегнати 12208 реда)Ето кода с дефиницията на T-SQL функцията GetNums_AdamMachanic2_8192 (съкратено):
СЪЗДАВАНЕ ИЛИ ПРОМЕНЯНЕ НА ФУНКЦИЯ dbo.GetNums_AdamMachanic2_8192(@max_base КАТО BIGINT, @add_base КАТО BIGINT) ВРЪЩА TABLEASRETURN SELECT TOP (@max_base) V.i + @add_base КАТО VAL FROM (0), (1 VALUES) (3), (4), ... (8187), (8188), (8189), (8190), (8191) ) AS V(i);GOВажно: Тук също трябва да подчертая, че подобно на това, което казах за второто решение на Kamil, тук Адам прави предположение, че ТОП филтърът ще извлече горните редове въз основа на реда на появата на редове в конструктора на стойностите на таблицата, което всъщност не е гарантирано. Ако добавите клауза ORDER BY за поддръжка на TOP или промените филтъра на филтър WHERE, ще получите детерминиран филтър, но това може напълно да промени профила на производителност на решението.
И накрая, ето най-външната T-SQL функция, dbo.GetNums_AdamMachanic2, която крайният потребител извиква, за да получи серия от числа:
СЪЗДАВАНЕ ИЛИ ПРОМЕНЯНЕ НА ФУНКЦИЯ dbo.GetNums_AdamMachanic2(@low AS BIGINT =1, @high AS BIGINT) ВРЪЩА TABLEASRETURN SELECT Y.val AS n FROM ( SELECT max_base, base_add FROM dbo.GetNums_AdamMachanic, @high AS BIGINT) X КРЪСТ ПРИЛОЖИ dbo.GetNums_AdamMachanic2_8192(X.max_base, X.base_add) КАТО YGOТази функция използва оператора CROSS APPLY за прилагане на вътрешната T-SQL функция dbo.GetNums_AdamMachanic2_8192 на ред, върната от вътрешната CLR функция dbo.GetNums_AdamMachanic2_8192_base.
Нека първо тестваме това решение, използвайки агрегатната техника, когато редът няма значение:
ИЗБЕРЕТЕ MAX(n) КАТО mx ОТ dbo.GetNums_AdamMachanic2(1, 100000000);Получих плана, показан на фигура 7 за това изпълнение.
Фигура 7:План за функция dbo.GetNums_AdamMachanic2
Получих следните статистически данни за времето за този тест:
SQL Server време за анализиране и компилиране :CPU време =313 ms, изминало време =339 ms .
SQL Server време на изпълнение :време на процесора =8859 мс, изминало време =8849 мс .Не бяха необходими логически четения.
Времето за изпълнение не е лошо, но забележете голямото време за компилиране поради използвания голям конструктор на стойност на таблица. Вие бихте платили толкова голямо време за компилиране, независимо от размера на диапазона, който поискате, така че това е особено трудно, когато използвате функцията с много малки диапазони. И това решение все още е по-бавно от тези на T-SQL.
Нека тестваме функцията с ред:
ДЕКЛАРИРАНЕ @n КАТО ГОЛЯМО; ИЗБЕРЕТЕ @n =n ОТ dbo.GetNums_AdamMachanic2(1, 100000000) ПОРЪЧАЙТЕ ПО n;Получих плана, показан на фигура 8 за това изпълнение.
Фигура 8:План за функция dbo.GetNums_AdamMachanic2 с ORDER BY
Подобно на второто решение на Kamil, в плана е необходимо изрично сортиране, което води до значително намаляване на производителността. Ето статистическите данни за времето, които получих за този тест:
Време за изпълнение:време на процесора =54891 мс, изминало време =60981 мс .Плюс това, все още има голямото наказание за време за компилиране от около една трета от секундата.
Заключение
Беше интересно да се тестват базирани на CLR решения за предизвикателството на числовите серии, защото много хора първоначално предположиха, че най-ефективното решение вероятно ще бъде базирано на CLR. Камил и Адам използваха подобни подходи, като първият опит използва прост цикъл, който увеличава брояча и дава ред със следващата стойност на итерация, и по-сложния втори опит, който комбинира CLR и T-SQL части. Лично аз не се чувствам комфортно от факта, че и във второто решение на Камил, и във второто решение на Адам те разчитаха на недетерминистичен TOP филтър и когато го преобразувах в детерминиран в собственото си тестване, това имаше неблагоприятно въздействие върху производителността на решението . Either way, our two T-SQL solutions perform better than the CLR ones, and do not result in explicit sorting in the plan when you need the rows ordered. So I don’t really see the value in pursuing the CLR route any further. Figure 9 has a performance summary of the solutions that I presented in this article.
Figure 9:Time performance comparison
To me, GetNums_AlanCharlieItzikBatch should be the solution of choice when you require absolutely no I/O footprint, and GetNums_SQKWiki should be preferred when you don’t mind a small I/O footprint. Of course, we can always hope that one day Microsoft will add this critically useful tool as a built-in one, and hopefully if/when they do, it will be a performant solution that supports batch processing and parallelism. So don’t forget to vote for this feature improvement request, and maybe even add your comments for why it’s important for you.
I really enjoyed working on this series. I learned a lot during the process, and hope that you did too.