SELECT Explanation, Example FROM Pro.Knowledge
FacebookRSS

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.

Łączenie tabel – pobieranie danych z wielu źródeł

W klauzuli FROM, określamy przede wszystkim źródła (zbiory) z których chcemy pobierać dane. Możliwości w zakresie pobierania danych z jednego zbioru, opisuję w artykule dot. źródeł danych stosowanych we FROM.

W zapytaniu SQL możemy odwoływać się do jednego lub wielu zbiorów. Jeśli chcemy wybierać z przynajmniej dwóch, powinniśmy określić sposób ich połączenia oraz warunki.

W artykule tym, znajdziesz opis pełnego zakresu możliwości FROM – czyli wszystkie sposoby łączenia tabel, zbiorów oraz określania warunków złączeń. Pamiętaj, że mogą to być tabele, widoki, wspólne wyrażenia tablicowe (CTE), zmienne i funkcje tabelaryczne czy podzapytania. Dla uproszczenia, będę je nazywał wymiennie – tabele / zbiory – mając na myśli wszystkie obiekty tabelaryczne, do których możemy odwoływać się we FROM.

Łączenie tabel SQL – zasady ogólne

Dla przypomnienia, FROM jest pierwszym krokiem przetwarzania zapytania. Każdy kolejny, bazuje na pośredniej, wynikowej tabeli wirtualnej, poprzedniego. Zbiór otrzymany po przetworzeniu całego kroku (np. FROM) jest wejściem, następnego (WHERE).

Niezależnie od wybranego typu złączenia, w wyniku przetwarzania FROM, otrzymujemy zawsze zbiór elementów (virtual table VT), opisany za pomocą wszystkich kolumn tabel wejściowych. Nie ma znaczenia czy łączysz dwie czy więcej tabel połączeniem wewnętrznym, zewnętrznym. Elementy (rekordy, wiersze) tabeli wynikowej, będą określone zawsze przez wszystkie atrybuty (kolumny) łączonych zbiorów.

Przykładowo, zbiór wynikowy (VT) operacji łączenia trzech tabel złączeniem wewnętrznym INNER JOIN, będzie opisany przez wszystkie kolumny, trzech tabel wejściowych.
JOIN_02
Inną sprawą jest to, czy będziemy chcieli wszystkie z nich zwracać w kwerendzie. Pewnie nie, ale to określamy dopiero w SELECT. Mamy, więc wyjaśnioną pierwszą kwestię – strukturę zbioru wynikowego tabeli pośredniej.

Po wybraniu tabel, musimy określić jeszcze typ złączenia oraz ich warunki. Tutaj stosujemy w praktyce wiedzę na temat relacyjności bazy – czyli sposobów powiązań tabel między sobą.

Łączenie wielu zbiorów (trzech i więcej) sprowadza się do wielokrotnego wykonania operacji łączenia dwóch tabel
. W kolejnym rozdziale tego kursu, opisuję szczegóły przetwarzania zapytań do wielu tabel.

Zaprezentuję teraz po kolei wszystkie możliwe typy złączeń dwóch tabel, na przykładzie prostego scenariusza.

Istnieje firma X, której część pracowników (tabela EMP) posiada samochód służbowy (tabela CAR). W firmie samochody służbowe mogą być używane przez różne osoby, ale tylko jedna jest bezpośrednio przypisana i jest w pełni za niego odpowiedzialna.
Ponieważ firma dynamicznie się rozwija, część samochodów stoi na placu – nie przypisana jeszcze do nikogo. Obowiązkiem każdego pracownika, z wyjątkiem BOSS’a, jest prowadzenie comiesięcznego rozliczania użytkowanego pojazdu (tabela HIST)

BaseStructure
Kod źródłowy struktur na których prezentuję poszczególne przykłady, do pobrania tutaj.


INNER JOIN – łączenie wewnętrzne

W wyniku złączenia wewnętrznego (INNER JOIN) otrzymujemy tabelę wynikową (VT), składającą się ze wszystkich kolumn tabel wejściowych.

