Database
 sql >> Baza danych >  >> RDS >> Database

Podstawy wyrażeń tabelarycznych, Część 5 – CTE, rozważania logiczne

Ten artykuł jest piątą częścią serii o wyrażeniach tabelowych. W części 1 przedstawiłem tło wyrażeń tabelowych. W części 2, części 3 i części 4 omówiłem zarówno logiczne, jak i optymalizacyjne aspekty tabel pochodnych. W tym miesiącu rozpoczynam omówienie wspólnych wyrażeń tabelowych (CTE). Podobnie jak w przypadku tabel pochodnych, najpierw zajmę się logicznym traktowaniem CTE, a w przyszłości przejdę do kwestii optymalizacji.

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.

CTE

Zacznijmy od terminu powszechne wyrażenie tabelowe . Ani ten termin, ani jego akronim CTE nie występują w specyfikacji normy ISO/IEC SQL. Możliwe więc, że termin pochodzi z jednego z produktów bazodanowych, a później został przyjęty przez niektórych innych dostawców baz danych. Znajdziesz go w dokumentacji Microsoft SQL Server i Azure SQL Database. T-SQL obsługuje to począwszy od SQL Server 2005. Standard używa terminu wyrażenie zapytania do reprezentowania wyrażenia, które definiuje jeden lub więcej CTE, w tym zapytanie zewnętrzne. Używa terminu z elementem listy do reprezentowania tego, co T-SQL nazywa CTE. Wkrótce przedstawię składnię wyrażenia zapytania.

Pomijając źródło terminu, powszechne wyrażenie tabelowe lub CTE , jest terminem powszechnie używanym przez praktyków T-SQL dla struktury, na której koncentruje się ten artykuł. Więc najpierw zastanówmy się, czy jest to odpowiedni termin. Doszliśmy już do wniosku, że termin wyrażenie tabelowe jest odpowiedni dla wyrażenia, które koncepcyjnie zwraca tabelę. Tabele pochodne, CTE, widoki i funkcje z wartościami w tabeli wbudowanej to wszystkie typy nazwanych wyrażeń tabelowych obsługiwanych przez T-SQL. Tak więc wyrażenie tabeli część wspólnego wyrażenia tabelowego z pewnością wydaje się właściwe. Co do wspólnego częścią tego terminu, prawdopodobnie ma to związek z jedną z zalet konstrukcyjnych współczynników CTE nad tabelami pochodnymi. Pamiętaj, że nie można ponownie użyć nazwy tabeli pochodnej (a dokładniej nazwy zmiennej zakresu) w zapytaniu zewnętrznym. I odwrotnie, nazwa CTE może być użyta wielokrotnie w zapytaniu zewnętrznym. Innymi słowy nazwa CTE jest powszechna do zewnętrznego zapytania. Oczywiście zademonstruję ten aspekt projektowania w tym artykule.

CTE dają podobne korzyści do tabel pochodnych, w tym umożliwiają tworzenie rozwiązań modułowych, ponowne wykorzystywanie aliasów kolumn, pośrednią interakcję z funkcjami okna w klauzulach, które normalnie na nie nie pozwalają, wspieranie modyfikacji, które pośrednio polegają na FETCH TOP lub OFFSET FETCH ze specyfikacją zamówienia, i inni. Istnieją jednak pewne zalety projektowe w porównaniu z tabelami pochodnymi, które omówię szczegółowo po dostarczeniu składni struktury.

Składnia

Oto standardowa składnia wyrażenia zapytania:

7.17


Funkcja
Określ tabelę.


Format
::=
[ ]
[ ] [ ] [ ]
::=WITH [ RECURSIVE ]
::= [ { }… ]
::=
[ ]
AS [ ]
::=
::=

| UNION [ ALL | DISTINCT ]
[ ]
| Z WYJĄTKIEM [ ALL | DISTINCT ]
[ ]
::=

| PRZECIĘCIE [ ALL | DISTINCT ]
[ ]
::=

|
[ ] [ ] [ ]

