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

Podstawy wyrażeń tabelarycznych, Część 3 – Tablice pochodne, względy optymalizacyjne

W części 1 i 2 tej serii omówiłem ogólnie logiczne lub pojęciowe aspekty nazwanych wyrażeń tabelarycznych, aw szczególności tabel pochodnych. W tym i następnym miesiącu omówię fizyczne aspekty przetwarzania tabel pochodnych. Przypomnij sobie z części 1 niezależność danych fizycznych zasada teorii relacji. Model relacyjny i oparty na nim standardowy język zapytań mają zajmować się tylko konceptualnymi aspektami danych i pozostawić fizyczne szczegóły implementacji, takie jak przechowywanie, optymalizacja, dostęp i przetwarzanie danych do platformy bazy danych (wdrożenie ). W przeciwieństwie do koncepcyjnego przetwarzania danych, które opiera się na modelu matematycznym i standardowym języku, a zatem jest bardzo podobne w różnych systemach zarządzania relacyjnymi bazami danych, fizyczne przetwarzanie danych nie jest oparte na żadnym standardzie, a zatem ma tendencję do być bardzo specyficzne dla platformy. W moim omówieniu fizycznego traktowania nazwanych wyrażeń tabelowych w serii skupiam się na traktowaniu w Microsoft SQL Server i Azure SQL Database. Fizyczne traktowanie na innych platformach baz danych może być zupełnie inne.

Przypomnijmy, że to, co spowodowało tę serię, to pewne zamieszanie, które istnieje w społeczności SQL Server wokół nazwanych wyrażeń tabelowych. Zarówno pod względem terminologicznym, jak i optymalizacyjnym. W pierwszych dwóch częściach serii poruszyłem pewne kwestie terminologiczne, a więcej omówię w przyszłych artykułach, omawiając CTE, widoki i wbudowane TVF. Jeśli chodzi o optymalizację nazwanych wyrażeń tabel, istnieje zamieszanie wokół następujących elementów (wspominam tutaj o tabelach pochodnych, ponieważ na tym skupiamy się w tym artykule):

  • Trwałość: Czy tabela pochodna jest gdzieś utrwalona? Czy jest utrwalany na dysku i jak SQL Server obsługuje dla niego pamięć?
  • Projekcja kolumn: Jak działa dopasowywanie indeksów z tabelami pochodnymi? Na przykład, jeśli tabela pochodna rzutuje określony podzbiór kolumn z jakiejś tabeli bazowej, a zapytanie zewnętrzne rzutuje podzbiór kolumn z tabeli pochodnej, czy SQL Server jest wystarczająco inteligentny, aby określić optymalne indeksowanie na podstawie ostatniego podzbioru kolumn to jest rzeczywiście potrzebne? A co z uprawnieniami; czy użytkownik potrzebuje uprawnień do wszystkich kolumn, do których odwołują się wewnętrzne zapytania, czy tylko do tych ostatnich, które są rzeczywiście potrzebne?
  • Wiele odniesień do aliasów kolumn: Jeśli tabela pochodna zawiera kolumnę wynikową, która jest oparta na obliczeniach niedeterministycznych, np. wywołaniu funkcji SYSDATETIME, a zapytanie zewnętrzne ma wiele odwołań do tej kolumny, czy obliczenia zostaną wykonane tylko raz, czy osobno dla każdego odwołania zewnętrznego ?
  • Rozgnieżdżanie/podstawianie/wstawianie: Czy SQL Server rozpakowuje lub wstawia zapytanie dotyczące tabeli pochodnej? To znaczy, czy SQL Server wykonuje proces podstawienia, w ramach którego konwertuje oryginalny kod zagnieżdżony w jedno zapytanie, które jest kierowane bezpośrednio do tabel podstawowych? A jeśli tak, czy istnieje sposób na nakazanie SQL Serverowi uniknięcia tego procesu rozgnieżdżania?

To wszystko są ważne pytania, a odpowiedzi na te pytania mają znaczący wpływ na wydajność, więc dobrym pomysłem jest dokładne zrozumienie, jak te elementy są obsługiwane w SQL Server. W tym miesiącu zajmę się pierwszymi trzema punktami. Jest wiele do powiedzenia na temat czwartej pozycji, więc poświęcę jej osobny artykuł w przyszłym miesiącu (część 4).

W moich przykładach użyję przykładowej bazy danych o nazwie TSQLV5. Skrypt tworzący i wypełniający TSQLV5 można znaleźć tutaj, a jego diagram ER znajduje się tutaj.

Trwałość

