Database
 sql >> база данни >  >> RDS >> Database

Основи на табличните изрази, част 6 – Рекурсивни CTE

Тази статия е шестата част от поредица за изрази на таблици. Миналия месец в част 5 разгледах логическото третиране на нерекурсивните CTE. Този месец разглеждам логическото лечение на рекурсивните CTE. Описвам поддръжката на T-SQL за рекурсивни CTE, както и стандартни елементи, които все още не се поддържат от T-SQL. Предоставям логически еквивалентни решения за T-SQL за неподдържаните елементи.

Примерни данни

За примерни данни ще използвам таблица, наречена Служители, която съдържа йерархия на служителите. Колоната empid представлява идентификационния номер на служителя на подчинения (дъщерския възел), а колоната mgrid представлява идентификационния номер на служителя на мениджъра (родителския възел). Използвайте следния код, за да създадете и попълните таблицата на служителите в базата данни tempdb:

ЗАДАДЕТЕ NOCOUNT ON; ИЗПОЛЗВАЙТЕ tempdb;ПРОСТЪПНЕТЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА dbo.Employees;ИДТЕ СЪЗДАВАЙТЕ ТАБЛИЦА dbo.Employees( empid INT НЕ НУЛВО ОГРАНИЧЕНИЕ PK_Employees ПЪРВИЧЕН КЛЮЧ, mgrid INT NULL ОГРАНИЧЕНИЕ FK_Employees_Employees, dnuFEERY_Employees, dnufeeery_Employees, dnufeery_Employees, dnufeech_Employees. ПРОВЕРКА (empid <> mgrid)); INSERT INTO dbo.Employees(empid, mgrid, empname, salary) VALUES(1, NULL, 'David' , $10000.00), (2, 1, 'Eitan' , $7000.00), (3, 1, 'Ina' , 0 $7500). , (4, 2, 'Seraph' , $5000,00), (5, 2, 'Jiru', $5500,00), (6, 2, 'Steve', $4500,00), (7, 3, 'Aaron', $5000,00), ( 8, 5, 'Lilach' , $3500,00), (9, 7, 'Rita', $3000,00), (10, 5, 'Sean', $3000,00), (11, 7, 'Gabriel', $3000,00), (12, 9, 'Емилия' , 2000,00 $), (13, 9, 'Майкъл', 2000,00 $), (14, 9, 'Диди', 1500,00 $); СЪЗДАЙТЕ УНИКАЛЕН ИНДЕКС idx_unc_mgrid_empid НА dbo.Служители(mgrid, empid) ВКЛЮЧЕТЕ(empname, заплата);

Обърнете внимание, че коренът на йерархията на служителите – главният изпълнителен директор – има NULL в колоната mgrid. Всички останали служители имат мениджър, така че колоната им mgrid е настроена на идентификатора на служителя на техния мениджър.

Фигура 1 има графично изображение на йерархията на служителите, показващо името на служителя, идентификатора и местоположението му в йерархията.

Фигура 1:Йерархия на служителите

Рекурсивни CTEs

Рекурсивните CTE имат много случаи на употреба. Класическата категория употреби е свързана с обработката на графични структури, като нашата йерархия на служителите. Задачите, включващи графики, често изискват преминаване на пътища с произволна дължина. Например, като се има предвид идентификационният номер на служител на някой мениджър, върнете входния служител, както и всички подчинени на входния служител на всички нива; т.е. преки и непреки подчинени. Ако сте имали известно малко ограничение за броя на нивата, които може да се наложи да следвате (степента на графиката), бихте могли да използвате заявка с обединяване на ниво на подчинени. Въпреки това, ако степента на графиката е потенциално висока, в един момент става непрактично да се пише заявка с много присъединявания. Друга възможност е да се използва императивен итеративен код, но такива решения обикновено са дълги. Това е мястото, където рекурсивните CTE наистина блестят – те ви позволяват да използвате декларативни, кратки и интуитивни решения.

Ще започна с основите на рекурсивните CTE, преди да стигна до по-интересните неща, където обхващам възможностите за търсене и циклиране.

Не забравяйте, че синтаксисът на заявка към CTE е както следва:

С [ ( <със списък на колони> ) ]
AS
(
<израз на таблица>
)
<външна заявка>;

Тук показвам един CTE, дефиниран с помощта на клаузата WITH, но както знаете, можете да дефинирате множество CTE, разделени със запетаи.

В обикновените, нерекурсивни CTE, изразът на вътрешната таблица се основава на заявка, която се оценява само веднъж. В рекурсивните CTE изразът на вътрешната таблица се основава на обикновено две заявки, известни като членове , въпреки че можете да имате повече от две, за да се справите с някои специализирани нужди. Членовете обикновено са разделени от оператор UNION ALL, за да се посочи, че техните резултати са унифицирани.

