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

Основи на табличните изрази, част 5 – CTEs, логически съображения

Тази статия е петата част от поредица за изрази на таблици. В част 1 предоставих фона на табличните изрази. В част 2, част 3 и част 4 покрих както логическите, така и оптимизационните аспекти на извлечените таблици. Този месец започвам отразяването на общите таблични изрази (CTE). Подобно на извлечените таблици, първо ще разгледам логическото третиране на CTE, а в бъдеще ще стигна до съображения за оптимизация.

В моите примери ще използвам примерна база данни, наречена TSQLV5. Можете да намерите скрипта, който го създава и попълва тук, и неговата ER диаграма тук.

CTEs

Нека започнем с термина израз за обща таблица . Нито този термин, нито неговият акроним CTE се появяват в стандартните спецификации на ISO/IEC SQL. Така че може да се окаже, че терминът произхожда от един от продуктите за бази данни и по-късно е възприет от някои от другите доставчици на бази данни. Можете да го намерите в документацията на Microsoft SQL Server и Azure SQL база данни. T-SQL го поддържа, като се започне от SQL Server 2005. Стандартът използва термина израз на заявка за представяне на израз, който дефинира един или повече CTE, включително външната заявка. Той използва термина с елемент от списък за да представи това, което T-SQL нарича CTE. Скоро ще предоставя синтаксиса за израз на заявка.

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

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

Синтаксис

Ето стандартния синтаксис за израз на заявка:

7.17 <израз на заявка>


Функция
Посочете таблица.


Формат
<израз на заявка> ::=
[ <с клауза> ] <тело на израза на заявка>
[ <подреждане по клауза> ] [ <клауза за изместване на резултата> ] [ <извличане на първа клауза> ]
<с клауза> ::=С [ РЕКУРСИВ ] <със списък>
<със списък> ::=<с елемент от списък> [ { <запетая> <с елемент от списък> }… ]
<със списъчен елемент> ::=
<име на заявка> [ <ляво скоба> <със списък на колони> <дясна скоба> ]
AS <подзаявка на таблица> [ <клауза за търсене или цикъл> ]
<със списък с колони> ::=<списък с имена на колона>
<тяло на израза на заявката> ::=
<термин на заявка>
| <тело на израз на заявка> UNION [ ВСИЧКИ | РАЗЛИЧЕН ]
[ <съответстваща спецификация> ] <термин на заявка
| <тело на израз на заявка> ОСВЕН [ ВСИЧКИ | DISTINCT ]
[ <съответна спецификация> ] <термин на заявка>
<термин на заявка> ::=
<основна заявка>
| <термин на заявка> ПРЕСЕЧИ [ ВСИЧКИ | DISTINCT ]
[ <съответна спецификация> ] <основна заявка>
<основна заявка> ::=
<проста таблица>
| <ляво скоба> <тяло на израза на заявката>
[ <подреждане по клауза> ] [ <клауза за изместване на резултата> ] [ <извличане на първа клауза> ]
<дясна скоба>
<проста таблица> ::=
<спецификация на заявката> | <конструктор на стойност на таблица> | <изрична таблица>
<изрична таблица> ::=TABLE <име на таблица или заявка>
<съответна спецификация> ::=
CORRESPONDING [ BY <ляво скоба> <съответстващ списък с колони> <дясна скоба> ]
<съответен списък с колони> ::=<списък с имена на колона>
<подреждане по клауза> ::=ORDER BY <списък със спецификации за сортиране>
<клауза за изместване на резултата> ::=OFFSET <брой на изместените редове> { ROW | ROWS }
<извличане на първа клауза> ::=
FETCH { FIRST | СЛЕДВАЩ } [ <извличане на първо количество> ] { РЕД | РЕДОВЕ } {САМО | С ВРЪЗКИ }
<извличане на първо количество> ::=
<извличане на брой на първия ред>
| <извличане на първия процент>
<отместен брой редове> ::=<спецификация на проста стойност>
<извличане на брой на първия ред> ::=<спецификация на проста стойност>
<извличане на първи процент> ::=<опростена спецификация на стойност> PERCENT


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


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


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