Tabela wynikowa zawierać będzie tylko takie elementy, dla których warunki złączenia wewnętrznego będą spełnione (w logice trójwartościowej, wynik musi być TRUE).
Klauzula FROM wraz z warunkami określonymi w ON jest pierwszym miejscem filtrowania rekordów. Wszystkie elementy dla których wynik nie będzie spełniony (FALSE oraz UNKNOWN), zostaną odrzucone.
INNER_JOIN

Logika łączenia tabel INNER JOIN

Zrozumienie zasad działania złączeń wewnętrzych jest kluczowe. Jest to część wspólna wszystkich typów złączeń INNER oraz OUTER JOIN.

W pierwszym kroku wykonywany jest iloczyn kartezjański obu tabel. Jest to połączenie każdego elementu zbioru A ze wszystkimi zbioru B.

Może to być dla Ciebie trudno do zaakceptowania :), ale uspokoję Cię – logiczne przetwarzanie zapytania a fizyczna jego realizacja to zupełnie co innego. Silnik relacyjny świetnie sobie z tym radzi. Nie oznacza to, że za każdym razem odwołując się do tabeli czytane są wszystkie jej rekordy. Jednak patrząc przez pryzmat zasad panujących w SQL, kroków logicznych – zakładamy, że tak właśnie jest.

Wiem że jeśli choć raz zdarzyło Ci się napisać lub spotkać z iloczynem kartezjańskim, tym trudniej będzie Ci ten fakt zaakceptować. Pod koniec tego akapitu – udowodnię tą zasadę za pomocą prostego przykładu, które napewno Cię przekona.

Po wyznaczeniu iloczynu kartezjańskiego, dla każdego wiersza obliczany jest wynik warunku (lub warunków) określonych w ON. Tu spotykamy się z logiką trójwartościową. Wynik może być spełniony = TRUE, niespełniony = FALSE lub nieznany czyli UNKNOWN (np. porównanie z NULL – więcej na ten temat w artykule o NULL).

Ostatnim krokiem jest odrzucenie wszystkich wierszy niespełniających warunków w ON. W zbiorze wynikowym zostają tylko te elementy, dla których wynik = TRUE.

Całość procesu łączenia wewnętrznego obrazuje poniższy diagram :
INNER_JOIN_01

Załóżmy więc, że w firmie X potrzebny jest raport z informacjami o pracownikach, którzy mają przypisany samochód służbowy.

SELECT * 
FROM dbo.EMP as e INNER JOIN dbo.CAR as c ON e.IdPrac=c.IdPrac

INNER_JOIN_02

INNER_JOIN_03

INNER JOIN jest złączeniem symetrycznym i nie ma specjalnego znaczenia czy łączymy tabelę A z B czy odwrotnie. Podobnie z warunkami w ON. Dla porządku, sensownie jest jednak zachować kolejność atrybutów w ON po tej samej stronie co określenie tabel źródłowych wobec operatora JOIN.

Poniższe warunki łączenia są równoważne :

FROM dbo.EMP e INNER JOIN dbo.CAR c ON e.IdPrac=c.IdPrac
 
FROM dbo.EMP e INNER JOIN dbo.CAR c ON c.IdPrac=e.IdPrac
 
FROM dbo.CAR c INNER JOIN dbo.EMP e ON e.IdPrac=c.IdPrac
 
FROM dbo.CAR c INNER JOIN dbo.EMP e ON c.IdPrac=e.IdPrac

Potencjalne problemy i błędy

To w jaki sposób zapiszemy warunki złączeń, podobnie jak w WHERE może mieć wpływ na wydajność (stosowanie funkcji etc.).

Generalnie INNER JOIN raczej nie stwarza kłopotów. Przyjrzyjmy się jednak tak zapisanemu warunkowi złączenia :

SELECT * 
FROM dbo.EMP e INNER JOIN dbo.CAR c ON e.IdPrac = 1

INNER_JOIN_04

Ponieważ tylko jeden wiersz tabeli dbo.EMP spełnia warunek określony w ON (IdPrac=1), zostaną zwrócone wszystkie wiersze będące wynikiem iloczynu kartzjańskiego tego wiersza z całym zbiorem rekordów tabeli dbo.CAR.