Niektórzy intuicyjnie zakładają, że SQL Server utrwala wynik części wyrażenia tabelowego z tabeli pochodnej (wynik zapytania wewnętrznego) w tabeli roboczej. W dniu pisania tego tekstu tak nie jest; Jednak ponieważ względy dotyczące trwałości są wyborem dostawcy, Microsoft może zdecydować się na zmianę tego w przyszłości. Rzeczywiście, SQL Server jest w stanie utrwalić pośrednie wyniki zapytań w tabelach roboczych (zwykle w tempdb) w ramach przetwarzania zapytań. Jeśli zdecyduje się to zrobić, zobaczysz w planie jakąś formę operatora buforowania (Spool, Eager Spool, Lazy Spool, Table Spool, Index Spool, Window Spool, Row Count Spool). Jednak decyzja SQL Server dotycząca tego, czy buforować coś w tabeli roboczej, czy nie, nie ma obecnie nic wspólnego z użyciem nazwanych wyrażeń tabel w zapytaniu. SQL Server czasami buforuje wyniki pośrednie ze względu na wydajność, na przykład unikanie powtarzania pracy (choć obecnie nie jest to związane z użyciem nazwanych wyrażeń tabel), a czasami z innych powodów, takich jak ochrona przed Halloween.

Jak wspomniałem, w przyszłym miesiącu przejdę do szczegółów rozgnieżdżania tabel pochodnych. Na razie wystarczy powiedzieć, że SQL Server zwykle stosuje proces rozgnieżdżania/wstawiania do tabel pochodnych, w którym zastępuje zagnieżdżone zapytania zapytaniem dotyczącym bazowych tabel bazowych. Cóż, trochę upraszczam. To nie jest tak, że SQL Server dosłownie konwertuje oryginalny ciąg zapytania T-SQL z tabelami pochodnymi na nowy ciąg zapytania bez nich; raczej SQL Server stosuje przekształcenia do wewnętrznego drzewa logicznego operatorów, a rezultatem jest to, że tabele pochodne zazwyczaj nie są zagnieżdżone. Kiedy patrzysz na plan wykonania zapytania obejmującego tabele pochodne, nie widzisz o nich żadnej wzmianki, ponieważ dla większości celów optymalizacji nie istnieją. Widoczny jest dostęp do struktur fizycznych, które przechowują dane dla bazowych tabel podstawowych (sterty, indeksów magazynu wierszy B-drzewa i indeksów magazynu kolumn dla tabel opartych na dysku oraz indeksów drzewa i skrótu dla tabel zoptymalizowanych pod kątem pamięci).

Istnieją przypadki, które uniemożliwiają SQL Server rozpakowanie tabeli pochodnej, ale nawet w tych przypadkach SQL Server nie utrwala wyniku wyrażenia tabeli w tabeli roboczej. Podam szczegóły wraz z przykładami w przyszłym miesiącu.

Ponieważ SQL Server nie utrzymuje tabel pochodnych, a raczej bezpośrednio współdziała ze strukturami fizycznymi, które przechowują dane dla bazowych tabel podstawowych, kwestia obsługi pamięci w tabelach pochodnych jest dyskusyjna. Jeśli bazowe tabele bazowe są oparte na dyskach, ich odpowiednie strony muszą zostać przetworzone w puli buforów. Jeśli bazowe tabele są zoptymalizowane pod kątem pamięci, ich odpowiednie wiersze w pamięci muszą zostać przetworzone. Ale nie różni się to od sytuacji, gdy samodzielnie wykonujesz zapytania do tabel źródłowych bez korzystania z tabel pochodnych. Więc nie ma tu nic specjalnego. W przypadku korzystania z tabel pochodnych SQL Server nie musi uwzględniać w nich żadnych specjalnych kwestii dotyczących pamięci. Dla większości celów optymalizacji zapytań nie istnieją.

Jeśli masz przypadek, w którym musisz zachować jakiś wynik pośredniego kroku w tabeli roboczej, musisz użyć tabel tymczasowych lub zmiennych tabeli — a nie nazwanych wyrażeń tabel.

Projekcja kolumny i słowo na SELECT *

Projekcja jest jednym z pierwotnych operatorów algebry relacyjnej. Załóżmy, że masz relację R1 z atrybutami x, y i z. Rzutowanie R1 na pewien podzbiór jego atrybutów, np. x i z, jest nową relacją R2, której nagłówek jest podzbiorem rzutowanych atrybutów z R1 (w naszym przypadku x i z), a ciałem jest zbiór krotek utworzone z oryginalnej kombinacji przewidywanych wartości atrybutów z krotek R1.

Przypomnijmy, że ciało relacji — będące zbiorem krotek — z definicji nie ma duplikatów. Jest więc rzeczą oczywistą, że krotki relacji wynikowej są odrębną kombinacją wartości atrybutów rzutowanych z oryginalnej relacji. Należy jednak pamiętać, że treść tabeli w SQL jest zbiorem wierszy, a nie zbiorem, i normalnie SQL nie wyeliminuje zduplikowanych wierszy, chyba że zostanie to poinstruowane. Mając tabelę R1 z kolumnami x, y i z, następujące zapytanie może potencjalnie zwrócić zduplikowane wiersze, a zatem nie jest zgodne z semantyką zwracania zbioru operatora projekcji relacyjnej:

SELECT x, zFROM R1;

Dodając klauzulę DISTINCT, eliminujesz zduplikowane wiersze i dokładniej przestrzegasz semantyki projekcji relacyjnej:

WYBIERZ RÓŻNE x, zFROM R1;

Oczywiście istnieją sytuacje, w których wiadomo, że wynik zapytania zawiera odrębne wiersze bez konieczności stosowania klauzuli DISTINCT, np. gdy podzbiór zwracanych kolumn zawiera klucz z tabeli, której dotyczy zapytanie. Na przykład, jeśli x jest kluczem w R1, powyższe dwa zapytania są logicznie równoważne.

W każdym razie przypomnij sobie pytania, o których wspomniałem wcześniej, dotyczące optymalizacji zapytań dotyczących tabel pochodnych i projekcji kolumnowej. Jak działa dopasowywanie indeksów? Jeśli tabela pochodna rzutuje określony podzbiór kolumn z jakiejś tabeli bazowej, a zapytanie zewnętrzne rzutuje podzbiór kolumn z tabeli pochodnej, czy SQL Server jest wystarczająco inteligentny, aby wymyślić optymalne indeksowanie na podstawie końcowego podzbioru kolumn, który jest w rzeczywistości potrzebne? A co z uprawnieniami; czy użytkownik potrzebuje uprawnień do wszystkich kolumn, do których odwołują się wewnętrzne zapytania, czy tylko do tych ostatnich, które są rzeczywiście potrzebne? Załóżmy również, że zapytanie wyrażenia tabelowego definiuje kolumnę wynikową opartą na obliczeniach, ale zapytanie zewnętrzne nie rzutuje tej kolumny. Czy obliczenia są w ogóle oceniane?

Zaczynając od ostatniego pytania, spróbujmy. Rozważ następujące zapytanie:

USE TSQLV5;GO SELECT custid, miasto, 1/0 AS div0errorFROM Sprzedaż.Klienci;

Jak można się spodziewać, to zapytanie kończy się niepowodzeniem z błędem dzielenia przez zero:

Msg 8134, poziom 16, stan 1
Wystąpił błąd dzielenia przez zero.

Następnie zdefiniuj tabelę pochodną o nazwie D opartą na powyższym zapytaniu, a w zewnętrznym zapytaniu projektu D tylko na podstawie custid i city, na przykład:

SELECT custid, miastoFROM ( SELECT custid, miasto, 1/0 AS div0error FROM Sales.Customers ) AS D;

Jak wspomniano, SQL Server zwykle stosuje rozgnieżdżanie/podstawianie, a ponieważ w tym zapytaniu nie ma nic, co wstrzymuje rozgnieżdżanie (więcej na ten temat w następnym miesiącu), powyższe zapytanie jest równoważne następującemu zapytaniu:

SELECT custid, cityFROM Sprzedaż.Klienci;

Znowu nieco upraszczam. Rzeczywistość jest nieco bardziej złożona niż te dwa zapytania, które uważa się za naprawdę identyczne, ale zajmę się tymi zawiłościami w przyszłym miesiącu. Chodzi o to, że wyrażenie 1/0 nie pojawia się nawet w planie wykonania zapytania i nie jest w ogóle oceniane, więc powyższe zapytanie działa pomyślnie bez błędów.

Mimo to wyrażenie tabelowe musi być poprawne. Rozważmy na przykład następujące zapytanie:

WYBIERZ krajFROM ( WYBIERZ * FROM Sprzedaż.Klienci GRUPA WG kraju ) AS D;

Mimo że zapytanie zewnętrzne rzutuje tylko kolumnę z zestawu grupowania zapytania wewnętrznego, zapytanie wewnętrzne nie jest prawidłowe, ponieważ próbuje zwrócić kolumny, które nie są częścią zestawu grupowania ani nie są zawarte w funkcji agregującej. To zapytanie kończy się niepowodzeniem z następującym błędem:

Komunikat 8120, poziom 16, stan 1
Kolumna „Sales.Customers.custid” jest nieprawidłowa na liście wyboru, ponieważ nie jest zawarta ani w funkcji agregującej, ani w klauzuli GROUP BY.

Następnie zajmijmy się pytaniem o dopasowanie indeksu. Jeśli zapytanie zewnętrzne rzutuje tylko podzbiór kolumn z tabeli pochodnej, czy SQL Server będzie wystarczająco inteligentny, aby wykonać dopasowywanie indeksu na podstawie tylko zwróconych kolumn (i oczywiście wszelkich innych kolumn, które w przeciwnym razie odgrywają znaczącą rolę, na przykład filtrowanie, grupowanie i tak dalej)? Ale zanim zajmiemy się tym pytaniem, możesz się zastanawiać, dlaczego w ogóle się tym przejmujemy. Dlaczego miałbyś mieć wewnętrzne kolumny zwracające zapytanie, których nie potrzebuje zapytanie zewnętrzne?

