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

Основи на табличните изрази, част 2 – Производни таблици, логически съображения

Миналия месец предоставих фон за таблични изрази в T-SQL. Обясних контекста от релационната теория и SQL стандарта. Обясних как една таблица в SQL е опит да се представи релация от релационната теория. Обясних също, че релационният израз е израз, който оперира с една или повече релации като входни данни и води до релация. По същия начин, в SQL изразът за таблица е израз, работещ върху една или повече входни таблици и в резултат на което се получава таблица. Изразът може да бъде заявка, но не е задължително. Например изразът може да бъде конструктор на стойност на таблица, както ще обясня по-късно в тази статия. Обясних също, че в тази серия се фокусирам върху четири специфични типа изрази за именувани таблици, които T-SQL поддържа:извлечени таблици, общи изрази на таблици (CTE), изгледи и вградени функции с стойност на таблица (TVF).

Ако сте работили с T-SQL от известно време, вероятно сте се натъкнали на доста случаи, в които или трябва да използвате таблични изрази, или е било някак по-удобно в сравнение с алтернативни решения, които не ги използват. Ето само няколко примера за случаи на употреба, които ви идват на ум:

  • Създайте модулно решение, като разбиете сложните задачи на стъпки, всяка от които е представена от различен табличен израз.
  • Смесване на резултати от групирани заявки и подробности, в случай че решите да не използвате функциите на прозореца за тази цел.
  • Обработка на логическа заявка обработва клаузите на заявката в следния ред:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. В резултат на това на същото ниво на влагане псевдоними на колони, които дефинирате в клаузата SELECT, са достъпни само за клаузата ORDER BY. Те не са достъпни за останалите клаузи за заявка. С табличните изрази можете да използвате повторно псевдоними, които дефинирате във вътрешна заявка във всяка клауза на външната заявка и по този начин избягвате повторението на дълги/сложни изрази.
  • Функциите на прозореца могат да се появяват само в клаузите SELECT и ORDER BY на заявката. С табличните изрази можете да присвоите псевдоним на израз въз основа на функция на прозорец и след това да използвате този псевдоним в заявка срещу израза на таблицата.
  • Операторът PIVOT включва три елемента:групиране, разпространение и агрегиране. Този оператор идентифицира групиращия елемент имплицитно чрез елиминиране. Използвайки табличен израз, можете да проектирате точно трите елемента, за които се предполага, че участват, и да накарате външната заявка да използва израза на таблицата като входна таблица на оператора PIVOT, като по този начин контролирате кой елемент е групиращият елемент.
  • Промените с TOP не поддържат клауза ORDER BY. Можете да контролирате кои редове да се избират непряко, като дефинирате табличен израз въз основа на заявка SELECT с филтър TOP или OFFSET-FETCH и клауза ORDER BY и да приложите модификацията спрямо табличния израз.

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

В статията от този месец се фокусирам конкретно върху логическото третиране на извлечените таблици.

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

Производни таблици

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

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

Като настрана проверих как стандартът на SQL дефинира базова таблица и веднага съжалявах, че притесних.

4.15.2 Основни таблици

Базовата таблица е или постоянна базова таблица, или временна таблица.

Постоянната базова таблица е или обикновена постоянна базова таблица, или таблица с версия на системата.

Редовната базова таблица е или обикновена постоянна базова таблица, или временна.“

Добавено тук без повече коментари...

В T-SQL можете да създадете основна таблица с израз CREATE TABLE, но има и други опции, например SELECT INTO и DECLARE @T КАТО ТАБЛИЦА.

Ето дефиницията на стандарта за производни таблици в общия смисъл:

4.15.3 Производни таблици

Произведена таблица е таблица, извлечена директно или непряко от една или повече други таблици чрез оценка на израз, като например <обединена таблица>, <таблица за промяна на данни>, <израз на заявка> или <израз на таблица>. <израз на заявка> може да съдържа по избор <подреждане по клауза>. Подреждането на редовете на таблицата, определено от <израза на заявката>, е гарантирано само за <израза на заявката>, който непосредствено съдържа .”

Тук има няколко интересни неща, които трябва да се отбележат относно производните таблици в общия смисъл. Едното е свързано с коментара относно поръчката. Ще стигна до това по-късно в статията. Друго е, че извлечената таблица в SQL може да бъде валиден самостоятелен табличен израз, но не е задължително. Например, следният израз представлява производна таблица и е също се счита за валиден самостоятелен табличен израз (можете да го стартирате):

ИЗБЕРЕТЕ custid, companynameFROM Sales.CustomersWHERE country =N'USA'

Обратно, следният израз представлява производна таблица, но не е валиден самостоятелен табличен израз:

T1 INNER JOIN T2 ON T1.keycol =T2.keycol

T-SQL поддържа редица таблични оператори, които дават производна таблица, но не се поддържат като самостоятелни изрази. Това са:ПРИСЪЕДИНЕТЕ, ВЪРНЕТЕ, ОТМЕНИТЕ и ПРИЛОЖИТЕ. Имате нужда от клауза, в която да работят (обикновено FROM, но също и клаузата USING на оператора MERGE) и заявка за хост.

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

Синтаксис

Произведена таблица може да бъде дефинирана като част от външен оператор SELECT в неговата клауза FROM. Може също да се дефинира като част от операторите DELETE и UPDATE в тяхната клауза FROM и като част от оператор MERGE в неговата клауза USING. Ще предоставя повече подробности за синтаксиса, когато се използва в оператори за модификация по-късно в тази статия.

Ето синтаксиса за опростена заявка SELECT спрямо получена таблица:

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

Дефиницията на извлечената таблица се появява там, където може нормално да се появи базова таблица, в клаузата FROM на външната заявка. Може да бъде вход за оператор на таблица, като JOIN, APPLY, PIVOT и UNPIVOT. Когато се използва като правилен вход за оператор APPLY, частта

от извлечената таблица има право да има корелации с колони от външна таблица (повече за това в специална бъдеща статия от поредицата). В противен случай табличният израз трябва да бъде самостоятелен.

Външният израз може да има всички обичайни елементи за запитване. В случай на оператор SELECT:WHERE, GROUP BY, HAVING, ORDER BY и както споменахме, таблични оператори в клаузата FROM.

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

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

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

custid companyname------- ---------------32 Клиент YSIQX36 Клиент LVJSO43 Клиент UISOJ45 Клиент QXPPT48 Клиент DVFMB55 Клиент KZQZT65 Клиент NYUHS71 Клиент LCOUJ75 Клиент XOJYP77 Клиент LCYBZYP Клиент Клиент EYHKM89 Клиент YBQTI

Има три основни части за идентифициране в израз, включващ дефиниция на производна таблица:

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

Изразът на таблицата трябва да представлява таблица и като такъв трябва да удовлетворява определени изисквания, които нормалната заявка не трябва да отговаря непременно. Скоро ще предоставя подробностите в раздела „Изразът на таблица е таблица“.

Що се отнася до името на таблицата, извлечена от целта; често срещано предположение сред разработчиците на T-SQL е, че това е просто име или псевдоним, който присвоявате на целевата таблица. По същия начин, разгледайте следната заявка:

ИЗБЕРЕТЕ custid, companynameFROM Sales.Customers AS CWHERE country =N'USA';

Също така тук общоприетото допускане е, че AS C е просто начин за преименуване или псевдоним на таблицата Клиенти за целите на тази заявка, като се започне от стъпката на обработка на логическата заявка, където се присвоява името и нататък. Въпреки това, от гледна точка на релационната теория, има по-дълбок смисъл в това, което C представлява. C е това, което е известно като променлива на диапазона. C е производна релационна променлива, която се простира над кортежите във входната релационна променлива Клиенти. В горния пример C обхвата кортежите в Customers и оценява предиката държава =N'USA'. Кортежи, за които предикатът се оценява като истина, стават част от резултатната връзка C.

Табличен израз е таблица

С предисторията, която предоставих досега, това, което ще обясня по-нататък, не би трябвало да е изненада. Частта

от дефиниция на производна таблица е таблица . Това е така, дори ако е изразено като заявка. Помните ли свойството на затваряне на релационната алгебра? Същото важи и за останалата част от гореспоменатите именовани таблични изрази (CTE, изгледи и вградени TVF). Както вече научихте, таблицата на SQL е аналогът на отношението на релационната теория , макар и не идеален аналог. По този начин табличният израз трябва да удовлетворява определени изисквания, за да гарантира, че резултатът е таблица – такива, които заявка, която не се използва като табличен израз, не трябва непременно. Ето три специфични изисквания:

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

Нека разделим тези изисквания едно по едно, като обсъдим уместността както за релационната теория, така и за SQL.

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

Не забравяйте, че връзката има заглавие и тяло. Заглавието на релация е набор от атрибути (колони в SQL). Атрибутът има име и име на тип и се идентифицира с името си. Заявка, която не се използва като табличен израз, не трябва непременно да присвоява имена на всички целеви колони. Разгледайте следната заявка като пример:

ИЗБЕРЕТЕ empid, име, фамилия, CONCAT_WS(N'/', държава, регион, град)ОТ HR.Employees;

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