Dowodzi to, że faktycznie wykonywany jest iloczyn kartezjański w logicznym przetwarzaniu złączeń. Najpierw A x B, potem dopiero filtrowanie. Możemy pójść dalej i zapisać warunek w tym przkłądzie jako ON 1=1 – wtedy pełen iloczyn kartezjański gwarantowany bo nic nie zostanie odfiltrowane.


OUTER JOIN – łączenie zewnętrzne

Realizacja dowolnych połączeń zewnętrznych jest wykonywana, w pewnym zakresie, dokładnie tak samo jak wewnętrzne. Trzy pierwsze kroki logicznego przetwarzania są identyczne.

  1. Najpierw wykonywany jest iloczyn kartezjański tabeli A oraz tabeli B (łączymy każdy z każdym).
  2. Dla każdego wiersza, określany jest wynik warunków połączeń (zdefiniowane w ON) – TRUE, FALSE lub UNKNOWN.
  3. Następnie usunięcie wszystkich elementów z pośredniego zbioru wynikowego, dla których wynik połączenia (z p.2) jest różny od TRUE/

W połączeniach wewnętrznych to było wszystko. W zewnętrznych dodany jest jeszcze jeden krok.

W zależności od typu – LEFT, RIGHT lub FULL JOIN, wykonywane jest dopełnienie zbioru, o wszystkie elementy tabeli występującej po LEWEJ, PRAWEJ lub OBYDWU operatora JOIN, dla których wynik warunków nie był spełniony (FALSE lub UNKNOWN).

Brzmi to może trochę zawile, ale jest naprawdę proste i jeśli wiesz już jak działa połączenie wewnętrzne – tutaj dojdzie tylko ten jeden, dodatkowy krok. Zerknij na poniższe przykłady i z pewnością wszystko stanie się jasne.

LEFT OUTER JOIN – połączenie lewostronne otwarte

Postępujemy identycznie jak w INNER JOIN. Na koniec uzupełniamy zbiór wynikowy (INNER JOIN to tylko element C) o wszystkie elementy tabeli stojącej po LEWEJ stronie operatora JOIN (będą to rekordy A oraz B).

Ponieważ wiersze dopełniające muszą być również opisane, przez wszystkie kolumny łączonych tabel.- wartości atrybutów w tym przypadku TabeliB (po prawej stronie JOINa) będą nieznane czyli będą po prostu NULLami.

LEFT_JOIN_01

W naszym scenariuszu, niech będzie to zapytanie wyciągające dane o wszystkich pracownikach pracujących w Firmie oraz informacja czy dany pracownik ma przypisany samochód służbowy.

SELECT e.Imie, e.Nazwisko, e.Stanowisko , c.Marka 
-- LEFT JOIN oraz LEFT OUTER JOIN oznaczają dokładnie to samo
FROM dbo.EMP e LEFT JOIN dbo.CAR c ON e.IdPrac=c.IdPrac

LEFT_JOIN_02

RIGHT OUTER JOIN – łączenie zewnętrzne prawostronne

Ta sama historia co z LEFT JOIN tylko w drugą stronę :). Łączone są najpierw wewnętrznie dwa zbiory (INNER JOIN), na koniec dodawane są wszystkie niedopasowane elementy tabeli po PRAWEJ stronie operatora JOIN (elementy D oraz E). Ponieważ wartości kolumn tabeli po lewej stronie są nieznane, będą NULLami.

RIGHT_JOIN_01

W naszym scenariuszu może to być pytanie o szczegóły wszystkich samochodów służbowych, wraz z dodatkową informacją o osobie przypisanej

select  c.Marka, c.NrRej, c.Rocznik, e.Imie + ' ' + e.Nazwisko as Pracownik
from dbo.EMP e RIGHT JOIN dbo.CAR c on e.IdPrac=c.IdPrac

RIGHT_JOIN_02

Jak łatwo zauważyć, złączenia zewnętrzne LEFT i RIGHT nie są symetryczne. Wynik zależy od pozycji tabel względem operatora JOIN. Nie ma znaczenia zapis warunków (czyli to co jest po ON). Powyższe zapytanie, moglibyśmy zapisać równie dobrze jako połączenie LEFT JOIN i wynik będzie identyczny.