Член може да бъде закотвен член — което означава, че се оценява само веднъж; или може да бъде рекурсивен член — което означава, че се оценява многократно, докато не върне празен набор от резултати. Ето синтаксиса на заявка срещу рекурсивен CTE, който се основава на един котвен член и един рекурсивен член:

С [ ( <със списък на колони> ) ]
AS
(
<ковен член>
UNION ALL
<рекурсивен член>
)
<външна заявка>;

Това, което прави член рекурсивен, е да има препратка към името на CTE. Тази препратка представлява набора от резултати от последното изпълнение. При първото изпълнение на рекурсивния член препратката към името на CTE представлява резултантния набор на закотвяния член. При N-то изпълнение, където N> 1, препратката към името на CTE представлява резултатния набор от (N – 1) изпълнение на рекурсивния член. Както споменахме, след като рекурсивният член върне празен набор, той не се изпълнява отново. Препратка към името на CTE във външната заявка представлява унифицираните набори от резултати от единичното изпълнение на закрепващия член и всички изпълнения на рекурсивния член.

Следният код изпълнява гореспоменатата задача за връщане на поддървото на служителите за даден мениджър за въвеждане, предавайки ID на служител 3 като главен служител в този пример:

ДЕКЛАРИРАНЕ @root КАТО INT =3; С C AS( ИЗБЕРЕТЕ empid, mgrid, empname ОТ dbo.Employees WHERE empid =@root UNION ВСИЧКИ SELECT S.empid, S.mgrid, S.empname ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid)ИЗБЕРЕТЕ empid, mgrid, empnameFROM C;

Заявката за котва се изпълнява само веднъж, връщайки реда за входния главен служител (служител 3 в нашия случай).

Рекурсивната заявка се присъединява към набора от служители от предишното ниво – представено от препратката към името на CTE, псевдоним M за мениджъри – с техните преки подчинени от таблицата Служители, псевдоним S за подчинените. Предикатът за присъединяване е S.mgrid =M.empid, което означава, че стойността mgrid на подчинения е равна на empid стойността на мениджъра. Рекурсивната заявка се изпълнява четири пъти, както следва:

  1. Връщане на подчинени на служител 3; изход:служител 7
  2. Връщане на подчинени на служител 7; продукция:служители 9 и 11
  3. Връщане на подчинени на служители 9 и 11; изход:12, 13 и 14
  4. Връщане на подчинени на служители 12, 13 и 14; изход:празен набор; рекурсията спира

Препратката към името на CTE във външната заявка представлява унифицираните резултати от еднократното изпълнение на анкерната заявка (служител 3) с резултатите от всички изпълнения на рекурсивната заявка (служители 7, 9, 11, 12, 13 и 14). Този код генерира следния изход:

empid mgrid empname------ ------ --------3 1 Ina7 3 Aaron9 7 Rita11 7 Gabriel12 9 Emilia13 9 Michael14 9 Didi

Често срещан проблем в решенията за програмиране е безкраен код като безкрайни цикли, обикновено причинени от грешка. При рекурсивни CTE, грешка може да доведе до изпълнение на рекурсивния член на пистата. Например, да предположим, че в нашето решение за връщане на подчинените на входен служител, сте имали грешка в предиката за присъединяване на рекурсивния член. Вместо да използвате ON S.mgrid =M.empid, вие сте използвали ON S.mgrid =S.mgrid, така:

ДЕКЛАРИРАНЕ @root КАТО INT =3; С C AS( ИЗБЕРЕТЕ empid, mgrid, empname ОТ dbo.Employees WHERE empid =@root UNION ВСИЧКИ SELECT S.empid, S.mgrid, S.empname ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =S .mgrid)ИЗБЕРЕТЕ empid, mgrid, empnameFROM C;

Като се има предвид поне един служител с ненулев идентификатор на мениджър, присъстващ в таблицата, изпълнението на рекурсивния член ще продължи да връща непразен резултат. Не забравяйте, че неявното условие за прекратяване на рекурсивния член е, когато неговото изпълнение връща празен набор от резултати. Ако върне непразен набор от резултати, SQL Server го изпълнява отново. Разбирате, че ако SQL Server нямаше механизъм за ограничаване на рекурсивните изпълнения, той просто ще продължи да изпълнява рекурсивния член многократно завинаги. Като мярка за безопасност T-SQL поддържа опция за заявка MAXRECURSION, която ограничава максимално разрешения брой изпълнения на рекурсивния член. По подразбиране това ограничение е настроено на 100, но можете да го промените на всяка неотрицателна стойност SMALLINT, като 0 представлява липса на ограничение. Задаването на максималната рекурсивна граница на положителна стойност N означава, че опитът за изпълнение на рекурсивния член N + 1 е прекратен с грешка. Например, изпълняването на горния бъг код, както е, означава, че разчитате на лимита за максимална рекурсия по подразбиране от 100, така че изпълнението на 101 на рекурсивния член е неуспешно:

