sql >> Databáze >  >> RDS >> Database

Základy tabulkových výrazů, 2. část – Odvozené tabulky, logické úvahy

Minulý měsíc jsem poskytl pozadí k tabulkovým výrazům v T-SQL. Vysvětlil jsem souvislosti z relační teorie a standardu SQL. Vysvětlil jsem, jak je tabulka v SQL pokusem o reprezentaci vztahu z relační teorie. Vysvětlil jsem také, že relační výraz je výraz fungující na jedné nebo více relacích jako vstupech a vedoucí k relaci. Podobně v SQL je tabulkový výraz výraz fungující na jedné nebo více vstupních tabulkách, jehož výsledkem je tabulka. Výraz může být dotaz, ale také nemusí. Výraz může být například konstruktorem hodnot tabulky, jak vysvětlím později v tomto článku. Vysvětlil jsem také, že v této sérii se zaměřuji na čtyři konkrétní typy pojmenovaných tabulkových výrazů, které T-SQL podporuje:odvozené tabulky, běžné tabulkové výrazy (CTE), pohledy a vložené funkce s hodnotou tabulky (TVF).

Pokud už nějakou dobu pracujete s T-SQL, pravděpodobně jste narazili na nemálo případů, kdy jste buď museli používat tabulkové výrazy, nebo to bylo nějak pohodlnější ve srovnání s alternativními řešeními, která je nepoužívají. Zde je jen několik příkladů případů použití, které vás napadnou:

  • Vytvořte modulární řešení rozdělením složitých úkolů do kroků, z nichž každý představuje jiný tabulkový výraz.
  • Míchání výsledků seskupených dotazů a podrobností v případě, že se pro tento účel rozhodnete nepoužívat funkce okna.
  • Logické zpracování dotazů zpracovává klauzule dotazu v následujícím pořadí:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Výsledkem je, že na stejné úrovni vnoření jsou aliasy sloupců, které definujete v klauzuli SELECT, dostupné pouze pro klauzuli ORDER BY. Nejsou dostupné pro ostatní klauzule dotazu. Pomocí tabulkových výrazů můžete znovu použít aliasy, které definujete ve vnitřním dotazu v jakékoli klauzuli vnějšího dotazu, a vyhnout se tak opakování zdlouhavých/složitých výrazů.
  • Funkce okna se mohou objevit pouze v klauzulích SELECT a ORDER BY dotazu. Pomocí tabulkových výrazů můžete přiřadit alias výrazu založenému na funkci okna a poté tento alias použít v dotazu proti tabulkovému výrazu.
  • Operátor PIVOT zahrnuje tři prvky:seskupování, šíření a agregaci. Tento operátor identifikuje prvek seskupení implicitně eliminací. Pomocí tabulkového výrazu můžete promítnout přesně ty tři prvky, které by se měly týkat, a nechat vnější dotaz použít tabulkový výraz jako vstupní tabulku operátoru PIVOT, a tak řídit, který prvek je seskupovacím prvkem.
  • Úpravy s TOP nepodporují klauzuli ORDER BY. Můžete řídit, které řádky budou vybrány nepřímo, definováním tabulkového výrazu založeného na dotazu SELECT s filtrem TOP nebo OFFSET-FETCH a klauzulí ORDER BY a aplikováním modifikace na tabulkový výraz.

Toto zdaleka není vyčerpávající seznam. Některé z výše uvedených případů použití a další ukážu v této sérii. Jen jsem zde chtěl zmínit některé případy použití, abych ilustroval, jak důležité jsou tabulkové výrazy v našem kódu T-SQL a proč se vyplatí investovat do dobrého pochopení jejich základů.

V tomto článku se zaměřím na logické zpracování odvozených tabulek konkrétně.

Ve svých příkladech použiji ukázkovou databázi nazvanou TSQLV5. Skript, který jej vytváří a naplňuje, najdete zde a jeho ER diagram zde.

Odvozené tabulky

Termín odvozená tabulka se v SQL a T-SQL používá s více než jedním významem. Nejprve chci tedy ujasnit, na který z nich v tomto článku odkazuji. Mám na mysli konkrétní jazykovou konstrukci, kterou obvykle, ale nejen, definujete v klauzuli FROM vnějšího dotazu. Brzy poskytnu syntaxi této konstrukce.

