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

Podstawy wyrażeń tabelarycznych, Część 8 – CTE, rozważania dotyczące optymalizacji ciąg dalszy

Ten artykuł jest ósmą częścią serii dotyczącej wyrażeń tabelowych. Do tej pory przedstawiłem tło wyrażeń tabelarycznych, obejmujące zarówno aspekty logiczne, jak i optymalizacyjne tabel pochodnych, aspekty logiczne CTE oraz niektóre aspekty optymalizacji CTE. W tym miesiącu kontynuuję omówienie aspektów optymalizacji CTE, w szczególności dotyczących obsługi wielu odwołań CTE.

W moich przykładach będę kontynuował korzystanie z przykładowej bazy danych TSQLV5. Skrypt tworzący i wypełniający TSQLV5 można znaleźć tutaj, a jego diagram ER znajduje się tutaj.

Wiele odniesień i niedeterminizm

W zeszłym miesiącu wyjaśniłem i pokazałem, że CTE nie są zagnieżdżone, podczas gdy tymczasowe tabele i zmienne tabel faktycznie zachowują dane. Przedstawiłem zalecenia dotyczące tego, kiedy sensowne jest użycie CTE, a kiedy ma sens użycie tymczasowych obiektów z punktu widzenia wydajności zapytań. Poza wydajnością rozwiązania należy jednak wziąć pod uwagę inny ważny aspekt optymalizacji CTE lub przetwarzania fizycznego — sposób obsługi wielu odwołań do CTE z zapytania zewnętrznego. Ważne jest, aby zdać sobie sprawę, że jeśli masz zapytanie zewnętrzne z wieloma odwołaniami do tego samego CTE, każde z nich zostaje rozgnieżdżone osobno. Jeśli masz obliczenia niedeterministyczne w wewnętrznym zapytaniu CTE, obliczenia te mogą mieć różne wyniki w różnych odwołaniach.

Powiedzmy na przykład, że wywołujesz funkcję SYSDATETIME w wewnętrznym zapytaniu CTE, tworząc kolumnę wyników o nazwie dt. Ogólnie rzecz biorąc, zakładając brak zmian w danych wejściowych, funkcja wbudowana jest oceniana raz na zapytanie i odwołanie, niezależnie od liczby zaangażowanych wierszy. Jeśli odwołujesz się do CTE tylko raz z zapytania zewnętrznego, ale wielokrotnie wchodzisz w interakcję z kolumną dt, wszystkie odwołania powinny reprezentować tę samą ocenę funkcji i zwracać te same wartości. Jeśli jednak wielokrotnie odwołujesz się do CTE w zapytaniu zewnętrznym, czy to z wieloma podzapytaniami odnoszącymi się do CTE, czy z połączeniem między wieloma instancjami tego samego CTE (powiedzmy, aliasami C1 i C2), odwołania do C1.dt i C2.dt reprezentuje różne oceny podstawowego wyrażenia i może skutkować różnymi wartościami.

Aby to zademonstrować, rozważ następujące trzy partie:

-- Batch 1
 
DECLARE @i AS INT = 1;
 
WHILE @@ROWCOUNT = 1
  SELECT @i += 1 WHERE SYSDATETIME() = SYSDATETIME();
 
PRINT @i;
GO
 
-- Batch 2
 
DECLARE @i AS INT = 1;
 
WHILE @@ROWCOUNT = 1
  WITH C AS ( SELECT SYSDATETIME() AS dt )
    SELECT @i += 1 FROM C WHERE dt = dt;
 
PRINT @i;
GO
 
-- Batch 3
 
DECLARE @i AS INT = 1;
 
WHILE @@ROWCOUNT = 1
  WITH C AS ( SELECT SYSDATETIME() AS dt ) 
    SELECT @i += 1 WHERE (SELECT dt FROM C) = (SELECT dt FROM C);
 
PRINT @i;
GO

Na podstawie tego, co właśnie wyjaśniłem, czy możesz określić, które partie mają nieskończoną pętlę, a które zatrzymają się w pewnym momencie z powodu dwóch wartości porównawczych predykatu oceniających różne wartości?

Pamiętaj, że powiedziałem, że wywołanie wbudowanej funkcji niedeterministycznej, takiej jak SYSDATETIME, jest oceniane raz na zapytanie i odwołanie. Oznacza to, że w Batch 1 masz dwie różne oceny i po wystarczającej liczbie iteracji pętli dadzą różne wartości. Spróbuj. Ile iteracji zgłosił kod?

