SELECT Explanation, Example FROM Pro.Knowledge
FacebookRSS

Zapytania do źródeł zewnętrznych

W dzisiejszych czasach, rzadko można spotkać komercyjny system będący „samotną wyspą”. Umiejętność sięgania do danych z różnych serwerów czy plików jest niezwykle przydatna w codziennej pracy.

W ramach tego kursu umieściłem kilka, moim zdaniem obowiązkowych, artykułów dotyczących możliwości pisania zapytań SQL do źródeł zewnętrznych.


Pisanie kwerend rozproszonych

W każdej większej firmie z którą współpracowałem, działało komercyjnie wiele różnych aplikacji. Pracowały one często w diametralnie innych środowiskach (Windows / Unix). Zazwyczaj na dedykowanych platformach sprzętowych czy maszynach wirtualnych.

Dzieje się tak zazwyczaj w większych korporacjach czy podmiotach o bogatej historii informatycznej. Systemy są mocno zróżnicowane, wiekowe aplikacje działają obok najnowszych wdrożeń. Krytyczne są separowane od mniej ważnych.
Jest to także konsekwencja stosowania dedykowanych platform serwerowych, dostarczanych „w pakiecie” przez producentów sprzętu czy aplikacji.

Zapytania rozproszone w SQL Server

Bardzo ważną cechą jest także możliwość integracji z systemami zewnętrznymi – np. bazami Dostawców czy Klientów. Muszą istnieć metody, swobodnej wymiany informacji np. za pomocą plików XML czy usług sieciowych.

Stąd potrzeba pobierania danych z wielu źródeł na jednej platformie. W szczególności, możliwość pisania zapytań SQL do serwerów rozproszonych lub plików z danymi.


Metody dostępu do zewnętrznych danych

W SQL Server mamy szereg mechanizmów, pozwalających na sięganie do danych poza instancję z której wykonujemy zapytania. Przypomnę, że na jednej fizycznej maszynie, może być zainstalowanych wiele instancji SQL Server. Wtedy sięganie do innych np. starszych wersji serwera, rozpatrujemy również jako kwerendy rozproszone.

Pisanie kwerend rozproszonych to przeważnie część procesów ETL (Extract, Transform & Load), związanych np. z zasilaniem hurtowni danych, replikacją czy wymianą danych między serwerami. Dedykowanym środowiskiem do tworzenia tego typu procesów jest Integration Services (dostępne w edycji Standard i wyższych).

W rozdziale tym skupiamy się na metodach uniwersalnych, bezpośredniego dostępu do danych z poziomu pojedynczej kwerendy SQL. Dostępnych również w wersjach darmowych – Express.

Najważniejsze i najbardziej praktyczne z nich, opisałem w trzech dedykowanych artykułach :


Podsumowanie

Każda z obecnych na rynku, liczących się platform bazodanowych tj. SQL Server, Oracle, MySQL, Postgre czy DB2, jest w stanie obsłużyć zdecydowaną większość wdrażanych aplikacji. Myślę, że jedną z ważniejszych cech dobrego produktu, jest prostota, elastyczność jego integracji i możliwość pracy w mocno zróżnicowanym środowisku. W SQL Server stosunkowo prosto można ten cel osiągnąć za pomocą opisanych powyżej metod oraz szerokich możliwości jakie dają paczki SSIS (SQL Server Integration Services).

Podzapytania w SQL

Język SQL, pomimo ściśle ustalonego szyku bloków logicznych (SELECT, FROM, WHERE…. ) jest dość elastyczny. W rozdziałach dotyczących elementów składniowych zapytań, do znudzenia podkreślałem fundament na którym zbudowane są relacyjne bazy danych i SQL. Jest to matematyczna teoria zbiorów. Podzapytania idealnie obrazują te zasady w praktyce i są często stosowanymi konstrukcjami.

Podzapytania, jak sama nazwa wskazuje, są częścią podrzędną innego zapytania. Możemy podzielić je na dwie kategorie ze względu na powiązanie z kwerendą nadrzędną :

  • niezależne – funkcjonować mogą w całkowicie oderwanym kontekście. Można je uruchomić jako osobne kwerendy – o nich właśnie jest ten artykuł.
  • skorelowane – są bezpośrednio powiązane z zapytaniem nadrzędnym. Opisuję je w kolejnym rozdziale tego kursu.

Podzapytania niezależne

Przypomnijmy podstawowe zasady, które intuicyjnie nakierowują na właściwie tory jeśli chodzi o temat podzapytań. Jeśli przerabiasz ten kurs od początku, reguły te powinny być dla Ciebie już oczywiste.

Każde zapytanie SQL to operacja na zbiorze lub zbiorach elementów. Tabele i widoki do których zazwyczaj odwołujemy się w kwerendach to tylko przykłady zbiorów. Wynikiem działania dowolnej kwerendy jest także zbiór.

Rozważmy na przykład zapytanie do zbioru elementów (tabeli) dbo.Customers. Wybierzemy tylko takie elementy (rekordy), dla których wartość atrybutu (kolumny) City, jest równa ciągowi znaków ‘London’.

USE Northwind
GO
 
SELECT CompanyName, City, Country 
FROM dbo.Customers
WHERE City = 'London'

Podzapytania_SQL_01

To co chciałem na tym banalnym przykładzie podkreślić, to fakt że zapytanie, odwołuje się do zbioru (lub zbiorów) i zwraca jeden zbiór. Skoro więc zwraca zbiór, to możemy ten zbiór także odpytywać jak zwykłą tabelę. Będzie to pierwszy przykład z wykorzystaniem typowego podzapytania niezależnego we FROM :

SELECT *
FROM 
(
	-- wstępna, selekcja elementów i atrybutów zbioru dbo.Customers
	-- może tu być dowolna skomplikowana kwerenda.
	SELECT CompanyName, City, Country 	FROM dbo.Customers	where City = 'London' 
) AS MojePodzapytanie
WHERE CompanyName like '[A-C]%'

Podzapytania_SQL_02

Zauważ, że w każdej chwili możesz to podzapytanie uruchomić zaznaczając tylko jego zakres. Jest ono niezależne w stosunku do zapytania zewnętrznego. Wykonane zostanie raz, w trakcie całego procesu logicznego przetwarzania tej kwerendy.

