Na przestrzeni ostatnich lat było mi dane oglądać dużą ilość kodu TSQL napisanego przez kogoś innego. W dużej ilości przypadków spotykałem się z dosyć ciekawymi konstrukcjami, które raz działały lepiej, a raz gorzej. Jedną z takich konstrukcji było umieszczanie warunków filtrujących w sekcji złączenia ON. Czy ma to jakiekolwiek znaczenie? Czy ma to wpływ na wydajność i/lub otrzymane rezultaty w porównaniu do umieszczenia warunków filtrujących w WHERE? Na te i kilka innych pytań postaram się odpowiedzieć w ramach niniejszego artykułu – zapraszam do lektury.
Na potrzeby naszej demonstracji stworzymy sobie w bazie systemowej tempdb dwie tabele tymczasowe- jedna z nich zawiera słownikowe dane produktów druga z kolei zawiera grupy produktowe do których dane produkty należą:
USE tempdb GO CREATE TABLE #Product ( ProductId INT, Name varchar(20), Price INT, ProductGroupID INT ) GO CREATE TABLE #ProductGroup ( ProductGroupID INT, ProductGroupName VARCHAR(20) ) GO
Następnie wstawmy do tych tabel przykładowe dane – samych danych nie ma zbyt wiele gdyż nie jest to dla naszych potrzeb konieczne:
INSERT INTO #ProductGroup (ProductGroupID,ProductGroupName) VALUES (1,'Owoce'), (2,'Warzywa'), (3,'Inne') GO INSERT INTO #Product (ProductId,Name,Price,ProductGroupID) VALUES (1,'Jabłko',2,1), (2,'Pomidor',3,2), (3,'Gruszka',4,1), (4,'Banan',4,1) GO
Z powyższego zapytania można wywnioskować, że w tabeli #ProductGroup znajduje się grupa “Inne”, do której nie jest przypisany żaden produkt – pamiętajmy o tym gdyż będzie to istota naszych kolejnych testów. Zacznijmy nasz test – na pierwszy ogień weźmy złączenie wewnętrzne. W pierwszym z dwóch zapytań umieściłem warunek filtrujący w sekcji WHERE, w drugim z kolei warunek znajduje się w sekcji JOIN ON:
SELECT * FROM #ProductGroup AS PG JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID WHERE P.Price>2 SELECT * FROM #ProductGroup AS PG JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID AND P.Price>2
Rezultat obu zapytań jest dokładnie taki sam, czyli zwrócone zostały po 3 pasujące wiersze:
W przypadku tego typu zapytań optymalizator rozpozna sytuację i wygeneruje również dokładnie taki sam plan wykonania. Niepasujące wiersze zostaną “odrzucone” w ramach operatora Hash Match:
Tak więc mamy już pierwsze podejrzenie tj. w przypadku złączeń wewnętrznych różnic w rezultacie i wydajnościowych nie ma. Sytuacja staje się nieco ciekawsza gdy użyjemy złączeń zewnętrznych:
SELECT * FROM #ProductGroup AS PG LEFT JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID WHERE P.Price>2 SELECT * FROM #ProductGroup AS PG LEFT JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID AND P.Price>2
W tym przypadku rezultat jest już całkowicie inny:
Pierwsze zapytanie zawierało wyrażenie filtrujące w sekcji WHERE, a więc filtracja została dokonana dopiero po złączeniu obu tabel. W drugim przypadku filtracja na tabeli #Product została dokonana jeszcze przed złączeniem dlatego zwrócone zostały wszystkie wiersze. Póki co wszystko było w miarę przejrzyste prawda?
Spróbujmy zatem zbudować zapytanie, które zwróci nam wszystkie grupy produktów i tylko te produkty, które mają przypisaną grupę produktową i ich cena jest większa niż 2. Na początku zapiszmy nasze warunki w JOIN ON:
SELECT * FROM #ProductGroup AS PG LEFT JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID AND P.Price>2 OR P.Price IS NULL
Wszystko wygląda w porządku. To teraz spróbujmy przenieść nasz warunek do sekcji WHERE:
SELECT * FROM #ProductGroup AS PG LEFT JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID WHERE P.Price>2 OR P.Price IS NULL
Rezultat pozostał taki sam i może się wydawać, że oba wyrażenia są substytutami – spójrzmy jednak w tym miejscu na plany wykonania obu zapytań:
Są one zdecydowanie inne, w przypadku zapytania z filtracją w JOIN wiersze wybierane są wewnątrz operatora Table Scan – czyli nastąpiło przesunięcie filtracji do jak najwcześniejszego elementu planu (pisałem o tym tutaj). W przypadku drugiego zapytania filtracja zgodnie z oczekiwaniami została wykonana dopiero po złączeniu obu tabel. Czy ma to dla nas jakieś większe znaczenie? Zaraz się przekonamy! Dodajmy do tabeli produktów wiersz, który będzie łączył się z grupą produktową o identyfikatorze 3 (to ta grupa, która nie miała żadnego przypisanego produktu):
INSERT INTO #Product (ProductId,Name,Price,ProductGroupID) VALUES (5,'Piłka',1,3)
Następnie wykonajmy oba wcześniej testowane zapytania raz jeszcze:
SELECT * FROM #ProductGroup AS PG LEFT JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID AND P.Price>2 OR P.Price IS NULL SELECT * FROM #ProductGroup AS PG LEFT JOIN #Product AS P ON PG.ProductGroupID=P.ProductGroupID WHERE P.Price>2 OR P.Price IS NULL
Rezultaty są inne?! Jak to w ogóle możliwe? Odpowiedź mimo, iż na pierwszy rzut oka niewidoczna w rzeczywistości jest bardzo prosta – wszystko zależy od momentu w którym dane zostały przefiltrowane. W przypadku zapisu z warunkiem filtrującym w JOIN ON wyglądało to następująco:
- Pobrano wiersze z tabeli #Product i od razu je przefiltrowano:
2. Dokonano złączenia zewnętrznego z tabelą #ProductGroup na już przefiltrowanych danych tabeli #Product:
W przypadku warunku filtrującego w WHERE wyglądało to następująco:
- Złączono tabele na nieprzefiltrowanym zestawie danych:
2. Dokonano filtracji bazując na warunku zapisanym w WHERE:
Teraz już wiemy, że brak zrozumienia tego jakie konsekwencje niesie ze sobą filtrowanie w WHERE oraz JOIN ON może mieć skutki nie tylko w wydajności, ale również może prowadzić do błędnych rezultatów naszych zapytań.
Podsumowując można powiedzieć, że w przypadku złączeń wewnętrznych to gdzie umieścimy warunki filtrujące nie ma znaczenia. Gdy nasze zapytania zawierają złączenia zewnętrzne – wtedy należy wziąć pod uwagę powyższą sytuację aby uniknąć późniejszych problemów z tym, że raz coś zadziałało, a raz nie. Ogólnie rzecz biorąc tak jak zawsze warto wypracować sobie pewien sprawdzony standard odpowiadający poszczególnym scenariuszom i trzymać się go w każdej sytuacji. Tematyka ta mimo, że dosyć podstawowa to może powodować problemy i niezrozumienie bo jak wiemy diabeł zawsze tkwi w szczegółach.
- Avoiding Issues: Monitoring Query Pushdowns in Databricks Federated Queries - October 27, 2024
- Microsoft Fabric: Using Workspace Identity for Authentication - September 25, 2024
- Executing SQL queries from Azure DevOps using Service Connection credentials - August 28, 2024
W typowych tutorialach do SQL-a zwykle omawia się warunki filtrowania w WHERE. Rzadko kiedy trafiam na opisy filtrowania w JOIN ON. Tak z ciekawości sprawdziłem jak powyższe przykłady działają w Oracle. Podobnie, ale SQL Server ma niezaprzeczalną zaletę, a mianowicie czytelność planów zapytań.
To prawda rzadko mówi się o filtrowaniu w ON – jednakże ono występuje. Plany zapytania są świetną wskazówką gdy chcemy zrozumieć jak zapytanie działa chociaż na pierwszy rzut oka nie musi to być takie oczywiste.
W zapytaniu gdy dodajesz “OR P.Price IS NULL” do złączenia masz
SELECT *
FROM
#ProductGroup AS PG
LEFT JOIN #Product AS P
ON PG.ProductGroupID=P.ProductGroupID
AND P.Price>2 OR P.Price IS NULL
– A AND B OR C
Natomiast gdy “OR P.Price IS NULL” przerzucasz do where uzyskujesz
SELECT *
FROM
#ProductGroup AS PG
LEFT JOIN #Product AS P
ON PG.ProductGroupID=P.ProductGroupID
WHERE P.Price>2 OR P.Price IS NULL
– A AND (B OR C)
Czyli :
załóżmy warunki A=0, B=0, C=1
Wyniki
1. A AND B OR C = 1
2. A AND (B OR C) = 0
Nie wiem jak dla Ciebie, ale oba te zapytania prezentują zupełnie inne wymagania biznesowe. Wszystko przez zastosowanie operatora OR.
Natomiast pozostała część artykułu jest spoko. Dzięki.
Do trafnego komentarza Adam dodałbym spostrzeżenie, że sytuacja robi się ciekawa, gdy w tabeli produktów umieścimy np. Warzywo bez ceny, np.:
INSERT INTO Product
(ProductId,Name,Price,ProductGroupID)
VALUES
(6,’Ogórek’,NULL,2)
Ależ oczywiście, że to będzie co innego 🙂 Wydawało mi się, że dokładnie taki jest wydźwięk artykułu – czasami umiejscowienie filtracji nie ma znaczenia, a czasami ma bo powoduje zmianę logiki zapytania 🙂 Pozdrawiam
Adam, dzięki za komentarz 🙂 Oczywiście, że zapytania odzwierciedlają inne wymogi – chciałem pokazać fakt, że czasem umieszczenie warunku filtrującego czy to w WHERE czy to w JOIN nie ma znaczenia (pierwsza część artykułu), a czasami jest całkowicie zmienia logikę dlatego w artykule zawarłem “Rezultat pozostał taki sam i może się wydawać, że oba wyrażenia są substytutami”