7.3 <конструктор на стойност на таблица>


Функция
Посочете набор от s, които да бъдат конструирани в таблица.


Формат
<конструктор на стойност на таблица> ::=VALUES <списък с изрази за стойност на редове>
<списък с изрази за стойност на редове> ::=
<израз за стойност на ред в таблица> [ { <запетая> <ред на таблица стойностен израз> }… ]
<конструктор на стойности на таблицата с контекста> ::=
VALUES <списък с изрази за контекстно въведени стойности на ред>>
<списък с изрази на стойности на контекстно въведени в ред> ::=
<контекстно въведен израз за стойност на ред>
[ { <запетая> <контекстно въведен израз за стойност на ред> }… ]

Стандартният термин израз на заявка представлява израз, включващ клауза WITH, списък , който се състои от един или повече със списъчни елементи и външна заявка. T-SQL се отнася до стандартния елемент със списък като CTE.

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

Ето синтаксиса на T-SQL за опростена заявка срещу CTE:

С <име на таблица> [ (<целеви колони>) ] AS( <израз на таблица>)SELECT <изберете списък>ОТ <име на таблица>;

Ето пример за проста заявка към CTE, представляващ клиенти от САЩ:

С UC AS( ИЗБЕРЕТЕ custid, companyname FROM Sales.Customers WHERE country =N'USA')SELECT custid, companynameFROM UC;

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

  1. Изразът на таблицата (вътрешната заявка)
  2. Името, присвоено на табличния израз (име на променливата на диапазона)
  3. Външната заявка

Това, което е различно в дизайна на CTE в сравнение с извлечените таблици е къде в кода се намират тези три елемента. При извлечените таблици вътрешната заявка е вложена в клаузата FROM на външната заявка, а името на израза на таблицата се присвоява след самия израз на таблицата. Елементите са някак преплетени. Обратно, при CTEs кодът разделя трите елемента:първо присвоявате името на израза на таблицата; второ, задавате табличния израз — от начало до край без прекъсвания; трето, посочвате външната заявка – от началото до края без прекъсвания. По-късно под „Съображения за дизайна“ ще обясня последиците от тези разлики в дизайна.

Няколко думи за CTE и използването на точка и запетая като терминатор на израза. За съжаление, за разлика от стандартния SQL, T-SQL не ви принуждава да прекратявате всички изрази с точка и запетая. Въпреки това, има много малко случаи в T-SQL, когато без терминатор кодът е двусмислен. В тези случаи прекратяването е задължително. Един такъв случай се отнася до факта, че клаузата WITH се използва за множество цели. Единият е да се дефинира CTE, друг е да се дефинира подсказка за таблица за заявка и има няколко допълнителни случая на използване. Като пример, в следния израз клаузата WITH се използва за принудително ниво на изолация, което може да се сериализира, с намек за таблица:

ИЗБЕРЕТЕ потребител, държава ОТ Продажби. Клиенти С (СЕРИАЛИЗИРУЕМИ);

Потенциалът за двусмислие е, когато имате непрекъснат израз, предшестващ CTE дефиниция, в който случай анализаторът може да не е в състояние да каже дали клаузата WITH принадлежи към първия или втория израз. Ето пример, демонстриращ това:

ИЗБЕРЕТЕ клиент, държава ОТ Sales.Customers С UC AS( SELECT custid, companyname FROM Sales.Customers WHERE country =N'USA')SELECT custid, companynameFROM UC

Тук анализаторът не може да каже дали клаузата WITH трябва да се използва за дефиниране на подсказка за таблицата за таблицата Customers в първия израз, или да стартира CTE дефиниция. Получавате следната грешка:

Съобщение 336, ниво 15, състояние 1, ред 159
Неправилен синтаксис близо до „UC“. Ако това е предназначено да бъде общ табличен израз, трябва изрично да прекратите предишния израз с точка и запетая.

Поправката, разбира се, е да прекратите изявлението, предхождащо дефиницията на CTE, но като най-добра практика наистина трябва да прекратите всичките си изявления:

ИЗБЕРЕТЕ потребител, държава ОТ Sales.Customers; С UC AS( ИЗБЕРЕТЕ custid, име на фирма ОТ Sales.Customers WHERE country =N'USA')ИЗБЕРЕТЕ custid, companynameFROM UC;

Може да сте забелязали, че някои хора започват своите CTE дефиниции с точка и запетая като практика, като така:

;С UC AS( ИЗБЕРЕТЕ custid, companyname FROM Sales.Customers WHERE country =N'USA')SELECT custid, companynameFROM UC;
;

Целта на тази практика е да се намали потенциалът за бъдещи грешки. Ами ако на по-късен етап някой добави неограничено изявление точно преди вашата CTE дефиниция в скрипта и не си направи труда да провери пълния скрипт, а само своето изявление? Вашата точка и запетая точно преди клаузата WITH ефективно става терминатор на изявлението. Със сигурност можете да видите практичността на тази практика, но е малко неестествена. Това, което се препоръчва, макар и по-трудно за постигане, е да се насаждат добри програмни практики в организацията, включително прекратяване на всички изявления.

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

  • Всички колони на табличния израз трябва да имат имена
  • Всички имена на колони на табличния израз трябва да са уникални
  • Редовете на табличния израз нямат ред

За подробности вижте раздела „Изразът на таблица е таблица“ в част 2 от поредицата.

Съображения за дизайн

Ако анкетирате опитни разработчици на T-SQL дали предпочитат да използват извлечени таблици или CTE, не всеки ще се съгласи кое е по-добро. Естествено, различните хора имат различни предпочитания за стил. Понякога използвам производни таблици и понякога CTE. Добре е да можете съзнателно да идентифицирате специфичните разлики в езиковия дизайн между двата инструмента и да избирате въз основа на вашите приоритети във всяко дадено решение. С времето и опита вие правите избора си по-интуитивно.

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

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

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

ИЗБЕРЕТЕ година на поръчка, numcustsFROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts FROM ( SELECT YEAR(orderdate) AS orderyear, custid FROM Sales.Orders ) КАТО D1 ГРУПА ПО година на поръчка ) AS D2 WHERE numcusts> 70;> 

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

Ето решение на гореспоменатата задача с помощта на CTEs:

С C1 AS( ИЗБЕРЕТЕ ГОДИНА (дата на поръчка) КАТО година на поръчка, custid FROM Sales.Orders),C2 AS( SELECT order year, COUNT(DISTINCT custid) AS numcusts ОТ C1 GROUP BY orderyear)SELECT year на поръчка, numcustsFROM C2WHERE numcusts> 

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

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

SELECT CUR.orderyear, CUR.numorders, CUR.numorders - PRV.numorders AS diffFROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate) ) AS CUR LEFT OUTER JOIN ( ИЗБЕРЕТЕ ГОДИНА (дата на поръчка) КАТО година на поръчка, COUNT(*) КАТО номера на поръчки ОТ Sales.Orders ГРУПА ПО ГОДИНА(дата на поръчка) ) КАТО PRV ON CUR.orderyear =PRV.orderyear + 1;

Това решение връща годините на поръчките, броя на поръчките за година и разликата между броя на текущата и предходната година. Да, можете да го направите по-лесно с функцията LAG, но моят фокус тук не е да намеря най-добрия начин за постигане на тази много специфична задача. Използвам този пример, за да илюстрирам някои аспекти на дизайна на езика на изразите на именувани таблици.

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

WITH OrdCount AS( SELECT YEAR(orderdate) AS order year, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate))SELECT CUR.orderyear, CUR.numorders, CUR.numorders - PRV.numorders AS diffFROM OrdCount AS CUR LEFT OUTER JOIN OrdCount AS PRV ON CUR.orderyear =PRV.orderyear + 1;

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