Każdy zbiór do którego odnosimy się we FROM musi być nazwany i w pełni określony. Stąd konieczność stosowania aliasów oraz unikalnych nazw kolumn w ramach podzapytań.

Miejsca w których możemy stosować podzapytania

Podzapytania możemy stosować praktycznie w dowolnym bloku logicznym kwerendy. Jedynym ograniczeniem jest rodzaj zwracanego zbioru. Musi pasować do miejsca w którym chcemy go użyć. Na przykład we FROM, może to być dowolny zbiór (jednoelementowy, wieloelementowy itd), z kolei w SELECT musi to być wartość skalarna czyli zbiór jednoelementowy opisany jednym atrybutem.

W dalszych przykładach, będę wykorzystywał bazę testową AdventureWorks2008, aby zaprezentować typowe zastosowania podzapytań w różnych miejscach kwerendy.

Pobierzemy informacje o zleceniach z czerwca 2014, z rejonu Wielkiej Brytanii (CountryRegionCode = ‘GB’), dla których wartość (TotalDue), przekroczyła średnią liczoną dla wszystkich zleceń.

Zacznijmy od kwerendy, która zwróci nam informacje o średniej wartości dla wszystkich zamówień.

USE AdventureWorks2008
GO
 
SELECT AVG(TotalDue) as AVG_TotalDue
FROM [Sales].[SalesOrderHeader]

Podzapytania_SQL_03

Zwracany zbiór jest szczególny. Jednoelementowy, opisany jednym atrybutem (jedną kolumną) – czyli to zwykła wartość skalarna.

Taki zbiór możemy umieścić w każdym miejscu kwerendy – jako podzapytanie. Najczęściej będziemy go stosować w warunkach WHERE lub w SELECT. Może być też stosowany w innych miejscach gdzie tworzymy wyrażenia, filtracji grup w HAVING czy warunki złączeń w ON.

Wykorzystajmy teraz te informacje, aby odfiltrować rekordy w WHERE i dodatkowo wyświetlić ją w SELECT jako wartość dodatkowej kolumny.

SELECT SalesOrderID, OrderDate, TotalDue, st.Name AS TerritoryName,   
	(  
               -- podzapytanie w SELECT – średnia dla wszystkich zleceń
	     SELECT AVG(TotalDue)  	     FROM [Sales].[SalesOrderHeader]  	) AS AVG_TotalDue
FROM [Sales].[SalesOrderHeader] soh 
     inner join [Sales].[SalesTerritory] st ON soh.TerritoryID = st.TerritoryID
WHERE st.CountryRegionCode = 'GB' and OrderDate between '2004-06-01' and '2004-06-30'  
      and  TotalDue >= 
          (
             -- podzapytanie w filtracji w WHERE
             SELECT AVG(TotalDue) AS  AVG_TotalDue             FROM [Sales].[SalesOrderHeader]            )

Podzapytania_SQL_04

Warto podkreślić, że jeśli podzapytanie nie zwróciłoby tu żadnego rekordu, to wynikiem w kwerendzie zewnętrznej, będzie jeden element opisany NULLami. Trzeba mieć to na uwadze bo jeśli zdarzyłaby się taka sytuacja w podzapytaniu w WHERE – to otrzymamy pusty zbiór. Żaden z rekordów nie spełni przecież warunku TotalDue >= NULL. Każde porównanie z NULL to wartość nieznana, więc każdy rekord będzie odfiltrowany.

Usystematyzujmy dotychczasowe informacje. W SELECT może znaleźć się tylko takie podzapytanie, które zwraca wartość skalarną. We FROM możemy wykorzystać każde podzapytanie, definiujące jakikolwiek zbiór. Tworzenie warunków połączeń w ON , wyrażeń w WHERE oraz filtracji grup w HAVING, dopuszcza różne zbiorów w zależności od zastosowanych operatorów. Standardowo będą to operatory porównujące wartości skalarne ( =, <, >, <>, itd.) – wtedy tylko takie podzapytania, które zwracają skalar.

Są też specjalne operatory działające na zbiorach np. IN, ANY (SOME) , ALL. Operatory te, działają na wektorze wartości. Wektor to zbiór elementów opisanych jednym atrybutem (czyli wartości skalarnych). Zatem w tych przypadkach, podzapytania mogą zwracać wektor.

Pozostał jeszcze jeden specjalny operator – EXISTS / NOT EXISTS, który możemy stosować np. w WHERE. Za jego pomocą sprawdzamy tylko czy zbiór podzapytania jest pusty czy nie. W tym przypadku nie ma znaczenia jakiego typu są to elementy. Jeśli są, to zwracana jest wartość TRUE, jeśli nie – FALSE.


Podzapytania z operatorami IN, ANY (SOME), ALL

Weźmy za przykład kwerendę, która da nam informacje o wszystkich zleceniach, dla trzech najlepszych (pod względem generowania obrotów firmy) Klientów.

Najpierw skupmy się na podzapytaniu, które powinno zwrócić nam wektor 3-elementowy z identyfikatorami najlepszych Klientów. Trzech najdroższych nam Klientów otrzymamy za pomocą takiego zapytania :

SELECT TOP 3 CustomerID , SUM(TotalDue) as TotalSales
FROM  [Sales].[SalesOrderHeader] soh 
GROUP BY CustomerID
ORDER BY TotalSales DESC

Podzapytania_SQL_05

To jeszcze nie jest wektor, ale już coś. Jeśli spróbujemy teraz zbudować kwerendę w oparciu o takie podzapytanie, filtrując w WHERE z wykorzystaniem operatora IN :

SELECT SalesOrderID, OrderDate, TotalDue, CustomerID from [Sales].[SalesOrderHeader] soh 
WHERE  CustomerID in (
 
	SELECT TOP 3 CustomerID , SUM(TotalDue) as TotalSales	FROM  [Sales].[SalesOrderHeader] soh 	GROUP BY CustomerID	ORDER BY TotalSales DESC)

Otrzymamy komunikat o błędzie, ponieważ podzapytanie generuje niepoprawny zbiór (nie jest to wektor – posiada dwie kolumny).