Odpowiedź jest prosta, aby skrócić kod, używając do wewnętrznego zapytania niesławnego SELECT *. Wszyscy wiemy, że użycie SELECT * to zła praktyka, ale dzieje się tak głównie wtedy, gdy jest używane w najbardziej zewnętrznym zapytaniu. Co się stanie, jeśli wyślesz zapytanie do tabeli z określonym nagłówkiem, a później ten nagłówek zostanie zmieniony? Aplikacja może skończyć się błędami. Nawet jeśli nie skończysz z błędami, możesz wygenerować niepotrzebny ruch sieciowy, zwracając kolumny, których aplikacja tak naprawdę nie potrzebuje. Ponadto w takim przypadku wykorzystujesz indeksowanie mniej optymalnie, ponieważ zmniejszasz szanse na dopasowanie indeksów pokrywających, które są oparte na naprawdę potrzebnych kolumnach.

To powiedziawszy, w rzeczywistości czuję się całkiem komfortowo, używając SELECT * w wyrażeniu tabelowym, wiedząc, że i tak zamierzam rzutować tylko naprawdę potrzebne kolumny w najbardziej zewnętrznym zapytaniu. Z logicznego punktu widzenia jest to całkiem bezpieczne z kilkoma drobnymi zastrzeżeniami, do których wkrótce przejdę. Tak długo, jak w takim przypadku dopasowanie indeksów odbywa się optymalnie, i to dobra wiadomość.

Aby to zademonstrować, załóżmy, że musisz wykonać zapytanie do tabeli Sales.Orders, zwracając trzy najnowsze zamówienia dla każdego klienta. Planujesz zdefiniować tabelę pochodną o nazwie D na podstawie kwerendy obliczającej numery wierszy (kolumna wynikowa rownum), które są podzielone na partycje według custid i uporządkowane według orderdate DESC, orderid DESC. Zewnętrzne zapytanie zostanie odfiltrowane z D (relacyjne ograniczenie ) tylko wiersze, w których rownum jest mniejsze lub równe 3, a projekt D na custid, orderdate, orderid i rownum. Teraz Sales.Orders ma więcej kolumn niż te, które musisz zaprojektować, ale dla zwięzłości chcesz, aby zapytanie wewnętrzne używało SELECT * plus obliczenia numeru wiersza. Jest to bezpieczne i będzie optymalnie obsługiwane pod względem dopasowania indeksów.

Użyj poniższego kodu, aby utworzyć optymalny indeks pokrywający do obsługi zapytania:

UTWÓRZ INDEKS idx_custid_odD_oidD ON Sales.Orders(custid, data zamówienia DESC, identyfikator zamówienia DESC);

Oto zapytanie, które archiwizuje bieżące zadanie (nazwiemy je Zapytanie 1):

SELECT custid, datazam 

Zwróć uwagę na SELECT * wewnętrznego zapytania i jawną listę kolumn zapytania zewnętrznego.

Plan dla tego zapytania, renderowany przez SentryOne Plan Explorer, pokazano na rysunku 1.

Rysunek 1:Plan dla zapytania 1

Zauważ, że jedynym indeksem używanym w tym planie jest optymalny indeks pokrycia, który właśnie utworzyłeś.

Jeśli podświetlisz tylko wewnętrzne zapytanie i przeanalizujesz jego plan wykonania, zobaczysz użyty indeks klastrowy tabeli, po którym nastąpi operacja sortowania.

To dobra wiadomość.

Jeśli chodzi o uprawnienia, to już inna historia. W przeciwieństwie do dopasowywania indeksów, w którym indeks nie musi zawierać kolumn, do których odwołują się zapytania wewnętrzne, o ile ostatecznie nie są one potrzebne, wymagane jest posiadanie uprawnień do wszystkich kolumn, do których się odwołują.

Aby to zademonstrować, użyj następującego kodu, aby utworzyć użytkownika o nazwie user1 i przypisz niektóre uprawnienia (uprawnienia SELECT do wszystkich kolumn z Sales.Customers i tylko do trzech kolumn z Sales.Orders, które są ostatecznie istotne w powyższym zapytaniu):

CREATE USER user1 BEZ LOGOWANIA; GRANT SHOWPLAN użytkownikowi1; GRANT SELECT ON Sales.Customers TO user1; GRANT SELECT ON Sales.Orders(custid, orderdate, orderid) TO user1;

Uruchom następujący kod, aby podszyć się pod użytkownika 1:

WYKONAJ JAKO UŻYTKOWNIK ='użytkownik1';

Spróbuj wybrać wszystkie kolumny z Sales.Orders:

WYBIERZ * Z Sales.Orders;

Zgodnie z oczekiwaniami otrzymujesz następujące błędy z powodu braku uprawnień w niektórych kolumnach:

Msg 230, Level 14, State 1
Odmowa uprawnienia SELECT w kolumnie 'empid' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Wiadomość 230 , Poziom 14, Stan 1
Odmowa uprawnienia SELECT w kolumnie 'requireddate' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, Stan 1
Odmowa uprawnienia SELECT w kolumnie 'shippeddate' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, Stan 1
Odmowa uprawnienia SELECT w kolumnie 'shipperid' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, State 1
Uprawnienie SELECT zostało odrzucone w kolumnie „fracht” obiektu „Zamówienia”, baza danych „TSQLV5”, schemat „Sprzedaż”.

Wiadomość 230, poziom 14, stan 1
Odmowa uprawnienia SELECT w kolumnie „shipname” obiektu „Orders”, bazy danych „TSQLV5”, schematu „Sales”.

Wiadomość 230, poziom 14, stan 1
Odmowa uprawnienia SELECT w kolumnie „shipaddress” obiektu „Orders”, bazy danych „TSQLV5”, schematu „Sales”.

Wiadomość 230, poziom 14, stan 1
Odmówiono uprawnienia SELECT w kolumnie 'shipcity' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Wiadomość 230, Poziom 14, Stan 1
SELECT odmówiono uprawnień do kolumny 'shipregion' obiektu 'Orders', bazy danych 'TSQLV5', schematu 'Sales'.

Msg 230, Level 14, State 1
Uprawnienie SELECT było odmowa w kolumnie 'shippostalcode' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, State 1
Uprawnienie SELECT zostało odrzucone w dniu kolumna „shipcountry” obiektu „Orders”, baza danych „TSQLV5”, schemat „Sales”.

Wypróbuj następujące zapytanie, projekcja i interakcja tylko z kolumnami, do których użytkownik1 ma uprawnienia:

SELECT custid, datazam 

Mimo to otrzymujesz błędy uprawnień do kolumn z powodu braku uprawnień w niektórych kolumnach, do których odwołuje się wewnętrzne zapytanie poprzez jego SELECT *:

Msg 230, Level 14, State 1
Odmowa uprawnienia SELECT w kolumnie 'empid' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Wiadomość 230 , Poziom 14, Stan 1
Odmowa uprawnienia SELECT w kolumnie 'requireddate' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, Stan 1
Odmowa uprawnienia SELECT w kolumnie 'shippeddate' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, Stan 1
Odmowa uprawnienia SELECT w kolumnie 'shipperid' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, State 1
Uprawnienie SELECT zostało odrzucone w kolumnie „fracht” obiektu „Zamówienia”, baza danych „TSQLV5”, schemat „Sprzedaż”.

Wiadomość 230, poziom 14, stan 1
Odmowa uprawnienia SELECT w kolumnie „shipname” obiektu „Orders”, bazy danych „TSQLV5”, schematu „Sales”.

Wiadomość 230, poziom 14, stan 1
Odmowa uprawnienia SELECT w kolumnie „shipaddress” obiektu „Orders”, bazy danych „TSQLV5”, schematu „Sales”.

Wiadomość 230, poziom 14, stan 1
Odmówiono uprawnienia SELECT w kolumnie 'shipcity' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Wiadomość 230, Poziom 14, Stan 1
SELECT odmówiono uprawnień do kolumny 'shipregion' obiektu 'Orders', bazy danych 'TSQLV5', schematu 'Sales'.

Msg 230, Level 14, State 1
Uprawnienie SELECT było odmowa w kolumnie 'shippostalcode' obiektu 'Orders', baza danych 'TSQLV5', schemat 'Sales'.

Msg 230, Level 14, State 1
Uprawnienie SELECT zostało odrzucone w dniu kolumna „shipcountry” obiektu „Orders”, baza danych „TSQLV5”, schemat „Sales”.

Jeśli rzeczywiście w Twojej firmie praktyką jest przypisywanie użytkownikom uprawnień tylko do odpowiednich kolumn, z którymi muszą wchodzić w interakcję, sensowne byłoby użycie nieco dłuższego kodu i wyraźne określenie listy kolumn zarówno w zapytaniach wewnętrznych, jak i zewnętrznych. tak:

SELECT custid, datazam /pre> 

Tym razem zapytanie działa bez błędów.

Inną odmianą, która wymaga, aby użytkownik miał uprawnienia tylko do odpowiednich kolumn, jest wyraźne określenie nazw kolumn na liście SELECT wewnętrznego zapytania i użycie SELECT * w zapytaniu zewnętrznym, na przykład:

SELECT *FROM ( SELECT custid, datazam 

To zapytanie również działa bez błędów. Jednak widzę tę wersję jako podatną na błędy na wypadek, gdyby później dokonano pewnych zmian na jakimś wewnętrznym poziomie zagnieżdżenia. Jak wspomniano wcześniej, najlepszą praktyką jest dla mnie jednoznaczne określenie listy kolumn w najbardziej zewnętrznym zapytaniu. Tak więc, o ile nie masz żadnych obaw dotyczących braku uprawnień w niektórych kolumnach, czuję się komfortowo z SELECT * w zapytaniach wewnętrznych, ale jawną listą kolumn w zapytaniu zewnętrznym. Jeśli stosowanie określonych uprawnień do kolumn jest powszechną praktyką w firmie, najlepiej jest po prostu wyraźnie określić nazwy kolumn na wszystkich poziomach zagnieżdżenia. Pamiętaj, że wyraźne określenie nazw kolumn na wszystkich poziomach zagnieżdżenia jest w rzeczywistości obowiązkowe, jeśli zapytanie jest używane w obiekcie powiązanym ze schematem, ponieważ powiązanie schematu uniemożliwia użycie SELECT * w dowolnym miejscu zapytania.

W tym momencie uruchom następujący kod, aby usunąć indeks utworzony wcześniej w Sales.Orders:

DROP INDEKSU, JEŚLI ISTNIEJE idx_custid_odD_oidD ON Sales.Orders;

Jest jeszcze inny przypadek z podobnym dylematem dotyczącym zasadności użycia SELECT *; w wewnętrznym zapytaniu predykatu EXISTS.

Rozważ następujące zapytanie (nazwiemy je Zapytanie 2):

SELECT custidFROM Sales.Customers AS CWHERE EXISTS (SELECT * FROM Sales.Orders AS O WHERE O.custid =C.custid);

Plan dla tego zapytania pokazano na rysunku 2.

Rysunek 2:Plan dla zapytania 2

Podczas stosowania dopasowywania indeksów optymalizator stwierdził, że indeks idx_nc_custid jest indeksem pokrywającym Sales.Orders, ponieważ zawiera kolumnę custid — jedyną prawdziwie odpowiednią kolumnę w tym zapytaniu. Dzieje się tak pomimo faktu, że ten indeks nie zawiera żadnej innej kolumny poza custid, a wewnętrzne zapytanie w predykacie EXISTS mówi SELECT *. Jak dotąd zachowanie wydaje się podobne do użycia SELECT * w tabelach pochodnych.

To, co różni to zapytanie, to to, że działa ono bez błędów, mimo że użytkownik1 nie ma uprawnień do niektórych kolumn z Sales.Orders. Istnieje argument uzasadniający nie wymaganie uprawnień we wszystkich kolumnach w tym miejscu. W końcu predykat EXISTS musi tylko sprawdzić, czy istnieją pasujące wiersze, więc lista SELECT wewnętrznego zapytania jest naprawdę bez znaczenia. Prawdopodobnie byłoby najlepiej, gdyby SQL w ogóle nie wymagał listy SELECT w takim przypadku, ale ten statek już odpłynął. Dobrą wiadomością jest to, że lista SELECT jest skutecznie ignorowana — zarówno pod względem dopasowania indeksów, jak i wymaganych uprawnień.

Wydaje się również, że istnieje inna różnica między tabelami pochodnymi a tabelami EXISTS podczas używania SELECT * w zapytaniu wewnętrznym. Zapamiętaj to zapytanie z wcześniejszego artykułu:

WYBIERZ krajFROM ( WYBIERZ * FROM Sprzedaż.Klienci GRUPA WG kraju ) AS D;

Jeśli pamiętasz, ten kod wygenerował błąd, ponieważ wewnętrzne zapytanie jest nieprawidłowe.

Wypróbuj to samo zapytanie wewnętrzne, tylko tym razem w predykacie EXISTS (nazwiemy to oświadczenie 3):

JEŚLI ISTNIEJE ( WYBIERZ * Z GRUPA GRUPY GRUPY KLIENTÓW WEDŁUG KRAJU ) DRUKUJ 'To działa! Dzięki Dmitrimu Korotkevitchowi za wskazówkę!';

Co dziwne, SQL Server uważa ten kod za poprawny i działa pomyślnie. Plan dla tego kodu pokazano na rysunku 3.

Rysunek 3:Plan dla instrukcji 3

Ten plan jest identyczny z planem, który otrzymałbyś, gdyby wewnętrzne zapytanie było po prostu SELECT * FROM Sales.Customers (bez GROUP BY). W końcu sprawdzasz istnienie grup, a jeśli są wiersze, to naturalnie są też grupy. W każdym razie uważam, że fakt, że SQL Server uznaje to zapytanie za prawidłowe, jest błędem. Z pewnością kod SQL powinien być poprawny! Ale rozumiem, dlaczego niektórzy mogą twierdzić, że lista SELECT w zapytaniu EXISTS ma być ignorowana. W każdym razie plan korzysta z sondowanego lewego sprzężenia częściowego, które nie musi zwracać żadnych kolumn, a jedynie sonduje tabelę, aby sprawdzić istnienie jakichkolwiek wierszy. Indeks klientów może być dowolnym indeksem.

W tym momencie możesz uruchomić następujący kod, aby zatrzymać podszywanie się pod użytkownika 1 i usunąć go:

COFNIJ; DROP USER IF EXISTS użytkownik1;

Wracając do tego, że uważam za wygodną praktykę używanie SELECT * na wewnętrznych poziomach zagnieżdżania, im więcej masz poziomów, tym bardziej ta praktyka skraca i upraszcza twój kod. Oto przykład z dwoma poziomami zagnieżdżenia:

SELECT id zamówienia, rok zamówienia, custid, empid, shipperidFROM ( SELECT *, DATEFROMPARTS(rok zamówienia, 12, 31) AS na koniec roku FROM ( SELECT *, YEAR(data zamówienia) AS rok zamówienia FROM Sales.Orders ) AS D1 ) AS D2WHERE datazamowienia =koniec roku;

Są przypadki, w których ta praktyka nie może być zastosowana. Na przykład, gdy wewnętrzne zapytanie łączy tabele o wspólnych nazwach kolumn, jak w poniższym przykładzie:

SELECT custid, nazwa firmy, datazam Sales.Orders AS O ON C.custid =O.custid ) AS DWHERE rownum <=3;

