JOIN z funkcją tabelaryczną to łączenie tabeli z zestawem wierszy zwracanym przez funkcję zdefiniowaną przez użytkownika.
Rola funkcji tabelarycznej w zapytaniu JOIN
JOIN z funkcją tabelaryczną polega na połączeniu tabeli, widoku albo innego źródła danych z wynikiem funkcji, która zwraca wiele wierszy i określony zestaw kolumn. W SQL Server taka funkcja jest traktowana w klauzuli FROM podobnie jak tabela. Funkcja może przyjmować parametry, wykonywać operacje na danych i zwracać tabelę wynikową, którą następnie łączy się z innym źródłem za pomocą warunku ON. Z punktu widzenia zapytania wynik funkcji ma strukturę relacyjną: składa się z wierszy, kolumn i typów danych.
Przykładowo funkcja tabelaryczna może zwracać listę zamówień należących do określonego klienta, a zapytanie może połączyć ten wynik z tabelą klientów. Warunek złączenia powinien wskazywać odpowiadające sobie kolumny, na przykład identyfikator klienta. Jeżeli użyty zostanie INNER JOIN, pozostaną tylko te wiersze, dla których funkcja i tabela mają zgodne wartości klucza. LEFT JOIN zachowa natomiast również wiersze z tabeli znajdującej się po lewej stronie, nawet wtedy, gdy funkcja nie zwróci dla nich dopasowania.
Inline TVF a funkcja wieloinstrukcyjna
SQL Server rozróżnia przede wszystkim funkcje tabelaryczne inline oraz funkcje tabelaryczne wieloinstrukcyjne. Inline table-valued function, określana skrótem iTVF, zwraca wynik pojedynczej instrukcji SELECT. Jej rezultat ma schemat wynikający bezpośrednio z zapytania, a optymalizator może uwzględnić jej treść podczas budowania planu wykonania zapytania nadrzędnego. Taka funkcja jest często używana do parametryzowania powtarzalnego zapytania bez tworzenia fizycznej tabeli pośredniej.
Multi-statement table-valued function, czyli mTVF, korzysta z zadeklarowanej zmiennej tabelarycznej, do której wynik jest zapisywany za pomocą jednej lub większej liczby instrukcji. Funkcja kończy działanie po zwróceniu tej zmiennej jako rezultatu. Ten wariant pozwala zbudować bardziej rozbudowaną logikę, ale może utrudniać optymalizację zapytania nadrzędnego, ponieważ rzeczywista liczba wierszy zwracanych przez funkcję nie zawsze jest znana optymalizatorowi na etapie wyboru planu. Przy dużych zbiorach danych może to prowadzić do niekorzystnego sposobu łączenia, nadmiernego użycia pamięci albo wielokrotnego odczytu danych.
Przy projektowaniu funkcji trzeba również określić, czy może zwracać wartości NULL, jakie są typy kolumn oraz czy dane wynikowe mają unikatowy klucz. Błędne założenie dotyczące unikatowości może spowodować zwielokrotnienie wierszy po połączeniu. Jeżeli jeden wiersz tabeli głównej pasuje do kilku wierszy zwróconych przez funkcję, rezultat będzie zawierał kilka wierszy dla tego samego elementu źródłowego. Nie jest to błąd samego operatora JOIN, lecz konsekwencja relacji jeden-do-wielu.
Warunek złączenia i użycie APPLY
Jeżeli funkcja tabelaryczna otrzymuje stałe parametry niezależne od bieżącego wiersza tabeli, można umieścić jej wynik w klauzuli FROM i połączyć go zwykłym JOIN. Sytuacja zmienia się wtedy, gdy parametr funkcji ma pochodzić z kolumny aktualnie przetwarzanego wiersza. W SQL Server do takiego skorelowanego wywołania służy operator CROSS APPLY albo OUTER APPLY. Operator CROSS APPLY wywołuje funkcję dla każdego wiersza źródła i usuwa wiersze, dla których funkcja nie zwróciła rezultatu.
OUTER APPLY działa podobnie, ale zachowuje wiersz źródłowy również wtedy, gdy funkcja nie zwróci żadnych danych. Kolumny pochodzące z funkcji przyjmują wtedy wartości NULL. Jest to zachowanie zbliżone do LEFT JOIN, lecz zastosowanie APPLY wynika z tego, że funkcja może być wywoływana osobno dla każdego wiersza tabeli nadrzędnej. Przykładem może być funkcja zwracająca historię zmian dla identyfikatora odczytanego z tabeli urządzeń. CROSS APPLY pokaże tylko urządzenia mające historię, natomiast OUTER APPLY uwzględni także urządzenia bez zapisanych zmian.
Warunek logiczny powinien być zapisany tak, aby nie zmieniać przypadkowo rodzaju złączenia. Umieszczenie filtra dotyczącego funkcji w klauzuli WHERE po użyciu LEFT JOIN może usunąć wiersze z wartościami NULL i w praktyce nadać zapytaniu cechy INNER JOIN. Jeżeli filtr ma ograniczać dane pochodzące z prawej strony, często właściwsze jest umieszczenie go w warunku ON. Analiza takiego zapytania wymaga więc sprawdzenia zarówno parametrów funkcji, jak i miejsca zastosowania każdego predykatu.
Kontrola wyników i wydajności połączenia
Podczas diagnostyki zapytania z funkcją tabelaryczną należy najpierw uruchomić samą funkcję z reprezentatywnymi parametrami i sprawdzić liczbę zwracanych wierszy, wartości kluczy oraz ewentualne duplikaty. Następnie warto wykonać samo zapytanie bazowe bez JOIN, aby ustalić, ile wierszy pochodzi z tabeli głównej. Porównanie tych dwóch wyników pomaga rozpoznać, czy problem wynika z braku dopasowań, wielokrotnych dopasowań, nieprawidłowego parametru czy filtra zastosowanego po złączeniu.
Istotne jest również sprawdzenie zgodności typów danych używanych w warunku połączenia. Łączenie kolumn o różnych typach może wymusić niejawną konwersję i ograniczyć możliwość efektywnego użycia indeksu. Znaczenie mają także długość tekstu, sposób porównywania wartości oraz obecność wartości NULL. Dwa identyfikatory wyglądające tak samo w aplikacji mogą nie być równoważne w bazie, jeśli różnią się typem, spacjami, wielkością liter zależną od kolacji albo sposobem reprezentacji braku wartości.
Wydajność należy oceniać na rzeczywistym rozmiarze danych, analizując plan wykonania, odczyty logiczne i czas trwania zapytania. Funkcja wywoływana przez APPLY może zostać wykonana dla wielu wierszy tabeli źródłowej, dlatego koszt pojedynczego wywołania może zostać zwielokrotniony. Warto sprawdzić indeksy na kolumnach używanych do filtrowania i łączenia, ograniczyć funkcję do niezbędnych kolumn oraz unikać pobierania danych, które później nie są wykorzystywane. Prawidłowy rezultat wymaga jednoczesnej kontroli semantyki relacji, parametrów funkcji i planu wykonania.