empid mgrid empname------ ------ --------3 1 Ina2 1 Eitan3 1 Ina...
Съобщение 530, ниво 16, състояние 1, ред 121
Изявлението е прекратено. Максималната рекурсия 100 е изчерпана преди завършването на оператора.

Кажете, че във вашата организация е безопасно да се предположи, че йерархията на служителите няма да надвишава 6 нива на управление. Можете да намалите максималното ограничение на рекурсията от 100 до много по-малко число, като например 10, за да сте в безопасност, така:

ДЕКЛАРИРАНЕ @root КАТО INT =3; С C AS( ИЗБЕРЕТЕ empid, mgrid, empname ОТ dbo.Employees WHERE empid =@root UNION ВСИЧКИ SELECT S.empid, S.mgrid, S.empname ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =S .mgrid)ИЗБЕРЕТЕ empid, mgrid, empnameFROM COPTION (MAXRECURSION 10);

Сега, ако има грешка, водеща до бързо изпълнение на рекурсивния член, тя ще бъде открита при 11 опит за изпълнение на рекурсивния член вместо при 101 изпълнение:

empid mgrid empname------ ------ --------3 1 Ina2 1 Eitan3 1 Ina...
Съобщение 530, ниво 16, състояние 1, ред 149
Изявлението е прекратено. Максималната рекурсия 10 е изчерпана преди завършването на оператора.

Важно е да се отбележи, че максималната граница на рекурсия трябва да се използва главно като мярка за безопасност за прекратяване на изпълнението на бъги бегъл код. Не забравяйте, че при надвишаване на лимита изпълнението на кода се прекъсва с грешка. Не трябва да използвате тази опция, за да ограничите броя на нивата за посещение с цел филтриране. Не искате да се генерира грешка, когато няма проблем с кода.

За целите на филтрирането можете да ограничите броя нива, които да посетите програмно. Вие дефинирате колона, наречена lvl, която проследява номера на нивото на текущия служител в сравнение с входния главен служител. В анкерната заявка задавате колоната lvl на константа 0. В рекурсивната заявка я задавате на стойността на lvl на мениджъра плюс 1. За да филтрирате толкова нива под входния корен като @maxlevel, добавете предиката M.lvl <@ maxlevel към клаузата ON на рекурсивната заявка. Например следният код връща служител 3 и две нива на подчинени на служител 3:

ДЕКЛАРИРАНЕ @root КАТО INT =3, @maxlevel КАТО INT =2; С C AS( SELECT empid, mgrid, empname, 0 AS lvl ОТ dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, M.lvl + 1 КАТО lvl ОТ C AS M INNER ПРИСЪЕДИНЕТЕ СЕ към dbo.Employees КАТО С НА S.mgrid =M.empid И M.lvl <@maxlevel)ИЗБЕРЕТЕ empid, mgrid, empname, lvlFROM C;

Тази заявка генерира следния изход:

empid mgrid empname lvl------ ------ -------- ----3 1 Ina 07 3 Aaron 19 7 Rita 211 7 Gabriel 2

Стандартни клаузи SEARCH и CYCLE

Стандартът ISO/IEC SQL дефинира две много мощни опции за рекурсивни CTE. Едната е клауза, наречена SEARCH, която контролира реда на рекурсивно търсене, а другата е клауза, наречена CYCLE, която идентифицира цикли в преминатите пътища. Ето стандартния синтаксис за двете клаузи:

7.18 <клауза за търсене или цикъл>

Функция

Посочете генерирането на информация за подреждане и откриване на цикъл в резултат на рекурсивни изрази на заявка.

Формат

<със списъчен елемент> ::=
<име на заявка> [ <ляво скоба> <със списък на колони> <дясна скоба> ]
AS <подзаявка на таблица> [ <клауза за търсене или цикъл> ]
<клауза за търсене или цикъл> ::=
<клауза за търсене> | <клауза за цикъл> | <клауза за търсене> <клауза за цикъл>
<клауза за търсене> ::=
ТЪРСЕНЕ <ред за рекурсивно търсене> SET <колона за последователност>
<ред за рекурсивно търсене> ::=
DEPTH FIRST BY <списък с имена на колони> | BREADTH FIRST BY <списък с имена на колона>
<колона с последователност> ::=<име на колона>
<цикълна клауза> ::=
CYCLE <списък с колони на цикъл> SET <колона с маркер на цикъла> ДО <стойност на маркировка на цикъла>
ПО ПОДРАЗБИРАНЕ <стойност на нецикличен знак> ИЗПОЛЗВАНЕ <колона на пътя>
<списък с колони на цикъл> ::=<колона на цикъл> [ { <запетая> <колона на цикъл> }… ]
<цикъл колона> ::=<име на колона>
<маркировка на цикъл колона> ::=<име на колона>
<колона път> ::=<име на колона>
<стойност на маркировка на цикъл> ::=<израз за стойност>
<стойност на нецикличен знак> ::=<израз за стойност>