Obecnější použití termínu odvozená tabulka v SQL je protějškem odvozeného vztahu z relační teorie. Odvozená relace je relace výsledku, která je odvozena z jednoho nebo více vstupních základních relací aplikací relačních operátorů z relační algebry, jako je projekce, průnik a další, na tyto základní vztahy. Podobně, v obecném smyslu, odvozená tabulka v SQL je výsledková tabulka, která je odvozena z jedné nebo více základních tabulek vyhodnocením výrazů proti těmto vstupním základním tabulkám.

Kromě toho jsem zkontroloval, jak standard SQL definuje základní tabulku, a okamžitě mě mrzelo, že jsem se obtěžoval.

4.15.2 Základní tabulky

Základní tabulka je buď trvalá základní tabulka, nebo dočasná tabulka.

Trvalá základní tabulka je buď běžná trvalá základní tabulka nebo tabulka se systémovou verzí.

Běžná základní tabulka je buď běžná trvalá základní tabulka, nebo dočasná tabulka.“

Přidáno sem bez dalších komentářů…

V T-SQL můžete vytvořit základní tabulku pomocí příkazu CREATE TABLE, ale existují i ​​jiné možnosti, např. SELECT INTO a DECLARE @T AS TABLE.

Zde je definice standardu pro odvozené tabulky v obecném smyslu:

4.15.3 Odvozené tabulky

Odvozená tabulka je tabulka odvozená přímo nebo nepřímo z jedné nebo více jiných tabulek vyhodnocením výrazu, jako je , , nebo . může obsahovat nepovinné . Pořadí řádků tabulky určené je zaručeno pouze pro , který bezprostředně obsahuje .“

O odvozených tabulkách v obecném smyslu je zde třeba poznamenat několik zajímavých věcí. Jeden má co do činění s komentářem o objednávce. K tomu se dostanu později v článku. Další je, že odvozená tabulka v SQL může být platným samostatným tabulkovým výrazem, ale nemusí. Například následující výraz představuje odvozenou tabulku a je také považován za platný samostatný tabulkový výraz (můžete jej spustit):

SELECT custid, companynameFROM Sales.CustomersWHERE country =N'USA'

A naopak, následující výraz představuje odvozenou tabulku, ale není platný samostatný tabulkový výraz:

VNITŘNÍ PŘIPOJENÍ T1 T2 NA T1.keycol =T2.keycol

T-SQL podporuje řadu tabulkových operátorů, které poskytují odvozenou tabulku, ale nejsou podporovány jako samostatné výrazy. Jsou to:PŘIPOJIT SE, PIVOT, UNPIVOT a APPLY. Potřebujete klauzuli, ve které budou fungovat (obvykle FROM, ale také klauzuli USING příkazu MERGE) a hostitelský dotaz.

Od této chvíle budu používat termín odvozená tabulka k popisu specifičtějšího jazykového konstruktu, nikoli v obecném smyslu popsaném výše.

Syntaxe

Odvozenou tabulku lze definovat jako součást vnějšího příkazu SELECT v jeho klauzuli FROM. Může být také definován jako součást příkazů DELETE a UPDATE v jejich klauzuli FROM a jako součást příkazu MERGE v jeho klauzuli USING. Další podrobnosti o syntaxi při použití v příkazech modifikace poskytnu později v tomto článku.

Zde je syntaxe pro zjednodušený dotaz SELECT proti odvozené tabulce:

VYBRAT
Z ( ) [ AS ] [ () ];

Definice odvozené tabulky se objeví tam, kde se může normálně objevit základní tabulka, v klauzuli FROM vnějšího dotazu. Může to být vstup pro operátora tabulky, jako je JOIN, APPLY, PIVOT a UNPIVOT. Při použití jako správný vstup pro operátor APPLY může část

odvozené tabulky mít korelace se sloupci z vnější tabulky (více o tom ve vyhrazeném budoucím článku v této sérii). Jinak musí být tabulkový výraz samostatný.