Jeśli chodzi o Batch 2, kod ma dwa odniesienia do kolumny dt z tej samej instancji CTE, co oznacza, że ​​oba reprezentują tę samą ocenę funkcji i powinny reprezentować tę samą wartość. W konsekwencji Partia 2 ma nieskończoną pętlę. Uruchom go na dowolny czas, ale w końcu będziesz musiał zatrzymać wykonywanie kodu.

Jeśli chodzi o Batch 3, zewnętrzne zapytanie zawiera dwa różne podzapytania współdziałające z CTE C, z których każde reprezentuje inną instancję, która przechodzi oddzielnie przez proces rozgnieżdżania. Kod nie przypisuje wyraźnie różnych aliasów do różnych wystąpień CTE, ponieważ dwa podzapytania pojawiają się w niezależnych zakresach, ale aby ułatwić zrozumienie, możesz pomyśleć o używaniu różnych aliasów, takich jak C1 w jednym podzapytaniu i C2 w drugim. To tak, jakby jedno podzapytanie współdziałało z C1.dt, a drugie z C2.dt. Różne odniesienia reprezentują różne oceny podstawowego wyrażenia, a zatem mogą skutkować różnymi wartościami. Spróbuj uruchomić kod i zobacz, czy w pewnym momencie się zatrzymuje. Ile iteracji zajęło, zanim się zatrzymało?

Interesujące jest próba zidentyfikowania przypadków, w których masz pojedynczą lub wiele ocen bazowego wyrażenia w planie wykonania zapytania. Rysunek 1 przedstawia graficzne plany wykonania dla trzech partii (kliknij, aby powiększyć).

Rysunek 1:Graficzne plany wykonania dla Partii 1, Partii 2 i Partii 3

Niestety nie ma radości z graficznych planów wykonawczych; wszystkie wydają się identyczne, chociaż semantycznie te trzy partie nie mają identycznych znaczeń. Dzięki @CodeRecce i Forrest (@tsqladdict) jako społeczności udało nam się dotrzeć do sedna sprawy innymi sposobami.

Jak odkrył @CodeRecce, plany XML zawierają odpowiedź. Oto odpowiednie części XML dla trzech partii:

−− Partia 1



−− Partia 2



−− Partia 3


W planie XML dla partii 1 widać wyraźnie, że predykat filtra porównuje wyniki dwóch oddzielnych bezpośrednich wywołań wewnętrznej funkcji SYSDATETIME.

W planie XML dla partii 2 predykat filtra porównuje ze sobą wyrażenie stałe ConstExpr1002 reprezentujące jedno wywołanie funkcji SYSDATETIME.

W planie XML dla partii 3 predykat filtra porównuje dwa różne wyrażenia stałe o nazwach ConstExpr1005 i ConstExpr1006, z których każde reprezentuje oddzielne wywołanie funkcji SYSDATETIME.

Jako inną opcję Forrest (@tsqladdict) zasugerował użycie flagi śledzenia 8605, która pokazuje początkową reprezentację drzewa zapytań utworzoną przez program SQL Server, po włączeniu flagi śledzenia 3604, która powoduje, że dane wyjściowe TF 8605 mają być kierowane do klienta SSMS. Użyj następującego kodu, aby włączyć obie flagi śledzenia:

DBCC TRACEON(3604); -- direct output to client
GO
 
DBCC TRACEON(8605); -- show initial query tree
GO

Następnie uruchamiasz kod, dla którego chcesz uzyskać drzewo zapytań. Oto odpowiednie części danych wyjściowych, które otrzymałem z TF 8605 dla trzech partii:

−− Partia 1


*** Przekonwertowane drzewo:***

LogOp_Project COL:Expr1000

LogOp_Select

LogOp_ConstTableGet (1) [pusty]

ScaOp_Comp x_cmpEq

ScaOp_Intrinsic sysdatetime

ScaOp_Intrinsic sysdatetime

AncOp_PrjList

AncOp_PrjEl COL:Expr1000

ScaOp_Arithmetic x_aopDodaj

ScaOp_Identifier COL:@i

ScaOp_Const TI(int,ML=4) XVAR(int,Nie posiadane,Wartość=1)

−− Partia 2


*** Przekonwertowane drzewo:***

LogOp_Project COL:Expr1001

LogOp_Select

LogOp_ViewKotwica

LogOp_Project

LogOp_ConstTableGet (1) [pusty]