::=
| |
::=TABLE
::=
CORRESPONDING [ BY ]
::=
::=ORDER BY
::=PRZESUNIĘCIE { WIERSZ | ROWS }
::=
FETCH { FIRST | NEXT } [ ] { ROW | WIERSZE } { TYLKO | WITH TIES }
::=

|
::=
::=
::= PROCENT


7.18


Funkcja
Określ generowanie informacji o kolejności i wykrywaniu cykli w wyniku rekurencyjnych wyrażeń zapytań.


Format
::=
| |
::=
SEARCH SET
::=
GŁĘBOKOŚĆ FIRST BY | BREADTH FIRST BY
::=
::=
CYCLE SET TO
DEFAULT USING
::= [ { }… ]
::=
::=
::=
::=
::=


7.3


Funkcja
Określ zestaw , które mają być skonstruowane w tabeli.


Format
::=VALUES
::=
[ { }… ]
::=
WARTOŚCI
::=

[ { }… ]

Termin standardowy wyrażenie zapytania reprezentuje wyrażenie zawierające klauzulę WITH, z listą , który składa się z co najmniej jednego z elementami listy i zapytanie zewnętrzne. T-SQL odnosi się do standardu z elementem listy jako CTE.

T-SQL nie obsługuje wszystkich standardowych elementów składni. Na przykład nie obsługuje niektórych bardziej zaawansowanych rekurencyjnych elementów zapytań, które pozwalają kontrolować kierunek wyszukiwania i obsługiwać cykle w strukturze wykresu. Zapytania rekurencyjne są tematem artykułu w przyszłym miesiącu.

Oto składnia T-SQL dla uproszczonego zapytania względem CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
SELECT < select list >
FROM < table name >;

Oto przykład prostego zapytania dotyczącego CTE reprezentującego klientów z USA:

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

Znajdziesz te same trzy części w oświadczeniu przeciwko CTE, tak jak w przypadku oświadczenia przeciwko tabeli pochodnej:

  1. Wyrażenie tabeli (wewnętrzne zapytanie)
  2. Nazwa przypisana do wyrażenia tabeli (nazwa zmiennej zakresu)
  3. Zewnętrzne zapytanie

To, co różni się od projektu CTE w porównaniu do tabel pochodnych, to lokalizacja w kodzie tych trzech elementów. W przypadku tabel pochodnych zapytanie wewnętrzne jest zagnieżdżone w klauzuli FROM zapytania zewnętrznego, a nazwa wyrażenia tabelowego jest przypisywana po samym wyrażeniu tabelowym. Elementy są ze sobą powiązane. I odwrotnie, w przypadku CTE kod oddziela trzy elementy:najpierw przypisujesz nazwę wyrażenia tabeli; po drugie określasz wyrażenie tabeli — od początku do końca bez przerw; po trzecie określasz zapytanie zewnętrzne — od początku do końca bez przerw. Później w sekcji „Rozważania projektowe” wyjaśnię implikacje tych różnic projektowych.

Słowo o CTE i użyciu średnika jako terminatora instrukcji. Niestety, w przeciwieństwie do standardowego SQL, T-SQL nie wymusza zakończenia wszystkich instrukcji średnikiem. Jednak w T-SQL jest bardzo niewiele przypadków, w których kod bez terminatora jest niejednoznaczny. W takich przypadkach wypowiedzenie jest obowiązkowe. Jeden z takich przypadków dotyczy faktu, że klauzula WITH jest używana do wielu celów. Jednym z nich jest zdefiniowanie CTE, drugim jest zdefiniowanie podpowiedzi tabeli dla zapytania, a jest kilka dodatkowych przypadków użycia. Na przykład w poniższej instrukcji klauzula WITH jest używana do wymuszenia serializowanego poziomu izolacji za pomocą wskazówki dotyczącej tabeli:

SELECT custid, country FROM Sales.Customers WITH (SERIALIZABLE);