Vnější příkaz může mít všechny obvyklé dotazovací prvky. V případě příkazu SELECT:WHERE, GROUP BY, HAVING, ORDER BY a jak již bylo zmíněno, tabulkové operátory v klauzuli FROM.

Zde je příklad jednoduchého dotazu na odvozenou tabulku představující zákazníky z USA:

SELECT custid, companynameFROM ( SELECT custid, companyname FROM Sales.Customers WHERE country =N'USA' ) AS UC;

Tento dotaz generuje následující výstup:

název zákaznické společnosti------- ---------------32 Zákazník YSIQX36 Zákazník LVJSO43 Zákazník UISOJ45 Zákazník QXPPT48 Zákazník DVFMB55 Zákazník KZQZT65 Zákazník NYUHS71 Zákazník LCOUJTYP Zákazník XOJCYBZ78 Zákazník L2CYBZ78 Zákazník L2CYBZ78 Zákazník Zákazník EYHKM89 Zákazník YBQTI

V příkazu obsahujícím odvozenou definici tabulky je třeba identifikovat tři hlavní části:

  1. Tabulkový výraz (vnitřní dotaz)
  2. Odvozený název tabulky nebo přesněji to, co se v teorii relací považuje za proměnnou rozsahu
  3. Vnější prohlášení

Tabulkový výraz má představovat tabulku a jako takový musí splňovat určité požadavky, které běžný dotaz nutně splňovat nemusí. Podrobnosti poskytnu brzy v sekci „Tabulkový výraz je tabulka“.

Pokud jde o název odvozené cílové tabulky; běžným předpokladem mezi vývojáři T-SQL je, že jde pouze o název nebo alias, který přiřadíte cílové tabulce. Podobně zvažte následující dotaz:

SELECT custid, companynameFROM Sales.Customers AS CWHERE country =N'USA';

Také zde je společným předpokladem, že AS C je pouze způsob, jak přejmenovat nebo alias, tabulku Zákazníci pro účely tohoto dotazu, počínaje krokem zpracování logického dotazu, kde je jméno přiřazeno, a dále. Z hlediska teorie vztahů však to, co C představuje, má hlubší význam. C je to, co je známé jako proměnná rozsahu. C je odvozená relační proměnná, která se pohybuje přes n-tice ve vstupní relační proměnné Zákazníci. Ve výše uvedeném příkladu se C pohybuje nad n-ticemi v Zákazníkech a vyhodnocuje predikát země =N'USA'. N-tice, pro které je predikát vyhodnocen jako pravdivý, se stávají součástí výsledné relace C.

Tabulkový výraz je tabulka

S ohledem na pozadí, které jsem doposud uvedl, by to, co se chystám vysvětlit dále, mělo být malým překvapením. část odvozené definice tabulky je tabulka . Je tomu tak, i když je to vyjádřeno jako dotaz. Pamatujete na uzavírací vlastnost relační algebry? Totéž platí pro zbytek výše uvedených pojmenovaných tabulkových výrazů (CTE, pohledy a inline TVF). Jak jste se již dozvěděli, tabulka SQL je protějškem vztahu relační teorie , i když ne dokonalým protějškem. Tabulkový výraz tedy musí splňovat určité požadavky, aby bylo zajištěno, že výsledkem je tabulka – takové, které dotaz, který není použit jako tabulkový výraz, nutně nemusí. Zde jsou tři konkrétní požadavky:

  • Všechny sloupce tabulkového výrazu musí mít názvy
  • Všechny názvy sloupců tabulkového výrazu musí být jedinečné
  • Řádky tabulkového výrazu nemají pořadí

Pojďme si tyto požadavky rozebrat jeden po druhém a probrat význam jak pro relační teorii, tak pro SQL.

Všechny sloupce musí mít názvy

Pamatujte, že vztah má nadpis a tělo. Záhlaví relace je sada atributů (sloupců v SQL). Atribut má název a název typu a je identifikován svým názvem. Dotaz, který se nepoužívá jako tabulkový výraz, nemusí nutně přiřazovat názvy všem cílovým sloupcům. Jako příklad zvažte následující dotaz:

SELECT empid, jméno, příjmení, CONCAT_WS(N'/', země, region, město)FROM HR.Employees;