Msg 116, Level 16, State 1, Line 9
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS.

No to zróbmy z niego wektor – podzapytanie z podzapytania :

SELECT SalesOrderID, OrderDate, CustomerID 
FROM [Sales].[SalesOrderHeader] soh 
WHERE CustomerID IN (
         -- podzapytanie z podzapytania w WHERE
	SELECT CustomerID	FROM (		SELECT TOP 3 CustomerID , SUM(TotalDue) as TotalSales		FROM  [Sales].[SalesOrderHeader] soh 		GROUP BY CustomerID		ORDER BY TotalSales DESC 	) AS WektorIdentyfikatorowKlientow)

W powyższym przykładzie, widzimy inną ważną właściwość podzapytań – możliwość zagnieżdżania ich w sobie. Możemy tworzyć podzapytania z podzapytań do 32 poziomów.
Oczywiście można by było powyższą kwerendę zapisać inaczej, z zastosowaniem podzapytania we FROM.

SELECT SalesOrderID, OrderDate, soh.CustomerID 
FROM [Sales].[SalesOrderHeader] soh INNER JOIN (
          -- podzapytanie we FROM
	SELECT TOP 3 CustomerID , SUM(TotalDue) as TotalSales	FROM  [Sales].[SalesOrderHeader] soh 	GROUP BY CustomerID	ORDER BY TotalSales DESC  
) a on soh.CustomerID = a.CustomerID

Wynik i nawet plan wykonania w tej sytuacji będzie identyczny. Często filtracja w jak najwcześniejszym kroku procesu przetwarzania kwerendy przynosi lepsze rezultaty, choć nie zawsze.

Rozważmy bardziej skomplikowany przykład. Chcemy wyświetlić trzy najdroższe zamówienia dla trzech naszych najlepszych Klientów. Zapytanie to zapiszemy z zastosowaniem funkcji szeregującej RANK oraz funkcji okna OVER.

SELECT * FROM
 (
 
	SELECT SalesOrderID, TotalDue, 
              RANK() OVER(Partition by soh.CustomerID order by TotalDue desc) as Majority, 
              soh.CustomerID
	FROM [Sales].[SalesOrderHeader] soh inner join 
	( 
			SELECT TOP 3 CustomerID , SUM(TotalDue) as TotalSales
			FROM  [Sales].[SalesOrderHeader] soh 
			GROUP BY CustomerID
			ORDER BY TotalSales DESC
	) b on soh.CustomerID = b.CustomerID
) a 
WHERE Majority  <= 3

Podzapytania_SQL_06

Inny sposób na osiągnięcie tego samego celu, z filtracją Klientów w WHERE.

SELECT * 
FROM
 (
	-- wychwycenie tylko najdroższych zamówień per Klient
	SELECT SalesOrderID, TotalDue, 
               RANK() OVER(Partition by CustomerID order by TotalDue desc) as Majority,
               CustomerID
	FROM [Sales].[SalesOrderHeader] soh 
) a 
WHERE Majority <= 3 AND CustomerID IN (
 
	-- zrobienie wektora
	SELECT CustomerID
	FROM (
		-- wychwycenie 3 najdroższych nam Klientów
		SELECT TOP 3 CustomerID , SUM(TotalDue) as TotalSales
		FROM  [Sales].[SalesOrderHeader] soh 
		GROUP BY CustomerID
		order by TotalSales desc 
	) a 
)

To zapytanie generuje jednak bardziej kosztowny plan wykonania (w porównaniu do poprzedniego 31% : 69%).

Podzapytania_SQL_07


W SQL jest wiele sposobów na osiągnięcie tego samego rezultatu. Zagadnienia związane z wydajnością, poruszam w ostatnim rozdziale tego kursu.

Przetwarzanie zapytań SQL do wielu tabel

W rozdziale tym znajdziesz opis procesu przetwarzania zapytań SQL, skoncentrowany na klauzuli FROM. Jest to kontynuacja zagadnień, poruszonych w artykule – logiczne przetwarzanie zapytań. Temat ten podzieliłem na dwie części, ze względu na zakres i konieczność omówienia wcześniej zasad łączenia tabel w SQL. Polecam uwadze te dwa artykuły, które prezentują fundamentalne reguły pisania zapytań SQL.

W tej części, znajdziesz informacje o sposobie realizacji zapytań do wielu tabel. Przedstawię wpływ kolejności JOINów na wydajność oraz typowe błędy logiczne.


Zapytania SQL do wielu tabel

Przypomnijmy podstawowe zasady kolejności przetwarzania zapytań SQL. Pierwszym krokiem jest określenie źródeł danych, czyli wykonywanie poleceń umieszczonych we FROM.

W przypadku kwerend odwołujących się do jednego zbioru sprawa jest bardzo prosta. Wynikiem przetwarzania kroku FROM, jest wtedy tabela wirtualna VT1. Zawiera ona wszystkie rekordy ze zbioru źródłowego. Jest tożsama z nią, posiada te same kolumny i rekordy. Zbiór VT1 jest także źródłem kolejnego kroku – przetwarzania WHERE. Jak widać w zapytaniach do jednej tabeli nie ma szczególnej filozofii.

W rzeczywistych środowiskach bazodanowych, kwerendy dotyczą zazwyczaj wielu tabel.

Dla przykładu rozważmy scenariusz zapytania SQL w bazie testowej Northwind. Interesują nas informacje o Klientach z określonego miasta – Madrytu. Chcemy znać detale ich zamówień, złożonych we wrześniu 1996 – nazwy i ilości produktów, które kupili.

Struktura tej bazy danych jest znormalizowana. Informacje które nas interesują, wymagają połączenia 4 tabel.
Zapytania_SQL_do_wielu_tabel_2

Przykładowa kwerenda, realizująca to zadanie, może wyglądać tak :

SELECT  c.CompanyName, o.OrderID, od.Quantity, p.ProductName
FROM   dbo.Customers c  
       LEFT OUTER JOIN dbo.Orders o on c.CustomerID = o.CustomerID 
       INNER JOIN dbo.[Order Details] od on od.OrderID = o.OrderID
       INNER JOIN dbo.Products p on od.ProductID = p.ProductID
