Arsenał programisty T-SQL Common Table Expression (CTE Autor: Paweł Potasiński, Marek Adamczuk Ostatnie dwie wersje systemu Microsoft SQL Server 2005 i 2008 dostarczają zestaw nowości w języku Transact-SQL (T-SQL, dzięki którym rozwiązywanie najbardziej wyrafinowanych problemów stawianych przed programistami baz danych staje się o wiele prostsze. Seria Arsenał programisty T-SQL ma na celu przedstawienie tych konstrukcji dostępnych w T-SQL w SQL Server 2008, które mogą się przydad programistom najbardziej. Jedną z takich konstrukcji jest Common Table Expression (CTE. CTE podstawy CTE to nazwane zapytanie reprezentujące tymczasowy zestaw rekordów definiowany w zasięgu jednego polecenia, INSERT, UPDATE lub DELETE. Składniowo CTE przypomina podzapytanie w z języka Transact-SQL lub elementy obliczane z języka MDX. Składnię CTE łatwo rozpoznad po charakterystycznym słowie WITH. Słowo to jest w języku Transact-SQL wykorzystywane między innymi do podawania wskazówek (ang. hint dla optymalizatora lub do deklarowania przestrzeni nazw XML (klauzula WITH XMLNAMESPACES. Aby uniknąd błędów parsowania, definicję CTE należy oddzielid średnikiem od poprzedniego polecenia. Przy braku średnika przed słowem WITH parser zwróci następujący komunikat błędu: Msg 319, Level 15, State 1, Line 2 Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. Przykład prostego CTE: WITH MojePierwszeCTE AS ( Name FROM HumanResources.Department * FROM MojePierwszeCTE; Name -------------------------- Document Control Engineering Executive Facilities and Maintenance... Tool Design (16 row(s affected 1
Zestawem generowanym przez CTE o nazwie MojePierwszeCTE jest zbiór wartości kolumny Name z tabeli HumanResources.Department. Zapytanie wybiera wszystkie dane ze zbioru generowanego przez CTE. Zmieomy nieco definicję CTE z powyższego przykładu. WITH MojePierwszeCTE (Department AS ( Name FROM HumanResources.Department * FROM MojePierwszeCTE; Podobnie jak przy definiowaniu widoków, użytkownik może zdefiniowad nazwy dla kolumn generowanych przez CTE (tu kolumna została nazwana Department. Należy pamiętad, że nazwy kolumn w CTE muszą byd unikalne. Nie mamy tu do czynienia z klasycznymi aliasami określanymi dla kolumn w zapytaniach. Analogicznie działa składnia, którą można by uznad za podanie aliasów dla kolumn: WITH MojePierwszeCTE AS ( Name AS Department FROM HumanResources.Department * FROM MojePierwszeCTE; W powyższym przykładzie ponownie słowo Department nie jest aliasem dla kolumny Name, ale jest nazwą kolumny generowanej przez CTE. Jedno CTE czy więcej? Definicji CTE nie można bezpośrednio zagnieżdżad. To ograniczenie można jednak łatwo obejśd poprzez zastosowanie więcej niż jednego CTE dla pojedynczego polecenia DML. Ta właściwośd CTE daje ogromne możliwości uzyskiwania krok po kroku pożądanego wyniku. Każde kolejne definiowane CTE może odwoływad się do CTE wcześniej zdefiniowanych. W poniższym przykładzie, dla każdego z dostawców, do których złożyliśmy zamówienia prezentujemy datę ostatniego i ewentualnego poprzedniego zamówienia: WITH cte_vendors AS ( v.businessentityid AS VendorID, v.name FROM Purchasing.Vendor v WHERE v.activeflag = 1, cte_orders AS ( ROW_NUMBER( OVER (PARTITION BY o.vendorid ORDER BY o.shipdate DESC rn, o.vendorid, o.shipdate 2
FROM Purchasing.PurchaseOrderHeader o INNER JOIN cte_vendors cv ON o.vendorid = cv.vendorid cv.vendorid, cv.name, co1.shipdate LastShipDate, co2.shipdate PrevShipDate FROM cte_vendors cv INNER JOIN cte_orders co1 ON co1.vendorid = cv.vendorid AND co1.rn = 1 LEFT OUTER JOIN cte_orders co2 ON co2.vendorid = cv.vendorid AND co2.rn = 2; Przykład ten pokazuje również, w jaki sposób obejśd ograniczenie składni zabraniające odwoływania się do funkcji rankingowych poza listą pól i klauzulą ORDER BY. Nie tylko O ile zapytanie stanowiące CTE jest edytowalne (reguły jak w widoku, to i na samym CTE możemy wykonywad operacje modyfikujące, tzn. INSERT, UPDATE czy DELETE. Co więcej, dzięki CTE możliwe jest obejście pewnego ograniczenia związanego z nową składnią UPDATE i DELETE. Otóż od wersji SQL Server 2005 wprowadzono do składni poleceo UPDATE i DELETE klauzulę TOP, pozwalającą usuwad czy nadpisywad zadaną liczbę wierszy. Niestety, nowej składni nie rozszerzono o klauzulę ORDER BY, w ten sposób znacznie ograniczając użytecznośd klauzuli TOP. Bez ORDER BY nie jesteśmy w stanie kontrolowad, które konkretnie wiersze zostaną nadpisane lub usunięte. I tu znowu z pomocą spieszy nam CTE. W poniższym przykładzie z tabeli dbo.numbers o 100 kolejnych liczbach usuniemy najpierw ostatnie 30, a następnie pierwsze 30 wierszy. WITH cte_numbers AS ( TOP 99.9999999999999 PERCENT * FROM dbo.numbers ORDER BY Number DESC DELETE TOP (30 FROM cte_numbers; -- usuwa wiersze od 71 do 100 WITH cte_numbers AS ( TOP 99.9999999999999 PERCENT * FROM dbo.numbers ORDER BY Number ASC DELETE TOP (30 FROM cte_numbers; - usuwa wiersze od 1 do 30 Rozwiązanie polega na zastosowaniu żądanego ORDER BY wewnątrz CTE. Klauzula TOP 99.999999999 PERCENT jest niezbędna, bowiem bez niej w CTE nie możemy stosowad ORDER BY, podobnie jak w widokach czy podzapytaniach. Nie możemy również użyd TOP 100 PERCENT, bowiem dla tej wartości ignorowane jest ORDER BY, jako niestanowiące warunku filtrowania dla TOP. 3
Rekursywne CTE CTE w przeciwieostwie do podzapytao, ma pewną unikalną właściwośd. Polega ona na możliwości odwołania CTE do samego siebie wewnątrz definicji. Rekursywne CTE składa się z co najmniej dwóch zapytao połączonych operatorem zbiorowym UNION ALL. Pierwszy człon, to zapytanie inicjujące, np. szczyt hierarchii. Drugie zapytanie, odwołujące się do samej definicji CTE będzie rekursywnie dołączad rekordy do zbioru wynikowego. Przy czym ważne, by pamiętad, że każda iteracja w rekursywnym CTE wykonywana jest w oparciu o rekordy zwrócone przez poprzednią iterację (oczywiście wyjątkiem jest tu zapytanie inicjujące. Drugie zapytanie w rekursywnym CTE będzie wykonywane dopóki będą zwracane jakiekolwiek nowe rekordy lub zostanie przekroczony poziom zagnieżdżenia. Nie należy mylid poziomu zagnieżdżenia CTE z poziomem zagnieżdżenia wywołao obiektów zwracanym przez funkcję @@NESTLEVEL. Domyślna wartośd poziomu zagnieżdżenia CTE jest ustawiona na 100 i nie ma niestety żadnego parametru, którym można byłoby ją regulowad. Możliwa jest natomiast modyfikacja maksymalnego poziomu zagnieżdżenia w samym zapytaniu opcją MAXRECURSION. Ustawienie tej opcji na 0 powoduje wyłączenie kontroli poziomu zagnieżdżenia. Zapytanie będące członem rekursywnym CTE jest obarczone pewnymi ograniczeniami nie może zawierad: słowa kluczowego DISTINCT, klauzuli GROUP BY, klauzuli HAVING, agregacji skalarnej, klauzuli TOP, złączeo zewnętrznych (LEFT JOIN, RIGHT JOIN, FULL JOIN, podzapytao, wskazówek dla optymalizatora (jeżeli dotyczą samego CTE. Poniższe fragmenty kodu ilustrują metodę generowania tabeli liczb (tu -od 1 do 100 oraz tabeli dat (tu od 1 stycznia 1990 do 31 grudnia 2020. DECLARE @MinNumber int, @MaxNumber int; @MinNumber = 1, @MaxNumber = 100; WITH Numbers AS ( @MinNumber AS Number UNION ALL Number + 1 FROM Numbers WHERE Number < @MaxNumber Number INTO dbo.numbers 4
FROM Numbers ORDER BY Number OPTION (MAXRECURSION 0; -- bez ograniczenia poziomu rekursji DECLARE @MinDate datetime, @MaxDate datetime; @MinDate = '19900101', @MaxDate = '20201231'; WITH Dates AS ( @MinDate AS Date UNION ALL DATEADD(day, 1, Date FROM Dates WHERE Date < @MaxDate Date, YEAR(Date AS Year, MONTH(Date AS Month, DATENAME(weekday, Date AS Weekday INTO dbo.dates FROM Dates ORDER BY Date OPTION (MAXRECURSION 0; Po wygenerowaniu w powyższy sposób tabele dobrze jest poindeksowad (np. dodając klucze główne z indeksem grupowanym odpowiednio na kolumnach Number i Date. Dzięki rekursywnemu CTE można z łatwością wydobyd informacje o strukturze hierarchii, m.in. numer poziomu, na jakim znajduje się dany wiersz w hierarchii. Przykład: USE AdventureWorksLT WITH Categories AS ( ProductCategoryID, Name, 0 AS Level, CAST('/' + Name AS nvarchar(max AS Path FROM SalesLT.ProductCategory WHERE ParentProductCategoryID IS NULL UNION ALL PC.ProductCategoryID, PC.Name, C.Level + 1, CAST(C.Name + '/' + PC.Name AS nvarchar(max FROM Categories AS C INNER JOIN SalesLT.ProductCategory AS PC ON PC.ParentProductCategoryID = C.ProductCategoryID * FROM Categories; 5
W powyższym przykładzie w CTE o nazwie Categories w pierwszej iteracji wybierane są kategorie, które nie mają kategorii. Podsumowanie Common Table Expression to potężna broo w rękach programisty T-SQL. Dzięki ogromnej elastyczności wynikającej z możliwości definiowania wielu CTE dla jednego zapytania oraz z tego, że w prosty sposób można zbudowad w nich zaimplementowad rekursywnośd, jest to jeden z najbardziej docenianych elementów składni języka T-SQL. Opanowanie CTE jest, w naszym odczuciu, obowiązkowe dla każdego, kto pracuje z systemem SQL Server 2005 lub 2008. Autor Paweł Potasiński (MVP, MCT, MCDBA, MCSE, MCSD, MCITP Starszy programista w Asseco Business Solutions S.A. W codziennej pracy zajmuje się rozwojem aplikacji ERP oraz zagadnieniami z zakresu R&D. Wykładowca akademicki na kilku uczelniach w Warszawie. Wcześniej pracował jako administrator baz danych oraz trener. Założyciel i lider Polskiej Grupy Użytkowników SQL Server. Wolontariusz organizacji GITCA wspierającej działalnośd grup pasjonackich IT Pro. Prelegent na licznych konferencjach krajowych i zagranicznych, m.in. European PASS Conference i Microsoft Technology Summit. Microsoft Most Valuable Professional (MVP w kategorii SQL Server od lipca 2008 roku. Marek Adamczuk (MVP, MCSE, MCDBA, MCTS Kierownik Działu Zarządzania Wiedzą i Szlokeo w Asseco Business Solutions (dawniej Softlab. Współtworzy system klasy ERP zbudowany na platformie SQL Server. Aktywnie uczestniczy we wdrożeniach modułów Logistyka i Sprzedaż oraz CRM, a także w budowie specjalizowanych modułów na potrzeby poszczególnych wdrożeo. Specjalizuje się w budowie mechanizmów autogeneracji kodu T-SQL i zapewnienia wysokiej jego wysokiej jakości. Moderator i opiekun działu SQL Server na portalu wss.pl. Aktywny prelegent Polskiej Grupy Użytkowników SQL Server. Absolwent Kierunku Fizyka, specjalności Fizyka Komputerowa Uniwersytetu Marii Curie-Skłodowskiej w Lublinie. W lipcu 2009 roku wyróżniony przez Microsoft tytułem Most Valuable Professional (MVP w kategorii SQL Server. 6