Potencjał niejednoznaczności występuje wtedy, gdy masz niezakończoną instrukcję poprzedzającą definicję CTE, w którym to przypadku parser może nie być w stanie stwierdzić, czy klauzula WITH należy do pierwszej czy drugiej instrukcji. Oto przykład ilustrujący to:

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

W tym przypadku parser nie może stwierdzić, czy klauzula WITH ma zostać użyta do zdefiniowania podpowiedzi do tabeli Customers w pierwszej instrukcji, czy też do uruchomienia definicji CTE. Pojawia się następujący błąd:

Msg 336, poziom 15, stan 1, wiersz 159
Nieprawidłowa składnia w pobliżu „UC”. Jeśli ma to być wspólne wyrażenie tabelowe, musisz wyraźnie zakończyć poprzednią instrukcję średnikiem.

Poprawka polega oczywiście na zamknięciu instrukcji poprzedzającej definicję CTE, ale najlepszą praktyką jest zakończenie wszystkich instrukcji:

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

Być może zauważyłeś, że niektórzy ludzie zaczynają swoje definicje CTE jako praktyka od średnika, na przykład:

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

Celem tej praktyki jest zmniejszenie możliwości przyszłych błędów. Co się stanie, jeśli później ktoś doda niezakończoną wypowiedź tuż przed twoją definicją CTE w skrypcie i nie zawraca sobie głowy sprawdzaniem całego skryptu, a jedynie swoją wypowiedź? Twój średnik tuż przed klauzulą ​​WITH faktycznie staje się ich terminatorem instrukcji. Z pewnością widać praktyczność tej praktyki, ale jest to trochę nienaturalne. Zalecane, choć trudniejsze do osiągnięcia, jest zaszczepienie w organizacji dobrych praktyk programistycznych, w tym usunięcie wszystkich oświadczeń.

Pod względem reguł składni, które mają zastosowanie do wyrażenia tabelowego używanego jako zapytanie wewnętrzne w definicji CTE, są one takie same, jak te, które mają zastosowanie do wyrażenia tabelowego używanego jako zapytanie wewnętrzne w definicji tabeli pochodnej. Są to:

  • 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

Aby uzyskać szczegółowe informacje, zobacz sekcję „Wyrażenie tabelowe to tabela” w części 2 serii.

Rozważania projektowe

Jeśli zapytasz doświadczonych programistów T-SQL, czy wolą używać tabel pochodnych, czy CTE, nie wszyscy zgodzą się, co jest lepsze. Oczywiście różni ludzie mają różne preferencje stylizacyjne. Czasami używam tabel pochodnych, a czasami CTE. Dobrze jest być w stanie świadomie identyfikować specyficzne różnice językowe między tymi dwoma narzędziami i wybierać w oparciu o swoje priorytety w danym rozwiązaniu. Z czasem i doświadczeniem dokonujesz wyborów bardziej intuicyjnie.

Ponadto ważne jest, aby nie mylić użycia wyrażeń tabelowych i tabel tymczasowych, ale jest to dyskusja związana z wydajnością, którą omówię w przyszłym artykule.

CTE mają rekurencyjne możliwości zapytań, a tabele pochodne nie. Tak więc, jeśli musisz na nich polegać, naturalnie wybierzesz CTE. Zapytania rekurencyjne są tematem artykułu w przyszłym miesiącu.

W części 2 wyjaśniłem, że zagnieżdżanie tabel pochodnych jest dla mnie zwiększaniem złożoności kodu, ponieważ utrudnia to przestrzeganie logiki. Podałem następujący przykład, określając lata zamówień, w których zamówienia złożyło ponad 70 klientów:

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;

CTE nie obsługują zagnieżdżania. Dlatego podczas przeglądania lub rozwiązywania problemów z rozwiązaniem opartym na CTE nie gubisz się w zagnieżdżonej logice. Zamiast zagnieżdżania, budujesz bardziej modułowe rozwiązania, definiując wiele CTE w ramach tej samej instrukcji WITH, oddzielone przecinkami. Każdy z CTE jest oparty na zapytaniu pisanym od początku do końca bez przerw. Uważam to za dobrą rzecz z punktu widzenia przejrzystości kodu i łatwości konserwacji.