WHERE c.City = 'Madrid' AND o.OrderDate BETWEEN '1996-09-01' AND '1996-09-30'

Żeby było ciekawiej (trochę zaczepnie), zastosowałem jako pierwsze połączenie – LEFT OUTER JOIN. Chcę tym samym przy okazji analizy złączeń, pokazać jeden z częstszych błędów logicznych.

W wyniku okazuje się, że jest tylko jeden Klient, który w dodatku złożył tylko jedno zamówienie w podanym przedziale dat.
Zapytania_SQL_do_wielu_tabel_3

Zanim przystąpimy do szczegółowej analizy naszego zapytania, możemy podzielić klauzulę FROM na etapy. Bierzemy pod uwagę ilość występujących tu złączeń i związanych z nimi operacji. Mamy trzy złączenia – będą więc trzy etapy.

Zapiszmy FROM bardziej obrazowo :

FROM 
	-- ETAP 1 – łączenie dwóch pierwszych tabel
          dbo.Customers c 	
             LEFT OUTER JOIN  dbo.Orders o      ON c.CustomerID = o.CustomerID 
 
	-- ETAP 2 – do wyniku ETAPu 1 łączymy tabelę dbo.[Order details]
             INNER JOIN dbo.[Order Details] od  ON od.OrderID = o.OrderID     
 
	-- ETAP 3 – do wyniku ETAPu 2 łączymy tabelę dbo.Products
             INNER JOIN dbo.Products p          ON od.ProductID = p.ProductID

Zasady połączeń, obowiązujące w zapytaniach do wielu tabel SQL, są identyczne jak te opisane dla przykładu łączenia dwóch tabel.

Zaczynamy zawsze od łączenia dwóch pierwszych, stojących zaraz po FROM. Możemy także stosować nawiasy (o tym w następnym przykładzie). Mają one priorytet – czyli łączona jest tabela z wynikiem operacji w nawiasach.

Przy łączeniu trzech i więcej tabel, każda kolejna jest dołączana do wyniku poprzedniego kroku. Algorytm działa iteracyjnie od lewej do prawej, aż do przetworzenia ostatniego zbioru. Prześledźmy zatem dokładnie kolejne etapy przetwarzania FROM naszej kwerendy.


ETAP 1

Łączenie zewnętrzne (LEFT OUTER JOIN) pierwszych dwóch tabel :

SELECT  c.CompanyName, o.OrderID 
FROM   dbo.Customers c  
		LEFT OUTER JOIN dbo.Orders o ON c.CustomerID = o.CustomerID

Krok ten, zostanie przetworzony zgodnie z typowymi regułami logicznego przetwarzania złączeń :

  • Pobierz wszystkie dane z tabeli dbo.Customers
  • Pobierz wszystkie dane z tabeli dbo.Orders
  • Wykonaj złączenie zewnętrzne
    • Najpierw iloczyn kartezjański tych dwóch tabel
    • Następnie filtracja rekordów zgodna z wyrażeniami określonymi w ON
    • Dodanie „odfiltrowanych” rekordów z tabeli stojącej po LEWEJ stronie operatora JOIN (z tabeli dbo.Customers). Wartości atrybutów tabeli dbo.Orders dla tych „niedopasowanych elementów” z kroku poprzedniego uzupełnij NULLami.

Zapytania_SQL_do_wielu_tabel_4

W wyniku otrzymamy wszystkich Klientów (całą zawartość tabeli dbo.Customers) wraz z informacjami o ich zleceniach. Zwróć uwagę na pierwsze dwa rekordy. Są to elementy „dorzucone” w krokach charakterystycznych dla połączeń zewnętrznych – klienci którzy nie złożyli żadnych zleceń.

CompanyName                              OrderID
---------------------------------------- -----------
FISSA Fabrica Inter. Salchichas S.A.     NULL
Paris spécialités                        NULL
Vins et alcools Chevalier                10248
Toms Spezialitäten                       10249
...
Richter Supermarkt                       11075
Bon app'                                 11076
Rattlesnake Canyon Grocery               11077

(832 row(s) affected)

ETAP 2

Zbiór wynikowy poprzedniego etapu (VT1.1), będzie tabelą stojącą po LEWEJ stronie kolejnego złączenia. Tym razem będzie to złączenie wewnętrzne. VT1.1 umieściłem dla lepszego zobrazowania w nawiasie.

SELECT  c.CompanyName, o.OrderID , od.Quantity 
FROM   -- w nawiasie wynik ETAPu 1
       (
           dbo.Customers c  
	 LEFT OUTER JOIN dbo.Orders o on c.CustomerID = o.CustomerID 
       )
           INNER JOIN dbo.[Order Details] od on od.OrderID = o.OrderID

Wykonywane są więc kroki :

  • Pobierz wszystkie dane ze zbioru będącego wynikiem pierwszego złączenia – VT1.1
  • Pobierz wszystkie dane z tabeli dbo.[Order Details].
  • Wykonaj złączenie wewnętrzne tych zbiorów
    • iloczyn kartezjański
    • filtrowanie rekordów

Zapytania_SQL_do_wielu_tabel_5

CompanyName                              OrderID     Quantity
---------------------------------------- ----------- --------
Vins et alcools Chevalier                10248       12
Vins et alcools Chevalier                10248       10
Vins et alcools Chevalier                10248       5
Toms Spezialitäten                       10249       9
...
Rattlesnake Canyon Grocery               11077       2
Rattlesnake Canyon Grocery               11077       4
Rattlesnake Canyon Grocery               11077       2

(2155 row(s) affected)

Zwróć uwagę co się stało z Klientami, którzy nie złożyli żadnych zleceń. W poprzednim kroku, dzięki LEFT JOIN, byli oni uwzględnieni (dorzuceni do wyniku). Teraz, przy kolejnym złączeniu – elementy te zostały odfiltrowane.

W naszym scenariuszu nie miało to znaczenia na wynik końcowy. W końcu chcemy uzyskać informacje tylko o Klientach, którzy złożyli zamówienia. Tak naprawdę pierwszym złączeniem, powinien być INNER JOIN.

