Наскоро ме запознах с ново предизвикателство за острови от моя приятел Ерланд Сомарског. Базира се на въпрос от обществен форум за база данни. Самото предизвикателство не е сложно за справяне, когато се използват добре познати техники, които основно използват функции на прозорци. Тези техники обаче изискват изрично сортиране въпреки наличието на поддържащ индекс. Това се отразява на мащабируемостта и времето за реакция на решенията. Заобиколен от предизвикателства, се заех да намеря решение за минимизиране на броя на явните оператори за сортиране в плана или още по-добре, да премахна напълно необходимостта от тях. И намерих такова решение.
Ще започна с представянето на обобщена форма на предизвикателството. След това ще покажа две решения, базирани на познати техники, последвани от новото решение. Накрая ще сравня ефективността на различните решения.
Препоръчвам ви да опитате да намерите решение, преди да приложите моето.
Предизвикателството
Ще представя обобщена форма на оригиналното предизвикателство за островите на Ерланд.
Използвайте следния код, за да създадете таблица, наречена T1, и да я попълните с малък набор от примерни данни:
ЗАДАДЕТЕ NOCOUNT ON; ИЗПОЛЗВАЙТЕ tempdb; ИЗПУСКАНЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА dbo.T1 СЪЗДАВАНЕ НА ТАБЛИЦА dbo.T1( grp VARCHAR(10) NOT NULL, ord INT NOT NULL, val VARCHAR(10) НЕ NULL, ОГРАНИЧЕНИЕ PK_T1 ПЪРВИЧЕН КЛЮЧ(grp, ord IN));GO DBO INSERT. T1(grp, ord, val) СТОЙНОСТИ („Група A“, 1002, „Y“), („Група A“, 1003, „Y“), („Група A“, 1005, „Y“), (“ Група A“, 1007, „N“, („Група A“, 1011, „N“), („Група A“, 1013, „N“), („Група A“, 1017, „Y“), („Група A“, 1019, „Y“), („Група A“, 1023, „N“), („Група A“, 1029, „N“), („Група B“, 1001, „X“ ), („Група B“, 1002, „X“), („Група B“, 1003, „Z“), („Група B“, 1005, „Z“), („Група B“, 1008, „ Z“), („Група B“, 1013, „Z“), („Група B“, 1021, „Y“), („Група B“, 1034, „Y“);
Предизвикателството е следното:
Приемайки разделяне въз основа на колоната grp и подреждане въз основа на колоната ord, изчислете последователни номера на редове, започващи с 1 във всяка последователна група от редове със същата стойност в колоната val. Следва желаният резултат за дадения малък набор от примерни данни:
grp ord val seqno-------- ----- ---- ------Група A 1002 Y 1 Група A 1003 Y 2 Група A 1005 Y 3 Група A 1007 N 1 Група A 1011 N 2Group A 1013 N 3Group A 1017 Y 1Group A 1019 Y 2Group A 1023 N 1Group A 1029 N 2Group B 1001 X 1Group B 1002 X 2Group B 1003 Z 1Group B 1005 Z 2Group B 1008 Z 3Group B 1013 Z 4Group B 1021 Y 1Group B 1034 Y 2
Обърнете внимание на дефиницията на ограничението на първичния ключ въз основа на съставния ключ (grp, ord), което води до клъстериран индекс, базиран на същия ключ. Този индекс може потенциално да поддържа прозоречни функции, разделени от grp и подредени по ord.
За да тествате производителността на вашето решение, ще трябва да попълните таблицата с по-големи обеми примерни данни. Използвайте следния код, за да създадете помощната функция GetNums:
СЪЗДАВАНЕ НА ФУНКЦИЯ dbo.GetNums(@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 AS B ), L3 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L2 КАТО КРЪСТНО ПРИСЪЕДИНЯВАНЕ L2 AS B ), Nums AS ( ИЗБЕРЕТЕ ROW_NUMBER() НАД (ПОРЪЧАЙТЕ BY (SELECT NULL)) КАТО rownum ОТ L3 ) SELECT TOP(@high - @low + 1) rownum AS rn, @high + 1 - rownum AS op, @low - 1 + rownum AS n FROM Nums ORDER BY rownum;GO
Използвайте следния код, за да попълните T1, след като промените параметрите, представляващи броя на групите, броя на редовете на група и броя на отделните стойности, както желаете:
DECLARE @numgroups AS INT =1000, @rowspergroup AS INT =10000, -- тест с 1000 до 10000 тук @uniquevalues AS INT =5; ПРОМЕНИ ТАБЛИЦА dbo.T1 ОГРАНИЧЕНИЕ ЗА ОТПУСКАНЕ PK_T1; ТАБЛИЦА ОТСЪЖАНЕ dbo.T1; INSERT INTO dbo.T1 WITH(TABLOCK) (grp, ord, val) SELECT CAST(G.n AS VARCHAR(10)) AS grp, CAST(R.n AS INT) AS ord, CAST(ABS(CHECKSUM(NEWID())) % @uniquevalues + 1 КАТО VARCHAR(10)) КАТО val FROM dbo.GetNums(1, @numgroups) КАТО G КРЪСТО ПРИСЪЕДИНЯВАНЕ dbo.GetNums(1, @rowspergroup) AS R; ПРОМЕНИ ТАБЛИЦА dbo.T1 ДОБАВЯНЕ НА ОГРАНИЧЕНИЕ PK_T1 ПЪРВИЧЕН КЛЮЧ КОЛУСТРИРАН(grp, ord);
В моите тестове за производителност попълних таблицата с 1000 групи, между 1000 и 10 000 реда на група (така че 1M до 10M реда) и 5 различни стойности. Използвах SELECT INTO
оператор за запис на резултата във временна таблица.
Моята тестова машина има четири логически процесора, работещи със SQL Server 2019 Enterprise.
Предполагам, че използвате среда, предназначена да поддържа пакетен режим в магазина на редове или директно, например, като използвате SQL Server 2019 Enterprise издание като моето, или индиректно, чрез създаване на фиктивен индекс на columnstore в таблицата.
Запомнете, допълнителни точки, ако успеете да излезете с ефективно решение без изрично сортиране в плана. Успех!
Необходим ли е оператор за сортиране при оптимизирането на функциите на прозореца?
Преди да разгледам решенията, малко предистория за оптимизация, така че това, което ще видите в плановете за заявка по-късно, ще има повече смисъл.
Най-често срещаните техники за решаване на задачи за острови като нашата включват използване на някаква комбинация от функции на обобщени и/или прозоречни функции за класиране. SQL Server може да обработва такива функции на прозореца, като използва или серия от по-стари оператори за режим на ред (Segment, Sequence Project, Segment, Window Spool, Stream Aggregate) или по-новия и обикновено по-ефективен оператор Window Aggregate в пакетен режим.
И в двата случая операторите, обработващи изчислението на функцията на прозореца, трябва да приемат данните, подредени от елементите за разделяне и подреждане на прозореца. Ако нямате поддържащ индекс, естествено SQL Server ще трябва да въведе оператор за сортиране в плана. Например, ако имате множество функции на прозореца във вашето решение с повече от една уникална комбинация от разделяне и подреждане, вие сте длъжни да имате изрично сортиране в плана. Но какво ще стане, ако имате само една уникална комбинация от разделяне и подреждане и поддържащ индекс?
Методът на по-стария режим на ред може да разчита на предварително подредени данни, погълнати от индекс, без да е необходим изричен оператор за сортиране както в сериен, така и в паралелен режим. По-новият оператор на пакетен режим елиминира голяма част от неефективността на по-старата оптимизация на режима на ред и има присъщите предимства на обработката в пакетен режим. Въпреки това, текущата му паралелна реализация изисква междинен оператор за паралелен режим на сортиране, дори когато е налице поддържащ индекс. Само серийната му реализация може да разчита на реда на индекса без оператор Sort. Това е всичко, което трябва да кажем, когато оптимизаторът трябва да избере стратегия за обработка на вашата функция на прозореца, като приемем, че имате поддържащ индекс, обикновено това ще бъде една от следните четири опции:
- Режим на ред, сериен, без сортиране
- Режим на ред, паралелен, без сортиране
- Пакетен режим, сериен, без сортиране
- Пакетен режим, паралелно, сортиране
Който и да доведе до най-ниската цена на плана, ще бъде избран, като разбира се, че са изпълнени предпоставките за паралелизъм и пакетен режим, когато се разглеждат тези. Обикновено, за да може оптимизаторът да оправдае паралелен план, ползите от паралелизма трябва да надвишават допълнителната работа като синхронизиране на нишки. При опция 4 по-горе ползите от паралелизма трябва да надвишават обичайната допълнителна работа, свързана с паралелизма, плюс допълнителното сортиране.
Докато експериментирах с различни решения на нашето предизвикателство, имах случаи, когато оптимизаторът избра вариант 2 по-горе. Той го избра въпреки факта, че методът на редовия режим включва няколко неефективности, тъй като ползите от паралелизъм и липса на сортиране доведоха до план с по-ниска цена от алтернативите. В някои от тези случаи налагането на сериен план с намек доведе до опция 3 по-горе и до по-добра производителност.
Имайки предвид този фон, нека разгледаме решенията. Ще започна с две решения, разчитащи на известни техники за задачи на островите, които не могат да избегнат изричното сортиране в плана.
Решение, базирано на известна техника 1
Следва първото решение на нашето предизвикателство, което се основава на техника, която е известна от известно време:
WITH C AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) - ROW_NUMBER() OVER(PARTITION BY grp, val ORDER BY ord) КАТО остров FROM dbo.T1)SELECT grp, ord, val , ROW_NUMBER() НАД (ДЯЛ ПО grp, val, island ORDER BY ord) КАТО seqnoFROM C;
Ще го нарека като Известно решение 1.
CTE, наречен C, се основава на заявка, която изчислява два номера на реда. Първият (ще го наричам rn1) е разделен на grp и подреден по ord. Вторият (ще го наричам rn2) е разделен на grp и val и е подреден по ord. Тъй като rn1 има пропуски между различни групи от една и съща val, а rn2 не, разликата между rn1 и rn2 (колона, наречена остров) е уникална константна стойност за всички редове с еднакви стойности на grp и val. Следва резултатът от вътрешната заявка, включително резултатите от изчисленията на числа от два реда, които не се връщат като отделни колони:
grp ord val rn1 rn2 island-------- ----- ---- ---- ---- -------Група A 1002 Y 1 1 0Група A 1003 Y 2 2 0 Група A 1005 Y 3 3 0 Група A 1007 N 4 1 3 Група A 1011 N 5 2 3 Група A 1013 N 6 3 3 Група A 1017 Y 7 4 3 Група A 1007 N 4 1 3 Група A 1011 N 5 2 3 Група A 1013 N 6 3 3 Група A 1017 Y 7 4 3 Група A 1017 Y 7 4 3 Група A 1051 G 0 Група 0 10 G 0 Група 0 5 5Група B 1001 X 1 1 0Група B 1002 X 2 2 0Група B 1003 Z 3 1 2Група B 1005 Z 4 2 2Група B 1008 Z 5 3 2Група B 1043 X 2 0 Група B 1003 Z 3 1 2 Група B 1005 Z 4 2 Група B 1008 Z 5 3 2 Група B 1043 2 Z 6 0 B 1 2 G 6 0
Това, което остава на външната заявка, е да изчисли последователността на колоната за резултат с помощта на функцията ROW_NUMBER, разделена по grp, val и island и подредена по ord, генерирайки желания резултат.
Обърнете внимание, че можете да получите една и съща островна стойност за различни стойности на val в рамките на един и същи дял, като например в изхода по-горе. Ето защо е важно да използвате grp, val и island като елементи за разделяне на прозореца, а не само grp и island.
По същия начин, ако се занимавате със задача за острови, която изисква да групирате данните по острова и да изчислите групови агрегати, ще групирате редовете по grp, val и island. Но това не е така с нашето предизвикателство. Тук ви беше възложена само да изчислите номера на редове независимо за всеки остров.
Фигура 1 показва плана по подразбиране, който получих за това решение на моята машина, след като попълних таблицата с 10 милиона реда.
Фигура 1:Паралелен план за известно решение 1
Изчисляването на rn1 може да разчита на реда на индекса. И така, оптимизаторът избра стратегията без сортиране + паралелен ред за този (първи оператори за сегмент и проект за последователност в плана). Тъй като изчисленията както на rn2, така и на seqno използват свои собствени уникални комбинации от елементи за разделяне и подреждане, изричното сортиране е неизбежно за тези, независимо от използваната стратегия. И така, оптимизаторът избра стратегията за сортиране + паралелен пакетен режим и за двете. Този план включва два изрични оператора за сортиране.
В моя тест за производителност това решение отне 3,68 секунди за изпълнение срещу 1M реда и 43,1 секунди срещу 10M реда.
Както споменахме, тествах всички решения също като наложих сериен план (с намек за MAXDOP 1). Серийният план за това решение е показан на Фигура 1.
Фигура 2:Сериен план за известно решение 1
Както се очакваше, този път изчислението на rn1 също използва стратегията за пакетен режим без предходен оператор за сортиране, но планът все още има два оператора за сортиране за следващите изчисления на номера на редове. Серийният план се представи по-лошо от паралелния на моята машина с всички входни размери, които тествах, като отне 4,54 секунди за завършване с 1M редове и 61,5 секунди с 10M реда.
Решение на базата на известна техника 2
Второто решение, което ще представя, се основава на брилянтна техника за откриване на острови, която научих от Пол Уайт преди няколко години. Следва пълният код на решението, базиран на тази техника:
С C1 AS( SELECT *, CASE WHEN val =LAG(val) НАД (РАЗДЕЛЯНЕ ПО grp ПОРЪЧКА ПО ord) THEN 0 ELSE 1 END AS isstart FROM dbo.T1),C2 AS( SELECT *, SUM(isstart) НАД(ДЯЛ ПО grp ПОРЪЧКА ПО ред РЕДОВЕ НЕОГРАНИЧЕН ПРЕДШЕСТВАЩ) КАТО остров ОТ C1)ИЗБЕРЕТЕ grp, ord, val, ROW_NUMBER() НАД(ДЯЛ ПО grp, остров ORDER BY ord) КАТО seqnoFROM C2;
Ще наричам това решение като Известно решение 2.
Заявката, дефинираща изчисленията на CTE C1, използва израз CASE и функцията на прозореца LAG (разделена по grp и подредена по ord), за да изчисли колона за резултат, наречена isstart. Тази колона има стойност 0, когато текущата стойност е същата като предишната и 1 в противен случай. С други думи, това е 1, когато редът е първият в остров и 0 в противен случай.
Следва изходът от заявката, дефинираща C1:
grp ord val isstart-------- ----- ---- --------Група A 1002 Y 1 Група A 1003 Y 0 Група A 1005 Y 0 Група A 1007 N 1 Група A 1011 n 0group a 1013 n 0group a 1017 y 1group a 1019 y 0group a 1023 n 1group a 1029 n 0group b 1001 x 1group b 1002 x 0group b 1003 z 1group b 1005 z 0group b 1008 z 0group b 1013 z 0group b 1021 y 1Група B 1034 Y 0
Магията, що се отнася до откриването на острови, се случва в CTE C2. Заявката, която го дефинира, изчислява идентификатор на остров, използвайки функцията на прозореца SUM (също разделена по grp и подредена по ord), приложена към колоната isstart. Резултатната колона с идентификатора на острова се нарича остров. Във всеки дял получавате 1 за първия остров, 2 за втория остров и т.н. Така че комбинацията от колони grp и island е идентификатор на острова, който можете да използвате в задачи за острови, които включват групиране по остров, когато е уместно.
Следва изходът от заявката, дефинираща C2:
grp ord val isstart island-------- ----- ---- -------- -------Група A 1002 Y 1 1 Група A 1003 Y 0 1 Група A 1005 Y 0 1 Група A 1007 N 1 2 Група A 1011 N 0 2 Група A 1013 N 0 2 Група A 1017 Y 1 3 Група A 1019 Y 0 3 Група A 1011 N 0 2 Група A 1013 N 0 2 Група A 1017 Y 1 3 Група A 1019 Y 0 3 Група A 1023 G 0 Група 0 X 1023 G 0 Група 0 0 Група 0 1 G2 1 Група B 1003 Z 1 2 Група B 1005 Z 0 2 Група B 1008 Z 0 2 Група B 1013 Z 0 2 Група B 1021 Y 1 3 Група B 1034 Y 0 3
И накрая, външната заявка изчислява желаната колона за резултат seqno с функция ROW_NUMBER, разделена по grp и island и подредена по ord. Забележете, че тази комбинация от елементи за разделяне и подреждане е различна от тази, използвана от предишните функции на прозореца. Докато изчисляването на първите две функции на прозореца може потенциално да разчита на реда на индексите, последната не може.
Фигура 3 показва плана по подразбиране, който получих за това решение.
Фигура 3:Паралелен план за известно решение 2
Както можете да видите в плана, изчислението на първите две функции на прозореца използва стратегията без сортиране + паралелен ред, а изчислението на последната използва стратегията за сортиране + паралелен пакетен режим.
Времената за изпълнение, които получих за това решение, варираха от 2,57 секунди срещу 1M реда до 46,2 секунди срещу 10M реда.
Когато налагам серийна обработка, получих плана, показан на фигура 4.
Фигура 4:Сериен план за известно решение 2
Както се очакваше, този път всички изчисления на функциите на прозореца разчитат на стратегията за пакетен режим. Първите две без предварително сортиране, а последните с. И паралелният, и серийният план включват един изричен оператор за сортиране. Серийният план се представи по-добре от паралелния план на моята машина с входните размери, които тествах. Времената на изпълнение, които получих за принудителния сериен план, варираха от 1,75 секунди срещу 1M реда до 21,7 секунди срещу 10M реда.
Решение, базирано на нова техника
Когато Erland представи това предизвикателство в частен форум, хората бяха скептични относно възможността за решаването му със заявка, която беше оптимизирана с план без изрично сортиране. Това е всичко, което имах нужда да чуя, за да ме мотивира. И така, ето какво измислих:
WITH C1 AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) AS rn, LAG(val) OVER(PARTITION BY grp ORDER BY ord) AS prev FROM dbo.T1),C2 AS( SELECT *, MAX(CASE WHEN val =prev THEN NULL ELSE rn END) OVER(РАЗДЕЛЯНЕ ПО grp ПОРЪЧКА ПО ord РЕДОВЕ НЕОГРАНИЧЕНИ ПРЕДИШНИ) КАТО firstrn ОТ C1)ИЗБЕРЕТЕ grp, ord, val, rn - firstrn + 1 КАТО seqnoFROM C2;предварително>Решението използва три функции на прозореца:LAG, ROW_NUMBER и MAX. Важното тук е, че и трите се основават на разделяне на grp и подреждане на ord, което е в съответствие с поддържащия ред на ключове на индекса.
Заявката, дефинираща CTE C1, използва функцията ROW_NUMBER за изчисляване на номера на редове (рн колона) и функцията LAG за връщане на предишната стойност на val (предишна колона).
Ето изхода от заявката, дефинираща C1:
grp ord val rn prev-------- ----- ---- --- -----Група A 1002 Y 1 NULL Група A 1003 Y 2 Y Група A 1005 Y 3 Y Група A 1007 N 4 YGroup A 1011 N 5 NGroup A 1013 N 6 NGgroup A 1017 Y 7 NGgroup A 1019 Y 8 YGroup A 1023 N 9 YGroup A 1029 N 10 NGroup B 1001 10 NGro B 1001 1005 Z 4 ZGroup B 1008 Z 5 ZGroup B 1013 Z 6 ZGroup B 1021 Y 7 ZGroup B 1034 Y 8 YЗабележете, когато val и prev са еднакви, това не е първият ред на острова, иначе е.
Въз основа на тази логика, заявката, дефинираща CTE C2, използва израз CASE, който връща rn, когато редът е първият в остров и NULL в противен случай. След това кодът прилага функцията на прозореца MAX към резултата от израза CASE, връщайки първия rn на острова (първа колона).
Ето изхода на заявката, дефинираща C2, включително изхода на израза CASE:
grp ord val rn prev CASE firstrn-------- ----- ---- --- ----- ----- --------Група A 1002 Y 1 NULL 1 1 Група A 1003 Y 2 Y NULL 1 Група A 1005 Y 3 Y NULL 1 Група A 1007 N 4 Y 4 4 Група A 1011 N 5 N NULL 4 Група A 1013 NULL N 70 Група A 1013 NULL N70 Y 8 Y NULL 7 Група A 1023 N 9 Y 9 9 Група A 1029 N 10 N NULL 9 Група B 1001 X 1 NULL 1 1 Група B 1002 X 2 X NULL 1 Група B 1003 3 Z 3 G NULL 0 3 Z G NULL 0 3 Z G NULL 0 5 Z NULL 3 Група B 1013 Z 6 Z NULL 3 Група B 1021 Y 7 Z 7 7 Група B 1034 Y 8 Y NULL 7Това, което остава за външната заявка, е да се изчисли желаната колона за резултат последователно като rn минус firstrn плюс 1.
Фигура 5 показва паралелния план по подразбиране, който получих за това решение, когато го тествам с помощта на оператор SELECT INTO, записвайки резултата във временна таблица.
Фигура 5:Паралелен план за ново решение
В този план няма изрични оператори за сортиране. Въпреки това, и трите функции на прозореца се изчисляват с помощта на стратегията без сортиране + паралелен ред, така че пропускаме предимствата на пакетната обработка. Времената за изпълнение, които получих за това решение с паралелния план, варираха от 2,47 секунди срещу 1M редове и 41,4 срещу 10M реда.
Тук има доста голяма вероятност сериен план с пакетна обработка да се справи значително по-добре, особено когато машината няма много процесори. Припомнете си, че тествам решенията си срещу машина с 4 логически процесора. Фигура 6 показва плана, който получих за това решение при принудителна серийна обработка.
Фигура 6:Сериен план за ново решение
И трите функции на прозореца използват стратегията без сортиране + сериен пакетен режим. И резултатите са доста впечатляващи. Времето за изпълнение на това решение варира от 0,5 секунди срещу 1M редове и 5,49 секунди срещу 10M реда. Това, което също е любопитно за това решение, е, че когато го тествате като нормален оператор SELECT (с отхвърлени резултати), за разлика от оператор SELECT INTO, SQL Server избра серийния план по подразбиране. С другите две решения получих паралелен план по подразбиране както с SELECT, така и с SELECT INTO.
Вижте следващия раздел за пълните резултати от теста за производителност.
Сравнение на производителността
Ето кода, който използвах, за да тествам трите решения, разбира се, декоментирах намека на MAXDOP 1 за тестване на серийните планове:
-- Тествайте известно решение 1 ИЗПУСКАНЕ НА ТАБЛИЦА, АКО СЪЩЕСТВУВА #Резултат; С C AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) - ROW_NUMBER() OVER(PARTITION BY grp, val ORDER BY ord) КАТО остров FROM dbo.T1)SELECT grp, ord, val, ROW_NUMBER( ) НАД (РАЗДЕЛЕНИЕ ПО grp, val, island ПОРЪЧКА ПО ord) КАТО секвенно INTO #ResultFROM C/* OPTION(MAXDOP 1) */; -- разкоментирайте за сериен testGO -- Тест Известно решение 2 ИЗПУСКАНЕ НА ТАБЛИЦАТА, АКО СЪЩЕСТВУВА #Резултат; С C1 AS( SELECT *, CASE WHEN val =LAG(val) НАД(ДЯЛ ПО grp ПОРЪЧКА ПО ord) THEN 0 ELSE 1 END AS стартира ОТ dbo.T1),C2 AS( SELECT *, SUM(isstart) OVER(PARTITION BY grp ORDER BY ord РЕДОВЕ НЕОГРАНИЧЕН ПРЕДШЕСТВУВАЩ) КАТО остров ОТ C1)ИЗБЕРЕТЕ grp, ord, val, ROW_NUMBER() НАД(ДЯЛ ПО grp, остров ORDER BY ord) КАТО seqnoINTO #ResultFROM C2/* OPTION(MAXDOP 1) */; GO -- Тествайте ново решение ОТПУСКАНЕТЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА #Резултат; С C1 AS( SELECT *, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY ord) AS rn, LAG(val) OVER(PARTITION BY grp ORDER BY ord) AS prev FROM dbo.T1),C2 AS( SELECT *, MAX (СЛУЧАЙ, КОГАТО val =prev THEN NULL ELSE rn END) OVER(РАЗДЕЛЯНЕ ПО grp ПОРЪЧКА ПО ред РЕДОВЕ НЕОГРАНИЧЕНИ ПРЕДШЕСТВА) КАТО firstrn ОТ C1)ИЗБЕРЕТЕ grp, ord, val, rn - firstrn + 1 КАТО seqnoINTO #ResultFROM( C2/* MAXDOP 1) */;Фигура 7 показва времето за изпълнение както на паралелния, така и на серийния план за всички решения спрямо различни входни размери.
Фигура 7:Сравнение на производителността
Новото решение, използващо сериен режим, е безспорен победител. Има страхотна производителност, линейно мащабиране и моментално време за реакция.
Заключение
Задачите за острови са доста често срещани в реалния живот. Много от тях включват както идентифициране на острови, така и групиране на данните по остров. Предизвикателството на островите на Ерланд, което беше фокусът на тази статия, е малко по-уникално, защото не включва групиране, а вместо това подреждане на редовете на всеки остров с номера на редове.
Представих две решения, базирани на известни техники за идентифициране на острови. Проблемът и с двете е, че те водят до планове, включващи изрично сортиране, което се отразява негативно на производителността, мащабируемостта и времето за реакция на решенията. Представих и нова техника, която доведе до план без никакво сортиране. Серийният план за това решение, който използва стратегия без сортиране + сериен пакетен режим, има отлична производителност, линейно мащабиране и моментално време за реакция. Жалко е, поне засега, не можем да имаме стратегия без сортиране + паралелен пакетен режим за работа с функциите на прозореца.