empid име фамилно име (без име на колона)------ ---------- ---------- ------------- ----1 Сара Дейвис USA/WA/Seattle2 Don Funk USA/WA/Tacoma3 Judy Lew USA/WA/Kirkland4 Yael Peled USA/WA/Redmond5 Sven Mortensen UK/London6 Paul Suurs UK/London7 Russell King UK/London8 Мария Камерън САЩ/WA/Сиатъл9 Патриша Дойл UK/Лондон

Изходът на заявката има анонимна колона, получена от конкатенацията на атрибутите на местоположението с помощта на функцията CONCAT_WS. (Между другото, тази функция беше добавена в SQL Server 2017, така че ако изпълнявате кода в по-ранна версия, не се колебайте да замените това изчисление с алтернативно изчисление по ваш избор.) Следователно тази заявка не върнете таблица, да не говорим за релация. Следователно не е валидно да се използва такава заявка като израз на таблица/част от вътрешна заявка от дефиниция на производна таблица.

Опитайте:

SELECT *FROM ( SELECT empid, име, фамилия, CONCAT_WS(N'/', държава, регион, град) FROM HR.Employees ) AS D;

Получавате следната грешка:

Съобщение 8155, ниво 16, състояние 2, ред 50
Не е посочено име на колона за колона 4 от „D“.

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

Решението, разбира се, е да се уверите, че изрично присвоявате имена на колони, които са резултат от изчисления. T-SQL поддържа доста техники за именуване на колони. Ще спомена две от тях.

Можете да използвате вградена техника за именуване, при която присвоявате името на целевата колона след изчислението и незадължителна AS клауза, както е в < expression > [ AS ] < column name > , така:

ИЗБЕРЕТЕ empid, име, фамилия, custlocationFROM ( SELECT empid, име, фамилия, CONCAT_WS(N'/', държава, регион, град) КАТО custlocation FROM HR.Employees ) AS D;

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

empid име фамилно име custlocation------ ---------- ---------- ----------------1 Сара Дейвис USA/WA/Seattle2 Don Funk USA/WA/Tacoma3 Judy Lew USA/WA/Kirkland4 Yael Peled USA/WA/Redmond5 Sven Mortensen UK/London6 Paul Suurs UK/London7 Russell King UK/London8 Мария Камерън САЩ/WA/Seattle9 Патриша Дойл Великобритания/Лондон

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

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

ИЗБЕРЕТЕ empid, име, фамилия, custlocationFROM ( SELECT empid, име, фамилия, CONCAT_WS(N'/', държава, регион, град) FROM HR.Employees ) AS D(empid, собствено име, фамилия, custlocation); 

С тази техника обаче трябва да изброите имената за всички колони - включително тези, които вече имат имена. Присвояването на имената на целевите колони се извършва по позиция, отляво надясно, т.е. името на първата целева колона представлява първия израз в списъка SELECT на вътрешната заявка; името на втората целева колона представлява втория израз; и така нататък.

Имайте предвид, че в случай на несъответствие между вътрешните и външните имена на колони, да речем, поради грешка в кода, обхватът на вътрешните имена е вътрешната заявка или по-точно променливата на вътрешния диапазон (тук имплицитно HR.Employees AS Employees) - и обхватът на външните имена е променливата на външния диапазон (D в нашия случай). Има малко повече участие в обхвата на имената на колони, което е свързано с обработката на логическа заявка, но това е елемент за по-късни дискусии.

Потенциалът за грешки с външния синтаксис за именуване се обяснява най-добре с пример.

Разгледайте изхода от предишната заявка с пълния набор от служители от таблицата HR.Employees. След това помислете за следната заявка и преди да я стартирате, опитайте се да разберете кои служители очаквате да видите в резултата:

ИЗБЕРЕТЕ empid, име, фамилия, custlocationFROM ( ИЗБЕРЕТЕ empid, име, фамилия, CONCAT_WS(N'/', държава, регион, град) ОТ HR.Служители КЪДЕ фамилно име LIKE N'D%' ) КАТО D(empid, фамилно име, собствено име, custlocation)КЪДЕ собствено име КАТО N'D%';

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

Сега стартирайте заявката и проверете действителния изход:

empid име фамилно име custlocation------ ---------- --------- ---------------1 Дейвис Сара САЩ/WA/Сиатъл9 Дойл Патриша Великобритания/Лондон

Какво се случи?

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

Синтаксисът на вградения псевдоним не е склонен към подобни грешки. От една страна, обикновено не поставяте псевдоним на колона, която вече има име, от което сте доволни. Второ, дори ако искате да зададете различен псевдоним за колона, която вече има име, не е много вероятно със синтаксиса AS да зададете грешен псевдоним. Помисли за това; колко е вероятно да пишете така:

ИЗБЕРЕТЕ empid, име, фамилия, custlocationFROM ( SELECT empid КАТО empid, име КАТО фамилия, фамилия КАТО първо име, CONCAT_WS(N'/', държава, регион, град) КАТО custlocation FROM HR.Служители КЪДЕ фамилия КАТО N'D %' ) КАТО DWHERE първо име КАТО N'D%';

Очевидно, не е много вероятно.

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

Обратно към факта, че заглавието на релация е набор от атрибути и като се има предвид, че атрибутът е идентифициран с име, имената на атрибути трябва да бъдат уникални за същата релация. В дадена заявка винаги можете да се обърнете към атрибут, като използвате име от две части с име на променлива на диапазон като квалификатор, както в <име на променлива на диапазон>.<име на колона>. Когато името на колоната без квалификатора е недвусмислено, можете да пропуснете префикса на името на променливата на диапазона. Това, което е важно да запомните обаче, е това, което казах по-рано за обхвата на имената на колоните. В код, който включва израз на таблица с име, както с вътрешна заявка (изразът на таблицата), така и с външна заявка, обхватът на имената на колоните във вътрешната заявка е променливите на вътрешния диапазон, а обхватът на имената на колоните във външната query са променливите на външния диапазон. Ако вътрешната заявка включва множество изходни таблици с едно и също име на колона, все пак можете да се обърнете към тези колони по недвусмислен начин, като добавите името на променливата на диапазона като префикс. Ако не присвоите изрично име на променлива на диапазона, ще получите такава, присвоена имплицитно, сякаш сте използвали <име на таблица> AS <име на таблица>.

Помислете за следната самостоятелна заявка като пример:

ИЗБЕРЕТЕ C.custid, O.custid, O.orderidFROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid;

Тази заявка не се проваля с грешка в името на дублиращата се колона, тъй като една колона custid всъщност се казва C.custid, а другата O.custid в обхвата на текущата заявка. Тази заявка генерира следния изход:

custid custid orderid----------- ----------- -----------1 1 106431 1 106921 1 107021 1 108351 1 109521 1 110112 2 103082 2 106252 2 107592 2 10926...

Опитайте обаче да използвате тази заявка като табличен израз в дефиницията на производна таблица с име CO, така:

SELECT *FROM ( SELECT C.custid, O.custid, O.orderid ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO;

Що се отнася до външната заявка, имате една променлива за диапазон с име CO и обхватът на всички имена на колони във външната заявка е тази променлива на диапазона. Имената на всички колони в дадена променлива на диапазона (не забравяйте, че променливата на диапазона е релационна променлива) трябва да са уникални. Следователно получавате следната грешка:

Съобщение 8156, ниво 16, състояние 1, ред 80
Колоната 'custid' е посочена няколко пъти за 'CO'.

Поправката, разбира се, е да се присвоят различни имена на колони на двете custid колони, що се отнася до променливата на диапазона CO, така:

SELECT *FROM ( SELECT C.custid AS custid, O.custid AS ordercustid, O.orderid ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO; 

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

custcustid ordercustid orderid----------- ----------- -----------1 1 106431 1 106921 1 107021 1 108351 1 109521 1 110112 2 103082 2 106252 2 107592 2 10926...

Ако следвате добрите практики, изрично изброявате имената на колоните в списъка SELECT на най-външната заявка. Тъй като има само една променлива на диапазона, не е нужно да използвате името от две части за препратките към външните колони. Ако желаете да използвате името от две части, добавяте префикс към имената на колоните с името на променливата за външен диапазон CO, така:

ИЗБЕРЕТЕ CO.custcustid, CO.ordercustid, CO.orderidFROM ( SELECT C.custid AS custcustid, O.custid AS ordercustid, O.orderid ОТ Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS CO;

Няма поръчка

Имам много неща, които трябва да кажа за изразите на именувани таблици и подреждането – достатъчно за отделна статия – така че ще посветя една бъдеща статия на тази тема. Все пак исках да засегна темата накратко тук, тъй като е толкова важна. Припомнете си, че тялото на релация е набор от кортежи и по подобен начин тялото на таблица е набор от редове. Комплектът няма поръчка. Все пак SQL позволява на най-външната заявка да има клауза ORDER BY, обслужваща значение за подреждане на презентацията, както демонстрира следната заявка:

ИЗБЕРЕТЕ orderid, valFROM Sales.OrderValuesORDER BY val DESC;

Това, което трябва да разберете обаче, е, че тази заявка не връща връзка като резултат. Дори от гледна точка на SQL, заявката не връща таблица като резултат и следователно не е се счита за табличен израз. Следователно е невалидно да се използва такава заявка като част от израза на таблица от дефиниция на производна таблица.

Опитайте да изпълните следния код:

ИЗБЕРЕТЕ идентификатор на поръчката, valFROM ( SELECT orderid, val FROM Sales.OrderValues ​​ORDER BY val DESC ) КАТО D;

Получавате следната грешка:

Съобщение 1033, ниво 15, състояние 1, ред 124
Клаузата ORDER BY е невалидна в изгледи, вградени функции, производни таблици, подзаявки и изрази за общи таблици, освен ако също не е указано TOP, OFFSET или FOR XML.

Ще се обърна към освен ако част от съобщението за грешка скоро.

Ако искате най-външната заявка да върне подреден резултат, трябва да посочите клаузата ORDER BY в най-външната заявка, както следва:

ИЗБЕРЕТЕ orderid, valFROM ( SELECT orderid, val FROM Sales.OrderValues ​​) КАТО DORDER BY val DESC;

Що се отнася до освен част от съобщението за грешка; T-SQL поддържа собствения TOP филтър, както и стандартния OFFSET-FETCH филтър. И двата филтъра разчитат на клауза ORDER BY в същия обхват на заявката, за да дефинират за тях кои горни редове да филтрират. Това за съжаление е резултат от капан в дизайна на тези функции, който не отделя подреждането на презентацията от подреждането на филтъра. Както и да е, както Microsoft с неговия TOP филтър, така и стандартът с неговия филтър OFFSET-FETCH, позволяват посочване на клауза ORDER BY във вътрешната заявка, стига да посочва също филтъра TOP или OFFSET-FETCH, съответно. Така че тази заявка е валидна, например:

ИЗБЕРЕТЕ идентификатор на поръчката, valFROM ( SELECT TOP (3) orderid, val FROM Sales.OrderValues ​​ORDER BY val DESC ) КАТО D;

Когато изпълних тази заявка в моята система, тя генерира следния изход:

поръчайте val------- ---------10865 16387.5010981 15810.0011030 12615.05

Това, което е важно да се подчертае, е, че единствената причина, поради която клаузата ORDER BY е разрешена във вътрешната заявка, е да поддържа филтъра TOP. Това е единствената гаранция, която получавате, що се отнася до поръчката. Тъй като външната заявка също няма клауза ORDER BY, вие не получавате гаранция за специфично подреждане на презентация от тази заявка, въпреки каквото и да е наблюдаваното поведение. Това е както в T-SQL, така и в стандарта. Ето цитат от стандарта, отнасящ се до тази част:

„Подреждането на редовете на таблицата, определено от <израза на заявката>, е гарантирано само за <израза на заявката>, който непосредствено съдържа <подреждане по клауза>.”

Както споменахме, има много повече да се каже за изразите на таблицата и подреждането, което ще направя в бъдеща статия. Ще дам също примери, демонстриращи как липсата на клауза ORDER BY във външната заявка означава, че не получавате никакви гаранции за подреждане на презентация.

И така, табличен израз, например вътрешна заявка в дефиниция на производна таблица, е таблица. По подобен начин самата извлечена таблица (в специфичния смисъл) също е таблица. Това не е основна маса, но въпреки това е маса. Същото важи и за CTE, изгледи и вградени TVF. Те не са базови таблици, а по-скоро извлечени (в по-общия смисъл), но въпреки това са таблици.

Недостатъци в дизайна

Производните таблици имат два основни недостатъка в дизайна си. И двете са свързани с факта, че извлечената таблица е дефинирана в клаузата FROM на външната заявка.

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

Ето един много основен пример, демонстриращ това:

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

Този код връща годините на поръчките и броя на клиентите, които са направили поръчки през всяка година, само за години, в които броят на клиентите, които са направили поръчки, е бил по-голям от 70.

Основната мотивация за използване на таблични изрази тук е да може да се препраща към псевдоним на колона многократно. Най-вътрешната заявка, използвана като табличен израз за извлечената таблица D1, отправя заявка към таблицата Sales.Orders и присвоява името на колоната orderyear на израза YEAR(orderdate), а също така връща колоната custid. Заявката към D1 групира редовете от D1 по година на поръчка и връща година на поръчка, както и отделния брой клиенти, които са направили поръчки през въпросната година, под псевдонима numcusts. Кодът дефинира производна таблица, наречена D2, въз основа на тази заявка. Най-външната заявка от заявките D2 и филтрира само години, в които броят на клиентите, които са направили поръчки, е бил по-голям от 70.

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

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

ИЗБЕРЕТЕ ГОДИНА(дата на поръчка) КАТО година на поръчка, COUNT(DISTINCT custid) AS numcustsFROM Sales.OrdersGROUP BY YEAR(orderdate)HAVING COUNT(DISTINCT custid)> 70;

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

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

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;

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

Обединяването е оператор на таблица, който третира двата си входа като набор - което означава, че няма ред между тях. Те се наричат ​​ляв и десен вход, така че можете да маркирате един от тях (или и двете) като запазена таблица във външно съединение, но все пак няма първо и второ сред тях. Разрешено е да използвате извлечени таблици като входни данни за обединяване, но името на променливата на диапазона, което присвоите на левия вход, не е достъпно в дефиницията на десния вход. Това е така, защото и двете са концептуално дефинирани в една и съща логическа стъпка, сякаш в един и същи момент от време. Следователно, когато обединявате извлечени таблици, не можете да дефинирате две променливи на диапазона на базата на един табличен израз. За съжаление трябва да повторите кода, дефинирайки две променливи на диапазона въз основа на две идентични копия на кода. Това разбира се усложнява поддръжката на кода и увеличава вероятността от грешки. Всяка промяна, която правите в един табличен израз, трябва да се приложи и към другия.

Както ще обясня в една бъдеща статия, CTE в своя дизайн не понасят тези два недостатъка, които възникват на извлечените таблици.

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

Конструктор на стойност на таблица ви позволява да конструирате стойност на таблица въз основа на самостоятелни скаларни изрази. След това можете да използвате такава таблица във външна заявка, точно както използвате производна таблица, която се основава на вътрешна заявка. В една бъдеща статия обсъждам странично извлечени таблици и корелациите в детайли и ще покажа по-сложни форми на конструктори за стойности на таблица. In this article, though, I’ll focus on a simple form that is based purely on self-contained scalar expressions.

The general syntax for a query against a table value constructor is as follows:

SELECT
) AS
(
);

The table value constructor is defined in the FROM clause of the outer query.

The table’s body is made of a VALUES clause, followed by a comma separated list of pairs of parentheses, each defining a row with a comma separated list of expressions forming the row’s values.

The table’s heading is a comma separated list of the target column names. I’ll talk about a shortcoming of this syntax regarding the table’s heading shortly.

The following code uses a table value constructor to define a table called MyCusts with three columns called custid, companyname and contractdate, and three rows:

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);

The above code is equivalent (both logically and in performance terms) in T-SQL to the following alternative:

SELECT custid, companyname, contractdateFROM ( SELECT 2, 'Cust 2', '20200212' UNION ALL SELECT 3, 'Cust 3', '20200118' UNION ALL SELECT 5, 'Cust 5', '20200401' ) AS MyCusts(custid, companyname, contractdate);

The two are internally algebrized the same way. The syntax with the VALUES clause is standard whereas the syntax with the unified FROMless queries isn’t, hence I prefer the former.

There is a shortcoming in the design of table value constructors in both standard SQL and in T-SQL. Remember that the heading of a relation is made of a set of attributes, and an attribute has a name and a type name. In the table value constructor’s syntax, you specify the column names, but not their data types. Suppose that you need the custid column to be of a SMALLINT type, the companyname column of a VARCHAR(50) type, and the contractdate column of a DATE type. It would have been good if we were able to define the column types as part of the definition of the table’s heading, like so (this syntax isn’t supported):

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);