Oto rozwiązanie powyższego zadania przy użyciu CTE:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
)
SELECT orderyear, numcusts
FROM C2
WHERE numcusts > 70;

Bardziej podoba mi się rozwiązanie oparte na CTE. Ale ponownie zapytaj doświadczonych programistów, które z powyższych dwóch rozwiązań preferują, i nie wszyscy się zgodzą. Niektórzy faktycznie wolą zagnieżdżoną logikę i możliwość zobaczenia wszystkiego w jednym miejscu.

Jedną z bardzo wyraźnych zalet CTE nad tabelami pochodnymi jest konieczność interakcji z wieloma wystąpieniami tego samego wyrażenia tabeli w rozwiązaniu. Zapamiętaj następujący przykład oparty na tabelach pochodnych z części 2 serii:

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;

To rozwiązanie zwraca lata zamówień, liczbę zamówień w ciągu roku oraz różnicę między liczbą zamówień w bieżącym i poprzednim roku. Tak, możesz to zrobić łatwiej dzięki funkcji LAG, ale nie skupiam się tutaj na znalezieniu najlepszego sposobu na osiągnięcie tego bardzo konkretnego zadania. Używam tego przykładu, aby zilustrować pewne aspekty projektowania języka nazwanych wyrażeń tabelowych.

Problem z tym rozwiązaniem polega na tym, że nie można przypisać nazwy do wyrażenia tabelowego i ponownie użyć go w tym samym kroku przetwarzania kwerendy logicznej. Nazywasz tabelę pochodną po samym wyrażeniu tabeli w klauzuli FROM. Jeśli zdefiniujesz i nazwiesz tabelę pochodną jako pierwsze dane wejściowe sprzężenia, nie można również ponownie użyć tej nazwy tabeli pochodnej jako drugich danych wejściowych tego samego sprzężenia. Jeśli musisz połączyć dwa wystąpienia tego samego wyrażenia tabelowego, w przypadku tabel pochodnych nie masz innego wyboru, jak tylko zduplikować kod. To właśnie zrobiłeś w powyższym przykładzie. I odwrotnie, nazwa CTE jest przypisywana jako pierwszy element kodu spośród wyżej wymienionych trzech (nazwa CTE, zapytanie wewnętrzne, zapytanie zewnętrzne). W terminach dotyczących przetwarzania zapytań logicznych, zanim dojdziesz do zapytania zewnętrznego, nazwa CTE jest już zdefiniowana i dostępna. Oznacza to, że możesz wchodzić w interakcje z wieloma wystąpieniami nazwy CTE w zewnętrznym zapytaniu, na przykład:

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

To rozwiązanie ma wyraźną przewagę programowalności w stosunku do rozwiązania opartego na tabelach pochodnych, ponieważ nie trzeba utrzymywać dwóch kopii tego samego wyrażenia tabelowego. Jest więcej do powiedzenia na ten temat z perspektywy przetwarzania fizycznego i porównaj to z użyciem tabel tymczasowych, ale zrobię to w przyszłym artykule, który skupia się na wydajności.

Jedna z zalet kodu opartego na tabelach pochodnych w porównaniu z kodem opartym na CTE ma związek z właściwością zamknięcia, którą powinno posiadać wyrażenie tabeli. Pamiętaj, że właściwość domknięcia wyrażenia relacyjnego mówi, że zarówno dane wejściowe, jak i dane wyjściowe są relacjami, a zatem wyrażenie relacyjne może być użyte tam, gdzie oczekuje się relacji, jako dane wejściowe do jeszcze innego wyrażenia relacyjnego. Podobnie wyrażenie tabelowe zwraca tabelę i powinno być dostępne jako tabela wejściowa dla innego wyrażenia tabelowego. Dotyczy to zapytania opartego na tabelach pochodnych — można go używać tam, gdzie oczekiwana jest tabela. Na przykład możesz użyć zapytania opartego na tabelach pochodnych jako wewnętrznego zapytania definicji CTE, jak w poniższym przykładzie:

WITH C AS
(
  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
)
SELECT orderyear, numcusts
FROM C;

Jednak to samo nie dotyczy zapytania opartego na CTE. Mimo że koncepcyjnie ma być uważane za wyrażenie tabelowe, nie można go używać jako wewnętrznego zapytania w definicjach tabel pochodnych, podzapytaniach i samych CTE. Na przykład poniższy kod nie jest poprawny w T-SQL:

SELECT orderyear, custid
FROM (WITH C1 AS
      (
        SELECT YEAR(orderdate) AS orderyear, custid
        FROM Sales.Orders
      ),
      C2 AS
      (
        SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
        FROM C1
        GROUP BY orderyear
      )
      SELECT orderyear, numcusts
      FROM C2
      WHERE numcusts > 70) AS D;

Dobrą wiadomością jest to, że możesz użyć zapytania opartego na CTE jako zapytania wewnętrznego w widokach i wbudowanych funkcjach z wartościami tabelarycznymi, które omówię w przyszłych artykułach.

Pamiętaj też, że zawsze możesz zdefiniować inne CTE na podstawie ostatniego zapytania, a następnie wywołać interakcję z zapytaniem zewnętrznym z tym CTE:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
),
C3 AS
(
  SELECT orderyear, numcusts
  FROM C2
  WHERE numcusts &gt; 70
)
SELECT orderyear, numcusts
FROM C3;

Z punktu widzenia rozwiązywania problemów, jak wspomniano, zwykle łatwiej jest mi postępować zgodnie z logiką kodu opartego na CTE w porównaniu z kodem opartym na tabelach pochodnych. Jednak rozwiązania oparte na tabelach pochodnych mają tę zaletę, że można podświetlić dowolny poziom zagnieżdżenia i uruchomić go niezależnie, jak pokazano na rysunku 1.

Rysunek 1:Potrafi podświetlić i uruchomić część kodu za pomocą tabel pochodnych

Z CTE rzeczy są trudniejsze. Aby kod zawierający CTE można było uruchomić, musi zaczynać się od klauzuli WITH, po której następuje jedno lub więcej nazwanych wyrażeń tabelowych w nawiasach oddzielonych przecinkami, po których następuje zapytanie bez nawiasów bez poprzedzającego przecinka. Jesteś w stanie podświetlić i uruchomić dowolne wewnętrzne zapytania, które są naprawdę niezależne, a także kompletny kod rozwiązania; jednak nie można wyróżnić i pomyślnie uruchomić żadnej innej pośredniej części rozwiązania. Na przykład Rysunek 2 pokazuje nieudaną próbę uruchomienia kodu reprezentującego C2.

Rysunek 2:Nie można podświetlić i uruchomić części kodu za pomocą CTE

Tak więc w przypadku CTE musisz uciec się do nieco niezręcznych środków, aby móc rozwiązać pośredni etap rozwiązania. Na przykład jednym z powszechnych rozwiązań jest tymczasowe wstawienie zapytania SELECT * FROM your_cte tuż pod odpowiednim CTE. Następnie podświetlasz i uruchamiasz kod, w tym wstrzyknięte zapytanie, a kiedy skończysz, usuwasz wstrzyknięte zapytanie. Rysunek 3 przedstawia tę technikę.

Rysunek 3:Wstrzyknij SELECT * poniżej odpowiedniego CTE

Problem polega na tym, że za każdym razem, gdy wprowadzasz zmiany w kodzie — nawet tymczasowe, drobne, takie jak powyżej — istnieje szansa, że ​​przy próbie powrotu do oryginalnego kodu wprowadzisz nowy błąd.

Inną opcją jest nieco inny styl kodu, tak aby każda niepierwsza definicja CTE zaczynała się osobnym wierszem kodu, który wygląda tak:

, cte_name AS (

Następnie za każdym razem, gdy chcesz uruchomić pośrednią część kodu do określonego CTE, możesz to zrobić przy minimalnych zmianach w kodzie. Używając komentarza do wiersza, komentujesz tylko ten jeden wiersz kodu, który odpowiada temu CTE. Następnie podświetlasz i uruchamiasz kod aż do wewnętrznego zapytania CTE, które jest teraz uważane za zapytanie zewnętrzne, jak pokazano na rysunku 4.

Rysunek 4:Zmień składnię, aby umożliwić komentowanie jednego wiersza kodu

Jeśli nie jesteś zadowolony z tego stylu, masz jeszcze jedną opcję. Możesz użyć komentarza blokowego, który zaczyna się tuż przed przecinkiem poprzedzającym interesujący CTE i kończy się po otwartym nawiasie, jak pokazano na rysunku 5.

Rysunek 5:Użyj komentarza blokującego

Sprowadza się to do osobistych preferencji. Zazwyczaj używam tymczasowo wstrzykniętej techniki zapytania SELECT *.

Konstruktor wartości tabeli

Istnieje pewne ograniczenie w obsłudze T-SQL dla konstruktorów wartości tabel w porównaniu ze standardem. Jeśli nie jesteś zaznajomiony z konstruktem, koniecznie zapoznaj się najpierw z częścią 2 serii, w której szczegółowo ją opiszę. Podczas gdy T-SQL umożliwia zdefiniowanie tabeli pochodnej na podstawie konstruktora wartości tabeli, nie pozwala na zdefiniowanie CTE na podstawie konstruktora wartości tabeli.

Oto obsługiwany przykład wykorzystujący tabelę pochodną:

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

Niestety podobny kod korzystający z CTE nie jest obsługiwany:

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

Ten kod generuje następujący błąd:

Msg 156, Level 15, State 1, Line 337
Nieprawidłowa składnia w pobliżu słowa kluczowego „VALUES”.

Istnieje jednak kilka obejść. Jednym z nich jest użycie zapytania do tabeli pochodnej, która z kolei jest oparta na konstruktorze wartości tabeli, jako wewnętrzne zapytanie CTE, na przykład:

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

Innym jest odwołanie się do techniki używanej przez ludzi przed wprowadzeniem konstruktorów z wartościami przechowywanymi w tabeli do T-SQL — przy użyciu serii zapytań FROMless oddzielonych operatorami UNION ALL, na przykład:

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

Zauważ, że aliasy kolumn są przypisywane zaraz po nazwie CTE.

Obie metody zostają zalgebrowane i zoptymalizowane w ten sam sposób, więc używaj tego, z którym Ci wygodniej.

Tworzenie ciągu liczb

Narzędziem, z którego dość często korzystam w swoich rozwiązaniach, jest pomocnicza tablica liczb. Jedną z opcji jest utworzenie tabeli liczb rzeczywistych w bazie danych i wypełnienie jej sekwencją o rozsądnej wielkości. Innym jest opracowanie rozwiązania, które generuje ciąg liczb w locie. W przypadku drugiej opcji chcesz, aby dane wejściowe były ogranicznikami pożądanego zakresu (nazwiemy je @low i @high ). Chcesz, aby Twoje rozwiązanie obsługiwało potencjalnie duże zasięgi. Oto moje rozwiązanie do tego celu, wykorzystujące CTE, z żądaniem zakresu od 1001 do 1010 w tym konkretnym przykładzie:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
WITH
  L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1)) AS D(c) ),
  L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
  L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
  L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
  L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
  L5 AS ( SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B ),
  Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
            FROM L5 )
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM Nums
ORDER BY rownum;

Ten kod generuje następujące dane wyjściowe:

n
-----
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010

Pierwsze CTE o nazwie L0 oparte jest na konstruktorze wartości tabeli z dwoma wierszami. Rzeczywiste wartości są tam nieistotne; ważne jest to, że ma dwa rzędy. Następnie istnieje sekwencja pięciu dodatkowych CTE o nazwach od L1 do L5, z których każdy stosuje połączenie krzyżowe między dwoma wystąpieniami poprzedniego CTE. Poniższy kod oblicza liczbę wierszy potencjalnie generowanych przez każdy z CTE, gdzie @L jest numerem poziomu CTE:

DECLARE @L AS INT = 5;
 
SELECT POWER(2., POWER(2., @L));

Oto liczby, które otrzymujesz dla każdego CTE:

CTE Kardynalność
L0 2
L1 4
L2 16
L3 256
L4 65 536
L5 4.294.967.296

Przejście na poziom 5 daje ponad cztery miliardy wierszy. Powinno to wystarczyć w każdym praktycznym przypadku użycia, o którym myślę. Kolejny krok odbywa się w CTE o nazwie Nums. Za pomocą funkcji ROW_NUMBER można wygenerować sekwencję liczb całkowitych rozpoczynającą się od 1 w oparciu o niezdefiniowaną kolejność (ORDER BY (SELECT NULL)) i nazwać kolumnę wyników rownum. Na koniec, zewnętrzne zapytanie używa filtru TOP opartego na kolejności numerów wierszy, aby filtrować tyle liczb, ile żądana kardynalność sekwencji (@high – @low + 1) i oblicza liczbę wyniku n jako @low + rownum – 1.

Tutaj możesz naprawdę docenić piękno projektu CTE i oszczędności, które zapewnia, gdy budujesz rozwiązania w sposób modułowy. Ostatecznie proces rozpakowywania rozpakowuje 32 tabele, z których każda składa się z dwóch wierszy opartych na stałych. Widać to wyraźnie w planie wykonania tego kodu, jak pokazano na rysunku 6 za pomocą SentryOne Plan Explorer.

Rysunek 6:Plan generowania sekwencji liczb w zapytaniach

Każdy operator Constant Scan reprezentuje tabelę stałych z dwoma wierszami. Chodzi o to, że operator Top jest tym, który żąda tych wierszy i zwiera spięcie po otrzymaniu żądanej liczby. Zwróć uwagę na 10 wierszy wskazanych nad strzałką przechodzącą do operatora Top.

Wiem, że ten artykuł koncentruje się na koncepcyjnym potraktowaniu CTE, a nie na rozważaniach fizycznych/wydajnościowych, ale patrząc na plan, możesz naprawdę docenić zwięzłość kodu w porównaniu z rozwlekłością tego, na co przekłada się za kulisami.

Korzystając z tabel pochodnych, można w rzeczywistości napisać rozwiązanie, w którym każde odwołanie CTE zostanie zastąpione bazowym zapytaniem, które reprezentuje. To, co otrzymujesz, jest dość przerażające:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
 
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D9
         CROSS JOIN
            ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D10 ) AS Nums
ORDER BY rownum;

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

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

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

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

Used in modification statements

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

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

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

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
DELETE [ FROM ] <table name>
[ WHERE <filter predicate> ];

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

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

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

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
UPDATE <table name>
  SET <assignments>
[ WHERE <filter predicate> ];

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

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

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

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

orderid     oldrequireddate newrequireddate
----------- --------------- ---------------
11008       2019-05-06      2020-07-16
11019       2019-05-11      2020-07-16
11039       2019-05-19      2020-07-16
11040       2019-05-20      2020-07-16
11045       2019-05-21      2020-07-16
11051       2019-05-25      2020-07-16
11054       2019-05-26      2020-07-16
11058       2019-05-27      2020-07-16
11059       2019-06-10      2020-07-16
11061       2019-06-11      2020-07-16

(10 rows affected)

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

Podsumowanie

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

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

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


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak filtrować rekordy za pomocą funkcji agregującej COUNT

  2. Wskazówki dotyczące przechowywania kopii zapasowych danych TimescaleDB w chmurze

  3. Podstawy wyrażeń tabelarycznych, część 12 – Wbudowane funkcje o wartościach tabelarycznych

  4. Ustalanie i identyfikacja celów rzędów w planach wykonawczych

  5. Dzielenie strun:kontynuacja