Operator APPLY w TSQL

Do napisania dzisiejszego artykułu skłonił mnie fakt, iż naprawdę niewiele osób piszących zapytania TSQL używa tzw. zaawansowanych elementów języka. Dlaczego tak się dzieje? Nie jestem pewien ale wydaje mi się, że słowo “zaawansowanych” już dostatecznie odstrasza potencjalnych zainteresowanych – w moim mniemaniu całkowicie niesłusznie. Jednym ze “straszniejszych” elementów TSQL jest bez wątpienia APPLY – część z was z pewnością  go używa, inni słyszeli ale nie widzą zastosowania, dla niektórych to zupełna nowość. Poniżej postaram się krótko przedstawić gdzie i w jaki sposób możemy niniejszego narzędzia użyć, a jest to narzędzie naprawdę potężne. Zaczynajmy!

Aby zrozumieć specyfikę działania naszego dzisiejszego bohatera musimy przypomnieć sobie teorię z zakresu operatorów złączenia czyli popularnych JOINów.  W teorii relacyjnych baz danych mamy do czynienia z tak zwanymi złączeniami wewnętrznymi (ang. INNER JOIN) oraz zewnętrznymi (ang. OUTER JOIN). W zależności od rodzaju złączenia dwa zestawy danych są ze sobą w określony sposób porównywane i zwracany jest rezultat. To co jest niezmierne istotne w działaniu złączeń to fakt, iż oba zestawy danych (na obrazku nazwane Table 1 oraz Table 2) muszą być predefiniowane – silnik musi mieć dane na temat obu stron operatora:rest

Co natomiast jeśli jedna ze stron, które chcemy dołączać nie zawiera predefiniowanego zbioru danych i nie możemy go zapisać przy pomocy znanych nam metod? Możemy użyć APPLY! Poniższy rysunek w pełni oddaje istotę działania APPLY.

rest

Jak można zauważyć APPLY oczekuje predefiniowanego zestawu po lewej stronie złączenia. Tak więc jeśli chcemy dla przykładu użyć funkcji tabelarycznej wiersz po wierszu nie możemy tego zrobić przy użyciu tradycyjnych złączeń. W tym momencie może Wam przyjść na myśl użycie kursorów – myślę, że możecie zapomnieć o tej myśli tak szybko jak przyszła Wam do głowy. Jak to działa sprawdźmy to na przykładzie! W naszych przykładach standardowo będziemy używać bazy Adventure Works. Jako nasz problem biznesowy weźmiemy sobie typową sytuację: potrzebujemy zapytania, które w dynamiczny sposób zwróci nam N ostatnich dat zamówień dla określonego klienta.

Po pierwsze potrzebujemy funkcji tabelarycznej, która nam coś takie zwróci – nie skupijamy się konkretnie jak ona działa – przyjmijmy ją jako czarną skrzynkę:

CREATE FUNCTION [dbo].[test]
(
   @custId int,
   @topn int
)
RETURNS TABLE
AS
RETURN
SELECT DISTINCT TOP (@topn) 
      dc.CustomerKey,
      fis.OrderDate 
FROM 
      AdventureWorksDW2012.dbo.DimCustomer AS dc
JOIN 
      AdventureWorksDW2012.dbo.FactInternetSales AS fis
ON 
      fis.CustomerKey=dc.CustomerKey
WHERE 
      dc.CustomerKey=@custId

Przetestujmy jej działanie w pierwszym parametrze podając identyfikator wybranego klienta, w drugim natomiast liczbę dat jakie chcemy otrzymać.

SELECT 
    * 
FROM 
    dbo.test(11001,2)

Jak widać poniżej funkcja działa zgodnie z przeznaczeniem.

Wreszcie nadszedł czas na przetestowanie naszego APPLY. Złączmy tabelę z klientami tak aby pokazać dwie ostatnie daty zakupów dla każdego z klientów:

USE AdventureWorksDW2012
GO

SELECT 
      dc.CustomerKey,
      t.OrderDate
FROM 
      dbo.DimCustomer AS dc
CROSS APPLY 
      dbo.test(dc.CustomerKey,2) AS t
ORDER BY 
      1 ASC,
      2 DESC