Zdarza się, że początkujący użytkownicy SQL, oczekują w takim przypadku, że w wyniku będą również mieli Klientów bez zleceń. Jest to dość często popełniany błąd logiczny na który chciałem zwrócić Twoją uwagę. Wynika on z niewiedzy, w jakiej kolejności wykonywane są złączenia. Być może także z nadinterpretacji zakresu działania LEFT OUTER JOIN, który przecież dotyczy tylko pierwszego złączenia.

Wróćmy do dalszej analizy naszego przykładu.


ETAP 3

Algorytm powtórzony zostanie jeszcze raz, dołączając finalnie do powyższego wyniku ostatnią tabelę – dbo.Products.

SELECT  c.CompanyName, o.OrderID , od.Quantity, p.ProductName
FROM   -- w nawiasie wynik ETAPu 2
       (
          dbo.Customers c  
          LEFT OUTER JOIN dbo.Orders o on c.CustomerID = o.CustomerID 
          INNER JOIN dbo.[Order Details] od on od.OrderID = o.OrderID 
       )
          INNER JOIN dbo.Products p on od.ProductID = p.ProductID

Wykonywane są znów standardowe kroki, jak przy każdym złączeniu dwóch zbiorów :

  • Pobierz wszystkie dane ze zbioru będącego wynikiem etapu 2 (umieszczone w nawiasie) – VT1.2.
  • Pobierz wszystkie dane z tabeli dbo.Products.
  • Wykonaj złączenie wewnętrzne tych zbiorów
    • Iloczn kartezjański
    • Filtrowanie rekordów.

Zapytania_SQL_do_wielu_tabel_6

CompanyName                              OrderID     Quantity ProductName
---------------------------------------- ----------- -------- ------------------------------
Vins et alcools Chevalier                10248       12       Queso Cabrales
Vins et alcools Chevalier                10248       10       Singaporean Hokkien Fried Mee
Vins et alcools Chevalier                10248       5        Mozzarella di Giovanni
Toms Spezialitäten                       10249       9        Tofu
...
Rattlesnake Canyon Grocery               11077       2        Röd Kaviar
Rattlesnake Canyon Grocery               11077       4        Rhönbräu Klosterbier
Rattlesnake Canyon Grocery               11077       2        Original Frankfurter grüne Soße

(2155 row(s) affected)

W tym momencie kończy się przetwarzanie klauzuli FROM. Wszystkie złączenia zostały wykonane. Zbiór wynikowy oznaczyłem jako VT2, aby odróżnić go od prostego przypadku zapytania do jednej tabeli (VT1). Zostaje on przekazany do kolejnego kroku – filtrowania rekordów w WHERE. Proces przetwarzania postępuje dalej zgodnie z opisanym tutaj logicznym porządkiem.


Stosowanie nawiasów w złączeniach tabel i filtracja we FROM

Rozważmy teraz scenariusz, ponownie w bazie Northwind, w którym chcemy uzyskać wiedzę o WSZYSTKICH Klientach z Madrytu, wraz z informacją o produktach, które zamówili w grudniu 1996. Jeśli nie złożyli żadnego zamówienia w tym okresie, to również chcemy ich zobaczyć.

Zapytanie realizujące to zadanie, powinno zwrócić następujący zbiór :

Zapytania_SQL_do_wielu_tabel_7

Jest to szczególny przypadek, w którym filtrowanie rekordów ze względu na datę zamówienia, powinno odbywać się we FROM zamiast w WHERE. Zobaczysz tu także praktyczne zastosowanie nawiasów we FROM, dzięki którym możemy sterować logiką połączeń. Nie będę stosował tu podzapytań, tylko czyste, proste złączenia.

Aby uzyskać informacje o wszystkich Klientach z Madrytu z ich zamówieniami (na razie bez filtrowania daty), można zapisać zapytanie w ten sposób :

SELECT  c.CompanyName, o.OrderID , od.Quantity, p.ProductName
FROM    dbo.Customers c  
        LEFT OUTER JOIN 
        ( 
              dbo.Orders o INNER JOIN dbo.[Order Details] od ON od.OrderID = o.OrderID
                           INNER JOIN dbo.Products p ON od.ProductID = p.ProductID 
        ) ON c.CustomerID = o.CustomerID 
WHERE c.City = 'Madrid'

Uzyskamy teraz faktyczny LEFT OUTER JOIN tabeli dbo.Customers wraz ze wszystkimi szczegółami zamówień.

Pierwsze złączenie będzie połączeniem tabeli dbo.Customers z wynikiem operacji ujętych w nawias. Wykonane zostaną najpierw wszystkie złączenia wewnątrz nawiasu. Najpierw tabeli dbo.Orders z dbo.[Order Details]. Następnie do wyniku dołączona zostanie tabela dbo.Products.

Dopiero na samym końcu, zostanie wykonane połączenie zewnętrzne, lewostronne, tabeli dbo.Customers z efektem działań w nawiasie (wszystkie szczegóły zamówień).

Alternatywnie, moglibyśmy połączenia zapisać bez nawiasów, z wykorzystaniem RIGHT OUTER JOIN :

SELECT  c.CompanyName, o.OrderID , od.Quantity, p.ProductName
FROM   dbo.Orders o 	
          INNER JOIN dbo.[Order Details] od  ON od.OrderID = o.OrderID
          INNER JOIN dbo.Products p          ON od.ProductID = p.ProductID 
          RIGHT OUTER JOIN dbo.Customers c   ON c.CustomerID = o.CustomerID 
WHERE c.City = 'Madrid'

Wynik będzie identyczny. Najpierw zostaną wykonane kolejno wszystkie INNER JOINy. Na samym końcu dołączona zostanie tabela dbo.Cutomers, dorzucając w ostatnim kroku wszystkich Klientów bez zleceń (RIGHT OUTER JOIN).

Teraz zastanów się, gdzie powinniśmy wykonywać filtrację rekordów jeśli chodzi o daty zamówień. Co uzyskamy stosując filtrację w WHERE ?