Към момента на писане T-SQL не поддържа тези опции, но в следните връзки можете да намерите заявки за подобрение на функции, които искат включването им в T-SQL в бъдеще, и да гласувате за тях:

  • Добавете поддръжка за ISO/IEC SQL клауза SEARCH към рекурсивни CTE в T-SQL
  • Добавете поддръжка за ISO/IEC SQL CYCLE клауза към рекурсивни CTE в T-SQL

В следващите раздели ще опиша двете стандартни опции и ще предоставя логически еквивалентни алтернативни решения, които в момента са налични в T-SQL.

Клауза SEARCH

Стандартната клауза SEARCH дефинира реда на рекурсивно търсене като BREADTH FIRST или DEPTH FIRST чрез някои зададени колони за подреждане и създава нова колона с последователност с порядковите позиции на посетените възли. Посочвате BREADTH FIRST, за да търсите първо във всяко ниво (ширина), а след това надолу по нивата (дълбочина). Посочвате DEPTH FIRST, за да търсите първо надолу (дълбочина) и след това напречно (ширина).

Посочвате клаузата SEARCH точно преди външната заявка.

Ще започна с пример за първо рекурсивно търсене в ширина. Само не забравяйте, че тази функция не е налична в T-SQL, така че няма да можете действително да стартирате примерите, които използват стандартните клаузи в SQL Server или Azure SQL база данни. Следният код връща поддървото на служител 1, като иска поръчка за първо търсене в ширина по empid, създавайки колона за последователност, наречена seqbreadth с порядковите позиции на посетените възли:

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( ИЗБЕРЕТЕ empid, mgrid, empname ОТ dbo.Employees WHERE empid =@root UNION ВСИЧКИ SELECT S.empid, S.mgrid, S.empname ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid) ПЪРВО ТЪРСЕНЕ ПО ШИРИНА ПО empid SET seqbreadth---------------------------------------- ---- ИЗБЕРЕТЕ empid, mgrid, empname, seqbreadthFROM CORDER BY seqbreadth;

Ето желания изход на този код:

empid mgrid empname seqbreadth------ ------ -------- -----------1 NULL David 12 1 Eitan 23 1 Ina 34 2 Seraph 45 2 Джиру 56 2 Стив 67 3 Аарон 78 5 Лилач 89 7 Рита 910 5 Шон 1011 7 Габриел 1112 9 Емилия 1213 9 Майкъл 1314 9 Диди 14

Не се обърквайте от факта, че стойностите на seqbreadth изглежда са идентични с empid стойностите. Това е просто случайно в резултат на начина, по който ръчно присвоих стойностите на empid в примерните данни, които създадох.

Фигура 2 има графично изображение на йерархията на служителите, с пунктирана линия, показваща първия ред в ширината, в който са търсени възлите. Стойностите empid се показват точно под имената на служителите, а присвоените стойности на seqbreadth се показват в долния ляв ъгъл на всяко поле.

Фигура 2:Първо ширината на търсене

Тук е интересно да се отбележи, че във всяко ниво възлите се търсят въз основа на посочения ред на колони (empid в нашия случай), независимо от родителската асоциация на възела. Например, забележете, че на четвърто ниво се осъществява достъп до Лилач първи, Рита втори, Шон трети и Габриел последен. Това е така, защото сред всички служители на четвърто ниво това е техният ред въз основа на техните емпидни ценности. Не се предполага, че Лилач и Шон трябва да бъдат претърсени последователно, защото са преки подчинени на един и същ мениджър Джиру.

Доста лесно е да излезете с T-SQL решение, което осигурява логическия еквивалент на стандартната опция SAERCH DEPTH FIRST. Създавате колона, наречена lvl, както показах по-рано, за да проследявате нивото на възела по отношение на корена (0 за първото ниво, 1 за второто и т.н.). След това изчислявате желаната колона за резултатна последователност във външната заявка с помощта на функция ROW_NUMBER. Като подреждане на прозореца първо посочвате lvl и след това желаната колона за поръчка (empid в нашия случай). Ето пълния код на решението:

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( SELECT empid, mgrid, empname, 0 AS lvl ОТ dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, M.lvl + 1 КАТО lvl ОТ C AS M INNER ПРИСЪЕДИНЕТЕ се към dbo.Employees КАТО S ON S.mgrid =M.empid)ИЗБЕРЕТЕ empid, mgrid, empname, ROW_NUMBER() НАД (ПОРЕДКА ПО lvl, empid) КАТО seqbreadthFROM CORDER BY seqbreadth;