That’s of course just wishful thinking.

The way it works in T-SQL, is that each literal that is based on a constant has a predetermined type irrespective of context. For instance, can you guess what the types of the following literals are:

  • 1
  • 2147483647
  • 2147483648
  • 1E
  • '1E'
  • '20200212'

Is 1 considered BIT, INT, SMALLINT, other?

Is 1E considered VARBINARY(1), VARCHAR(2), other?

Is '20200212' considered DATE, DATETIME, VARCHAR(8), CHAR(8), other?

There’s a simple trick to figure out the default type of a literal, using the SQL_VARIANT_PROPERTY function with the 'BaseType' property, like so:

SELECT SQL_VARIANT_PROPERTY(2147483648, 'BaseType');

What happens is that SQL Server implicitly converts the literal to SQL_VARIANT—since that’s what the function expects—but preserves its base type. It then reports the base type as requested.

Similarly, you can query other properties of the input value, like the maximum length (MaxLength), Precision, Scale, and so on.

Try it with the aforementioned literal values, and you will get the following:

  • 1:INT
  • 2147483647:INT
  • 2147483648:NUMERIC(10, 0)
  • 1E:FLOAT
  • '1E':VARCHAR(2)
  • '20200212':VARCHAR(8)

As you can see, SQL Server has default assumptions about the data type, maximum length, precision, scale, and so on.