SELECT  c.CompanyName, o.OrderID, od.Quantity, p.ProductName
FROM   dbo.Customers c  
       LEFT OUTER JOIN  
       ( dbo.Orders o INNER JOIN dbo.[Order Details] od ON od.OrderID = o.OrderID
                      INNER JOIN dbo.Products p ON od.ProductID = p.ProductID 
        ) ON c.CustomerID = o.CustomerID 
WHERE c.City = 'Madrid' 
    and ( o.OrderDate BETWEEN '1996-09-01' AND '1996-09-30' OR o.OrderDate is null)

Zapytania_SQL_do_wielu_tabel_8

Filtracja WHERE jest bezapelacyjna. Raz usunięte rekordy, nie mają prawa się więcej pojawić w zbiorze wynikowym. Nie ma tu mechanizmu „dorzucania” elementów takiego jak w przypadku połączeń zewnętrznych. Dlatego powyższy zapis odsiał nam niestety wszystkich Klientów z Madrytu, którzy złożyli zamówienia, ale w innych datach.

Przypomnę, że chcieliśmy ich także zobaczyć w wyniku końcowym. Straciliśmy więc tutaj wiedzę o firmie „Bólido Comidas preparadas”.

Rozwiązaniem tego problemu jest przeniesienie filtracji ze względu na datę do klauzuli FROM, tak aby dorzucić ewentualnych Klientów z zamówieniami złożonymi poza interesującym nas zakresem dat. Właściwie zapisana kwerenda będzie wyglądała tak :

SELECT  c.CompanyName, o.OrderID , od.Quantity, p.ProductName
FROM   dbo.Customers c  
       LEFT OUTER JOIN 
       ( dbo.Orders o  INNER JOIN dbo.[Order Details] od on od.OrderID = o.OrderID
                       INNER JOIN dbo.Products p on od.ProductID = p.ProductID 
       ) ON c.CustomerID = o.CustomerID 
            and o.OrderDate BETWEEN '1996-09-01' AND '1996-09-30' 
WHERE c.City = 'Madrid'

Dzięki temu zabiegowi, uzyskamy wiedzę o WSZYSTKICH Klientach z Madrytu. Dodatkowo o zamówieniach z określonego przedziału czasowego.


Logiczna a fizyczna kolejność łączenia zbiorów

Fizyczna realizacja połączeń zbiorów przez silnik bazodanowy (plan wykonania) może się różnić od przyjętych w teorii zasad (od lewej do prawej). Jednak wynik zawsze musi być identyczny z rachunkiem teoretycznym.

Zauważ, że czasem warto zajrzeć ciut dalej, np. do filtrowania WHERE aby już na etapie FROM, nie analizować wszystkich rekordów, które i tak zostaną później odfiltrowane. Ponadto fizyczna kolejność wykonywania połączeń, filtracji – też ma znaczenie na wydajność.

Wybór najkrótszej i najbardziej efektywnej metody dostępu do danych, to przede wszystkim zadanie dla optymalizatora zapytań. Najważniejszą zasadą jego działań jest zawsze przewidywalny efekt końcowy. Musi on być w 100% zgodny z opisanymi zasadami teoretycznymi.

Optymalizator potrafi ograniczać i efektywnie łączyć zbiory. Obecne w analizie teoretycznej iloczyny kartezjańskie czy też pobieranie wszystkich wierszy tabeli zazwyczaj nie mają miejsca.
Zapiszmy więc w różny sposób kwerendę realizującą zadanie z przykładu pierwszego :

-- Pierwszą tabelą do której odwołujemy się jest dbo.Customers
SELECT    c.CompanyName, o.OrderID, o.OrderDate , ProductName
FROM      dbo.Customers c 
	inner join dbo.Orders o on c.CustomerID = o.CustomerID 
	inner join dbo.[Order Details] od on od.OrderID = o.OrderID
	inner join dbo.Products p on od.ProductID = p.ProductID
WHERE c.City = 'Madrid' AND o.OrderDate BETWEEN '1996-09-01' AND '1996-09-30'
ORDER BY CompanyName, OrderDate DESC 
 
-- Pierwszą tabelą do której odwołujemy się jest dbo.Orders
SELECT    c.CompanyName, o.OrderID, o.OrderDate , ProductName
FROM      dbo.Orders o 
	inner join dbo.[Order Details] od on od.OrderID = o.OrderID 
	inner join dbo.Products p on od.ProductID = p.ProductID
	inner join  dbo.Customers c on c.CustomerID = o.CustomerID 
WHERE c.City = 'Madrid' AND o.OrderDate BETWEEN '1996-09-01' AND '1996-09-30'
ORDER BY CompanyName, OrderDate DESC 
 
-- Pierwszą tabelą do której odwołujemy się jest dbo.Products 
SELECT    c.CompanyName, o.OrderID, o.OrderDate , ProductName
FROM      dbo.Products p 
	inner join dbo.[Order Details] od on od.ProductID = p.ProductID
	inner join dbo.Orders o on od.OrderID = o.OrderID
	inner join  dbo.Customers c on c.CustomerID = o.CustomerID 
WHERE c.City = 'Madrid' AND o.OrderDate BETWEEN '1996-09-01' AND '1996-09-30'
ORDER BY CompanyName, OrderDate DESC

Pomimo, że złączenia zapisane są w różnej kolejności, silnik bazodanowy i tak po swojemu zrealizuje plan działania. Wszystkie mają identyczny :

Execution_PLAN_SQL

Pytanie które samo nasuwa się po analizie tego przypadku. Czy jednak kolejność joinów może mieć znaczenie? Jeśli tak to może spróbujmy „zoptymalizować” zapytanie i na siłę ją zmienić.


Kolejność połączeń a wydajność

Kolejność jest istotna. W końcu każde z takich łączeń, może produkować różnej wielkości zbiory pośrednie. Możemy założyć, że im bardziej selektywnie będą wykonywane złączenia od samego początku, tym bardziej efektywnie zostanie przetworzona cała kwerenda.

Sprawdźmy więc jakie plany zapytań, będą generowane dla pokazanych powyżej zapytań z zastosowaniem siłowego wymuszenia kolejności połączeń.

Mam możliwość ingerowania w proces optymalizacji poprzez zastosowanie HINTów. Do wymuszenia kolejności złączeń służy opcja :

          OPTION (FORCE ORDER)