Rezultat jest taki jak się spodziewaliśmy – powinniśmy pamiętać o tym, że konkretnie CROSS APPLY logicznie działa bardzo podobnie do złączenia wewnętrznego czyli zwraca tylko te wiersze, które znajdują się w obu zestawach.

Wygląda to świetnie, co jednak gdy chcemy poznać klientów, którzy w ogóle nie złożyli żadnego zamówienia? Jak już wspomniałem CROSS APPLY działa jak INNER JOIN – tak więc czy jest coś podobnego do OUTER JOIN? Oczywiście! Mamy do dyspozycji OUTER APPLY. Sprawdźmy to na przykładzie. W bazie Adventureworks2012DW nie ma klienta, który nigdy nie złożył zamówienia, więc możemy sobie to zasymulować np. poprzez dodanie wiersza do tabeli DimCustomer. W zapytaniu dodamy również klauzulę WHERE gdzie pokażemy sobie tylko tych klientów, którzy nie mają połączenia do tabeli z zamówieniami.

USE AdventureWorksDW2012
GO

SELECT 
     dc.CustomerKey,
     t.OrderDate
FROM
     dbo.DimCustomer AS dc
OUTER APPLY 
     dbo.test(dc.CustomerKey,2) AS t
WHERE 
     OrderDate IS NULL
ORDER BY 
      1 ASC,
      2 DESC

Jak widać poniżej w rezultacie otrzymaliśmy jednego klienta, który nigdy nie złożył żadnego zamówienia.

rest

Tak więc użycie operatorów APPLY jest naprawdę proste i przyjemne w użyciu. Ponadto rozwiązują one całą masę problemów w naszej codziennej pracy. Co jednak z kwestą wydajności? Jak możecie się domyślać nie jest najlepiej ze względu na fakt, iż funkcje tabelaryczne nie są najwydajniejszą strukturą – jednak mimo wszystko warto znać te funkcjonalności języka TSQL gdyż w wielu przypadkach naprawdę się sprawdzają.

8 Comments

    • Polecam testowanie w swoich procedurach – w moim przypadku plan zapytania z użyciem cross apply w porównaniu do inline sql był o wiele bardziej wydajny 🙂

  1. Ciekawostka:
    cross apply zadziała tak jak outer apply gdy będziem używać go na agregatach
    np.
    cross apply( select SUM(bar) from foo where 1=0) as xxx

  2. Tzw zaawansowane elementy T-SQLa nie są omijane ze strachu, tylko z przykrej konieczności. Otóż większość programistów tworzy oprogramowanie komercyjne, dla klienta. A klienci w ogromnej większości są tacy, że w czasie, kiedy pięć razy zmienią swój samochód, na upgrade serwera SQLowego nie wydadzą ani złotówki, skutkiem czego w 2020 roku trzeba się trzymać składni sprzed 15 lat na przykład.
    Oczywiście nie przeszkadza im to krzywić się, jak cała aplikacja chodzi coraz wolniej i wolniej…

      • A jaką wartość dla klienta ma wdrożenie nowej wersji platformy? Z jego punktu widzenia to koszt, który jest niezasadny jeśli można zrealizować jego cele tą platformą, którą obecnie ma.
        To czego nie wydał na nieuzasadnione z perspektywy jego biznesu (cel został osiągnięty na obecnej platformie) jest jego zyskiem, który przeznaczył na samochody 😉

        • Dzięki za komentarz!

          Wyższa wersja platformy = dłużej będzie wspierana przez dostawcę oprogramowania. Posiada również dodatkowe możliwości związane np. z zabezpieczaniem, analizą czy też wizualizacją danych. Wszystko zależy od tego jak zdefiniujemy “cel biznesowy”. Oczywiście jeśli obecne oprogramowanie spełnia założone cele to wszystko jest w jak najlepszym porządku, jednak zawsze są jakieś koszty utrzymania, a one często rosną jeśli to oprogramowanie jest po prostu stare. Sam widziałem takie przypadki gdzie ktoś miał wszystko ustawione na oprogramowaniu z zeszłego wieku i możliwości migracji były bardzo ograniczone żeby nie powiedzieć żadne co przysporzyło tej firmie wielu problemów wydajnościowych i nie tylko. Tak więc czym innym jest pogoń za tym co najnowsze jak najszybciej, a zasiedzenie się w technologii która utraciła wsparcie dostawcy:)

Leave a Reply