AncOp_PrjList

AncOp_PrjEl COL:Expr1000

ScaOp_Intrinsic sysdatetime

ScaOp_Comp x_cmpEq

ScaOp_Identifier COL:Expr1000

ScaOp_Identifier COL:Expr1000

AncOp_PrjList

AncOp_PrjEl COL:Wyr1001

ScaOp_Arithmetic x_aopDodaj

ScaOp_Identifier COL:@i

ScaOp_Const TI(int,ML=4) XVAR(int,Nie posiadane,Wartość=1)

−− Partia 3


*** Przekonwertowane drzewo:***

LogOp_Project COL:Expr1004

LogOp_Select

LogOp_ConstTableGet (1) [pusty]

ScaOp_Comp x_cmpEq

ScaOp_Subquery COL:Expr1001

LogOp_Project

LogOp_ViewKotwica

LogOp_Project

LogOp_ConstTableGet (1) [pusty]

AncOp_PrjList

AncOp_PrjEl COL:Expr1000

ScaOp_Intrinsic sysdatetime

AncOp_PrjList

AncOp_PrjEl COL:Wyr1001

ScaOp_Identifier COL:Expr1000

ScaOp_Subquery COL:Wyr1003

LogOp_Project

LogOp_ViewKotwica

LogOp_Project

LogOp_ConstTableGet (1) [pusty]

AncOp_PrjList

AncOp_PrjEl COL:Wyr1002

ScaOp_Intrinsic sysdatetime

AncOp_PrjList

AncOp_PrjEl COL:Wyr1003

ScaOp_Identifier COL:Wyr1002

AncOp_PrjList

AncOp_PrjEl COL:Wyr1004

ScaOp_Arithmetic x_aopDodaj

ScaOp_Identifier COL:@i

ScaOp_Const TI(int,ML=4) XVAR(int,brak własności,wartość=1)

W partii 1 można zobaczyć porównanie wyników dwóch oddzielnych ocen funkcji wewnętrznej SYSDATETIME.

W Partii 2 widać jedną ocenę funkcji, której wynikiem jest kolumna o nazwie Expr1000, a następnie porównanie między tą kolumną a nią samą.

W partii 3 widzisz dwie oddzielne oceny funkcji. Jeden w kolumnie o nazwie Expr1000 (później przewidywany przez kolumnę podzapytania o nazwie Expr1001). Kolejna w kolumnie o nazwie Expr1002 (później rzutowana przez kolumnę podzapytania o nazwie Expr1003). Następnie masz porównanie między Expr1001 i Expr1003.

Tak więc, trochę więcej wykopując poza to, co ujawnia graficzny plan wykonania, możesz faktycznie dowiedzieć się, kiedy bazowe wyrażenie jest oceniane tylko raz, a kiedy wiele razy. Teraz, gdy rozumiesz różne przypadki, możesz opracować swoje rozwiązania w oparciu o pożądane zachowanie, którego szukasz.

Funkcje okien z niedeterministycznym porządkiem

Istnieje inna klasa obliczeń, które mogą wpędzić Cię w kłopoty, gdy są używane w rozwiązaniach z wieloma odniesieniami do tego samego CTE. Są to funkcje okien, które opierają się na niedeterministycznym porządkowaniu. Jako przykład weźmy funkcję okna ROW_NUMBER. W przypadku korzystania z częściowego zamawiania (uporządkowanie według elementów, które nie identyfikują jednoznacznie wiersza), każda ocena bazowego zapytania może skutkować innym przypisaniem numerów wierszy, nawet jeśli bazowe dane nie uległy zmianie. W przypadku wielu odwołań CTE pamiętaj, że każde z nich jest niezagnieżdżone osobno i możesz otrzymać różne zestawy wyników. W zależności od tego, co zewnętrzne zapytanie robi z każdym odwołaniem, np. z którymi kolumnami z każdego odwołania wchodzi w interakcję i w jaki sposób, optymalizator może zdecydować o dostępie do danych dla każdej instancji przy użyciu różnych indeksów o różnych wymaganiach dotyczących kolejności.

Rozważ poniższy kod jako przykład:

USE TSQLV5;
 
WITH C AS
(
  SELECT *, ROW_NUMBER() OVER(ORDER BY orderdate) AS rownum
  FROM Sales.Orders
)
SELECT C1.orderid, C1.shipcountry, C2.orderid
FROM C AS C1
  INNER JOIN C AS C2
    ON C1.rownum = C2.rownum