След това нека разгледаме реда на търсене в дълбочина ПЪРВО. Съгласно стандарта, следният код трябва да върне поддървото на служител 1, като иска поръчка за първо търсене в дълбочина чрез empid, създавайки колона за последователност, наречена seqdepth, с порядковите позиции на търсените възли:

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( ИЗБЕРЕТЕ empid, mgrid, empname ОТ dbo.Employees WHERE empid =@root UNION ВСИЧКИ SELECT S.empid, S.mgrid, S.empname ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid) ДЪЛБОЧИНА НА ТЪРСЕНЕ ПЪРВО ПО empid SET seqdepth--------------------------------------- ИЗБЕРЕТЕ empid, mgrid, empname, seqdepthFROM CORDER BY seqdepth;

Следва желаният изход на този код:

empid mgrid empname seqdepth------ ------ --------- ---------1 NULL David 12 1 Eitan 24 2 Seraph 35 2 Jiru 48 5 Lilach 510 5 Sean 66 2 Steve 73 1 Ina 87 3 Aaron 99 7 Rita 1012 9 Emilia 1113 9 Michael 1214 9 Didi 1311 7 Gabriel 14

Гледайки желания изход от заявка, вероятно е трудно да разберете защо стойностите на последователността са присвоени по начина, по който са били. Фигура 3 трябва да помогне. Той има графично изображение на йерархията на служителите, с пунктирана линия, отразяваща поръчката за първо търсене в дълбочина, с присвоените стойности на последователността в долния ляв ъгъл на всяко поле.

Фигура 3:Първа дълбочина на търсене

Измислянето на T-SQL решение, което осигурява логическия еквивалент на стандартната опция SEARCH DEPTH FIRST, е малко сложно, но изпълнимо. Ще опиша решението в две стъпки.

В стъпка 1 за всеки възел изчислявате двоичен път за сортиране, който се състои от сегмент за предшественик на възела, като всеки сегмент отразява позицията на сортиране на предшественика сред неговите братя и сестри. Вие изграждате този път подобно на начина, по който изчислявате колоната lvl. Тоест, започнете с празен двоичен низ за основния възел в заявката за котва. След това в рекурсивната заявка свързвате пътя на родителя с позицията на сортиране на възела между братя и сестри, след като го преобразувате в двоичен сегмент. Използвате функцията ROW_NUMBER, за да изчислите позицията, разделена от родителя (mgrid в нашия случай) и подредена по съответната колона за подреждане (empid в нашия случай). Преобразувате номера на реда в двоична стойност с достатъчен размер в зависимост от максималния брой директни деца, които родителят може да има във вашата графика. BINARY(1) поддържа до 255 деца на родител, BINARY(2) поддържа 65,535, BINARY(3):16,777,215, BINARY(4):4,294,967,295. В рекурсивна CTE съответните колони в котвата и рекурсивните заявки трябва да са от един и същи тип и размер , така че не забравяйте да преобразувате пътя за сортиране в двоична стойност със същия размер и в двете.

Ето кода, който внедрява стъпка 1 в нашето решение (ако приемем, че BINARY(1) е достатъчен за сегмент, което означава, че имате не повече от 255 преки подчинени на мениджър):

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( SELECT empid, mgrid, empname, CAST(0x AS VARBINARY(900)) КАТО път за сортиране ОТ dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M. sortpath + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid) AS BINARY(1)) AS VARBINARY(900)) AS sortpath ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)ИЗБЕРЕТЕ empid, mgrid, empname, sortpathFROM C;

Този код генерира следния изход:

empid mgrid empname sortpath------ ------ -------- -----------1 NULL David 0x2 1 Eitan 0x013 1 Ina 0x027 3 Aaron 0x02019 7 Rita 0x02010111 7 Gabriel 0x02010212 9 Emilia 0x0201010113 9 Michael 0x02010214 9 Didi 0x020101034 2 Seraph 0x01015 2 Jiru 0x01026 2 

Обърнете внимание, че използвах празен двоичен низ като път за сортиране за основния възел на единичното поддърво, което отправяме заявка тук. В случай, че членът на закотвянето може потенциално да върне множество корени на поддърво, просто започнете със сегмент, който се основава на изчисление ROW_NUMBER в заявката за котва, подобно на начина, по който го изчислявате в рекурсивната заявка. В такъв случай пътят за сортиране ще бъде с един сегмент по-дълъг.