Tento dotaz generuje následující výstup:

empid jméno příjmení (žádný název sloupce)------ ---------- ---------- -------------- ----1 Sara Davis USA/WA/Seattle2 Don Funk USA/WA/Tacoma3 Judy Lew USA/WA/Kirkland4 Yael Peled USA/WA/Redmond5 Sven Mortensen Velká Británie/Londýn6 Paul Suurs Velká Británie/Londýn7 Russell King Velká Británie/Londýn8 Maria Cameron USA/WA/Seattle9 Patricia Doyle UK/Londýn

Výstup dotazu má anonymní sloupec, který je výsledkem zřetězení atributů umístění pomocí funkce CONCAT_WS. (Mimochodem, tato funkce byla přidána do SQL Server 2017, takže pokud spouštíte kód ve starší verzi, můžete tento výpočet nahradit alternativním výpočtem dle vašeho výběru.) Tento dotaz tedy vrátit tabulku, nemluvě o vztahu. Proto není platné používat takový dotaz jako část tabulkového výrazu/vnitřního dotazu v odvozené definici tabulky.

Zkuste to:

SELECT *FROM ( SELECT empid, jméno, příjmení, CONCAT_WS(N'/', země, region, město) FROM HR.Employees ) AS D;

Zobrazí se následující chyba:

Zpráva 8155, úroveň 16, stav 2, řádek 50
Pro sloupec 4 „D“ nebyl zadán žádný název sloupce.

Mimochodem, všimli jste si něčeho zajímavého na chybové zprávě? Stěžuje si na sloupec 4 a zdůrazňuje rozdíl mezi sloupci v SQL a atributy v relační teorii.

Řešením je samozřejmě ujistit se, že sloupcům, které jsou výsledkem výpočtů, explicitně přiřazujete názvy. T-SQL podporuje několik technik pojmenování sloupců. Zmíním se o dvou z nich.

Můžete použít techniku ​​inline pojmenování, kdy po výpočtu přiřadíte název cílového sloupce a volitelnou klauzuli AS, jako v < expression > [ AS ] < column name > , asi takhle:

SELECT empid, jméno, příjmení, custlocationFROM ( SELECT empid, jméno, příjmení, CONCAT_WS(N'/', země, region, město) AS custlocation FROM HR.Employees ) AS D;

Tento dotaz generuje následující výstup:

empid jméno příjmení vlastní umístění------ ---------- ---------- ----------------1 Sara Davis USA/WA/Seattle2 Don Funk USA/WA/Tacoma3 Judy Lew USA/WA/Kirkland4 Yael Peled USA/WA/Redmond5 Sven Mortensen Velká Británie/Londýn6 Paul Suurs Velká Británie/Londýn7 Russell King Velká Británie/Londýn8 Maria Cameron USA/WA/Seattle9 Patricia Doyle UK/Londýn

Pomocí této techniky je velmi snadné při kontrole kódu zjistit, který název cílového sloupce je přiřazen ke kterému výrazu. Také stačí pojmenovat sloupce, které jinak názvy ještě nemají.

Můžete také použít více externí techniku ​​pojmenování sloupců, kde zadáte názvy cílových sloupců v závorkách hned za odvozeným názvem tabulky, například takto:

SELECT empid, jmeno, prijmeni, custlocationFROM ( SELECT empid, jmeno, prijmeni, CONCAT_WS(N'/', zeme, region, mesto) FROM HR.Employees ) AS D(empid, jmeno, prijmeni, custlocation); 

S touto technikou však musíte uvést názvy všech sloupců – včetně těch, které již názvy mají. Přiřazení názvů cílových sloupců se provádí podle pozice, zleva doprava, tj. název prvního cílového sloupce představuje první výraz v seznamu SELECT vnitřního dotazu; název druhého cílového sloupce představuje druhý výraz; a tak dále.

Všimněte si, že v případě nekonzistence mezi názvy vnitřních a vnějších sloupců, řekněme kvůli chybě v kódu, je rozsahem vnitřních názvů vnitřní dotaz – nebo přesněji proměnná vnitřního rozsahu (zde implicitně HR.Employees Zaměstnanci AS)—a rozsahem vnějších jmen je proměnná vnějšího rozsahu (v našem případě D). Je to trochu více zapojeno do určování rozsahu názvů sloupců, které souvisí s logickým zpracováním dotazů, ale to je položka pro pozdější diskusi.