-- zamieniłem tylko kolejność tabel CAR i EMP oraz użyłem LEFT JOIN
SELECT  c.Marka, c.NrRej, c.Rocznik, e.Imie + ' ' + e.Nazwisko as Pracownik
FROM    dbo.CAR c LEFT JOIN dbo.EMP e on e.IdPrac=c.IdPrac

FULL OUTER JOIN – pełne złączenie zewnętrzne

Jeśli wiesz już jak działają INNER, LEFT, RIGHT – to wiesz także jak działa FULL JOIN ! Dopełnieniem zbioru wynikowego są wszystkie elementy obydwu łączonych zbiorów. Podobnie jak poprzednio, wartości nieznanych nie wymyślimy. Elementy dopełniające zbioru A (stojącego po lewej stronie operatora JOIN), będą miały uzupełnione wartości atrybutów tabeli B NULLami. Analogicznie będzie z dopełnieniem drugiego zbioru.

FULL_JOIN_01
W naszym przykładzie będzie to zapytanie zwracające informacje o pełnej relacji – wszystkich pracownikach i samochodach, zgodnie z ich przypisaniem.

SELECT e.Imie, e.Nazwisko, e.Stanowisko , c.Marka 
-- FULL JOIN to skrót od FULL OUTER JOIN 
FROM dbo.EMP e FULL JOIN dbo.CAR c ON e.IdPrac=c.IdPrac

FULL_JOIN_02

CROSS JOIN – iloczyn kartezjański

Raczej rzadko stosowane połączenie zbiorów. Jego sposób działania jest banalny – łączy każdy wiersz tabeli A z każdym wierszem tabeli B. Jako jedyne nie ma możliwości utworzenia warunków połączenia w ON bo z założenia ma połączyć wszystko ze wszystkim.

Skutek łączenia dwóch tabel zawierających po 100 rekordów – to tabela z 10000 wierszami, opisanych za pomocą wszystkich kolumn. Więc jeśli chcesz przetestować wytrzymałość DBA, możesz śmiało spróbować połączyć kilka średniej wielkości tabel :)

Złączenie typu CROSS JOIN jest realizowane również wtedy, gdy wyszczególnimy tabele we FROM, separując je tylko przecinkiem.

SELECT *
FROM dbo.EMP, dbo.CAR

Dlatego powinniśmy unikać stosowania warunków połączeń w WHERE (bardzo stary sposób, niezgodny z ANSI SQL:92).

SELF JOIN – połączenie tabeli z samą sobą

Wszystkie do tej pory prezentowane przykłady, zakładały łączenie dwóch różnych zbiorów. Język SQL jest elastyczny i jeśli coś jest zbiorem, może być użyte we FROM wiele razy.

Połączenia typu SELF JOIN to zawsze jedno z już poznanych – INNER, CROSS lub OUTER JOIN, w T-SQL nie stosuje się zapisu SELF JOIN. W strukturze tabeli dbo.EMP firmy X mamy zdefiniowaną referencję pomiedzy kolumnami IdManager i IdPrac.

select IdPrac, Imie, Nazwisko, Stanowisko, IdManager 
from EMP

SELF_JOIN_00
Wyświetlmy podstawowe dane dla wszystkich pracowników wraz z informacją o bezpośrednim przełożonym.
Ponieważ jest jedna osoba (BOSS), która nie ma przełożonego, musimy w tym zadaniu zastosować połączenie zewnętrzne. Aby móc połączyć dwie te same tabele, koniecznie musimy zastosować aliasy (e1 i e2).

SELECT e1.Imie + ' ' + e1.Nazwisko as Pracownik, e1.Stanowisko, 
	e2.Imie + ' ' + e2.Nazwisko as Manager, e2.Stanowisko as ManStanowisko
FROM dbo.EMP e1 LEFT OUTER JOIN dbo.EMP e2 on e1.IdManager=e2.IdPrac

SELF_JOIN_01

Ten przykład bazuje na istniejącej relacji, kluczu obcym tabeli dbo.EMP do samej siebie, jednak wcale nie musi ta referencja być jawnie i permanentnie określona.

Wszystkie do tej pory prezentowane przykłady, łączyły tabele w naturalny sposób ich powiązań. Po kolumnach będącymi jednocześnie kluczami obcymi/podstawowymi tabel.