Która wymusza na optymalizatorze określoną kolejność od LEWEJ do PRAWEJ. Wykonaj poniższy skrypt, który obrazuje 4 kwerendy zwracające te same wyniki. Skorzystaj z opcji Include Actual Execution Plan, aby porównać plany wykonania i ocenić procentowy udział obciążeniowy każdego z tych poleceń.

-- Kwerenda 1 – dajmy szanse optymalizatorowi
select c.CompanyName, o.OrderID , o.OrderDate , ProductName
from dbo.Customers c 
	inner join dbo.Orders o on c.CustomerID = o.CustomerID 
	inner join dbo.[Order Details] od on od.OrderID = o.OrderID
	inner join dbo.Products p on od.ProductID = p.ProductID
where c.City = 'Madrid'    and o.OrderDate between '1996-09-01' and  '1996-09-30'
order by CompanyName, OrderDate desc
 
-- Kwerenda 2 z FORCE ORDER - identycznie zapisana jak Kwerenda 1
select c.CompanyName, o.OrderID , o.OrderDate , ProductName
from dbo.Customers c 
	inner join dbo.Orders o on c.CustomerID = o.CustomerID 
	inner join dbo.[Order Details] od on od.OrderID = o.OrderID
	inner join dbo.Products p on od.ProductID = p.ProductID
where c.City = 'Madrid'    and o.OrderDate between '1996-09-01' and  '1996-09-30'
order by CompanyName, OrderDate desc
OPTION (FORCE ORDER)
 
-- Kwerenda 3 z FORCE ORDER
select c.CompanyName, o.OrderID , o.OrderDate , ProductName
from dbo.Orders o 
	inner join dbo.[Order Details] od on od.OrderID = o.OrderID 
	inner join dbo.Products p on od.ProductID = p.ProductID
	inner join  dbo.Customers c on c.CustomerID = o.CustomerID 
where City = 'Madrid'    and OrderDate between '1996-09-01' and  '1996-09-30'
order by CompanyName, OrderDate desc
OPTION (FORCE ORDER)
 
-- Kwerenda 4 z FORCE ORDER
select c.CompanyName, o.OrderID , o.OrderDate , ProductName
from dbo.Products p 
	inner join dbo.[Order Details] od on od.ProductID = p.ProductID
	inner join dbo.Orders o on od.OrderID = o.OrderID
	inner join  dbo.Customers c on c.CustomerID = o.CustomerID 
where City = 'Madrid'    and OrderDate between '1996-09-01' and  '1996-09-30'
order by CompanyName, OrderDate desc
OPTION (FORCE ORDER)

Wydajnościowo, najlepsza okazała się kwerenda pierwsza, w której pozwoliliśmy optymalizatorowi, wybór własnego planu wykonania. Każda inna, wykorzystująca podpowiedź, siłowego narzucania kolejności działań Wyniki mówią same za siebie :
Zapytania_SQL_do_wielu_tabel_9


Stosowanie hintów bywa pomocne ale musi być stosowane świadomie. Rozkłady, optymalne sposoby dostępu do danych mogą się zmieniać. Raz utworzony plan, może po pewnym czasie nie być optymalnym, dlatego raczej odradzałbym stosowanie tego typu wskazówek początkującym użytkownikom SQL.

Funkcje, procedury i zmienne systemowe

Oprócz widoków systemowych, za pośrednictwem których, możesz dotrzeć do informacji (metadanych) o środowisku, mamy do dyspozycji szereg innych przydatnych obiektów.

W artykule tym, prezentuję kilka z nich. Podobnie jak widoki systemowe, ich różnorodność daleko wykracza poza zakres tego kursu. Warto mieć świadomość ich istnienia – potraktuj ten rozdział jako wstęp do obiektów, które dają wiedzę na temat środowiska.

Obiekty te, zazwyczaj wykorzystywane są przez administratorów baz danych (DBA DataBase Administrator), ale dla zwykłych użytkowników bywają również pomocne. Wszystkie informacje do których możemy za ich pośrednictwem dotrzeć, dostępne są również poprzez opisane wcześniej widoki systemowe.


Skalarne funkcje systemowe

Systemowe funkcje wbudowane zwracające metadane, stosujemy zazwyczaj w procedurach utrzymaniowych, np. do identyfikacji obiektów, ich parametrów czy wykonywania na nich działań administracyjnych.

Funkcje OBJECT_ID() oraz OBJECT_NAME() pozwalają pójść na skróty w odpytywaniu widoków systemowych.
Zwracają identyfikator obiektu lub jego nazwę. Bywają przydatne, szczególnie jeśli na szybko, chcemy wyciągnąć jakieś dane, a nie znamy identyfikatora danego obiektu i nie chce nam się łączyć dodatkowych widoków.

Dla przykładu – chcemy na szybko wyświetlić informacje o wszystkich indeksach w tabeli dbo.Orders. W skrótowej wersji, moglibyśmy zapisać zapytanie w taki sposób :

USE Northwind
GO
 
SELECT OBJECT_NAME(object_ID) AS TableName, * 
FROM sys.indexes
WHERE OBJECT_NAME(object_ID) = 'Orders'

Metadane_01

Co prawda od razu muszę tutaj przestrzec, że nie do końca jest to sposób idealny. Jak to bywa ze skrótami, czasem mogą nas wyprowadzić w pole.

Zauważ, że wyszukałem wszystkie indeksy dla tabeli Orders, bez podania nazwy schematu. W sytuacji, w której mielibyśmy dwie tabele o tych samych nazwach (tyle że w różnych schematach) wynik byłby mylący, bo zwrócone rekordy dotyczyły by wszystkich indeksów w obu tabelach. Warto o tym pamiętać.

Funkcje DB_ID() oraz DB_NAME(), analogicznie jak w przypadku poprzednio prezentowanych funkcji, zwracają translacje nazwy na identyfikator lub odwrotnie. Samo wywołanie, bez podania parametru, zwróci aktualną nazwę / identyfikator bazy danych do której jesteś podłączony.

SELECT		DB_ID() as ID_bazy, 
		DB_NAME() as Nazwa_bazy, 
		DB_NAME(1) as NazwaBazyId1