Potenciál pro chyby v externí syntaxi pojmenování je nejlépe vysvětlen na příkladu.

Prohlédněte si výstup předchozího dotazu s úplnou sadou zaměstnanců z tabulky HR.Employees. Poté zvažte následující dotaz a před jeho spuštěním se pokuste zjistit, které zaměstnance očekáváte ve výsledku:

SELECT empid, jméno, příjmení, custlocationFROM ( SELECT empid, jméno, příjmení, CONCAT_WS(N'/', země, region, město) FROM HR.Employees WHERE příjmení LIKE N'D%' ) AS D(empid, příjmení, jméno, vlastní umístění)WHERE jméno LIKE N'D%';

Pokud očekáváte, že dotaz vrátí prázdnou sadu pro daná ukázková data, protože v současné době nejsou žádní zaměstnanci s příjmením i jménem začínajícím na písmeno D, chybí vám chyba v kódu.

Nyní spusťte dotaz a prozkoumejte skutečný výstup:

empid jméno příjmení vlastní umístění------ ---------- --------- ---------------1 Davis Sara USA/WA/Seattle9 Doyle Patricia UK/Londýn

Co se stalo?

Vnitřní dotaz určuje jméno jako druhý sloupec a příjmení jako třetí sloupec v seznamu SELECT. Kód, který přiřazuje názvy cílových sloupců odvozené tabulky ve vnějším dotazu, určuje druhé příjmení a třetí jméno. Kódové jméno jako příjmení a příjmení jako jméno v proměnné rozsahu D. Ve skutečnosti pouze filtrujete zaměstnance, jejichž příjmení začíná písmenem D. Nefiltrujete zaměstnance s příjmením a křestním jménem, ​​které začínají s písmenem D.

Syntaxe inline aliasingu není náchylná k takovým chybám. Za prvé, normálně nevytváříte alias sloupce, který již má název, se kterým jste spokojeni. Za druhé, i když chcete přiřadit jiný alias pro sloupec, který již má název, není příliš pravděpodobné, že se syntaxí AS přiřadíte nesprávný alias. Přemýšlejte o tom; jaká je pravděpodobnost, že budete psát takto:

SELECT empid, firstname, lastname, custlocationFROM ( SELECT empid AS empid, firstname AS lastname, lastname AS firstname, CONCAT_WS(N'/', country, region, town) AS custlocation FROM HR.Employees WHERE lastname LIKE N'D %' ) AS DWHERE křestní jméno LIKE N'D%';

Očividně to není příliš pravděpodobné.

Všechny názvy sloupců musí být jedinečné

Zpět ke skutečnosti, že záhlaví relace je sada atributů, a vzhledem k tomu, že atribut je identifikován jménem, ​​musí být názvy atributů pro stejný vztah jedinečné. V daném dotazu můžete vždy odkazovat na atribut pomocí dvoudílného názvu s názvem proměnné rozsahu jako kvalifikátorem, jako v .. Pokud je název sloupce bez kvalifikátoru jednoznačný, můžete předponu názvu proměnné rozsahu vynechat. Co je však důležité si zapamatovat, je to, co jsem řekl dříve o rozsahu názvů sloupců. V kódu, který zahrnuje pojmenovaný tabulkový výraz, s vnitřním dotazem (tabulkovým výrazem) i vnějším dotazem, je rozsah názvů sloupců ve vnitřním dotazu proměnnými vnitřního rozsahu a rozsah názvů sloupců ve vnějším dotazu dotaz jsou proměnné vnějšího rozsahu. Pokud vnitřní dotaz zahrnuje více zdrojových tabulek se stejným názvem sloupce, můžete na tyto sloupce stále jednoznačně odkazovat přidáním názvu proměnné rozsahu jako předpony. Pokud název proměnné rozsahu nepřiřadíte explicitně, získáte jej implicitně, jako byste použili AS .

Jako příklad zvažte následující samostatný dotaz:

VYBERTE C.custid, O.custid, O.orderidFROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid;