There are some cases where you need to specify a literal of a certain type, but you cannot do it directly in T-SQL. For example, you cannot specify a literal of the following types directly:BIT, TINYINT, BIGINT, all date and time types, and quite a few others. Unfortunately, T-SQL doesn’t provide a selector property for its types, which would have served exactly the needed purpose of selecting a value of the given type. Of course, you can always convert an expression’s type explicitly using the CAST or CONVERT function, as in CAST(5 AS SMALLINT). If you don’t, SQL Server will sometimes need to implicitly convert some of your expressions to a different type based on its implicit conversion rules. For example, when you try to compare values of different types, e.g., WHERE datecol ='20200212', assuming datecol is of a DATE type. Another example is when you specify a literal in an INSERT or an UPDATE statement, and the literal’s type is different than the target column’s type.

If all this is not confusing enough, set operators like UNION ALL rely on data type precedence to define the target column types—and remember, a table value constructor is algebrized like a series of UNION ALL operations. Consider the table value constructor shown earlier:

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);

Each literal here has a predetermined type. 2, 3 and 5 are all of an INT type, so clearly the custid target column type is INT. If you had the values 1000000000, 3000000000 and 2000000000, the first and the third are considered INT and the second is considered NUMERIC(10, 0). According to data type precedence NUMERIC (same as DECIMAL) is stronger than INT, hence in such a case the target column type would be NUMERIC(10, 0).

If you want to figure out which data types SQL Server chooses for the target columns in your table value constructor, you have a few options. One is to use a SELECT INTO statement to write the table value constructor’s data into a temporary table, and then query the metadata for the temporary table, like so:

SELECT custid, companyname, contractdateINTO #MyCustsFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts');

Here’s the output of this code:

colname typename maxlength------------- ---------- ---------custid int 4companyname varchar 6contractdate varchar 8

You can then drop the temporary table for cleanup:

DROP TABLE IF EXISTS #MyCusts;

Another option is to use the SQL_VARIANT_PROPERTY, which I mentioned earlier, like so:

SELECT TOP (1) SQL_VARIANT_PROPERTY(custid, 'BaseType') AS custid_typename, SQL_VARIANT_PROPERTY(custid, 'MaxLength') AS custid_maxlength, SQL_VARIANT_PROPERTY(companyname, 'BaseType') AS companyname_typename, SQL_VARIANT_PROPERTY(companyname, 'MaxLength') AS companyname_maxlength, SQL_VARIANT_PROPERTY(contractdate, 'BaseType') AS contractdate_typename, SQL_VARIANT_PROPERTY(contractdate, 'MaxLength') AS contractdate_maxlengthFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate);

This code generates the following output (formatted for readability):

custid_typename custid_maxlength-------------------- ---------------- int 4 companyname_typename companyname_maxlength -------------------- --------------------- varchar 6 contractdate_typename contractdate_maxlength--------------------- ----------------------varchar 8

So, what if you need to control the types of the target columns? As mentioned earlier, say you need custid to be SMALLINT, companyname VARCHAR(50), and contractdate DATE.

Don’t be misled to think that it’s enough to explicitly convert just one row’s values. If a corresponding value’s type in any other row is considered stronger, it would dictate the target column’s type. Here’s an example demonstrating this:

SELECT custid, companyname, contractdateINTO #MyCusts1FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts1');

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

colname typename maxlength------------- --------- ---------custid int 4companyname varchar 50contractdate date 3

Notice that the type for custid is INT.

The same applies never mind which row’s values you explicitly convert, if you don’t convert all of them. For example, here the code explicitly converts the types of the values in the second row:

SELECT custid, companyname, contractdateINTO #MyCusts2FROM ( VALUES( 2, 'Cust 2', '20200212'), ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE) ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts2');

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

colname typename maxlength------------- --------- ---------custid int 4companyname varchar 50contractdate date 3

As you can see, custid is still of an INT type.

You basically have two main options. One is to explicitly convert all values, like so:

SELECT custid, companyname, contractdateINTO #MyCusts3FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)), ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE)), ( CAST(5 AS SMALLINT), CAST('Cust 5' AS VARCHAR(50)), CAST('20200401' AS DATE)) ) AS MyCusts(custid, companyname, contractdate); SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts3');

This code generates the following output, showing all target columns have the desired types:

colname typename maxlength------------- --------- ---------custid smallint 2companyname varchar 50contractdate date 3

That’s a lot of coding, though. Another option is to apply the conversions in the SELECT list of the query against the table value constructor, and then define a derived table against the query that applies the conversions, like so:

SELECT custid, companyname, contractdateINTO #MyCusts4FROM ( SELECT CAST(custid AS SMALLINT) AS custid, CAST(companyname AS VARCHAR(50)) AS companyname, CAST(contractdate AS DATE) AS contractdate FROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS D(custid, companyname, contractdate) ) AS MyCusts; SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlengthFROM tempdb.sys.columnsWHERE OBJECT_ID =OBJECT_ID(N'tempdb..#MyCusts4');

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