Ogólną zasadą łączenia jest możliwość jej realizacji po dowolnych kolumnach. Musi być spełniony tylko jeden warunek – kompatybilność typów danych łączonych atrybutów. To jak zapiszemy warunek i czy będzie miał sens, zależy tylko od nas – język SQL nie ogranicza tu naszej wyobraźni.

Dodatkowo, na wartościach atrybutów po których łączymy, możemy wykonywać dowolne operacje. Przetwarzać je za pomocą funkcji skalarnych, wykonywać działania arytmetyczne, łączenia stringów etc…

Na koniec bardzie „zaawansowany” przykład, łączący różne techniki pisania zapytań prezentowane w tym rozdziale kursu. Będzie obrazował połączenie wewnętrzne typu SELF JOIN tabeli dbo.HIST w której przechowywane są informacje o przebiegu samochodów w postaci „logu”.

Scenariusz jest taki, że co miesiąc, każdy z pracowników, musi wpisać stan licznika swojego samochodu służbowego. Zapytanie ma za zadanie wyświetlenie raportu o przebiegach miesięcznych, każdego samochodu za okres wakacyjny. W tym celu posłużę się technikami wspólnych wyrażeń tablicowych, łączeniem wewnętrznym oraz funkcją ROW_NUMBER(). Zauważ, że wykonuję operacje arytmetyczne na atrybutach łączących (dodaje 1, aby uzyskać przesunięcie odczytów) oraz łączę kilka warunków w klauzuli ON.

WITH LogTab as (
   -- mozesz uruchomić testowo tylko zawartość CTE, 
   -- żeby sprawdzić co zwraca i co będzie sednem (tabela LogTab) której potem używam
   SELECT  * , ROW_NUMBER() OVER(partition by NrRej order by DtPomiaru) as IdUniqueRange  
   FROM  dbo.HIST
   where DtPomiaru between '2012-06-01' and '2012-08-31'
 
)
 
SELECT  l1.NrRej, YEAR(l1.DtPomiaru) as Rok,MONTH(l1.DtPomiaru) as Miesiac, 
	l1.Przebieg as PrzebiegStart,l2.Przebieg as PrzebiegEND,
        l1.Przebieg - l2.Przebieg as Delta
FROM  LogTab l1 INNER JOIN LogTab l2 
	   ON l1.IdUniqueRange = l2.IdUniqueRange+1 and l1.NrRej = l2.NrRej
ORDER BY l1.NrRej, l1.DtPomiaru

SELF_JOIN_02


Zgodność składni łączenia tabel ze standardami ANSI SQL

W praktyce można spotkać różne możliwości określania warunków złączeń.

Przedstawione w tym artykule (INNER, LEFT OUTER, CROSS, warunek ON) są zgodne ze standardem ANSI SQL:92 i powinny być stosowane w produkcyjnych bazach. Czysto informacyjnie, istnieją inne, starsze metody zapisu, które jednak nie powinny być już stosowane.

Łączenie wewnętrzne z warunkiem zamiast w ON – w WHERE.

-- równoważnik INNER JOIN - warunek połączenia dopiero w filtrowaniu w WHERE 
-- UNIKAĆ !!! bo łatwo o pomyłkę i cartesian product gotowy :)
SELECT e.Imie, e.Nazwisko, e.Stanowisko , ISNULL(c.Marka , '-') as Samochod
FROM dbo.EMP e , dbo.CAR c 
WHERE e.IdPrac = c.IdPrac

Operacje na zbiorach UNION, EXCEPT i INTERSECT

Opisane w pierwszym artykule tego rozdziału, sposoby łączenia tabel w klauzuli FROM – dotyczyły łączenia zbiorów w sposób poziomy. Określane były typy złączeń (m.in. INNER, OUTER JOIN) oraz warunki dopasowania rekordów – wyszczególnione przed operatorem ON.

W wyniku otrzymywaliśmy zbiór elementów, opisywany za pomocą wszystkich kolumn (atrybutów) łączonych tabel. Dlatego nazywamy to połączenie jako poziome.

Drugą grupą operacji na zbiorach, są operacje pionowe, czyli :

  • UNION – suma zbiorów
  • EXCEPT – odejmowanie zbiorów
  • INTERSECT – iloczyn (część wspólna)