Zarówno Sales.Customers, jak i Sales.Orders mają kolumnę o nazwie custid. Aby zdefiniować tabelę pochodną D, używasz wyrażenia tabelowego opartego na sprzężeniu między dwiema tabelami. Pamiętaj, że nagłówek tabeli to zestaw kolumn, a jako zestaw nie możesz mieć zduplikowanych nazw kolumn. Dlatego to zapytanie kończy się niepowodzeniem z następującym błędem:

Msg 8156, poziom 16, stan 1
Kolumna „custid” została określona wiele razy dla „D”.

W tym przypadku musisz wyraźnie określić nazwy kolumn w zapytaniu wewnętrznym i upewnić się, że zwracasz custid tylko z jednej z tabel lub przypisujesz unikatowe nazwy kolumn do kolumn wynikowych w przypadku, gdy chcesz zwrócić obie. Częściej używałbyś pierwszego podejścia, na przykład:

SELECT custid, nazwa firmy, data zamówienia, identyfikator zamówienia, rownumFROM ( SELECT C.custid, C.nazwafirmy, O.datazamówienia, O.idzamówienia, ROW_NUMBER() OVER(PARTITION BY C.custid ORDER BY O.orderdate DESC, O. orderid DESC) AS rownum FROM Sales.Customers AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS DWHERE rownum <=3;

Ponownie, możesz podać wyraźne nazwy kolumn w zapytaniu wewnętrznym i użyć SELECT * w zapytaniu zewnętrznym, na przykład:

SELECT *FROM ( SELECT C.custid, C.nazwafirmy, O.orderdate, O.orderid, ROW_NUMBER() OVER(PARTITION BY C.custid ORDER BY O.orderdate DESC, O.orderid DESC) AS rownum FROM Sales .Klienci AS C LEFT OUTER JOIN Sales.Orders AS O ON C.custid =O.custid ) AS DWHERE rownum <=3;

Ale jak wspomniałem wcześniej, uważam, że złą praktyką jest nieopisywanie nazw kolumn w najbardziej zewnętrznym zapytaniu.

Wiele odniesień do aliasów kolumn

Przejdźmy do następnego elementu — wielu odwołań do kolumn tabeli pochodnej. Jeśli tabela pochodna zawiera kolumnę wynikową opartą na obliczeniach niedeterministycznych, a zapytanie zewnętrzne ma wiele odwołań do tej kolumny, czy obliczenia będą oceniane tylko raz czy osobno dla każdego odwołania?

Zacznijmy od tego, że wielokrotne odwołania do tej samej niedeterministycznej funkcji w zapytaniu mają być oceniane niezależnie. Rozważ następujące zapytanie jako przykład:

WYBIERZ NEWID() AS mojnwid1, NEWID() AS mojnwid2;

Ten kod generuje następujące dane wyjściowe pokazujące dwa różne identyfikatory GUID:

mojnewid1 mojnewid2------------------------------------ --------- ---------------------------7BF389EC-082F-44DA-B98A-DB85CD095506 EA1EFF65-B2E4-4060-9592-7116F674D406

I odwrotnie, jeśli masz tabelę pochodną z kolumną, która jest oparta na obliczeniach niedeterministycznych, a zapytanie zewnętrzne ma wiele odwołań do tej kolumny, obliczenia powinny być oceniane tylko raz. Rozważ następujące zapytanie (nazwiemy to zapytanie 4):

SELECT mynewid AS mynewid1, mynewid AS mynewid2FROM ( SELECT NEWID() AS mynewid ) AS D;

Plan dla tego zapytania pokazano na rysunku 4.

Rysunek 4:Plan dla zapytania 4

Zauważ, że w planie jest tylko jedno wywołanie funkcji NEWID. W związku z tym dane wyjściowe pokazują ten sam identyfikator GUID dwukrotnie:

mojnewid1 mojnewid2------------------------------------ --------- ---------------------------296A80C9-260A-47F9-9EB1-C2D0C401E74A 296A80C9-260A-47F9-9EB1-C2D0C401E74A

Tak więc powyższe dwa zapytania nie są logicznie równoważne i są przypadki, w których nie ma wstawiania/podstawiania.

W przypadku niektórych funkcji niedeterministycznych nieco trudniej jest zademonstrować, że wiele wywołań w zapytaniu jest obsługiwanych oddzielnie. Weźmy jako przykład funkcję SYSDATETIME. Ma precyzję 100 nanosekund. Jakie są szanse, że zapytanie, takie jak poniższe, pokaże dwie różne wartości?

SELECT SYSDATETIME() AS mydt1, SYSDATETIME() AS mydt2;

Jeśli się nudzisz, możesz wielokrotnie naciskać F5, aż to się stanie. Jeśli masz ważniejsze rzeczy do zrobienia w swoim czasie, możesz chcieć uruchomić pętlę, na przykład:

DECLARE @i AS INT =1; WHILE EXISTS( SELECT * FROM ( SELECT SYSDATETIME() AS mojdt1, SYSDATETIME() AS mojdt2 ) AS D WHERE mojdt1 =mojdt2 ) SET @i +=1; PRINT @i;

Na przykład, kiedy uruchomiłem ten kod, otrzymałem 1971.

Jeśli chcesz mieć pewność, że funkcja niedeterministyczna jest wywoływana tylko raz i polegać na tej samej wartości w wielu odwołaniach do zapytań, upewnij się, że definiujesz wyrażenie tabeli z kolumną opartą na wywołaniu funkcji i masz wiele odwołań do tej kolumny z zewnętrznego zapytania, tak jak to (nazwiemy to zapytanie 5):

SELECT mydt AS mydt1, mydt AS mydt1FROM ( SELECT SYSDATETIME() AS mydt ) AS D;

Plan dla tego zapytania pokazano na rysunku 5.

Rysunek 5:Plan dla zapytania 5

Zauważ w planie, że funkcja jest wywoływana tylko raz.

Now this could be a really interesting exercise in patients to hit F5 repeatedly until you get two different values. The good news is that a vaccine for COVID-19 will be found sooner.

You could of course try running a test with a loop:

DECLARE @i AS INT =1; WHILE EXISTS ( SELECT * FROM (SELECT mydt AS mydt1, mydt AS mydt2 FROM ( SELECT SYSDATETIME() AS mydt ) AS D1) AS D2 WHERE mydt1 =mydt2 ) SET @i +=1; PRINT @i;

You can let it run as long as you feel that it’s reasonable to wait, but of course it won’t stop on its own.

Understanding this, you will know to avoid writing code such as the following:

SELECT CASE WHEN RAND() <0.5 THEN STR(RAND(), 5, 3) + ' is less than half.' ELSE STR(RAND(), 5, 3) + ' is at least half.' END;

Because occasionally, the output will not seem to make sense, e.g.,

0.550 is less than half.
For more on evaluation within a CASE expression, see the section "Expressions can be evaluated more than once" in Aaron Bertrand's post, "Dirty Secrets of the CASE Expression."

Instead, you should either store the function’s result in a variable and then work with the variable or, if it needs to be part of a query, you can always work with a derived table, like so:

SELECT CASE WHEN rnd <0.5 THEN STR(rnd, 5, 3) + ' is less than half.' ELSE STR(rnd, 5, 3) + ' is at least half.' ENDFROM ( SELECT RAND() AS rnd ) AS D;

Podsumowanie

In this article I covered some aspects of the physical processing of derived tables.

When the outer query projects only a subset of the columns of a derived table, SQL Server is able to apply efficient index matching based on the columns in the outermost SELECT list, or that play some other meaningful role in the query, such as filtering, grouping, ordering, and so on. From this perspective, if for brevity you prefer to use SELECT * in inner levels of nesting, this will not negatively affect index matching. However, the executing user (or the user whose effective permissions are evaluated), needs permissions to all columns that are referenced in inner levels of nesting, even those that eventually are not really relevant. An exception to this rule is the SELECT list of the inner query in an EXISTS predicate, which is effectively ignored.

When you have multiple references to a nondeterministic function in a query, the different references are evaluated independently. Conversely, if you encapsulate a nondeterministic function call in a result column of a derived table, and refer to that column multiple times from the outer query, all references will rely on the same function invocation and get the same values.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Zestaw problemów 1 – Identyfikacja jednostek

  2. Migracje danych

  3. Jak używać „Lubię to” w SQL

  4. Błędy, pułapki i najlepsze praktyki T-SQL – przyłącza

  5. Minimalizowanie wpływu DBCC CHECKDB:DOS i DONT