WHERE C1.orderid <> C2.orderid;

Czy to zapytanie może kiedykolwiek zwrócić niepusty zestaw wyników? Być może twoją pierwszą reakcją jest to, że nie może. Ale pomyśl o tym, co właśnie wyjaśniłem nieco dokładniej, a zdasz sobie sprawę, że przynajmniej teoretycznie, dzięki dwóm oddzielnym procesom rozgnieżdżania CTE, które będą miały tutaj miejsce — jeden z C1, a drugi z C2 — jest to możliwe. Jednak jedną rzeczą jest teoretyzowanie, że coś może się wydarzyć, a inną zademonstrowanie tego. Na przykład, gdy uruchomiłem ten kod bez tworzenia nowych indeksów, otrzymywałem pusty zestaw wyników:

orderid     shipcountry     orderid
----------- --------------- -----------

(0 rows affected)

Mam plan pokazany na rysunku 23 dla tego zapytania.

Rysunek 2:Pierwszy plan zapytania z dwoma odniesieniami CTE

Co ciekawe, optymalizator zdecydował się na użycie różnych indeksów do obsługi różnych odwołań CTE, ponieważ właśnie to uznał za optymalne. W końcu każde odwołanie w zewnętrznym zapytaniu dotyczy innego podzbioru kolumn CTE. Jedno odwołanie skutkowało uporządkowanym skanowaniem w przód indeksu idx_nc_orderedate, a drugie nieuporządkowanym skanowaniem indeksu klastrowego, po którym następuje operacja sortowania rosnąco według daty zamówienia. Mimo że indeks idx_nc_orderedate jest jawnie zdefiniowany tylko w kolumnie orderdate jako klucz, w praktyce jest zdefiniowany w (orderdate, orderid) jako klucze, ponieważ orderid jest kluczem indeksu klastrowego i jest uwzględniany jako ostatni klucz we wszystkich indeksach nieklastrowanych. Tak więc uporządkowane skanowanie indeksu faktycznie emituje wiersze uporządkowane według daty zamówienia, id zamówienia. Jeśli chodzi o nieuporządkowane skanowanie indeksu klastrowego, na poziomie aparatu magazynu dane są skanowane w kolejności klucza indeksu (na podstawie identyfikatora zamówienia), aby spełnić minimalne oczekiwania co do spójności domyślnego poziomu izolacji zatwierdzonego odczytu. Operator Sort zatem pobiera dane uporządkowane według orderid, sortuje wiersze według orderdate i w praktyce emituje wiersze uporządkowane według orderdate, orderid.

Ponownie, teoretycznie nie ma pewności, że te dwa odniesienia zawsze będą reprezentować ten sam zestaw wyników, nawet jeśli dane bazowe się nie zmienią. Prostym sposobem na zademonstrowanie tego jest ustawienie dwóch różnych optymalnych indeksów dla dwóch odwołań, z których jeden porządkuje dane według daty zamówienia ASC, identyfikatora zamówienia ASC, a drugi porządkuje dane według daty zamówienia DESC, identyfikatora zamówienia ASC (lub dokładnie odwrotnie). Mamy już poprzedni indeks. Oto kod do stworzenia tego ostatniego:

CREATE INDEX idx_nc_odD_oid_I_sc
  ON Sales.Orders(orderdate DESC, orderid)
  INCLUDE(shipcountry);

Uruchom kod po raz drugi po utworzeniu indeksu:

WITH C AS
(
  SELECT *, ROW_NUMBER() OVER(ORDER BY orderdate) AS rownum
  FROM Sales.Orders
)
SELECT C1.orderid, C1.shipcountry, C2.orderid
FROM C AS C1
  INNER JOIN C AS C2
    ON C1.rownum = C2.rownum
WHERE C1.orderid <> C2.orderid;

Otrzymałem następujące dane wyjściowe podczas uruchamiania tego kodu po utworzeniu nowego indeksu:

orderid     shipcountry     orderid
----------- --------------- -----------
10251       France          10250
10250       Brazil          10251
10261       Brazil          10260
10260       Germany         10261
10271       USA             10270
...
11070       Germany         11073
11077       USA             11074
11076       France          11075
11075       Switzerland     11076
11074       Denmark         11077

(546 rows affected)

Ups.

Sprawdź plan zapytania dla tego wykonania, jak pokazano na rysunku 3:

Rysunek 3:Drugi plan zapytania z dwoma odniesieniami CTE