Operują one zawsze, na wynikach całych kwerend (tabel wejściowych) i zwracają tabelę wynikową, będącą zbiorem identycznie określonym jak pierwsza tabela wejściowa (liczba i nazwy kolumn). Zawierają jednak elementy (wiersze), zgodne z arytmetyką zbiorów, określoną przez operator : UNION, UNION ALL, EXCEPT lub INTERSECT. W jednym zapytaniu możemy dokonywać wiele operacji na zbiorach np. łączyć (UNION) wyniki 5 kwerend (tabel wejściowych).

Ogólne zasady operacji na zbiorach obrazuje poniższy schemat :

Zbiory

Warunki operacji na zbiorach

Jest kilka zasad, które muszą być spełnione. Warunkiem podstawowym, któregokolwiek ze sposobów operowania na zbiorach w sposób pionowy, jest podobna struktura tabel wejściowych.
Liczba kolumn w każdym zbiorze (kwerendzie), musi być identyczna oraz typy danych poszczególnych kolumn, muszą do siebie pasować. Nazwy kolumn, nie mają znaczenia. W zbiorze wynikowym, atrybuty będą nazwane tak jak w pierwszej z kwerend.

Możemy wykonywać wiele operacji na zbiorach, np. złączenie trzech wyników kwerend w jeden zbiór :

-- kwerenda pierwsza (zbiór elementów)
SELECT 'Pierwszy' as Opis, getdate() as Dt, 132 as liczba
 
UNION  -- operator łączenia zbiorów 
-- kwerenda druga (zbiór elementów)
SELECT 'Drugi' as ZupelnieInnyOpis, '2013-01-01' as DataZlecenia, 0.2
 
UNION  -- łączenie zbiorów, połączy wynik pierwszego UNION z kwerendą trzecią 
-- kwerenda trzecia (zbiór elementów)
SELECT 'Trzeci' as Opisik, '2012-11-21' as dt, 0

UNION_01
Jak widzimy w tym przykładzie, wszystkie trzy „kwerendy” zwracają po jednym elemencie opisanym 3 atrybutami. Niektóre z tych atrybutów są innego typu (np. 132 z pierwszego zbioru to typ integer, wartość 0.2 w drugim to decimal), ale są to typy kompatybilne (możliwa jest ich bezstratna niejawna konwersja).
Jeśli spróbowalibyśmy, połączyć zbiory o różnych typach w tych samych kolumnach, np. :

-- wartości w kolumnie 3 są typu integer
select 'Pierwszy' as Opis, getdate() as Data, 132 as liczba
UNION
-- z kolei tutaj w 3 kolumnie mamy tekst
select 'Drugi', '2013-01-01', 'sto dwa'

otrzymamy komunikat o błędzie :

Msg 245, Level 16, State 1, Line 2
Conversion failed when converting the varchar value 'sto dwa' to data type int.

Podobnie jeśli liczba kolumn nie będzie równa :

select 'Pierwszy' as Opis, getdate() as Data, 132 as liczba
union 
select 'Drugi', '2013-01-01'

Tym razem dostaniemy info o różnej liczbie kolumn w zbiorach, które mają być łączone :

Msg 205, Level 16, State 1, Line 1
All queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.

Trzeba pamiętać o tej podstawowej zasadzie i zadbać o to, aby była ona zawsze spełniona.
Zazwyczaj nie jest to trudne, bo jeśli masz dwie kwerendy, które chcesz połączyć i zawierają one różne liczby kolumn, wystarczy sztucznie „uzupełnić” brakującą liczbę, np. wartościami NULL :

select kol1, kol2, kol3
From tabela1
union 
select kol1, kol2, NULL
from tabela2

Podobnie jeśli typy danych nie są kompatybilne, zawsze można je zmienić (np. stosując funkcję CAST lub CONVERT), na typ bardziej ogólny – np. na varchar.

Podczas łączenia zbiorów, analizowana jest liczba i typ ich atrybutów (kolumn zwracanych w kwerendach), dlatego jest to kolejny powód dla którego nie powinniśmy stosować symbolu ‘*’ w SELECT w środowiskach produkcyjnych. Może to prowadzić do błędów – wystarczy, że jedna z tabel biorących udział w łączeniu zostanie zmodyfikowana (dodana lub usunięta kolumna).