Това, което остава да направите в Стъпка 2, е да създадете желаната колона за секвенция на резултата чрез изчисляване на номера на редове, които са подредени по пътя на сортиране във външната заявка, като така

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( SELECT empid, mgrid, empname, CAST(0x AS VARBINARY(900)) КАТО път за сортиране ОТ dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M. sortpath + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid) AS BINARY(1)) AS VARBINARY(900)) AS sortpath ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)ИЗБЕРЕТЕ empid, mgrid, empname, ROW_NUMBER() OVER(ORDER BY sortpath) КАТО seqdepthFROM CORDER BY seqdepth;

Това решение е логически еквивалент на използването на стандартната опция ПЪРВО НА ДЪЛБОЧИНАТА НА ТЪРСЕНЕ, показана по-рано, заедно с желания резултат.

След като имате колона за последователност, отразяваща порядъка на първо търсене в дълбочина (seqdepth в нашия случай), само с малко повече усилия можете да създадете много хубаво визуално изображение на йерархията. Ще ви трябва и гореспоменатата колона lvl. Вие подреждате външната заявка по колоната за последователност. Връщате някакъв атрибут, който искате да използвате за представяне на възела (да речем, empname в нашия случай), с префикс от някакъв низ (да речем ' | '), репликиран lvl пъти. Ето пълния код за постигане на такова визуално изображение:

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( SELECT empid, empname, 0 AS lvl, CAST(0x AS VARBINARY(900)) КАТО път за сортиране ОТ dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.empname, M.lvl + 1 AS lvl, CAST(M.sortpath + CAST(ROW_NUMBER() OVER(PARTITION BY S.mgrid ORDER BY S.empid) AS BINARY(1)) AS VARBINARY(900)) AS sortpath ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, ROW_NUMBER() OVER(ORDER BY sortpath) КАТО seqdepth, REPLICATE(' | ', lvl) + empname КАТО empFROM CORDER BY seqdepth;

Този код генерира следния изход:

empid seqdepth emp------ --------- --------------------1 1 David2 2 | Eitan4 3 | | Серафим5 4 | | Jiru8 5 | | | Lilach10 6 | | | Шон6 7 | | Стив3 8 | Ina7 9 | | Арон9 10 | | | Рита12 11 | | | | Емилия13 12 | | | | Майкъл14 13 | | | | Диди11 14 | | | Габриел

Това е много готино!

Клауза CYCLE

Графичните структури могат да имат цикли. Тези цикли могат да бъдат валидни или невалидни. Пример за валидни цикли е графика, представяща магистрали и пътища, свързващи градове и населени места. Това е, което се нарича циклична графика . Обратно, графика, представяща йерархия на служителите, каквато е в нашия случай, не трябва да има цикли. Това е, което се нарича ациклично графика. Да предположим обаче, че поради някаква погрешна актуализация в крайна сметка неволно ще получите цикъл. Да предположим например, че по погрешка актуализирате идентификатора на мениджъра на главния изпълнителен директор (служител 1) на служител 14, като изпълните следния код:

АКТУАЛИЗИРАНЕ Служители SET mgrid =14WHERE empid =1;

Вече имате невалиден цикъл в йерархията на служителите.

Независимо дали циклите са валидни или невалидни, когато преминавате през структурата на графиката, със сигурност не искате да продължавате да следвате цикличен път многократно. И в двата случая искате да спрете да следвате път, след като бъде намерено непърво появяване на същия възел, и да маркирате такъв път като цикличен.

И така, какво се случва, когато направите заявка към графика, която има цикли, използвайки рекурсивна заявка, без механизъм за откриване на тези? Това зависи от изпълнението. В SQL Server в даден момент ще достигнете максималното ограничение на рекурсия и вашата заявка ще бъде прекратена. Например, ако приемем, че сте изпълнили горната актуализация, която въведе цикъла, опитайте да изпълните следната рекурсивна заявка, за да идентифицирате поддървото на служител 1:

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( ИЗБЕРЕТЕ empid, mgrid, empname ОТ dbo.Employees WHERE empid =@root UNION ВСИЧКИ SELECT S.empid, S.mgrid, S.empname ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid)ИЗБЕРЕТЕ empid, mgrid, empnameFROM C;

Тъй като този код не задава изрично ограничение за максимална рекурсия, се приема ограничението по подразбиране от 100. SQL Server прекратява изпълнението на този код преди завършване и генерира грешка:

empid mgrid empname------ ------ --------1 14 David2 1 Eitan3 1 Ina7 3 Aaron9 7 Rita11 7 Gabriel12 9 Emilia13 9 Michael14 9 Didi1 14 David2 1 Eitan3 1 Ina7 3 Аарон...
Съобщение 530, ниво 16, състояние 1, ред 432
Изявлението е прекратено. Максималната рекурсия 100 е изчерпана преди завършването на оператора.

Стандартът на SQL дефинира клауза, наречена CYCLE за рекурсивни CTE, което ви позволява да се справяте с цикли във вашата графика. Посочвате тази клауза точно преди външната заявка. Ако има и клауза SEARCH, първо я посочвате и след това клаузата CYCLE. Ето синтаксиса на клаузата CYCLE:

CYCLE <списък с колони на цикъла>
SET <колона за маркировка на цикъл> НА <стойност на маркировка на цикъл> ПО ПОДРАЗБИРАНЕ <стойност на нециклични марки>
ИЗПОЛЗВАНЕ <колона за път>

Откриването на цикъл се основава на посочения списък с колони за цикъл . Например, в нашата йерархия на служителите вероятно бихте искали да използвате колоната empid за тази цел. Когато същата empid стойност се появи за втори път, се открива цикъл и заявката не следва този път повече. Посочвате нова колона с маркер за цикъл което ще покаже дали цикъл е намерен или не, и вашите собствени стойности като маркировка за цикъл и маркировка без цикъл стойности. Например, те могат да бъдат 1 и 0 или „Да“ и „Не“, или всякакви други стойности по ваш избор. В клаузата USING посочвате ново име на колона на масива, което ще съдържа пътя на възлите, намерени досега.

Ето как бихте обработили заявка за подчинени на някой входен главен служител, с възможност за откриване на цикли въз основа на колоната empid, указвайки 1 в колоната за маркировка на цикъла, когато е открит цикъл, и 0 в противен случай:

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( ИЗБЕРЕТЕ empid, mgrid, empname ОТ dbo.Employees WHERE empid =@root UNION ВСИЧКИ SELECT S.empid, S.mgrid, S.empname ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M .empid) ЦИКЛ empid НАСТРОЙТЕ цикъл НА 1 ПО ПОДРАЗБИРАНЕ 0----------------------------------- ИЗБЕРЕТЕ empid, mgrid, empnameFROM C;

Ето желания изход на този код:

empid mgrid empname цикъл------ ------ --------- ------1 14 David 02 1 Eitan 03 1 Ina 07 3 Aaron 09 7 Rita 011 7 Gabriel 012 9 Emilia 013 9 Michael 014 9 Didi 01 14 David 14 2 Seraph 05 2 Jiru 06 2 Steve 08 5 Lilach 010 5 Sean 0

T-SQL в момента не поддържа клаузата CYCLE, но има заобиколно решение, което предоставя логически еквивалент. Тя включва три стъпки. Припомнете си, че по-рано сте изградили двоичен път за сортиране за обработка на реда на рекурсивно търсене. По подобен начин, като първа стъпка в решението, вие изграждате базиран на символен низ път на предците, направен от идентификаторите на възела (идентификаторите на служители в нашия случай) на предците, водещи до текущия възел, включително текущия възел. Искате разделители между идентификаторите на възела, включително един в началото и един в края.

Ето кода за изграждане на такъв път на предците:

ДЕКЛАРИРАНЕ @root КАТО INT =1; С C AS( SELECT empid, mgrid, empname, CAST('.' + CAST(empid КАТО VARCHAR(4)) + '.' КАТО VARCHAR(900)) КАТО ancpath ОТ dbo.Employees КЪДЕ empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M.ancpath + CAST(S.empid КАТО VARCHAR(4)) + '.' КАТО VARCHAR(900)) КАТО ancpath ОТ C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)ИЗБЕРЕТЕ empid, mgrid, empname, ancpathFROM C;