Zauważ, że górna gałąź planu skanuje indeks idx_nc_orderdate w uporządkowany sposób, powodując, że operator projektu Sequence, który oblicza numery wierszy, pobiera dane w praktyce uporządkowane według orderdate ASC, orderid ASC. Dolna gałąź planu skanuje nowy indeks idx_nc_odD_oid_I_sc w uporządkowany wsteczny sposób, powodując, że operator projektu Sequence pobiera dane w praktyce uporządkowane według daty zamówienia ASC, id zamówienia DESC. Powoduje to inny układ numerów wierszy dla dwóch odwołań CTE, gdy występuje więcej niż jedno wystąpienie tej samej wartości daty zamówienia. W konsekwencji zapytanie generuje niepusty zestaw wyników.

Jeśli chcesz uniknąć takich błędów, jedną z oczywistych opcji jest utrwalenie wewnętrznego wyniku zapytania w obiekcie tymczasowym, takim jak tabela tymczasowa lub zmienna tabeli. Jeśli jednak masz sytuację, w której wolisz trzymać się CTE, prostym rozwiązaniem jest użycie całkowitego porządku w funkcji okna poprzez dodanie rozstrzygnięcia. Innymi słowy, upewnij się, że porządkujesz według kombinacji wyrażeń, które jednoznacznie identyfikują wiersz. W naszym przypadku możesz po prostu dodać orderid jawnie jako rozstrzygający remis, na przykład:

WITH C AS
(
  SELECT *, ROW_NUMBER() OVER(ORDER BY orderdate, orderid) AS rownum
  FROM Sales.Orders
)
SELECT C1.orderid, C1.shipcountry, C2.orderid
FROM C AS C1
  INNER JOIN C AS C2
    ON C1.rownum = C2.rownum
WHERE C1.orderid <> C2.orderid;

Otrzymasz pusty zestaw wyników zgodnie z oczekiwaniami:

orderid     shipcountry     orderid
----------- --------------- -----------

(0 rows affected)

Bez dodawania dalszych indeksów otrzymujesz plan pokazany na rysunku 4:

Rysunek 4:Trzeci plan dla zapytania z dwoma odniesieniami CTE

Górna gałąź planu jest taka sama, jak w poprzednim planie pokazanym na rysunku 3. Dolna gałąź jest jednak nieco inna. Nowy indeks utworzony wcześniej nie jest tak naprawdę idealny dla nowego zapytania w tym sensie, że nie ma uporządkowanych danych, jakich potrzebuje funkcja ROW_NUMBER (data zamówienia, identyfikator zamówienia). Jest to nadal najwęższy indeks pokrywający, jaki optymalizator mógł znaleźć dla odpowiedniego odniesienia CTE, więc został wybrany; jednak jest skanowany w sposób Ordered:False. Jawny operator Sort sortuje następnie dane według daty zamówienia, identyfikatora zamówienia, tak jak wymaga obliczeń ROW_NUMBER. Oczywiście można zmienić definicję indeksu, aby zarówno data zamówienia, jak i identyfikator zamówienia używały tego samego kierunku, a w ten sposób jawne sortowanie zostanie wyeliminowane z planu. Najważniejsze jest jednak to, że korzystając z całkowitego uporządkowania, unikasz kłopotów z powodu tego konkretnego błędu.

Kiedy skończysz, uruchom następujący kod w celu oczyszczenia:

DROP INDEX IF EXISTS idx_nc_odD_oid_I_sc ON Sales.Orders;

Wniosek

Ważne jest, aby zrozumieć, że wiele odwołań do tego samego CTE z zewnętrznego zapytania skutkuje oddzielnymi ocenami wewnętrznego zapytania CTE. Zachowaj szczególną ostrożność przy obliczeniach niedeterministycznych, ponieważ różne oceny mogą skutkować różnymi wartościami.

Podczas korzystania z funkcji okna, takich jak ROW_NUMBER i agregacji z ramką, upewnij się, że używasz całkowitej kolejności, aby uniknąć uzyskiwania różnych wyników dla tego samego wiersza w różnych odwołaniach CTE.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Podłączanie PowerShell do Salesforce.com

  2. 4 gotowe metody konwersji danych SQL i przypadki użycia

  3. Tworzenie serwera połączonego ODBC bez konfigurowania źródła danych

  4. Usługa migracji bazy danych AWS DMS

  5. ZDLRA – RMAN-20035 nieprawidłowy wysoki RECID