Tento dotaz se nezdaří s chybou duplicitního názvu sloupce, protože jeden custid sloupec se ve skutečnosti jmenuje C.custid a druhý O.custid v rozsahu aktuálního dotazu. Tento dotaz generuje následující výstup:

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

Zkuste však tento dotaz použít jako tabulkový výraz v definici odvozené tabulky s názvem CO, například takto:

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

Pokud jde o vnější dotaz, máte jednu proměnnou rozsahu s názvem CO a rozsah všech názvů sloupců ve vnějším dotazu je tato proměnná rozsahu. Názvy všech sloupců v dané proměnné rozsahu (nezapomeňte, že proměnná rozsahu je relační proměnná) musí být jedinečné. Proto se zobrazí následující chyba:

Msg 8156, Level 16, State 1, Line 80
Sloupec 'custid' byl pro 'CO' zadán vícekrát.

Opravou je samozřejmě přiřazení různých názvů sloupců ke dvěma cuctid sloupcům, pokud jde o proměnnou rozsahu CO, například takto:

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

Tento dotaz generuje následující výstup:

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

Pokud se budete řídit osvědčenými postupy, explicitně uvedete názvy sloupců v seznamu SELECT nejvzdálenějšího dotazu. Protože se jedná pouze o jednu proměnnou rozsahu, nemusíte pro odkazy na vnější sloupce používat dvoudílný název. Pokud si přejete použít dvoudílný název, přidejte před názvy sloupců název proměnné vnějšího rozsahu CO, například takto:

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

Žádná objednávka

O pojmenovaných tabulkových výrazech a řazení toho musím říct docela dost – dost na článek sám o sobě – takže tomuto tématu věnuji budoucí článek. Přesto jsem se zde chtěl krátce dotknout tématu, protože je tak důležité. Připomeňme, že tělo relace je množinou n-tic a podobně je tělo tabulky množinou řádků. Sada nemá pořadí. SQL přesto umožňuje, aby nejvzdálenější dotaz měl klauzuli ORDER BY sloužící významu uspořádání prezentace, jak ukazuje následující dotaz:

SELECT orderid, valFROM Sales.OrderValuesORDER BY val DESC;

Musíte však pochopit, že tento dotaz ve výsledku nevrací vztah. Ani z pohledu SQL dotaz nevrací tabulku jako výsledek, a proto není považován za tabulkový výraz. V důsledku toho je neplatné použít takový dotaz jako část tabulkového výrazu v odvozené definici tabulky.

Zkuste spustit následující kód:

SELECT orderid, valFROM ( SELECT orderid, val FROM Sales.OrderValues ​​ORDER BY val DESC ) AS D;

Zobrazí se následující chyba:

Zpráva 1033, Úroveň 15, Stav 1, Řádek 124
Klauzule ORDER BY je neplatná v pohledech, vložených funkcích, odvozených tabulkách, poddotazech a běžných tabulkových výrazech, pokud není specifikováno také TOP, OFFSET nebo FOR XML.

Budu se zabývat pokud část chybové zprávy.

Pokud chcete, aby nejvzdálenější dotaz vrátil seřazený výsledek, musíte v nejvzdálenějším dotazu zadat klauzuli ORDER BY, například takto:

SELECT orderid, valFROM ( SELECT orderid, val FROM Sales.OrderValues ​​) AS DORDER BY val DESC;

Pokud jde o pokud část chybové zprávy; T-SQL podporuje proprietární filtr TOP i standardní filtr OFFSET-FETCH. Oba filtry se spoléhají na klauzuli ORDER BY ve stejném oboru dotazu, která pro ně definuje, které horní řádky se mají filtrovat. To je bohužel výsledek pasti v návrhu těchto funkcí, která neodděluje pořadí prezentací od řazení filtrů. Ať je to jakkoli, jak Microsoft se svým filtrem TOP, tak i standard s filtrem OFFSET-FETCH umožňují zadat klauzuli ORDER BY ve vnitřním dotazu, pokud také specifikuje filtr TOP nebo OFFSET-FETCH. Tento dotaz je tedy platný, například:

SELECT orderid, valFROM ( SELECT TOP (3) orderid, value FROM Sales.OrderValues ​​ORDER BY val DESC ) AS D;

Když jsem na svém systému spustil tento dotaz, vygeneroval následující výstup:

číslo objednávky-------- ---------10865 16387.5010981 15810.0011030 12615.05

Je však důležité zdůraznit, že jediným důvodem, proč je ve vnitřním dotazu povolena klauzule ORDER BY, je podpora filtru TOP. To je jediná záruka, kterou dostanete, pokud jde o objednávku. Vzhledem k tomu, že vnější dotaz nemá ani klauzuli ORDER BY, nezískáte z tohoto dotazu záruku na žádné konkrétní uspořádání prezentace, bez ohledu na pozorované chování. To je případ T-SQL i standardu. Zde je citace ze standardu, který se zabývá touto částí:

"Pořadí řádků tabulky určené je zaručeno pouze pro , který bezprostředně obsahuje ."

Jak již bylo zmíněno, o tabulkových výrazech a řazení je toho třeba říci mnohem více, čemuž se budu věnovat v budoucím článku. Poskytnu také příklady, které demonstrují, jak absence klauzule ORDER BY ve vnějším dotazu znamená, že nezískáte žádné záruky objednání prezentace.

Takže tabulkový výraz, např. vnitřní dotaz v odvozené definici tabulky, je tabulka. Podobně samotná odvozená tabulka (ve specifickém smyslu) je také tabulkou. Není to základní stůl, ale přesto je to stůl. Totéž platí pro CTE, zobrazení a inline TVF. Nejsou to základní tabulky, spíše odvozené (v obecnějším smyslu), ale přesto jsou to tabulky.

Konstrukční nedostatky

Odvozené stoly mají dva hlavní nedostatky ve svém designu. Obojí souvisí se skutečností, že odvozená tabulka je definována v klauzuli FROM vnějšího dotazu.

Jedna konstrukční chyba souvisí se skutečností, že pokud potřebujete dotazovat odvozenou tabulku z vnějšího dotazu a následně tento dotaz použít jako tabulkový výraz v jiné definici odvozené tabulky, skončíte vnořením těchto odvozených dotazů na tabulku. Ve výpočetní technice má explicitní vnoření kódu zahrnující více úrovní vnoření tendenci vést ke složitému kódu, který se obtížně udržuje.

Zde je velmi základní příklad, který to demonstruje:

VYBERTE rok objednávky, numcustsFROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts FROM ( SELECT YEAR(datum objednávky) AS orderyear, custid FROM Sales.Orders ) AS D1 GROUP BY orderyear ) AS D2WHERE numcusts> 70;

Tento kód vrací roky objednávky a počet zákazníků, kteří zadali objednávky během každého roku, pouze pro roky, kdy počet zákazníků, kteří zadali objednávky, byl větší než 70.

Hlavní motivací pro použití tabulkových výrazů je zde možnost odkazovat na alias sloupce vícekrát. Nejvnitřnější dotaz použitý jako tabulkový výraz pro odvozenou tabulku D1 se dotazuje na tabulku Sales.Orders a přiřazuje název sloupce orderyear k výrazu YEAR(orderdate) a také vrací custid sloupec. Dotaz na D1 seskupuje řádky z D1 podle orderyear a vrací orderyear, stejně jako zřetelný počet zákazníků, kteří zadali objednávky během daného roku alias numcusty. Kód definuje odvozenou tabulku nazvanou D2 na základě tohoto dotazu. Nejvzdálenější dotaz než dotazy D2 a filtruje pouze roky, kde počet zákazníků, kteří zadali objednávky, byl větší než 70.

Pokus o kontrolu tohoto kódu nebo jeho odstraňování v případě problémů je ošemetný kvůli více úrovním vnoření. Namísto toho, abyste si kód prohlíželi přirozenějším způsobem shora dolů, musíte ho analyzovat od nejvnitřnější jednotky a postupně jít ven, protože to je praktičtější.

Smyslem použití odvozených tabulek v tomto příkladu bylo zjednodušit kód tím, že se vyhneme nutnosti opakovat výrazy. Nejsem si ale jistý, že toto řešení tohoto cíle dosáhne. V tomto případě je pravděpodobně lepší některé výrazy zopakovat, aniž byste museli používat odvozené tabulky úplně, například takto:

VYBERTE ROK(datum objednávky) JAKO rok objednávky, POČET (DISTINCT Custid) JAKO numcustsFROM Sales.OrdersGROUP BY YEAR(datum objednávky)HAVING COUNT(DISTINCT custid)> 70;

Mějte na paměti, že zde pro ilustraci uvádím velmi jednoduchý příklad. Představte si produkční kód s více úrovněmi vnoření a s delším a propracovanějším kódem a uvidíte, jak se jeho údržba podstatně zkomplikuje.

Další chyba v návrhu odvozených tabulek souvisí s případy, kdy potřebujete pracovat s více instancemi stejné odvozené tabulky. Jako příklad zvažte následující dotaz:

VYBERTE CUR.orderyear, CUR.čísla, CUR.čísla - PRV.čísla JAKO diffFROM ( VYBERTE ROK(datum objednávky) JAKO rok objednávky, POČET(*) JAKO čísla Z Prodeje.Objednávky SKUPINA PODLE ROKŮ(datum objednávky) ) JAKO CUR VLEVO OUTER JOIN ( VYBERTE ROK (datum objednávky) JAKO rok objednávky, POČET (*) JAKO čísla Z Prodeje. Objednávky SKUPINA PODLE ROKŮ (datum objednávky) ) JAKO PRV ON CUR.orderyear =PRV.orderyear + 1;

Tento kód počítá počet objednávek zpracovaných v každém roce a také rozdíl oproti předchozímu roku. Ignorujte skutečnost, že existují jednodušší způsoby, jak dosáhnout stejného úkolu pomocí funkcí okna – tento kód používám k ilustraci určitého bodu, takže samotný úkol a různé způsoby jeho řešení nejsou důležité.

Spojení je operátor tabulky, který zachází se svými dvěma vstupy jako s množinou – to znamená, že mezi nimi není žádné pořadí. Označují se jako levý a pravý vstup, takže jeden z nich (nebo oba) můžete označit jako zachovanou tabulku ve vnějším spojení, ale přesto mezi nimi není žádný první a druhý. Je povoleno používat odvozené tabulky jako vstupy spojení, ale název proměnné rozsahu, který přiřadíte levému vstupu, není v definici pravého vstupu dostupný. Je to proto, že oba jsou koncepčně definovány ve stejném logickém kroku, jako by ve stejném časovém okamžiku. V důsledku toho při spojování odvozených tabulek nemůžete definovat dvě proměnné rozsahu založené na jednom tabulkovém výrazu. Bohužel musíte kód opakovat a definovat dvě proměnné rozsahu na základě dvou identických kopií kódu. To samozřejmě komplikuje udržovatelnost kódu a zvyšuje pravděpodobnost chyb. Každá změna, kterou provedete v jednom tabulkovém výrazu, musí být aplikována i na druhý.

Jak vysvětlím v budoucím článku, CTE ve svém návrhu nemají tyto dvě chyby, které mají odvozené tabulky.

Konstruktor hodnot tabulky

Konstruktor hodnot tabulky vám umožňuje sestavit hodnotu tabulky na základě samostatných skalárních výrazů. Takovou tabulku pak můžete použít ve vnějším dotazu, stejně jako používáte odvozenou tabulku, která je založena na vnitřním dotazu. V budoucím článku pojednávám o laterálních odvozených tabulkách a korelace podrobně a ukážu sofistikovanější formy konstruktorů hodnot tabulek. V tomto článku se však zaměřím na jednoduchou formu, která je založena čistě na samostatných skalárních výrazech.

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');

Tento kód generuje následující výstup:

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');

Tento kód generuje následující výstup:

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');

Tento kód generuje následující výstup:

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);

When you’re done, run the following code for cleanup:

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.

Shrnutí

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. PostgreSQL Load Balancing pomocí HAProxy &Keepalived

  2. Jak SIGN() funguje v MariaDB

  3. Proč jsou nástroje pro monitorování cloudové databáze pro SQL Server cenné

  4. Jak obnovit databázi pomocí RMAN