Този код генерира следния изход, като все още следва циклични пътища и следователно прекратява преди завършване, след като максималната граница на рекурсия бъде надвишена:

empid mgrid empname ancpath------ ------ --------- -------------------1 14 Дейвид . 1.2 1 Ейтан .1.2.3 1 Ина .1.3.7 3 Аарон .1.3.7.9 7 Рита .1.3.7.9.11 7 Габриел .1.3.7.11.12 9 Емилия .1.3.7.9.12.13 3. Майкъл 13.14 9 Диди .1.3.7.9.14.1 14 Давид .1.3.7.9.14.1.2 1 Ейтан .1.3.7.9.14.1.2.3 1 Ина .1.3.7.9.14.1.3.7 3 Аарон .7..1. ...
Съобщение 530, ниво 16, състояние 1, ред 492
Изявлението е прекратено. Максималната рекурсия 100 е изчерпана преди завършването на оператора.

Eyeballing the output, you can identify a cyclic path when the current node appears in the path for the nonfirst time, such as in the path '.1.3.7.9.14.1.' .

In the second step of the solution you produce the cycle mark column. In the anchor query you simply set it to 0 since clearly a cycle cannot be found in the first node. In the recursive member you use a CASE expression that checks with a LIKE-based predicate whether the current node ID already appears in the parent’s path, in which case it returns the value 1, or not, in which case it returns the value 0.

