W zeszłym miesiącu dostarczyłem tło do wyrażeń tabelowych w T-SQL. Wyjaśniłem kontekst z teorii relacji i standardu SQL. Wyjaśniłem, że tabela w SQL jest próbą przedstawienia relacji z teorii relacyjnej. Wyjaśniłem również, że wyrażenie relacyjne jest wyrażeniem operującym na jednej lub kilku relacjach jako danych wejściowych i prowadzącym do relacji. Podobnie w języku SQL wyrażenie tabelowe jest wyrażeniem operującym na jednej lub kilku tabelach wejściowych, którego wynikiem jest tabela. Wyrażenie może być zapytaniem, ale nie musi nim być. Na przykład wyrażenie może być konstruktorem wartości tabeli, co wyjaśnię w dalszej części tego artykułu. Wyjaśniłem również, że w tej serii skupiam się na czterech konkretnych typach nazwanych wyrażeń tabelowych obsługiwanych przez T-SQL:tabele pochodne, wspólne wyrażenia tabelowe (CTE), widoki i wbudowane funkcje z wartościami tabelowymi (TVF).
Jeśli od jakiegoś czasu pracujesz z T-SQL, prawdopodobnie natknąłeś się na sporo przypadków, w których albo musiałeś użyć wyrażeń tabelowych, albo było to w jakiś sposób wygodniejsze w porównaniu z alternatywnymi rozwiązaniami, które ich nie używają. Oto tylko kilka przykładów użycia, które przychodzą mi do głowy:
- Utwórz rozwiązanie modułowe, dzieląc złożone zadania na kroki, z których każdy jest reprezentowany przez inne wyrażenie tabeli.
- Mieszanie wyników zgrupowanych zapytań i szczegółów, jeśli zdecydujesz się nie używać do tego celu funkcji okna.
- Przetwarzanie zapytań logicznych obsługuje klauzule zapytań w następującej kolejności:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. W rezultacie na tym samym poziomie zagnieżdżenia aliasy kolumn zdefiniowane w klauzuli SELECT są dostępne tylko dla klauzuli ORDER BY. Nie są dostępne dla pozostałych klauzul zapytania. Dzięki wyrażeniom tabelowym możesz ponownie używać aliasów, które definiujesz w zapytaniu wewnętrznym w dowolnej klauzuli zapytania zewnętrznego, dzięki czemu unikasz powtarzania długich/złożonych wyrażeń.
- Funkcje okna mogą pojawiać się tylko w klauzulach SELECT i ORDER BY zapytania. Za pomocą wyrażeń tabelowych możesz przypisać alias do wyrażenia na podstawie funkcji okna, a następnie użyć tego aliasu w zapytaniu względem wyrażenia tabelowego.
- Operator PIVOT składa się z trzech elementów:grupowania, rozkładania i agregacji. Ten operator identyfikuje element grupujący niejawnie przez eliminację. Używając wyrażenia tabeli, możesz rzutować dokładnie te trzy elementy, które mają być zaangażowane, a zewnętrzne zapytanie używa wyrażenia tabeli jako tabeli wejściowej operatora PRZESTAWNEGO, kontrolując w ten sposób, który element jest elementem grupującym.
- Modyfikacje TOP nie obsługują klauzuli ORDER BY. Możesz kontrolować, które wiersze są wybierane pośrednio, definiując wyrażenie tabelowe oparte na zapytaniu SELECT z filtrem TOP lub OFFSET-FETCH i klauzulą ORDER BY, a następnie stosując modyfikację względem wyrażenia tabelowego.
To nie jest wyczerpująca lista. Pokażę niektóre z powyższych przypadków użycia i inne w tej serii. Chciałem tylko wspomnieć tutaj o kilku przypadkach użycia, aby zilustrować, jak ważne są wyrażenia tabelowe w naszym kodzie T-SQL i dlaczego warto zainwestować w dobre zrozumienie ich podstaw.
W artykule w tym miesiącu skupiam się na logicznym traktowaniu tabel pochodnych.
W moich przykładach użyję przykładowej bazy danych o nazwie TSQLV5. Skrypt, który go tworzy i wypełnia, można znaleźć tutaj, a jego diagram ER znajduje się tutaj.
Tabele pochodne
Termin tabela pochodna jest używany w SQL i T-SQL w więcej niż jednym znaczeniu. Więc najpierw chcę wyjaśnić, do którego z nich mówię w tym artykule. Mam na myśli konkretną konstrukcję języka, którą definiujesz zazwyczaj, ale nie tylko, w klauzuli FROM zapytania zewnętrznego. Wkrótce przedstawię składnię tej konstrukcji.
Bardziej ogólne użycie terminu tabela pochodna w SQL jest odpowiednikiem relacji pochodnej z teorii relacyjnej. Relacja pochodna to relacja wynikowa, która wywodzi się z jednej lub więcej wejściowych relacji bazowych, poprzez zastosowanie operatorów relacji z algebry relacyjnej, takiej jak rzutowanie, przecięcie i inne, do tych relacji bazowych. Podobnie, w ogólnym sensie, tabela pochodna w SQL jest tabelą wynikową, która jest pochodną jednej lub więcej tabel bazowych, poprzez ocenę wyrażeń względem tych wejściowych tabel bazowych.
Na marginesie sprawdziłem, jak standard SQL definiuje tabelę bazową i od razu przepraszam, że się tym przejmowałem.
4.15.2 Tabele podstawoweTabela podstawowa jest albo trwałą tabelą podstawową, albo tabelą tymczasową.
Trwała tabela podstawowa to albo zwykła, trwała tabela podstawowa, albo tabela wersjonowana przez system.
Zwykła tabela podstawowa to albo zwykła, trwała tabela podstawowa, albo tabela tymczasowa”.
Dodano tutaj bez dalszych komentarzy…
W T-SQL możesz utworzyć tabelę bazową za pomocą instrukcji CREATE TABLE, ale są też inne opcje, np. SELECT INTO i DECLARE @T AS TABLE.
Oto definicja standardu dla tabel pochodnych w ogólnym znaczeniu:
4.15.3 Tabele pochodne
Tabela pochodna jest tabelą pochodną bezpośrednio lub pośrednio z jednej lub kilku innych tabel przez obliczenie wyrażenia, takiego jak
Należy tutaj zwrócić uwagę na kilka interesujących rzeczy dotyczących tabel pochodnych w ogólnym sensie. Trzeba mieć do czynienia z komentarzem dotyczącym zamawiania. Zajmę się tym w dalszej części artykułu. Innym jest to, że tabela pochodna w SQL może być prawidłowym samodzielnym wyrażeniem tabelowym, ale nie musi tak być. Na przykład poniższe wyrażenie reprezentuje tabelę pochodną, a jest jest również uważane za prawidłowe samodzielne wyrażenie tabelowe (możesz je uruchomić):
SELECT custid, companyname FROM Sales.Customers WHERE country = N'USA'
I odwrotnie, poniższe wyrażenie reprezentuje tabelę pochodną, ale nie jest prawidłowe samodzielne wyrażenie tabelowe:
T1 INNER JOIN T2 ON T1.keycol = T2.keycol
T-SQL obsługuje wiele operatorów tabel, które dają tabelę pochodną, ale nie są obsługiwane jako samodzielne wyrażenia. Są to:JOIN, PIVOT, UNPIVOT i APPLY. Potrzebujesz klauzuli, w której będą działać (zazwyczaj FROM, ale także klauzuli USING instrukcji MERGE) oraz zapytania hosta.
Odtąd będę używał terminu tabela pochodna, aby opisać bardziej konkretną konstrukcję językową, a nie w ogólnym sensie opisanym powyżej.
Składnia
Tabela pochodna może być zdefiniowana jako część zewnętrznej instrukcji SELECT w jej klauzuli FROM. Można go również zdefiniować jako część instrukcji DELETE i UPDATE w ich klauzuli FROM oraz jako część instrukcji MERGE w jej klauzuli USING. Więcej szczegółów na temat składni użytej w instrukcjach modyfikacji podam w dalszej części tego artykułu.
Oto składnia uproszczonego zapytania SELECT względem tabeli pochodnej:
SELECTFROM (
Definicja tabeli pochodnej pojawia się tam, gdzie normalnie może się pojawić tabela podstawowa, w klauzuli FROM zapytania zewnętrznego. Może to być dane wejściowe do operatora tabeli, takiego jak JOIN, APPLY, PIVOT i UNPIVOT. W przypadku użycia jako prawe dane wejściowe do operatora APPLY, część
Zewnętrzna instrukcja może zawierać wszystkie zwykłe elementy zapytań. W przypadku instrukcji SELECT:WHERE, GROUP BY, HAVING, ORDER BY i jak wspomniano, operatory tabel w klauzuli FROM.
Oto przykład prostego zapytania względem tabeli pochodnej reprezentującej klientów z USA:
SELECT custid, companyname FROM ( SELECT custid, companyname FROM Sales.Customers WHERE country = N'USA' ) AS UC;
To zapytanie generuje następujące dane wyjściowe:
custid companyname ------- --------------- 32 Customer YSIQX 36 Customer LVJSO 43 Customer UISOJ 45 Customer QXPPT 48 Customer DVFMB 55 Customer KZQZT 65 Customer NYUHS 71 Customer LCOUJ 75 Customer XOJYP 77 Customer LCYBZ 78 Customer NLTYP 82 Customer EYHKM 89 Customer YBQTI
W wyrażeniu zawierającym definicję tabeli pochodnej należy zidentyfikować trzy główne części:
- Wyrażenie tabeli (wewnętrzne zapytanie)
- Nazwa tabeli pochodnej, a dokładniej to, co w teorii relacji jest uważane za zmienną zakresu
- Oświadczenie zewnętrzne
Wyrażenie table ma reprezentować tabelę i jako takie musi spełniać pewne wymagania, których zwykłe zapytanie niekoniecznie musi spełniać. Szczegóły przedstawię wkrótce w sekcji „Wyrażenie tabelowe to tabela”.
Jeśli chodzi o nazwę docelowej tabeli pochodnej; powszechnym założeniem wśród programistów T-SQL jest to, że jest to tylko nazwa lub alias przypisywany do tabeli docelowej. Podobnie rozważ następujące zapytanie:
SELECT custid, companyname FROM Sales.Customers AS C WHERE country = N'USA';
Również w tym przypadku powszechne założenie jest takie, że ASC jest tylko sposobem na zmianę nazwy lub aliasu tabeli Klienci na potrzeby tego zapytania, zaczynając od logicznego kroku przetwarzania zapytania, w którym przypisywana jest nazwa, i dalej. Jednak z punktu widzenia teorii relacji istnieje głębsze znaczenie tego, co reprezentuje C. C to tak zwana zmienna zakresu. C jest pochodną zmienną relacyjną, która rozciąga się na krotki w wejściowej zmiennej relacyjnej Klienci. W powyższym przykładzie C obejmuje krotki w Customers i oblicza kraj predykatu =N'USA'. Krotki, dla których predykat ma wartość prawda, stają się częścią relacji wynikowej C.
Wyrażenie tabeli to tabela
Biorąc pod uwagę tło, które przedstawiłem do tej pory, to, co zaraz wyjaśnię, nie powinno dziwić.
- Wszystkie kolumny wyrażenia tabelowego muszą mieć nazwy
- Wszystkie nazwy kolumn wyrażenia tabeli muszą być unikalne
- Wiersze wyrażenia tabeli nie mają kolejności
Podzielmy te wymagania jeden po drugim, omawiając znaczenie zarówno dla teorii relacji, jak i SQL.
Wszystkie kolumny muszą mieć nazwy
Pamiętaj, że relacja ma nagłówek i treść. Nagłówek relacji to zestaw atrybutów (kolumn w SQL). Atrybut ma nazwę i nazwę typu i jest identyfikowany przez swoją nazwę. Zapytanie, które nie jest używane jako wyrażenie tabelowe, nie musi koniecznie przypisywać nazw do wszystkich kolumn docelowych. Rozważ następujące zapytanie jako przykład:
SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees;
To zapytanie generuje następujące dane wyjściowe:
empid firstname lastname (No column name) ------ ---------- ---------- ----------------- 1 Sara Davis USA/WA/Seattle 2 Don Funk USA/WA/Tacoma 3 Judy Lew USA/WA/Kirkland 4 Yael Peled USA/WA/Redmond 5 Sven Mortensen UK/London 6 Paul Suurs UK/London 7 Russell King UK/London 8 Maria Cameron USA/WA/Seattle 9 Patricia Doyle UK/London
Dane wyjściowe zapytania zawierają anonimową kolumnę powstałą w wyniku połączenia atrybutów lokalizacji przy użyciu funkcji CONCAT_WS. (Nawiasem mówiąc, ta funkcja została dodana w SQL Server 2017, więc jeśli używasz kodu we wcześniejszej wersji, możesz zastąpić to obliczenie innym, wybranym przez siebie). Dlatego to zapytanie nie działa zwrócić tabelę, nie mówiąc już o relacji. W związku z tym nie jest poprawne używanie takiego zapytania jako wyrażenia tabeli/wewnętrznej części zapytania definicji tabeli pochodnej.
Wypróbuj:
SELECT * FROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees ) AS D;
Pojawia się następujący błąd:
Msg 8155, poziom 16, stan 2, wiersz 50Nie określono nazwy kolumny dla kolumny 4 „D”.
Na marginesie, zauważyłeś coś interesującego w komunikacie o błędzie? Narzeka na kolumnę 4, podkreślając różnicę między kolumnami w SQL a atrybutami w teorii relacyjnej.
Rozwiązaniem jest oczywiście upewnienie się, że jawnie przypisujesz nazwy kolumnom, które wynikają z obliczeń. T-SQL obsługuje wiele technik nazewnictwa kolumn. Wspomnę o dwóch z nich.
Możesz użyć wbudowanej techniki nazewnictwa, w której po obliczeniu i opcjonalnej klauzuli AS przypiszesz nazwę kolumny docelowej, jak w < expression > [ AS ] < column name >
, jak tak:
SELECT empid, firstname, lastname, custlocation FROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) AS custlocation FROM HR.Employees ) AS D;
To zapytanie generuje następujące dane wyjściowe:
empid firstname lastname custlocation ------ ---------- ---------- ---------------- 1 Sara Davis USA/WA/Seattle 2 Don Funk USA/WA/Tacoma 3 Judy Lew USA/WA/Kirkland 4 Yael Peled USA/WA/Redmond 5 Sven Mortensen UK/London 6 Paul Suurs UK/London 7 Russell King UK/London 8 Maria Cameron USA/WA/Seattle 9 Patricia Doyle UK/London
Korzystając z tej techniki, bardzo łatwo jest podczas przeglądania kodu stwierdzić, która nazwa kolumny docelowej jest przypisana do którego wyrażenia. Ponadto wystarczy nazwać kolumny, które w przeciwnym razie nie mają jeszcze nazw.
Możesz także użyć bardziej zewnętrznej techniki nazewnictwa kolumn, w której nazwy kolumn docelowych określasz w nawiasach tuż po nazwie tabeli pochodnej, na przykład:
SELECT empid, firstname, lastname, custlocation FROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees ) AS D(empid, firstname, lastname, custlocation);
Jednak przy tej technice musisz podać nazwy wszystkich kolumn — w tym tych, które już mają nazwy. Przypisanie nazw kolumn docelowych odbywa się według pozycji, od lewej do prawej, tj. pierwsza nazwa kolumny docelowej reprezentuje pierwsze wyrażenie na liście SELECT wewnętrznego zapytania; druga nazwa kolumny docelowej reprezentuje drugie wyrażenie; i tak dalej.
Zwróć uwagę, że w przypadku niezgodności między wewnętrzną i zewnętrzną nazwą kolumny, powiedzmy, z powodu błędu w kodzie, zakres nazw wewnętrznych obejmuje zapytanie wewnętrzne — a dokładniej zmienna zakresu wewnętrznego (tutaj niejawnie HR.Employees AS Employees) — a zakres nazw zewnętrznych to zmienna zewnętrznego zakresu (w naszym przypadku D). Jest trochę bardziej zaangażowany w określanie zakresu nazw kolumn, które mają związek z logicznym przetwarzaniem zapytań, ale to temat do późniejszych dyskusji.
Potencjał błędów z zewnętrzną składnią nazewnictwa najlepiej wyjaśnić na przykładzie.
Sprawdź dane wyjściowe poprzedniego zapytania z pełnym zestawem pracowników z tabeli HR.Employees. Następnie rozważ następujące zapytanie i przed jego uruchomieniem spróbuj dowiedzieć się, jakich pracowników spodziewasz się zobaczyć w wyniku:
SELECT empid, firstname, lastname, custlocation FROM ( SELECT empid, firstname, lastname, CONCAT_WS(N'/', country, region, city) FROM HR.Employees WHERE lastname LIKE N'D%' ) AS D(empid, lastname, firstname, custlocation) WHERE firstname LIKE N'D%';
Jeśli oczekujesz, że zapytanie zwróci pusty zestaw dla podanych przykładowych danych, ponieważ obecnie nie ma pracowników o nazwisku i imieniu zaczynającym się na literę D, brakuje Ci błędu w kodzie.
Teraz uruchom zapytanie i sprawdź rzeczywisty wynik:
empid firstname lastname custlocation ------ ---------- --------- --------------- 1 Davis Sara USA/WA/Seattle 9 Doyle Patricia UK/London
Co się stało?
Zapytanie wewnętrzne określa imię jako drugą kolumnę, a nazwisko jako trzecią kolumnę na liście SELECT. Kod, który przypisuje nazwy kolumn docelowych tabeli pochodnej w zapytaniu zewnętrznym, określa nazwisko drugie i imię trzecie. Nazwy kodowe imię jako nazwisko i nazwisko jako imię w zmiennej zakresu D. W efekcie filtrujesz tylko pracowników, których nazwisko zaczyna się na literę D. Nie filtrujesz pracowników z nazwiskiem i imieniem rozpoczynającym się z literą D.
Składnia wbudowanego aliasingu nie jest podatna na takie błędy. Po pierwsze, zwykle nie aliasujesz kolumny, która ma już nazwę, z której jesteś zadowolony. Po drugie, nawet jeśli chcesz przypisać inny alias do kolumny, która ma już nazwę, jest mało prawdopodobne, że przy składni
SELECT empid, firstname, lastname, custlocation FROM ( SELECT empid AS empid, firstname AS lastname, lastname AS firstname, CONCAT_WS(N'/', country, region, city) AS custlocation FROM HR.Employees WHERE lastname LIKE N'D%' ) AS D WHERE firstname LIKE N'D%';
Oczywiście mało prawdopodobne.
Wszystkie nazwy kolumn muszą być unikalne
Wracając do faktu, że nagłówek relacji jest zbiorem atrybutów i biorąc pod uwagę, że atrybut jest identyfikowany przez nazwę, nazwy atrybutów muszą być unikalne dla tej samej relacji. W danym zapytaniu zawsze możesz odwołać się do atrybutu za pomocą dwuczęściowej nazwy z nazwą zmiennej zakresu jako kwalifikatorem, tak jak w przypadku
Rozważ następujące samodzielne zapytanie jako przykład:
SELECT C.custid, O.custid, O.orderid FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid = O.custid;
To zapytanie nie kończy się niepowodzeniem z powodu błędu zduplikowanej nazwy kolumny, ponieważ jedna kolumna custid nosi w rzeczywistości nazwę C.custid, a druga O.custid w zakresie bieżącego zapytania. To zapytanie generuje następujące dane wyjściowe:
custid custid orderid ----------- ----------- ----------- 1 1 10643 1 1 10692 1 1 10702 1 1 10835 1 1 10952 1 1 11011 2 2 10308 2 2 10625 2 2 10759 2 2 10926 ...
Spróbuj jednak użyć tego zapytania jako wyrażenia tabelowego w definicji tabeli pochodnej o nazwie CO, na przykład:
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;
Jeśli chodzi o zapytanie zewnętrzne, masz jedną zmienną zakresu o nazwie CO, a zakresem wszystkich nazw kolumn w zapytaniu zewnętrznym jest ta zmienna zakresu. Nazwy wszystkich kolumn w danej zmiennej zakresowej (pamiętaj, że zmienna zakresowa jest zmienną relacyjną) muszą być unikalne. W związku z tym pojawia się następujący błąd:
Msg 8156, poziom 16, stan 1, wiersz 80Kolumna „custid” została określona wiele razy dla „CO”.
Poprawka polega oczywiście na przypisaniu różnych nazw kolumn do dwóch kolumn custid, jeśli chodzi o zmienną zakresu CO, na przykład:
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;
To zapytanie generuje następujące dane wyjściowe:
custcustid ordercustid orderid ----------- ----------- ----------- 1 1 10643 1 1 10692 1 1 10702 1 1 10835 1 1 10952 1 1 11011 2 2 10308 2 2 10625 2 2 10759 2 2 10926 ...
Jeśli postępujesz zgodnie z dobrymi praktykami, jawnie wymieniasz nazwy kolumn na liście SELECT najbardziej zewnętrznego zapytania. Ponieważ w grę wchodzi tylko jedna zmienna zakresu, nie musisz używać dwuczęściowej nazwy dla zewnętrznych odwołań do kolumn. Jeśli chcesz użyć dwuczęściowej nazwy, poprzedź nazwy kolumn nazwą zmiennej poza zakresem CO, na przykład:
SELECT CO.custcustid, CO.ordercustid, CO.orderid 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;
Brak zamówienia
Mam sporo do powiedzenia na temat nazwanych wyrażeń tabelarycznych i kolejności — wystarczy na artykuł sam w sobie — więc poświęcę temu tematowi kolejny artykuł. Mimo to chciałem tu pokrótce poruszyć temat, ponieważ jest on tak ważny. Przypomnijmy, że ciało relacji jest zbiorem krotek i podobnie ciało tabeli jest zbiorem wierszy. Zestaw nie ma porządku. Mimo to SQL pozwala, aby najbardziej zewnętrzne zapytanie zawierało klauzulę ORDER BY obsługującą znaczenie porządkowania prezentacji, jak pokazuje poniższe zapytanie:
SELECT orderid, val FROM Sales.OrderValues ORDER BY val DESC;
Musisz jednak zrozumieć, że to zapytanie nie zwraca relacji jako wyniku. Nawet z perspektywy SQL zapytanie nie zwraca tabeli jako wyniku, a zatem nie jest uważane za wyrażenie tabeli. W związku z tym nieprawidłowe jest używanie takiego zapytania jako części wyrażenia tabelowego definicji tabeli pochodnej.
Spróbuj uruchomić następujący kod:
SELECT orderid, val FROM ( SELECT orderid, val FROM Sales.OrderValues ORDER BY val DESC ) AS D;
Pojawia się następujący błąd:
Msg 1033, Poziom 15, Stan 1, Wiersz 124Klauzula ORDER BY jest nieprawidłowa w widokach, funkcjach wbudowanych, tabelach pochodnych, podzapytaniach i typowych wyrażeniach tabel, chyba że określono również TOP, OFFSET lub FOR XML.
Zajmę się chyba część komunikatu o błędzie wkrótce.
Jeśli chcesz, aby najbardziej zewnętrzne zapytanie zwróciło uporządkowany wynik, musisz określić klauzulę ORDER BY w najbardziej zewnętrznym zapytaniu, na przykład:
SELECT orderid, val FROM ( SELECT orderid, val FROM Sales.OrderValues ) AS D ORDER BY val DESC;
Jeśli chodzi o chyba część komunikatu o błędzie; T-SQL obsługuje autorski filtr TOP, a także standardowy filtr OFFSET-FETCH. Oba filtry opierają się na klauzuli ORDER BY w tym samym zakresie zapytania, aby zdefiniować dla nich górne wiersze do filtrowania. To niestety wynik pułapki w konstrukcji tych funkcji, która nie oddziela uporządkowania prezentacji od uporządkowania filtrów. Tak czy inaczej, zarówno Microsoft z filtrem TOP, jak i standard z filtrem OFFSET-FETCH, pozwalają na określenie klauzuli ORDER BY w zapytaniu wewnętrznym, o ile określa również odpowiednio filtr TOP lub OFFSET-FETCH. Więc to zapytanie jest prawidłowe, na przykład:
SELECT orderid, val FROM ( SELECT TOP (3) orderid, val FROM Sales.OrderValues ORDER BY val DESC ) AS D;
Kiedy uruchomiłem to zapytanie w moim systemie, wygenerowało następujące dane wyjściowe:
orderid val -------- --------- 10865 16387.50 10981 15810.00 11030 12615.05
Należy jednak podkreślić, że jedynym powodem, dla którego klauzula ORDER BY jest dozwolona w zapytaniu wewnętrznym, jest obsługa filtra TOP. To jedyna gwarancja, jaką otrzymujesz, jeśli chodzi o zamawianie. Ponieważ zapytanie zewnętrzne również nie zawiera klauzuli ORDER BY, nie otrzymujesz gwarancji na żadną konkretną kolejność prezentacji z tego zapytania, niezależnie od zaobserwowanego zachowania. Tak jest zarówno w T-SQL, jak iw standardzie. Oto cytat ze standardu dotyczącego tej części:
„Kolejność wierszy w tabeli określona przezJak już wspomniałem, jest o wiele więcej do powiedzenia na temat wyrażeń tabelowych i porządkowania, co zrobię w przyszłym artykule. Podam również przykłady pokazujące, w jaki sposób brak klauzuli ORDER BY w zewnętrznym zapytaniu oznacza, że nie otrzymujesz żadnych gwarancji uporządkowania prezentacji.
Tak więc wyrażenie tabelowe, np. zapytanie wewnętrzne w definicji tabeli pochodnej, jest tabelą. Podobnie tabela pochodna (w konkretnym sensie) sama jest również tabelą. To nie jest stół podstawowy, ale mimo to jest stołem. To samo dotyczy CTE, wyświetleń i wbudowanych programów TVF. Nie są to tabele podstawowe, raczej pochodne (w bardziej ogólnym sensie), ale mimo to są tabelami.
Wady projektowe
Tabele pochodne mają dwie główne wady w swojej konstrukcji. Oba mają związek z faktem, że tabela pochodna jest zdefiniowana w klauzuli FROM zewnętrznego zapytania.
Jedna wada projektu wiąże się z faktem, że jeśli konieczne jest wykonanie zapytania do tabeli pochodnej z zapytania zewnętrznego, a następnie użycie tego zapytania jako wyrażenia tabelowego w innej definicji tabeli pochodnej, kończy się to zagnieżdżeniem tych zapytań dotyczących tabeli pochodnej. W informatyce jawne zagnieżdżanie kodu obejmujące wiele poziomów zagnieżdżania zwykle prowadzi do złożonego kodu, który jest trudny do utrzymania.
Oto bardzo prosty przykład ilustrujący to:
SELECT orderyear, numcusts FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts FROM ( SELECT YEAR(orderdate) AS orderyear, custid FROM Sales.Orders ) AS D1 GROUP BY orderyear ) AS D2 WHERE numcusts > 70;
Ten kod zwraca lata zamówienia i liczbę klientów, którzy złożyli zamówienia w ciągu każdego roku, tylko dla lat, w których liczba klientów, którzy złożyli zamówienia, była większa niż 70.
Główną motywacją do używania tutaj wyrażeń tabelarycznych jest możliwość wielokrotnego odwoływania się do aliasu kolumny. Najbardziej wewnętrzne zapytanie używane jako wyrażenie tabelowe dla tabeli pochodnej D1 wysyła zapytanie do tabeli Sales.Orders i przypisuje nazwę kolumny orderyear do wyrażenia YEAR(orderdate), a także zwraca kolumnę custid. Zapytanie przeciwko D1 grupuje wiersze z D1 według roku zamówienia i zwraca rok zamówienia oraz odrębną liczbę klientów, którzy złożyli zamówienia w danym roku, aliasem numcust. Kod definiuje tabelę pochodną o nazwie D2 na podstawie tego zapytania. Najbardziej zewnętrzne zapytanie niż zapytania D2 i filtry tylko w latach, w których liczba klientów, którzy złożyli zamówienia, była większa niż 70.
Próba sprawdzenia tego kodu lub rozwiązania go w przypadku problemów jest trudna ze względu na wiele poziomów zagnieżdżenia. Zamiast przeglądać kod w bardziej naturalny sposób od góry do dołu, musisz przeanalizować go, zaczynając od najbardziej wewnętrznej jednostki i stopniowo wychodząc na zewnątrz, ponieważ jest to bardziej praktyczne.
Cały punkt dotyczący używania tabel pochodnych w tym przykładzie polegał na uproszczeniu kodu poprzez uniknięcie konieczności powtarzania wyrażeń. Ale nie jestem pewien, czy to rozwiązanie osiąga ten cel. W takim przypadku prawdopodobnie lepiej będzie powtórzyć niektóre wyrażenia, unikając w ogóle potrzeby używania tabel pochodnych, na przykład:
SELECT YEAR(orderdate) AS orderyear, COUNT(DISTINCT custid) AS numcusts FROM Sales.Orders GROUP BY YEAR(orderdate) HAVING COUNT(DISTINCT custid) > 70;
Pamiętaj, że pokazuję tutaj bardzo prosty przykład w celach ilustracyjnych. Wyobraź sobie kod produkcyjny z większą liczbą poziomów zagnieżdżania i dłuższym, bardziej skomplikowanym kodem, a zobaczysz, jak jego utrzymanie staje się znacznie bardziej skomplikowane.
Kolejna wada w projektowaniu tabel pochodnych dotyczy przypadków, w których zachodzi potrzeba interakcji z wieloma wystąpieniami tej samej tabeli pochodnej. Rozważ następujące zapytanie jako przykład:
SELECT CUR.orderyear, CUR.numorders, CUR.numorders - PRV.numorders AS diff FROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate) ) AS CUR LEFT OUTER JOIN ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders FROM Sales.Orders GROUP BY YEAR(orderdate) ) AS PRV ON CUR.orderyear = PRV.orderyear + 1;
Ten kod oblicza liczbę zamówień przetworzonych w każdym roku, a także różnicę w stosunku do poprzedniego roku. Zignoruj fakt, że istnieją prostsze sposoby osiągnięcia tego samego zadania za pomocą funkcji okna — używam tego kodu, aby zilustrować pewien punkt, więc samo zadanie i różne sposoby jego rozwiązania nie mają znaczenia.
Sprzężenie to operator tabeli, który traktuje swoje dwa wejścia jako zbiór — co oznacza, że nie ma między nimi kolejności. Są one określane jako lewe i prawe dane wejściowe, dzięki czemu można oznaczyć jedno z nich (lub oba) jako zachowaną tabelę w sprzężeniu zewnętrznym, ale nadal nie ma wśród nich pierwszego i drugiego. Można używać tabel pochodnych jako danych wejściowych łączenia, ale nazwa zmiennej zakresu przypisana do lewego wejścia nie jest dostępna w definicji prawego wejścia. Dzieje się tak, ponieważ oba są koncepcyjnie zdefiniowane w tym samym logicznym kroku, jakby w tym samym momencie. W związku z tym podczas łączenia tabel pochodnych nie można zdefiniować dwóch zmiennych zakresu na podstawie jednego wyrażenia tabeli. Niestety trzeba powtórzyć kod, definiując dwie zmienne zakresowe na podstawie dwóch identycznych kopii kodu. To oczywiście komplikuje łatwość utrzymania kodu i zwiększa prawdopodobieństwo wystąpienia błędów. Każda zmiana wprowadzona w jednym wyrażeniu tabelowym musi zostać zastosowana również do drugiego.
Jak wyjaśnię w przyszłym artykule, CTE w swojej konstrukcji nie mają tych dwóch wad, które występują w tabelach pochodnych.
Konstruktor wartości tabeli
Konstruktor wartości tabeli umożliwia konstruowanie wartości tabeli na podstawie niezależnych wyrażeń skalarnych. Takiej tabeli można następnie użyć w zapytaniu zewnętrznym, tak jak w przypadku tabeli pochodnej opartej na zapytaniu wewnętrznym. W kolejnym artykule omówię boczne tabele pochodne i korelacji szczegółowo, a pokażę bardziej wyrafinowane formy konstruktorów wartości tabelarycznych. 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