Едно предимство, което кодът, базиран на извлечени таблици, има в сравнение с кода, базиран на CTE, е свързан със свойството на затваряне, което се предполага, че изразът на таблицата притежава. Не забравяйте, че свойството за затваряне на релационен израз казва, че както входовете, така и изходът са релации и че релационен израз може да се използва, когато се очаква релация, като вход за още един релационен израз. По същия начин табличният израз връща таблица и се предполага, че е наличен като входна таблица за друг израз на таблица. Това важи за заявка, която се основава на извлечени таблици - можете да я използвате, където се очаква таблица. Например, можете да използвате заявка, която се основава на извлечени таблици, като вътрешна заявка на CTE дефиниция, както е в следния пример:

WITH C AS( SELECT order year, numcusts FROM ( SELECT order year, COUNT(DISTINCT custid) AS numcusts FROM ( SELECT YEAR(orderdate) AS orderyear, custid FROM Sales.Orders ) КАТО D1 ГРУПА ПО година на поръчка ) КАТО D2 WHERE numcusts> 70)ИЗБЕРЕТЕ година на поръчка, numcustsFROM C;

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

ИЗБЕРЕТЕ година на поръчка, custidFROM (WITH C1 AS ( SELECT YEAR(orderdate) AS orderyear, custid FROM Sales.Orders ), C2 AS ( SELECT order year, COUNT(DISTINCT custid) AS numcusts FROM C1 GROUP BY orderyear ) SELECT order year, numcus ОТ C2 WHERE numcusts> 70) AS D;

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

Също така, не забравяйте, че винаги можете да дефинирате друг CTE въз основа на последната заявка и след това да накарате най-външната заявка да взаимодейства с тази CTE:

С C1 AS( ИЗБЕРЕТЕ ГОДИНА (дата на поръчка) КАТО година на поръчка, custid FROM Sales.Orders),C2 AS( SELECT order year, COUNT(DISTINCT custid) AS numcusts ОТ C1 GROUP BY orderyear),C3 AS( SELECT order year, numcusts ОТ C2 WHERE numcusts > 70)ИЗБЕРЕТЕ година на поръчка, numcustsFROM C3;

От гледна точка на отстраняването на неизправности, както споменахме, обикновено ми е по-лесно да следвам логиката на кода, който се основава на CTE, в сравнение с кода, базиран на производни таблици. Решенията, базирани на извлечени таблици, обаче имат предимство в това, че можете да маркирате всяко ниво на влагане и да го стартирате независимо, както е показано на фигура 1.

Фигура 1:Може да подчертава и изпълнява част от кода с извлечени таблици

С CTE нещата са по-трудни. За да може кодът, включващ CTE, да може да се изпълнява, той трябва да започва с клауза WITH, последвана от един или повече наименовани таблични изрази в скоби, разделени със запетаи, последвани от заявка без скоби без предходна запетая. Можете да маркирате и стартирате всяка от вътрешните заявки, които са наистина самостоятелни, както и кода на цялостното решение; обаче не можете да маркирате и да стартирате успешно друга междинна част от решението. Например, Фигура 2 показва неуспешен опит за изпълнение на кода, представляващ C2.

Фигура 2:Не може да се подчертае и стартира част от кода с CTEs

Така че при CTE трябва да прибягвате до малко неудобни средства, за да можете да отстраните неизправности в междинна стъпка от решението. Например, едно често срещано решение е временно да се инжектира заявка SELECT * FROM your_cte точно под съответния CTE. След това маркирате и стартирате кода, включително инжектираната заявка, и когато сте готови, изтривате инжектираната заявка. Фигура 3 демонстрира тази техника.

Фигура 3:Инжектиране на SELECT * под съответния CTE

Проблемът е, че всеки път, когато правите промени в кода – дори временни незначителни като горните – има шанс, когато се опитате да се върнете обратно към оригиналния код, в крайна сметка ще въведете нова грешка.

Друга възможност е да стилизирате кода си малко по различен начин, така че всяка непърва CTE дефиниция да започва с отделен ред код, който изглежда така:

, cte_name AS (

След това, когато искате да изпълните междинна част от кода до даден CTE, можете да го направите с минимални промени в кода си. Използвайки коментар на ред, вие коментирате само този ред код, който съответства на този CTE. След това маркирате и изпълнявате кода до и включително вътрешната заявка на CTE, която сега се счита за най-външната заявка, както е показано на фигура 4.

Фигура 4:Пренаредете синтаксиса, за да активирате коментирането на един ред код

Ако не сте доволни от този стил, имате още една възможност. Можете да използвате блоков коментар, който започва точно преди запетаята, която предхожда интересния CTE и завършва след отворената скоба, както е показано на фигура 5.

Фигура 5:Използвайте блоков коментар

Това се свежда до лични предпочитания. Обикновено използвам временно инжектираната SELECT * техника на заявка.

Конструктор на стойности на таблица

Има известно ограничение в поддръжката на T-SQL за конструктори на стойности на таблица в сравнение със стандарта. Ако не сте запознати с конструкцията, не забравяйте първо да разгледате част 2 от поредицата, където я описвам подробно. Докато T-SQL ви позволява да дефинирате извлечена таблица на базата на конструктор на стойност на таблица, той не ви позволява да дефинирате CTE въз основа на конструктор на стойност на таблица.

Ето поддържан пример, който използва производна таблица:

ИЗБЕРЕТЕ custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401') AS MyCusts (поръчител, име на фирма, дата на договора);

За съжаление, подобен код, който използва CTE, не се поддържа:

С MyCusts(custid, companyname, contractdate) AS( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', ' 20200401' ))ИЗБЕРЕТЕ custid, companyname, contractdateFROM MyCusts;

Този код генерира следната грешка:

Съобщение 156, ниво 15, състояние 1, ред 337
Неправилен синтаксис близо до ключовата дума 'VALUES'.

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

С MyCust AS( SELECT * FROM ( СТОЙНОСТИ( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401') ) КАТО MyCusts(custid, companyname, contractdate))SELECT custid, companyname, contractdateFROM MyCusts;

Друго е да се прибегне до техниката, която хората са използвали преди въвеждането на конструктори със стойности на таблица в T-SQL – като се използва серия от заявки FROMless, разделени от оператори UNION ALL, като така:

WITH MyCusts(custid, companyname, contractdate) AS( SELECT 2, 'Cust 2', '20200212' UNION ALL SELECT 3, 'Cust 3', '20200118' UNION ALL SELECT 5, 'Cust 5', 'Cust 5', '004' ')ИЗБЕРЕТЕ custid, име на фирма, договорна датаFROM MyCusts;

Забележете, че псевдонимите на колоните се присвояват непосредствено след името на CTE.

Двата метода се алгебризират и оптимизират еднакво, така че използвайте този, който ви е по-удобен.

Създаване на поредица от числа

Инструмент, който използвам доста често в своите решения, е помощна таблица с числа. Една от възможностите е да създадете действителна таблица с числа във вашата база данни и да я попълните с последователност с разумен размер. Друго е да се разработи решение, което произвежда поредица от числа в движение. За последната опция искате входните данни да бъдат ограничители на желания диапазон (ще ги наречем @low и @high ). Искате вашето решение да поддържа потенциално големи диапазони. Ето моето решение за тази цел, използвайки CTE, със заявка за диапазона от 1001 до 1010 в този конкретен пример:

ДЕКЛАРИРАНЕ @ниско КАТО ГОЛЯМО =1001, @високо КАТО ГОЛЯМО =1010; С L0 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ (СТОЙНОСТИ(1),(1)) КАТО D(c) ), L1 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L0 КАТО КРЪСТО СЪЕДИНИ L0 AS B ), L2 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L1 КАТО КРЪСТО СЪЕДИНЕНИЕ L1 AS B ), L3 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L2 КАТО КРЪСТО СЪЕДИНЕНИЕ L2 КАТО B ), L4 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L3 КАТО КРЪСТО СЪЕДИНИ L3 КАТО B ), L5 AS ( ИЗБЕРЕТЕ 1 КАТО c ОТ L4 КАТО КРЪСТО ПРИСЪЕДИНЕТЕ L4 КАТО B ), Nums КАТО ( ИЗБЕРЕТЕ РЕД_НОМЕРА() НАД (ПОРЪЧАЙТЕ ОТ (ИЗБЕРЕТЕ NULL)) КАТО номер на ред ОТ L5 )ИЗБЕРЕТЕ ВЪРХУ (@high - @low + 1) @low + rownum - 1 КАТО nFROM NumsORDER BY rownum;

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

n-----1001100210031004100510061007100810091010

Първият CTE, наречен L0, се основава на конструктор на стойност на таблица с два реда. Действителните стойности там са незначителни; важното е, че има два реда. След това има последователност от пет допълнителни CTE, наречени L1 до L5, като всеки прилага кръстосано свързване между два екземпляра на предходния CTE. Следният код изчислява броя на редовете, потенциално генерирани от всеки от CTE, където @L е номерът на нивото на CTE:

ДЕКЛАРИРАНЕ @L КАТО INT =5; ИЗБЕРЕТЕ POWER(2., POWER(2., @L));

Ето числата, които получавате за всеки CTE:

CTE Кардиналност
L0 2
L1 4
L2 16
L3 256
L4 65 536
L5 4,294,967,296

Изкачването до ниво 5 ви дава над четири милиарда реда. Това би трябвало да е достатъчно за всяка практическа употреба, за която се сещам. Следващата стъпка се извършва в CTE, наречен Nums. Използвате функция ROW_NUMBER, за да генерирате поредица от цели числа, започващи с 1, въз основа на неопределен ред (ORDER BY (SELECT NULL)) и именувайте колоната с резултата rownum. И накрая, външната заявка използва ТОП филтър, базиран на подреждане на rownum, за да филтрира толкова числа, колкото е желаната мощност на последователността (@high – @low + 1), и изчислява резултатното число n като @low + rownum – 1.

Тук можете наистина да оцените красотата на CTE дизайна и спестяванията, които той позволява, когато изграждате решения по модулен начин. В крайна сметка процесът на разопаковане разопакова 32 таблици, всяка от които се състои от два реда, базирани на константи. Това може ясно да се види в плана за изпълнение на този код, както е показано на Фигура 6 с помощта на SentryOne Plan Explorer.

Фигура 6:План за генериране на заявка последователност от числа

Всеки оператор Constant Scan представлява таблица с константи с два реда. Работата е там, че операторът Top е този, който изисква тези редове и късо съединение, след като получи желания номер. Забележете 10-те реда, посочени над стрелката, вливащи се в оператора Top.

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

Използвайки извлечени таблици, всъщност можете да напишете решение, което замества всяка CTE препратка с основната заявка, която представлява. Това, което получавате, е доста страшно:

ДЕКЛАРИРАНЕ @ниско КАТО ГОЛЯМО =1001, @високо КАТО ГОЛЯМО =1010; SELECT TOP(@high - @low + 1) @low + rownum - 1 КАТО nFROM ( SELECT ROW_NUMBER() НАД (ПОРЪЧАЙТЕ BY (SELECT NULL)) КАТО rownum FROM ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM ( SELECT 1 КАТО C ОТ ( ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01(c) КРЪСТО СЪЕДИНЯВАНЕ (СТОЙНОСТИ(1),(1)) КАТО D02(c) ) КАТО D3 КРЪСТО ПРИЕДИНЯВАНЕ (ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01(c) КРЪСТО СЪЕДИНЯВАНЕ (СТОЙНОСТИ(1),(1)) КАТО D02(c) ) КАТО D4 ) КАТО D5 КРЪСТО СЪЕДИНЕНИЕ (ИЗБЕРЕТЕ 1 КАТО C ОТ ( ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01(c) КРЪСТО ПРИЕДИНЯВАНЕ (СТОЙНОСТИ(1),(1)) КАТО D02(c) ) КАТО D3 КРЪСТО ПРИЕДИНЯВАНЕ (ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01(c) КРЪСТО СЪЕДИНЕНИЕ (СТОЙНОСТИ(1),( 1)) КАТО D02(c) ) КАТО D4 ) КАТО D6 ) КАТО D7 КРЪСТО СЪЕДИНЕНИЕ (ИЗБЕРЕТЕ 1 КАТО C ОТ (ИЗБЕРЕТЕ 1 КАТО C ОТ (ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01( в) КРЪСТО СЪЕДИНЯВАНЕ (СТОЙНОСТИ(1),(1)) КАТО D02(c) ) КАТО D3 КРЪСТО СЪЕДИНЕНИЕ (ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01(c) КРЪСТО СЪЕДИНЯВАНЕ (СТОЙНОСТИ( 1),(1)) AS D02(c) ) AS D4 ) КАТО D5 КРЪСТО СЪЕДИНЕНИ ( ИЗБЕРЕТЕ 1 КАТО C ОТ ( ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01(c) КРЪСТО СЪЕДИНЯВАНЕ (СТОЙНОСТИ(1),(1)) КАТО D02(c) ) КАТО D3 КРЪСТО ПРИЕДИНЯВАНЕ ( ИЗБЕРЕТЕ 1 КАТО C ОТ (СТОЙНОСТИ(1),(1)) КАТО D01(c) КРЪСТО ПРИЕДИНЯВАНЕ (СТОЙНОСТИ(1),(1)) КАТО D02(c) ) КАТО D4 ) КАТО D6 ) КАТО D8 ) КАТО D9 КРЕСТО JOIN ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1 )) AS D02(c) ) AS D3 CROSS JOIN ( SELECT 1 AS C FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5 CROSS JOIN ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) ) AS D3 CROSS JOIN ( SELECT 1 AS C FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7 CROSS JOIN ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM (VALUES(1),(1) ) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) ) AS D3 CROSS JOIN ( SELECT 1 AS C FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5 CROSS JOIN ( SELECT 1 AS C FROM ( SELECT 1 AS C FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) ) AS D3 CROSS JOIN ( SELECT 1 AS C FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D10 ) AS NumsORDER BY rownum;

Obviously, you don’t want to write a solution like this, but it’s a good way to illustrate what SQL Server does behind the scenes with your CTE code.

If you were really planning to write a solution based on derived tables, instead of using the above nested approach, you’d be better off simplifying the logic to a single query with 31 cross joins between 32 table value constructors, each based on two rows, like so:

DECLARE @low AS BIGINT =1001, @high AS BIGINT =1010; SELECT TOP(@high - @low + 1) @low + rownum - 1 AS nFROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum FROM (VALUES(1),(1)) AS D01(c) CROSS JOIN (VALUES(1),(1)) AS D02(c) CROSS JOIN (VALUES(1),(1)) AS D03(c) CROSS JOIN (VALUES(1),(1)) AS D04(c) CROSS JOIN (VALUES(1),(1)) AS D05(c) CROSS JOIN (VALUES(1),(1)) AS D06(c) CROSS JOIN (VALUES(1),(1)) AS D07(c) CROSS JOIN (VALUES(1),(1)) AS D08(c) CROSS JOIN (VALUES(1),(1)) AS D09(c) CROSS JOIN (VALUES(1),(1)) AS D10(c) CROSS JOIN (VALUES(1),(1)) AS D11(c) CROSS JOIN (VALUES(1),(1)) AS D12(c) CROSS JOIN (VALUES(1),(1)) AS D13(c) CROSS JOIN (VALUES(1),(1)) AS D14(c) CROSS JOIN (VALUES(1),(1)) AS D15(c) CROSS JOIN (VALUES(1),(1)) AS D16(c) CROSS JOIN (VALUES(1),(1)) AS D17(c) CROSS JOIN (VALUES(1),(1)) AS D18(c) CROSS JOIN (VALUES(1),(1)) AS D19(c) CROSS JOIN (VALUES(1),(1)) AS D20(c) CROSS JOIN (VALUES(1),(1)) AS D21(c) CROSS JOIN (VALUES(1),(1)) AS D22(c) CROSS JOIN (VALUES(1),(1)) AS D23(c) CROSS JOIN (VALUES(1),(1)) AS D24(c) CROSS JOIN (VALUES(1),(1)) AS D25(c) CROSS JOIN (VALUES(1),(1)) AS D26(c) CROSS JOIN (VALUES(1),(1)) AS D27(c) CROSS JOIN (VALUES(1),(1)) AS D28(c) CROSS JOIN (VALUES(1),(1)) AS D29(c) CROSS JOIN (VALUES(1),(1)) AS D30(c) CROSS JOIN (VALUES(1),(1)) AS D31(c) CROSS JOIN (VALUES(1),(1)) AS D32(c) ) AS NumsORDER BY rownum;