colname typename maxlength------------- --------- ---------custid smallint 2companyname varchar 50contractdate date 3

The reasoning for using the additional derived table is due to how logical query processing is designed. The SELECT clause is evaluated after FROM, WHERE, GROUP BY and HAVING. By applying the conversions in the SELECT list of the inner query, you allow expressions in all clauses of the outermost query to interact with the columns with the proper types.

Back to our wishful thinking, clearly, it would be good if we ever get a syntax that allows explicit control of the types in the definition of the table value constructor’s heading, like so:

SELECT custid, companyname, contractdateFROM ( VALUES( 2, 'Cust 2', '20200212' ), ( 3, 'Cust 3', '20200118' ), ( 5, 'Cust 5', '20200401' ) ) AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);

Когато приключите, изпълнете следния код за почистване:

DROP TABLE IF EXISTS #MyCusts1, #MyCusts2, #MyCusts3, #MyCusts4;

Used in modification statements

T-SQL allows you to modify data through table expressions. That’s true for derived tables, CTEs, views and inline TVFs. What gets modified in practice is some underlying base table that is used by the table expression. I have much to say about modifying data through table expressions, and I will in a future article dedicated to this topic. Here, I just wanted to briefly mention the types of modification statements that specifically support derived tables, and provide the syntax.

Derived tables can be used as the target table in DELETE and UPDATE statements, and also as the source table in the MERGE statement (in the USING clause). They cannot be used in the TRUNCATE statement, and as the target in the INSERT and MERGE statements.

For the DELETE and UPDATE statements, the syntax for defining the derived table is a bit awkward. You don’t define the derived table in the DELETE and UPDATE clauses, like you would expect, but rather in a separate FROM clause. You then specify the derived table name in the DELETE or UPDATE clause.

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

DELETE [ FROM ]

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

As an example (don’t actually run it), the following code deletes all US customers with a customer ID that is greater than the minimum for the same region (the region column represents the state for US customers):

DELETE FROM UCFROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY region ORDER BY custid) AS rownum FROM Sales.Customers WHERE country =N'USA' ) AS UCWHERE rownum> 1;

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

UPDATE

SET
FROM (
) [ AS ]
[ () ]
[ WHERE ];

As you can see, from the perspective of the definition of the derived table, it’s quite similar to the syntax of the DELETE statement.

As an example, the following code changes the company names of US customers to one using the format N'USA Cust ' + rownum, where rownum represents a position based on customer ID ordering:

BEGIN TRAN; UPDATE UC SET companyname =newcompanyname OUTPUT inserted.custid, deleted.companyname AS oldcompanyname, inserted.companyname AS newcompanynameFROM ( SELECT custid, companyname, N'USA Cust ' + CAST(ROW_NUMBER() OVER(ORDER BY custid) AS NVARCHAR(10)) AS newcompanyname FROM Sales.Customers WHERE country =N'USA' ) AS UC; 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 company names:

custid oldcompanyname newcompanyname------- --------------- ----------------32 Customer YSIQX USA Cust 136 Customer LVJSO USA Cust 243 Customer UISOJ USA Cust 345 Customer QXPPT USA Cust 448 Customer DVFMB USA Cust 555 Customer KZQZT USA Cust 665 Customer NYUHS USA Cust 771 Customer LCOUJ USA Cust 875 Customer XOJYP USA Cust 977 Customer LCYBZ USA Cust 1078 Customer NLTYP USA Cust 1182 Customer EYHKM USA Cust 1289 Customer YBQTI USA Cust 13

That’s it for now on the topic.

Резюме

Derived tables are one of the four main types of named table expressions that T-SQL supports. In this article I focused on the logical aspects of derived tables. I described the syntax for defining them and their scope.

Remember that a table expression is a table and as such, all of its columns must have names, all column names must be unique, and the table has no order.

The design of derived tables incurs two main flaws. In order to query one derived table from another, you need to nest your code, causing it to be more complex to maintain and troubleshoot. If you need to interact with multiple occurrences of the same table expression, using derived tables you are forced to duplicate your code, which hurts the maintainability of your solution.

You can use a table value constructor to define a table based on self-contained expressions as opposed to querying some existing base tables.

You can use derived tables in modification statements like DELETE and UPDATE, though the syntax for doing so is a bit awkward.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Най-близкото съвпадение, част 3

  2. Как да гарантираме, че базите данни нямат фрагментирани индекси

  3. Защитете своите Mongo клъстери с SSL

  4. SQL с чужд ключ:Всичко, което трябва да знаете за операциите с чужд ключ

  5. Идентифициране и коригиране на проблем с производителността на препратените записи