Dodatkowy warunek, wynika z definicji zbiorów i operacji na nich. Elementy w ramach zbioru nie są uporządkowane. Operacje na zbiorach działają więc zawsze na nieposortowanych elementach.
Ponieważ sortowanie ma duży wpływ na wydajność a zgodnie z tym co przed chwilą powiedzieliśmy, nie ma znaczenia na wynik – dlatego nie można używać operatora ORDER BY w kwerendach biorących udział w operacjach łączenia/odejmowania czy wyznaczania części wspólnej zbiorów.

Możemy dopisać ORDER BY na samym końcu – będzie się ono odnosiło do tabeli wynikowej (ostatecznego rezultatu wszystkich określonych operacji na zbiorach).

Operator UNION – łączenie zbiorów

UNION oznacza sumę zbiorów. W wyniku otrzymamy elementy znajdujące się zarówno w zbiorze pierwszym jak i drugim, ale domyślnie jest to operacja UNION DISTINCT, czyli z usunięciem wszystkich duplikatów. W szczególności znajdujących się jako część wspólna zbiorów, a także duplikatów istniejących w ramach tabel wejściowych.

Use Northwind
GO
 
-- pierwsze zaoytanie zwraca 9 elementów (niektóre się powtarzają)
select Country from [dbo].[Employees]
where Country like 'U%'
UNION-- drugie zapytanie zwraca 20 elementów (niektóre się powtarzają)
select Country from [dbo].[Customers]
where Country like 'U%'

UNION_02
Każde z zapytań biorących udział w operacji łączenia zbiorów zwraca w wyniku zdublowane wartości elementów (nie usuwamy ich za pomocą DISTINCT), robi to domyślnie operator UNION – czyli dostajemy tylko unikalne wartości elementów zbioru (A + B) – USA i UK. Usunięte są one zarówno z kwerend wejściowych, jak i z części wspólnej zbioru A i B).

UNION ALL – łączenie bez usuwania duplikatów

Drugim sposobem na dodawanie zbiorów jest UNION ALL – czyli bez usuwania duplikatów. Tym razem z każdego zbioru, bierzemy tylko 5 pierwszych wierszy (załatawia to TOP 5) i pomimo, że elementy się powtarzają, w wyniku dostaniemy 10 wierszy.

-- pierwsze zapytanie zwraca 9 elementów (niektóre się powtarzają)
-- tym razem bierzemy tylko 5 pierwszych - TOP 5
select top 5 Country 
from [dbo].[Employees]
where Country like 'U%'
UNION ALL-- drugie zapytanie zwraca 20 elementów (niektóre się powtarzają)
-- tym razem bierzemy tylko 5 pierwszych - TOP 5
select top 5 Country 
from [dbo].[Customers]
where Country like 'U%'

UNION_03

EXCEPT – odejmowanie zbiorów

Zasada działania jest prosta. Ze zbioru pierwszego (czyli po lewej stronie od operatora EXCEPT), odejmowane są wszystkie elementy wspólne ze zbiorem drugim (tabeli wynikowej, kwerendy po prawej stronie).

Odejmowanie zbiorów za pomocą EXCEPT zostało zaimplementowane w SQL Server tylko jako EXCEPT DISTINCT, czyli w zbiorze wynikowym, zawsze usuwane są wszystkie duplikaty rekordów. Przykład :

-- pierwsze zapytanie zwraca 4 miasta - Seattle, Tacoma, Kirkland i Redmond 
select city from [dbo].[Employees]
where Country = 'USA'
 
EXCEPT  -- operator odejmowania zbiorów 
-- drugie zapytanie zwraca znacznie więcej miast, 
-- wśród nich są Seattle i Kirkland
select city from [dbo].[Customers]
where Country = 'USA'

UNION_04
Jeśli jednak potrzebujemy zrobić EXCEPT ALL, bez usuwania duplikatów – możemy to zrealizować za pomocą ROW_NUMBER, funkcji szeregującej, która nada nam unikalność rekordów, w ramach duplikatów.