Here’s the code implementing Step 2:

DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname, CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, 0 AS cycle FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, CASE WHEN M.ancpath LIKE '%.' + CAST(S.empid AS VARCHAR(4)) + '.%' THEN 1 ELSE 0 END AS cycle FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid)SELECT empid, mgrid, empname, ancpath, cycleFROM C;

This code generates the following output, again, still pursuing cyclic paths and failing once the max recursion limit is reached:

empid mgrid empname ancpath cycle------ ------ -------- ------------------- ------1 14 David .1. 02 1 Eitan .1.2. 03 1 Ina .1.3. 07 3 Aaron .1.3.7. 09 7 Rita .1.3.7.9. 011 7 Gabriel .1.3.7.11. 012 9 Emilia .1.3.7.9.12. 013 9 Michael .1.3.7.9.13. 014 9 Didi .1.3.7.9.14. 01 14 David .1.3.7.9.14.1. 12 1 Eitan .1.3.7.9.14.1.2. 03 1 Ina .1.3.7.9.14.1.3. 17 3 Aaron .1.3.7.9.14.1.3.7. 1...
Msg 530, Level 16, State 1, Line 532
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.

The third and last step is to add a predicate to the ON clause of the recursive member that pursues a path only if a cycle was not detected in the parent’s path.

Here’s the code implementing Step 3, which is also the complete solution:

DECLARE @root AS INT =1; WITH C AS( SELECT empid, mgrid, empname, CAST('.' + CAST(empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, 0 AS cycle FROM dbo.Employees WHERE empid =@root UNION ALL SELECT S.empid, S.mgrid, S.empname, CAST(M.ancpath + CAST(S.empid AS VARCHAR(4)) + '.' AS VARCHAR(900)) AS ancpath, CASE WHEN M.ancpath LIKE '%.' + CAST(S.empid AS VARCHAR(4)) + '.%' THEN 1 ELSE 0 END AS cycle FROM C AS M INNER JOIN dbo.Employees AS S ON S.mgrid =M.empid AND M.cycle =0)SELECT empid, mgrid, empname, cycleFROM C;

This code runs to completion, and generates the following output:

empid mgrid empname cycle------ ------ -------- -----------1 14 David 02 1 Eitan 03 1 Ina 07 3 Aaron 09 7 Rita 011 7 Gabriel 012 9 Emilia 013 9 Michael 014 9 Didi 01 14 David 14 2 Seraph 05 2 Jiru 06 2 Steve 08 5 Lilach 010 5 Sean 0

You identify the erroneous data easily here, and fix the problem by setting the manager ID of the CEO to NULL, like so:

UPDATE Employees SET mgrid =NULLWHERE empid =1;

Run the recursive query and you will find that there are no cycles present.

Заключение

Recursive CTEs enable you to write concise declarative solutions for purposes such as traversing graph structures. A recursive CTE has at least one anchor member, which gets executed once, and at least one recursive member, which gets executed repeatedly until it returns an empty result set. Using a query option called MAXRECURSION you can control the maximum number of allowed executions of the recursive member as a safety measure. To limit the executions of the recursive member programmatically for filtering purposes you can maintain a column that tracks the level of the current node with respect to the root node.

The SQL standard supports two very powerful options for recursive CTEs that are currently not available in T-SQL. One is a clause called SEARCH that controls the recursive search order, and another is a clause called CYCLE that detects cycles in the traversed paths. I provided logically equivalent solutions that are currently available in T-SQL. If you find that the unavailable standard features could be important future additions to T-SQL, make sure to vote for them here:

  • Add support for ISO/IEC SQL SEARCH clause to recursive CTEs in T-SQL
  • Add support for ISO/IEC SQL CYCLE clause to recursive CTEs in T-SQL

This article concludes the coverage of the logical aspects of CTEs. Next month I’ll turn my attention to physical/performance considerations of CTEs.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Python REST API с Flask, Connexion и SQLAlchemy – част 3

  2. Значението на поддръжката на MSDB

  3. Работа с Java данни в Qlik Sense

  4. Модел на данни за сватбена организация

  5. Какво представляват последователните срещу паралелните потоци в Java?