Metadane_02

Funkcje sprawdzające, aktualne parametry bazy danych DATABASEPROPERTEX() oraz serwera SERVERPROPERTY(). Wywołując je z odpowiednimi argumentami, możemy otrzymać informacje, np. na temat sposobu porównywania wartości tekstowych (COLLATION).

SELECT DATABASEPROPERTYEX ( 'Northwind', 'Collation' ) as DBCollation,
	   SERVERPROPERTY ('Collation' ) as SrvCollartion

Metadane_03


Zmienne @@GLOBALNE

Zmienne globalne to bardzo podobne do funkcji skalarnych obiekty. Ich nazwa zaczyna się od @@ i jest ich raptem kilkadziesiąt. Poniżej przykład informacji o środowisku zwracanych przez tego typu obiekty :

SELECT	@@SERVICENAME as InstanceName, 
	@@VERSION as SQLServerVersion

Oprócz zmiennych typowo informacyjnych o środowisku, mamy kilka wykorzystywanych w logice programistycznej. Poniżej przykład wykorzystania @@TRANCOUNT, aby określić liczbę otwartych transakcji bieżącego połączenia.

SELECT @@TRANCOUNT as LiczbaOtwartychTransakcji
 
BEGIN TRANSACTION  --nowa transakcja
 
SELECT @@TRANCOUNT as LiczbaOtwartychTransakcji

Metadane_04Metadane_05


Procedury systemowe

Na koniec bardzo użyteczne źródło informacji dla każdego DBA dotyczące różnych aspektów baz, serwera czy aktywności użytkowników. Procedury systemowe to nic innego jak kompilacja widoków, mająca na celu w zwięzły i przystępny sposób, prezentować najważniejsze informacje.

Zamiast pisać kwerendy łączące wiele widoków systemowych, możemy skorzystać z istniejących procedur, wywołując je po prostu po nazwie.

Poniżej kilka dwa przykłady wyświetlania informacje o bazie Northwind :

exec sp_helpdb 'Northwind';
 
exec sp_spaceused;

Metadane_06
Jak widać forma – typowo dla administratorów. Każda z powyższych procedur zwraca osobne rekordsety. Procedura sp_spaceused, może być wywołana również dla konkretnego obiektu (tabeli) w bazie danych. Zwróci nam informacje o liczbie rekordów i miejscu alokowanym przez dany obiekt.

exec sp_spaceused 'dbo.Orders'

Metadane_07

Jedną z fajniejszych rzeczy, jest możliwość modyfikacji lub podglądu zawartości istniejących, wbudowanych procedur systemowych. W ten sposób możesz odkryć strukturę widoków systemowych. Definicje procedur, funkcji systemowych znajdziesz w bazie master.

Szczegółowe omówienie tych struktur, prezentuję na kursach z programowania i administracji SQL Server na które zapraszam.

Zdobywanie informacji o środowisku

Metadane to dane – o danych, służące do opisania składowanych w bazach informacji. Przyznam, że definicja ta (powielana w wielu publikacjach), gdy odkrywałem temat baz danych, była dla mnie masłem maślanym w czystej postaci.

W rozdziale tym, postaram się w bardziej przystępny sposób wyjaśnić sens i cel istnienia metadanych. Zaprezentuję także kilka praktycznych przykładów korzystania ze struktur, pozwalających poznać środowisko bazodanowe.


Dane o danych

Na początek, zastanówmy się czym właściwie są dane składowane w naszych bazach. Patrząc na nie w kompletnym oderwaniu od kontekstu, dane to zbiór cyfr (np. typu całkowitego), znaków (ciągi tekstowe), bitów i bajtów. Jeśli w ten sposób na nie spojrzymy – staną się chaotyczne i kompletnie bezużyteczne.

Wartość tak postrzeganych informacji będzie analogiczna, jak atomów z których składa się człowiek. W końcu to też parę kilo węgla, wodoru, tlenu i innych pierwiastków. Wartość tych atomów w kontekście otaczającego nas świata – zerowa.

Prawdziwy sens naszym danym, nadają właśnie metadane. Dzięki nim, te pojedyncze atomy, łączą się w informacje, a z nich możemy czerpać wiedzę. Do odkrycia tych powiązań, poprawnej interpretacji, potrzebujemy opisu = danych o danych.

Dzięki metadanym, wiemy jak interpretować wartość liczbową 1980 w tabeli Pracownicy w określonej kolumnie. Może to być rok urodzin, śmierci czy zatrudnienia albo wysokość podstawowego wynagrodzenia. Bez metadanych nie ma użytecznych informacji.

Czasem ta granica czym są metadane a dane właściwe jest dość płynna. Weźmy inną analogię – wszystkie “dodane” informacje, opisujące jakaś książkę. Metadanymi z pewnością może być jej tytuł, autor, wydawnictwo, rok i nakład. Z drugiej zaś strony, w bibliotece, te informacje mogą być danymi właściwymi… Podobnie jak w systemie zarządzającym setkami czy tysiącami baz danych, metadane z punktu widzenia pojedynczego obiektu, mogą stać się głównymi informacjami.


Metadane w SQL Server

Podobnie jak w innych serwerach bazodanowych, metadane dotyczą wszelkich struktur spotykanych w tych systemach. Idąc od najniższego poziomu – będą to nazwy i typy kolumn, tabel czy samych baz danych. To także informacje o środowisku serwera, wersji, użytkownikach czy loginach.
W SQL Server dostępnych mamy szereg różnych obiektów, za pomocą których możemy dotrzeć do informacji nadających sens składowanym danym. Opisuje je w osobnych artykułach, przedstawiając najczęściej wykorzystywane w praktyce

  • WIDOKI SYSTEMOWE są podstawowym i najważniejszym źródłem informacji. W wersji SQL Server 2012 jest ich kilkaset ! Dotyczą one wszelkich obiektów tworzonych w ramach serwera, ale także procesów czy statystyk.
  • FUNKCJE, PROCEDURY I ZMIENNE SYSTEMOWE – to obiekty, które stosujemy głównie w skryptach czy np. procedurach utrzymaniowych. Umożliwiają pójście na skróty w stosunku do widoków.