Still, the solution based on CTEs is obviously significantly simpler. The plans are identical.

Used in modification statements

CTEs can be used as the source and target tables in INSERT, UPDATE, DELETE and MERGE statements. They cannot be used in the TRUNCATE statement.

The syntax is pretty straightforward. You start the statement as usual with a WITH clause, followed by one or more CTEs separated by commas. Then you specify the outer modification statement, which interacts with the CTEs that were defined under the WITH clause as the source tables, target table, or both. Just like I explained in Part 2 about derived tables, also with CTEs what really gets modified is the underlying base table that the table expression uses. I’ll show a couple of examples using DELETE and UPDATE statements, but remember that you can use CTEs in MERGE and INSERT statements as well.

Here’s the general syntax of a DELETE statement against a CTE:

WITH  [ () ] AS( 
)DELETE [ FROM ]
[ WHERE ];

As an example (don’t actually run it), the following code deletes the 10 oldest orders:

WITH OldestOrders AS( SELECT TOP (10) * FROM Sales.Orders ORDER BY orderdate, orderid)DELETE FROM OldestOrders;

Here’s the general syntax of an UPDATE statement against a CTE:

WITH 
[ () ] AS(
)UPDATE
SET [ WHERE ];

As an example, the following code updates the 10 oldest unshipped orders that have an overdue required date, increasing the required date to 10 days from today:

BEGIN TRAN; WITH OldestUnshippedOrders AS( SELECT TOP (10) orderid, requireddate, DATEADD(day, 10, CAST(SYSDATETIME() AS DATE)) AS newrequireddate FROM Sales.Orders WHERE shippeddate IS NULL AND requireddate < CAST(SYSDATETIME() AS DATE) ORDER BY orderdate, orderid)UPDATE OldestUnshippedOrders SET requireddate =newrequireddate OUTPUT inserted.orderid, deleted.requireddate AS oldrequireddate, inserted.requireddate AS newrequireddate; ROLLBACK TRAN;

The code applies the update in a transaction that it then rolls back so that the change won’t stick.

This code generates the following output, showing both the old and the new required dates:

orderid oldrequireddate newrequireddate----------- --------------- ---------------11008 2019-05-06 2020-07-1611019 2019-05-11 2020-07-1611039 2019-05-19 2020-07-1611040 2019-05-20 2020-07-1611045 2019-05-21 2020-07-1611051 2019-05-25 2020-07-1611054 2019-05-26 2020-07-1611058 2019-05-27 2020-07-1611059 2019-06-10 2020-07-1611061 2019-06-11 2020-07-16(10 rows affected)

Of course you will get a different new required date based on when you run this code.

Резюме

I like CTEs. They have a few advantages compared to derived tables. Instead of nesting the code, you define multiple CTEs separated by commas, typically leading to a more modular solution that is easier to review and maintain. Also, you can have multiple references to the same CTE name in the outer statement, so you don’t need to repeat the inner table expression’s code. However, unlike derived tables, CTEs cannot be defined directly based on a table value constructor, and you cannot highlight and execute some of the intermediate parts of the code. The following table summarizes the differences between derived tables and CTEs:

Item Derived table CTE
Supports nesting Yes No
Supports multiple references No Yes
Supports table value constructor Yes No
Can highlight and run part of code Yes No
Supports recursion No Yes

As the last item says, derived tables do not support recursive capabilities, whereas CTEs do. Recursive queries are the focus of next month’s article.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Производителност на sys.partitions

  2. Модел на данни за заплати

  3. Оттеглени функции, които да извадите от кутията си с инструменти – част 3

  4. Слайд тестове и проби от #SQLintersection

  5. Откриване и класификация на SQL данни