Przykładowe działanie EXCEPT ALL, można przedstawić w takim scenariuszu :
Jeśli zdarzyłoby się, że w zbiorze A, pojawi się element X 3 razy, a w zbiorze B element X wystąpi 2 razy, to w zbiorze wynikowym, element X powinien pojawić się raz.
W tym przykładzie, każdy duplikat w ramach zbioru jest ponumerowany :

-- Q1 pierwsze zapytanie zwraca 4 miasta - Seattle (dwa razy), Tacoma, Kirkland i Redmond 
select city, ROW_NUMBER() OVER(partition by city order by city) as DuplikatNo
from [dbo].[Employees]
where Country = 'USA'
EXCEPT
-- Q2 drugie zapytanie zwraca znacznie więcej, 
-- wśród nich są również Seattle (tylko raz) i Kirkland
select city, ROW_NUMBER() OVER(partition by city order by city) as DuplikatNo
from [dbo].[Customers]
where Country = 'USA'

UNION_05
Jeśli sprawdzasz działanie powyższego zapytania – sugeruję, uruchomić najpierw każde z nich osobno, aby zaobserowować dokładnie jakie elementy są zwracane. Następnie całość.
Wynik działania obrazuje poniższy schemat :
UNION_06

INTERSECT – część wspólna zbiorów

Do wyznaczenia części wspólnej zbiorów, używamy operatora INTERSECT. Podobnie jak EXCEPT, zaimplementowany w SQL Server, został również tylko jako INTERSECT DISTINCT, czyli części wspólna dwóch zbiorów z usunięciem duplikatów.

-- Q1 pierwsze zapytanie zwraca 4 miasta - Seattle (dwa razy), Tacoma, Kirkland i Redmond 
select city
from [dbo].[Employees]
where Country = 'USA'
INTERSECT-- Q2 drugie zapytanie zwraca znacznie więcej, 
-- ale wśród nich są też Seattle (tylko raz) i Kirkland
select city
from [dbo].[Customers]
where Country = 'USA'

UNION_07
Jeśli chcesz wyznaczyć część wspólną bez usuwania duplikatów, możesz użyć dokładnie tej samej sztuczki co poprzednio czyli numeracji rekordów. Wtedy jeśli w dwóch zbiorach będą po dwa duplikaty tego samego elementu – zostaną zwrócone dzięki unikalności elementów, jaką wprowadzi nam ROW_NUMBER.

-- Q1 pierwsze zapytanie zwraca 2 kraje, ponumerowane za pomocą ROW_NUMBER
-- 4 duplikaty UK oraz 5 duplikatów USA
select country, ROW_NUMBER() OVER(partition by country order by country) as rn
from [dbo].[Employees]
where Country like 'U%'
INTERSECT
-- Q2 drugie zapytanie zwraca znacznie więcej, ale wśród nich są również 
-- przynajmniej 4 UK i min. 5 USA 
select country, ROW_NUMBER() OVER(partition by country order by country) as rn
from [dbo].[Customers]
where Country like 'U%'

UNION_08

Kolejność wykonywania operacji

Możemy operować na wielu zbiorach w ramach jednej kwerendy i stosować różne operacje. Obowiązuje tutaj kolejność wykonywania działań – dokładnie tak jak w matematyce.

Select kol1, kol2, kol3 from tabela1
UNION
Select kol1, kol2, kol3 from tabela2
EXCEPT
Select kol1, kol2, kol3 from tabela3
INTERSECT
(
Select kol1, kol2, kol3 from tabela4
UNION
Select kol1, kol2, kol3 from tabela5
)

Najpierw zostaną wykonane działania w nawiasach, czyli UNION ostatnich dwóch kwerend (wyciągających dane z tabeli 4 i 5). Następnie mnożenie zbiorów, czyli INTERSECT – część współna pomiędzy wynikiem wyznaczonym w kroku pierwszym a kwerendą wyciągającą dane z tabeli3.

W końcu, jeśli nie ma już żadnych nawiasów i iloczynów, zostaną wykonane wszystkie pozostałe kroki od lewej do prawej, czyli w tym przypadku najpierw, pierwszy UNION i w końcu EXCEPT.

Poprzez stosowanie nawiasów, mamy pełną kontrolę nad logiczną kolejnością wykonywanania działań.