Spis treści Od autora .......................................................................................... 5 Rozdział 1. Podstawowe informacje o serwerze .................................................... 9 Rozdział 2. Instalacja i konfiguracja środowiska ................................................. 13 Rozdział 3. Język zapytań SQL w MS SQL Server ................................................ 35 3.1. Zapytania wybierające ............................................................................................. 35 3.2. Zapytania modyfikujące dane .................................................................................. 94 3.3. Tworzenie i modyfikacja tabel i perspektyw ......................................................... 103 3.4. Modyfikowanie tabel ............................................................................................. 141 3.5. Perspektywy (widoki) ............................................................................................ 148 3.6. Tworzenie typu użytkownika ................................................................................. 167 3.7. Tworzenie indeksów .............................................................................................. 171 3.8. Inne narzędzia klienckie MS SQL Server .............................................................. 188
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL ............................... 193 Rozdział 5. Rozszerzenia proceduralne Transact-SQL ......................................... 221 5.1. Podstawowe instrukcje .......................................................................................... 221 5.2. Procedury składowane ........................................................................................... 227 5.3. Funkcje .................................................................................................................. 236 5.4. Synonimy i błędy użytkownika ............................................................................. 241 5.5. Procedury wyzwalane ............................................................................................ 247 5.6. Kursory .................................................................................................................. 280 5.7. Zmienna tabelaryczna i typ tabelaryczny ............................................................... 298
Rozdział 6. Przetwarzanie transakcyjne ............................................................ 303 6.1. Transakcje. Podstawy teoretyczne ......................................................................... 303 6.2. Transakcje. Przykłady realizacji ............................................................................ 307 6.3. Obsługa wyjątków ................................................................................................. 316
Rozdział 7. Typy złożone .................................................................................. 323 7.1. Typ tabelaryczny ................................................................................................... 323 7.2. Typ hierarchiczny .................................................................................................. 328 7.3. Typy geometry i geography ................................................................................... 334 7.4. Typy użytkownika CLR ........................................................................................ 349 7.5. Elementy proceduralne CLR ................................................................................. 359
4
MS SQL Server. Zaawansowane metody programowania
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego .................... 381 8.1. Klasyfikacja ........................................................................................................... 381 8.2. Funkcje agregujące definiowane przez użytkownika ............................................. 394 8.3. Analiza sieci powiązań .......................................................................................... 400
Zakończenie .................................................................................. 409 Literatura ....................................................................................... 411 Skorowidz ..................................................................................... 419
Od autora Szanowny Czytelniku! Zgodnie z obietnicą daną przed dwoma laty przedstawiam Ci kolejną książkę z niekończącej się opowieści o przetwarzaniu danych. Tym razem jest ona poświęcona środowisku MS SQL Server. Chociaż wydaje się, że na skutek postępującej standaryzacji wszystkie środowiska zbliżają się do siebie, to jednak każde z nich ma swoje cechy charakterystyczne, co powoduje, że musi być traktowane oddzielnie. Tym razem prezentuję informacje związane ze środowiskiem serwera bazy danych opracowanym przez Microsoft. Paradoksalnie było ono pierwszym dużym komercyjnym rozwiązaniem, z którym mogłem pracować, natomiast ta książka jest czwartą z kolei dotyczącą przetwarzania, a trzecią poświęconą bazom transakcyjnym [1] [2] [5]. Jeśli zapoznałeś się z poprzednimi moimi książkami, to tutaj znajdziesz elementy, które są Ci już dobrze znane. Chciałbym jednak, aby mogły korzystać z tej książki również te osoby, które pierwszy raz stykają się z tematyką baz danych. Specyfika serwera MS SQL Server sprawia, że już na początku znajdzie się wiele rozwiązań dla niego charakterystycznych, co pozwoli osobom, które znają poprzednie moje publikacje, uzyskać wiele nowych informacji. Natomiast w części poświęconej rozszerzeniu proceduralnemu i typom złożonym prezentowane rozwiązania są w całości oryginalne, co wynika z istotnych różnic składniowych między środowiskami, ale także z filozofii przetwarzania danych odmiennej niż np. w Oracle [1]. Moje dwudziestoletnie doświadczenie w prowadzeniu zajęć, zarówno wykładów, jak i laboratoriów z zastosowaniem tego serwera, pozwala mi omówić wiele złożonych rozwiązań. Chociaż przez lata rozwiązania były modyfikowane, to przełomowa zmiana nastąpiła dość dawno, między wersjami 6. a 6.5. Obecnie każda kolejna wersja rozszerza możliwości poprzedniej, nie wprowadzając istotnych zmian w istniejących elementach składni. W książce omawiam stan dla wersji 2012. Pokazuję zawsze, kiedy rozwiązanie jest charakterystyczne dla tego serwera, tak aby użytkownicy starszych wersji mieli świadomość, że jest ono nowe i że nie będzie funkcjonować na ich bazach. Jako inżynier jestem przywiązany do praktycznego podejścia, stąd książka zawiera bardzo dużą liczbę przykładów, które zostały sprawdzone na komputerze. Mam więc nadzieję, że nie pojawią się takie przykłady, których Czytelnik nie będzie mógł wykonać na swoim komputerze. Aby to było łatwiejsze, na serwerze FTP wydawnictwa zostanie umieszczona baza z przykładowymi danymi.
6
MS SQL Server. Zaawansowane metody programowania
Poza wieloma prostymi przykładami ilustrującymi elementy składni języka zapytań SQL i jego rozszerzenie Transact-SQL pojawią się w książce przykłady złożonych zadań, które z powodzeniem można rozwiązać, używając tego języka. Moim celem było zarówno pokazanie stopnia trudności problemów, jak i elegancji rozwiązań wykorzystujących silne strony baz danych. Podobne podejście znajdzie Czytelnik w książce Celko, jednak abstrahuje on od konkretnego środowiska przetwarzania [7]. Chciałem ponadto podkreślić, że trudne zadanie nie musi oznaczać bardzo złożonej struktury danych, lecz może tylko wskazywać na poziom oczekiwanych rezultatów. Pomimo pewnej dozy abstrakcji wszystkie stawiane zadania mają odzwierciedlenie w otaczającej nas rzeczywistości. Stanowią albo pełne, albo uproszczone, ale w pełni funkcjonalne rozwiązania. Pomimo dużej ilości przygotowanego wcześniej materiału, gromadzonego w trakcie zajęć prowadzonych od wielu lat na różnego rodzaju studiach czy też uzyskanego w wyniku realizowania własnych prób i eksperymentów, czas redagowania tekstu tej książki był wyjątkowo długi. Wynikało to w dużej mierze z licznych obowiązków zawodowych, co jest powszechnym udziałem większości aktywnych ludzi dzisiejszych czasów — signum temporis. Jednak najwięcej nakładów wymagało dokonanie wyboru przykładów, ich uporządkowanie i ostateczna weryfikacja numeryczna. Nie bez znaczenia był również czas związany z samym procesem redagowania tekstu. Podobnie jak w przypadku poprzednich książek, nie udało mi się dotrzymać słowa, że niniejsza publikacja pojawi się tuż po poprzedniej, za co przepraszam wszystkich Czytelników. Mam jednak nadzieję, że czas ten wpłynął pozytywnie na jakość i sposób prezentacji zawartej tutaj wiedzy. Nigdy nie działamy w próżni, oderwani od otaczających nas ludzi. Dlatego niemały wpływ na ostateczną postać książki wywarły kolejne edycje wykładów i studiów podyplomowych. Dziękuję wszystkim moim studentom, których dociekliwość i pytania zaowocowały poszukiwaniem lepszych sposobów wyjaśniania problemów, nowych ścieżek narracji, dogodniejszej kolejności realizacji tematów. Wiele zawdzięczam swoim doktorantom, którzy współprowadząc laboratoria, wskazywali, na jakie problemy dydaktyczne natrafiają w pracy ze studentami, co znacznie wzbogaciło doświadczenia wynikające z moich własnych obserwacji. Dotyczy to również zajęć w ramach Centrum Kształcenia Międzynarodowego prowadzonych w języku angielskim dla studentów pochodzących z innych krajów Europy. Bardzo wiele zawdzięczam corocznym spotkaniom ze specjalistami z różnych ośrodków akademickich. Spotkania te odbywają się w ramach konferencji BDAS (Bazy Danych, Aplikacje i Systemy). Chociaż nasze dyskusje nie zawsze były związane ze szczegółowymi rozwiązaniami formalnymi czy elementami składni, to na pewno były zawsze bardzo inspirujące. Za stworzenie takiej możliwości pragnę podziękować przede wszystkim głównemu organizatorowi i dobremu duchowi tego wydarzenia Prof. Stanisławowi Kozielskiemu. Wymienienie nazwisk wszystkich przyjaciół, którzy są związani z tymi bardzo ważnymi wydarzeniami naukowymi i środowiskowymi, czy to w roli organizatorów, członków Rady Programowej, czy uczestników, nie byłoby możliwe, dlatego pozwolę sobie na wyrażenie wspólnych serdecznych podziękowań i zarazem szacunku oraz uznania dla ich pracy i wiedzy.
Od autora
7
Odrębne podziękowanie należy się moim NAJBLIŻSZYM, Justynie i Magdzie, które wykazywały duże zrozumienie, kiedy wieczorami siedziałem przy komputerze, pisząc tę książkę, zamiast poświęcać czas rodzinie. Chociaż starałem się wypełniać swoje obowiązki rodzinne, to przecież na nich życie rodzinne się nie kończy. Dlatego dziękuję Wam, że mogłem poświęcić czas na aktywność twórczą.
8
MS SQL Server. Zaawansowane metody programowania
Rozdział 1.
Podstawowe informacje o serwerze SQL Server firmy Microsoft jest jednym z najpopularniejszych komercyjnych rozwiązań dostępnych na rynku. Powstał w roku 1989, a opracowano go przy ścisłej współpracy z firmą Sybase. Produkty MS SQL i Sybase SQL rozwijały się wspólnie do wersji 4.21. Od wersji 6.0 losy tych serwerów baz danych definitywnie się rozdzieliły. Pomimo tego oba narzędzia do dziś zachowują wiele cech wspólnych, zarówno funkcjonalnych, jak i tych najłatwiej zauważalnych, związanych z warstwą prezentacyjną — grafiką. W tabeli 1.1 przedstawiono skrótowo rozwój SQL Server. Tabela 1.1. Etapy rozwoju SQL Server Wersja
Rok wydania
Nazwa oficjalna
Nazwa firmowa
1.0 (OS/2)
1989
SQL Server 1.0 (wersja 16-bitowa)
–
1.1 (OS/2)
1991
SQL Server 1.1 (wersja 16-bitowa)
–
4.21 Win NT
1993
SQL Server 4.21
SQLNT
6.0
1995
SQL Server 6.0
SQL95
6.5
1996
SQL Server 6.5
Hydra
7.0
1999
SQL Server 7.0
Sphinx
7.0
1999
SQL Server 7.0 OLAP Tools
Plato
8.0
2000
SQL Server 2000
Shiloh
8.0
2003
SQL Server 2000 (wersja 64-bitowa)
Liberty
9.0
2005
SQL Server 2005
Yukon
10.0
2008
SQL Server 2008
Katmai
10.25
2010
SQL Azure
Matrix
10.5
2010
SQL Server 2008 R2
Kilimanjaro
11.0
2012
SQL Server 2012
Denali
Rozdział 1. Podstawowe informacje o serwerze
11
silnikiem połączone narzędzia do replikacji, tzn. do dystrybuowania danych zgromadzonych w centralnej bazie danych do subskrybentów oraz w kierunku przeciwnym. Silnik zapewnia synchronizację kopii danych i ich spójność. Korzystanie z tych narzędzi wymaga włączenia serwisu MS SQL Server Agent, który przy standardowej instalacji jest uruchamiany ręcznie. Obok silnika bazy danych działa silnik analityczny Analysis Services, w którego skład wchodzą narzędzia służące do: zarządzania strukturami wielowymiarowymi — hurtownie danych
(ang. data warehousing — OLAP); wnioskowania z danych — zgłębiania danych (ang. data mining).
Na rzecz obu silników działa Service Broker, który zapewnia możliwość tworzenia systemu przesyłania powiadomień (następca Notification Services) zarówno między bazami relacyjnymi, jak i między systemami analitycznymi oraz zewnętrznymi aplikacjami napisanymi w językach wyższego rzędu. Reporting Services to narzędzie do tworzenia raportów, ich przetwarzania i zarządzania nimi — ze struktur relacyjnych i wielowymiarowych. Pozwala na programowanie zdarzeń oraz dostosowanie funkcjonalności i dostępności raportów do potrzeb użytkownika. Narzędzie to zawiera własny serwer http odpowiedzialny za dostarczenie raportów za pośrednictwem sieci. Serwer ten zastępuje stosowany w starszych wersjach IIS (Internet Information Services). Na rzecz serwera bazy danych, serwera analitycznego oraz raportującego działa Integration Services, tj. narzędzie do migracji, integracji i transformacji danych pochodzących zarówno ze źródeł homologicznych, jak i heterogenicznych.
12
MS SQL Server. Zaawansowane metody programowania
Rozdział 2.
Instalacja i konfiguracja środowiska Proces instalacji MS SQL Serwer jest dość intuicyjny i nie wymaga bardzo szczegółowego omówienia. Dla porządku jednak zostaną przedstawione najistotniejsze kroki instalacji, ze szczególnym podkreśleniem tych, przy których mniej doświadczony użytkownik może mieć pewne wątpliwości. Po uruchomieniu instalatora pojawia się okno o nazwie SQL Server Installation Center, które zawiera zakładki: Planning, zawierającą elementy związane z wymaganiami oraz dostępną
dokumentacją: Hardware and Software Requirements; Security Documentation; Online Realise Notes; Setup Documentation; How to Get SQL Server Data Tools; System Configuration Checker; Install Upgrade Advisor; Online Installation Help; How to Get Started with SQL Server 2012 Failover Clustering; How to Get Started with PowerPivot for SharePoint Standalone Server
Installation; How to Get Started with Reporting Services SharePoint Integration on
a Standalone Server; Upgrade Documentation; Install SQL Server Migration Assistant (SSMA); How to apply SQL Server updates;
14
MS SQL Server. Zaawansowane metody programowania Installation (rysunek 2.1), która jest podstawową zakładką w procesie instalacji,
zawierającą elementy: New SQL Server stand-alone installation or add features to an existing
installation — nowa instalacja instancji serwera albo dodanie funkcjonalności do już zainstalowanej; New SQL Server failover cluster — nowa instancja węzła w przypadku
konfiguracji w postaci klastra (przetwarzanie w gridzie — sieci); Add node to a SQL Server failover cluster — dodanie węzła do istniejącej
instancji klastra; Upgrade from SQL Server 2005, SQL Server 2008 or SQL Server 2008 R2
— aktualizacja starszej wersji instancji serwera do wersji aktualnej; Maintenance (zarządzanie), zawierającą elementy: Edition Upgrade — aktualizacja istniejącej instancji serwera do bardziej
ogólnej (wyższej) edycji; Repair — naprawienie zainstalowanej instancji serwera; Remove node from a SQL Server failover cluster — usunięcie węzła z instancji
klastra; Launch Windows Update to search for product updates — wyszukiwanie
uaktualnień dla zainstalowanej instancji bazy danych;
Rysunek 2.1. Instalacja SQL Server 2012 — podstawowa zakładka
Rozdział 2. Instalacja i konfiguracja środowiska
15
Tools, zawierającą elementy: System Configuration Checker — sprawdzenie zgodności ze stanem
faktycznym wymagań systemowych dla instalacji serwera; Installed SQL Server features discovery report — wyświetlenie informacji
o dostępnych funkcjonalnościach instancji serwera; Microsoft Assessment and Planning (MAP) Toolkit for SQL Server —
wspomaganie migracji między serwerami baz danych różnych producentów; PowerPivot Configuration Tool — konfigurowanie PowerPivot dla SharePoint; Resources, pozwalającą na dostęp do informacji technicznej i zawierającą
elementy: SQL Server 2012 Books Online; SQL Server TechCenter; SQL Server Developer Center; SQL Server Evaluation Product Web site; License agreement; Register your copy of SQL Server 2012 Express; Microsoft Privacy Statement; Community; Codeplex samples Web site; Advanced, stanowiącą zestaw zaawansowanych narzędzi konfiguracyjnych
i zawierającą elementy: Install base on configuration file — instalacja serwera na podstawie wcześniej
utworzonego pliku konfiguracyjnego; Advanced cluster preparation — zaawansowane opcje instalacji serwera
do postaci klastra; Advanced cluster completion — dokończenie instalacji serwera do postaci
klastra; Image preparation of a stand-alone instance of SQL Server — przygotowanie
obrazu na podstawie instancji zainstalowanego serwera; Image completion of a prepared stand-alone instance of SQL Server
— dokończenie tworzenia obrazu na podstawie instancji zainstalowanego serwera; Options, pozwalająca na wybór procesora, na którym prowadzona jest instalacja
(x86, x64, ia64), oraz wskazanie napędu (folderu) zawierającego źródła do instalacji (domyślnie ustawiany na miejsce, z którego uruchomiono aplikację instalatora).
16
MS SQL Server. Zaawansowane metody programowania
Po wybraniu podstawowej, pojedynczej, nowej instancji SQL Serwer pojawia się okno, w którym możemy obserwować proces sprawdzania zgodności formalnych wymagań systemowych ze stanem rzeczywistym — rysunek 2.2.
Rysunek 2.2. Zakończenie procesu weryfikacji wymagań programu
Po pierwszym procesie sprawdzenia następuje kolejna weryfikacja wymagań, określana jako Setup Support Rules. Gdy weryfikacja zakończy się pozytywnie, w kolejnym oknie dokonujemy wyboru zakresu prowadzonej instalacji — rysunek 2.3. Domyślną opcją jest instalacja pełnego serwera bazy danych z możliwością wyboru odpowiadających użytkownikowi cech środowiska. Pozostałe pozwalają skonfigurować serwer dla potrzeb współpracy z MS SharePoint oraz instalacji bez możliwości ustalenia cech indywidualnie — wszystkie będą miały ustawione wartości domyślne ustalone przez producenta. W następnym kroku (rysunek 2.4) ustalany jest szczegółowy zakres instalacji. Wybieramy w nim te komponenty, które zostaną zainstalowane. Stan domyślny wskazuje na te elementy, których zainstalowanie jest niezbędne do poprawnego działania instancji serwera. Oczywiście podstawą jest silnik bazy danych (Database Engine), ale wskazane jest zainstalowanie również silnika analitycznego i raportującego oraz narzędzi integracyjnych. Dla mniej doświadczonych użytkowników wskazane jest wybranie pełnego zestawu narzędzi (o ile pozwalają na to zasoby sprzętowe), ze szczególnym uwzględnieniem plików pomocy, pozwalających na korzystanie ze wsparcia bez konieczności łączenia się z siecią.
Rozdział 2. Instalacja i konfiguracja środowiska
Rysunek 2.3. Wybór zakresu instalacji środowiska
Rysunek 2.4. Wybór komponentów do zainstalowania
17
18
MS SQL Server. Zaawansowane metody programowania
Gdy dokonamy wyboru, w kolejnym kroku następuje weryfikacja wymagań wynikających z wybranych komponentów (Installation Rules), która nie została przedstawiona graficznie. Jeśli zasoby nie będą wystarczające, wymagane jest wybranie innej lokalizacji albo zredukowanie liczby instalowanych elementów. Kolejny etap to ustalenie nazwy instalowanej instancji (rysunek 2.5). Kiedy instalujemy pierwszą instancję, wskazane jest pozostawienie opcji Default instance — co ustala jej nazwę na MSSQLSERVER. Ponieważ na jednym komputerze może być zainstalowanych wiele instancji, w przypadku instalowania kolejnej konieczne jest nadanie innej, niedomyślnej nazwy. W tym samym oknie dialogowym możemy ustawić inną niż domyślna ścieżkę do katalogu, w którym będzie odbywała się instalacja, oraz uzyskać informację o wcześniej zainstalowanych instancjach serwera. Niekiedy użytkownik może być zaskoczony tym, że pomimo iż nie instalował świadomie wcześniej żadnych instancji serwera, pojawia się informacja o już zainstalowanych komponentach. Dzieje się tak, gdy zostało zainstalowane środowisko .NET, dla którego domyślnym składnikiem jest SQL Server w wersji Express.
Rysunek 2.5. Definicja instancji serwera
Po tym kroku następuje sprawdzenie wymagań związanych z zasobami dyskowymi. Tak samo jak poprzednio, jeśli zasoby okażą się niewystarczające, należy albo zmienić lokalizację (dysk), albo cofając się do właściwego okna dialogowego, zmniejszyć liczbę instalowanych komponentów. Kolejny etap to wybór konta, na rzecz którego będą uruchamiane poszczególne serwisy serwera, oraz trybu ich uruchomienia (automatyczny, ręczny) — rysunek 2.6. Ten etap konfiguracji nastręcza sporo problemów, ponieważ wydaje się, że najlepszym wyborem będzie ustawienie konta lokalnego administratora. W takim przypadku
Rozdział 2. Instalacja i konfiguracja środowiska
19
Rysunek 2.6. Wybór sposobu uruchamiania usług
usługi nie będą włączały się automatycznie po uruchomieniu systemu. Dlatego najrozsądniejsze wydaje się przypisanie praw do uruchomienia serwisów jednej spośród usług, która jest automatycznie uruchamiana podczas startu systemu operacyjnego i w konsekwencji automatycznie uruchomi serwisy bazy danych. W wersjach 2008 i 2008 R2 dla większości serwisów do wyboru mieliśmy: usługę sieciową, usługę lokalną i system. Obecnie każdy serwis ma dedykowaną usługę i pomimo że można skorzystać z wyboru innej usługi za pomocą pozycji <
> (rysunek 2.7), to zalecam pozostawienie stanu domyślnego, który zapewnia poprawne funkcjonowanie środowiska.
Rysunek 2.7. Wybór niedomyślnych opcji uruchamiania usług serwera
20
MS SQL Server. Zaawansowane metody programowania
W starszych wersjach możliwe było jednoczesne przypisanie jednego serwisu do wszystkich usług za pomocą przycisku Use the same account for all SQL Server services, z czego w wersji 2012 zrezygnowano, co podkreśla zasadność stosowania ustawień domyślnych. Po zatwierdzeniu wyboru pozostaje tylko ustanowienie, które z usług będą mimo wszystko uruchamiane ręcznie. W przypadku dużej pamięci RAM można pozostawić ustawienia domyślne. Natomiast przy niewielkich zasobach proponuję pozostawienie tylko automatycznego uruchamiania serwisu silnika bazy danych. Pozostałe serwisy będą wtedy uruchamiane ręcznie za pomocą narzędzi zarządzania komputerem — pozycja Usługi. Kolejnym etapem jest ustalenie dostępnych trybów uwierzytelnienia (rysunek 2.8). Stanem domyślnym jest autoryzacja za pomocą systemu operacyjnego — Windows authentication mode. Dostępny jest wówczas tylko ten tryb uwierzytelnienia. Drugi stan to Mixed Mode, który pozwala na ustawienie dwóch trybów uwierzytelnienia: opartego na systemie operacyjnym oraz niezależnego od uwierzytelnienia w Windows trybu autoryzacji. Proponuję ustanowienie tego drugiego sposobu. W tym przypadku należy ustalić hasło dla tworzonego w tym trybie superadministratora o nazwie sa. W starszych wersjach MS SQL Server każdy administrator Windows stawał się automatycznie administratorem serwera bazy danych. Obecnie w kontrolce Specify SQL Server administrators należy podać tych użytkowników lub ich grupy, którzy lub które otrzymają takie uprawnienia. Możemy użyć przycisku Add Current User, który nadaje takie prawa bieżącemu (zalogowanemu w czasie procesu instalacji) użytkownikowi Windows, albo Add…, który pozwala na dokonanie wyboru innego użytkownika lub grupy.
Rysunek 2.8. Wybór trybów autoryzacji do SQL Server
Rozdział 2. Instalacja i konfiguracja środowiska
21
Proponuję w tym miejscu dodać przynajmniej grupę lokalnych administratorów lub administratorów domenowych. Zestaw użytkowników posiadających uprawnienia do logowania się w SQL Server bez podania hasła i tylko na podstawie poprawnego zalogowania do systemu może być dowolnie długi. Podobnego wyboru musimy dokonać dla serwisu analitycznego (rysunek 2.9) — Analysis Services Configuration. Podobnie jak w przypadku silnika bazy danych, proponuję dodać grupę lokalnych administratorów systemu.
Rysunek 2.9. Wybór autoryzacji do Analysis Services
Kolejny etap stanowi wybór trybu instalacji dla systemu raportującego (rysunek 2.10) — Reporting Services Configuration. Podobnie jak w przypadku silnika bazy danych, do wyboru mamy: instalację podstawową z ustawieniami natywnymi, zintegrowaną z SharePoint oraz instalację bez konfiguracji serwisu. W kolejnym etapie następuje podsumowanie wszystkich wybranych ustawień, które są widoczne w postaci strony WWW oraz w ostatnim oknie dialogowym instalatora, a po zatwierdzeniu następuje proces instalacji. Po pomyślnym jego zakończeniu powinniśmy w menu Start mieć dostępne wszystkie komponenty i narzędzia SQL Server. Na rysunku 2.11 przedstawiona została rozwinięta grupa instalacji w środowisku Windows 7. Najważniejszymi pozycjami są: SQL Server Management Studio, stanowiąca podstawowe narzędzie do zarządzania oraz uruchamiania zapytań i skryptów SQL, oraz SQL Server Business Intelligence Development, pozwalająca na utworzenie projektów analitycznych (hurtownie i zgłębianie danych), pakietów integracyjnych i systemów raportujących.
22
MS SQL Server. Zaawansowane metody programowania
Rysunek 2.10. Wybór sposobu instalacji Reporting Services
Rysunek 2.11. Zainstalowane komponenty MS SQL Server
Rozdział 2. Instalacja i konfiguracja środowiska
23
Jeśli uruchomimy SQL Server Management Studio, to jako pierwsze pojawi się okno logowania (rysunek 2.12), które zawiera kilka kontrolek. Pierwsza z nich pozwala na określenie silnika, z którym będziemy się łączyli. Dostępne jest połączenie z: serwerem bazy danych (Database Engine), hurtownią danych (Analysis Services), systemem raportującym (Reporting Services) oraz systemem integracji danych (Integration Services). Jeśli wybierzemy serwer danych, to konieczne będzie określenie nazwy tego, z którym będziemy się łączyli. Możliwe jest ręczne wpisanie nazwy albo wybranie serwera z listy. Domyślną nazwą serwera jest nazwa hosta (komputera), na którym jest on zainstalowany — w pokazywanym przypadku jest to AP. Jeśli jednak chcemy połączyć się z serwerem lokalnym (zainstalowanym na komputerze, z którego następuje logowanie), możemy użyć nazwy logicznej . (kropka). Różnica polega na tym, że w przypadku podania nazwy hosta serwer rozgłasza żądanie obsługi w sieci, a następnie „sam sobie odpowiada”, że jest tym hostem, z którym chcieliśmy się połączyć. Jeśli jawnie podamy, że jest to ten sam komputer (kropka), proces rozgłaszania nie jest potrzebny. Druga z definicji połączenia pozwala na znaczne ograniczenie ruchu w sieci.
Rysunek 2.12. Wybór rodzaju silnika oraz instancji serwera
Na rysunku 2.12 poza kropką oraz nazwą hosta ostatnią pozycją na liście jest ; konsekwencją jej wybrania jest pojawienie się okna dialogowego — rysunek 2.13.
Rysunek 2.13. Wykrywanie instancji serwerów lokalnych i zainstalowanych w domenie
24
MS SQL Server. Zaawansowane metody programowania
Okno to zawiera dwie zakładki. Pierwsza (domyślna) pokazuje wszystkie zainstalowane lokalnie silniki, zarówno bazy danych (pełnej oraz EXPRESS, jeśli zainstalowano), jak i wszystkich innych dostępnych narzędzi. Przejście do drugiej zakładki powoduje proces wykrywania serwerów baz danych zainstalowanych w domenie (grupie roboczej), do której należy nasz komputer. Aby serwer został wykryty, musi być uruchomiony komputer oraz serwis silnika bazy danych. Widoczne obok nazw liczby określają wersję serwera: 11.0 — wersja 2012, 10.0 — wersja 2008, w tym 2008 R2, 9.0 — wersja 2005; pojawienie się liczby 8.0 oznaczałoby wersję 2000. Możliwe jest dokonanie wyboru dowolnego serwera z obu zakładek okna dialogowego i zalogowanie się do niego, pod warunkiem że umiemy się uwierzytelnić. Jak widać, korzystając z lokalnego SQL Server Management Studio, możemy zarządzać dowolnym serwerem w domenie. Jeżeli dokonamy wyboru serwera (w pokazanym przykładzie wybrano serwer lokalny), pozostaje ustalenie trybu autoryzacji — rysunek 2.14. Do uwierzytelnienia systemowego Windows Authentication nie jest potrzebne podawanie żadnych dodatkowych danych (zarówno nazwa użytkownika, jak i hasło zostały zweryfikowane podczas logowania do Windows), natomiast dla SQL Server Authentication konieczne jest podanie zarówno nazwy użytkownika, jak i hasła. W przykładzie użyte zostały konto domyślnego superadministratora sa oraz hasło zdefiniowane podczas instalacji.
Rysunek 2.14. Rodzaje autoryzacji do serwera
Poza danymi do logowania możliwe jest określenie dodatkowych właściwości (rysunek 2.15), takich jak: czas na połączenie (Connection time-out), ograniczenie czasu wykonania poleceń SQL (Execution time-out), zastosowanie szyfrowania połączenia (Encrypt connection). W postaci list rozwijanych dostępne są kolejne dwa atrybuty: Connect to database z dopuszczalnymi opcjami i oraz Network protocol z dopuszczalnymi opcjami , Shared Memory, TCP/IP, Named Pipes. Dla każdego protokołu możliwe jest ustalenie wielkości pakietu Network packet size.
Rozdział 2. Instalacja i konfiguracja środowiska
25
Rysunek 2.15. Zaawansowane ustawienia właściwości połączenia
Po pomyślnym procesie logowania widzimy wnętrze Microsoft SQL Server Management Studio — rysunek 2.16. Lewy panel okna zawiera przedstawioną w postaci drzewa hierarchicznego strukturę instancji serwera (na rysunku została ona częściowo rozwinięta). Po prawej stronie w górnej części widoczne jest okno służące do tworzenia i uruchamiania zapytań i skryptów SQL. Pośrodku znajduje się dwuzakładkowa kontrolka służąca do wyświetlania rezultatów zapytań. Na rysunku 2.16 widoczna jest zakładka Results, zawierająca zestaw rekordów zwróconych przez zapytanie, z tyłu widać zakładkę Messages, gdzie pojawiają się komunikaty z bazy. Na dole znajduje się kontrolka Output, gdzie dostępne są komunikaty pochodzące z instancji serwera, głównie dotyczące błędów w jego działaniu (nie dotyczy to błędów przetwarzania zapytań, które również pojawiają się w środkowej części okna — zakładka Messages). W strukturze hierarchicznej widoczne są cztery występujące zawsze bazy systemowe: master — główna baza systemowa, zawierająca wszystkie obiekty systemowe
(tabele, perspektywy, procedury, etc.); na bazie tej nie powinno się ręcznie wykonywać żadnych operacji (z doświadczenia dydaktycznego wiem, że utworzenie dodatkowych obiektów, co często przytrafia się studentom, nie powoduje żadnych skutków ubocznych, natomiast usunięcie obiektu systemowego może prowadzić do niepożądanego zachowania serwera, aż do całkowitej utraty możliwości posługiwania się nim); msdb — jest bazą wykorzystywaną podczas pracy serwisu SQL Server Agent
do zarządzania zadaniami, alertami, pocztą, systemem powiadomień; również tej bazy dotyczą uwagi odnoszące się do ręcznej ingerencji użytkownika;
26
MS SQL Server. Zaawansowane metody programowania
Rysunek 2.16. Widok struktury serwera oraz panelu przetwarzania zapytań w Microsoft SQL Server Management Studio tempdb — jest bazą przeznaczoną na obiekty tymczasowe, między innymi:
pośrednie stany sortowań, informacje o stanie kursorów, lokalne i globalne tabele tymczasowe, stany pośrednie przed zatwierdzeniem transakcji; tworzenie przez użytkownika obiektów w tej bazie jest bezcelowe, ponieważ nie będą one utrwalone; model — jest bazą szablonem; wszystkie zawarte w tej bazie obiekty są
przepisywane do każdej nowo tworzonej bazy danych, dlatego opłaca się w niej tworzyć obiekty, które będą wykorzystywane przez wiele baz, np. tabele słownikowe, procedury i funkcje wykorzystywane w każdej z baz (walidacja danych); należy pamiętać, że nie jest to narzędzie typu CASE, to znaczy, że utworzone w tej bazie nowe obiekty nie będą się automatycznie przenosić do już istniejących baz i operację taką należy wykonać ręcznie. Ponadto istnieje baza systemowa resource, przeznaczona tylko do odczytu i widziana jako element schematu sys; w drzewie hierarchicznym niewidoczna, a dostępna tylko za pośrednictwem bazy master. Dla potrzeb realizacji replikacji, czyli synchronizacji danych, może zostać utworzona baza distributor. Dodatkowo w obrębie baz systemowych dostępny jest folder zawierający migawki baz danych — Database Snapshots. Poniżej baz systemowych widoczne są bazy treningowe dostarczone przez producenta (przy instalacji domyślnej są to bazy, których nazwa rozpoczyna się od frazy AdventureWorks) oraz bazy danych utworzone przez użytkowników. Aby utworzyć nową bazę danych z wykorzystaniem narzędzi wizualnych, należy prawym przyciskiem myszy kliknąć na poziomie węzła Databases. Na skutek takiego działania pojawi się menu kontekstowe, którego pierwszą pozycją jest New Database… — rysunek 2.17.
Rozdział 2. Instalacja i konfiguracja środowiska
27
Rysunek 2.17. Wizualne tworzenie nowej bazy w Microsoft SQL Server Management Studio
Po wybraniu tej pozycji pojawia się nowe okno dialogowe — rysunek 2.18. Na zakładce General konieczne jest podanie nazwy logicznej nowo tworzonej bazy danych np. nowa. Powoduje to zdefiniowanie dwóch zbiorów fizycznych. Pierwszy z nich, o domyślnej nazwie pochodzącej od nazwy logicznej nowa.mdf, jest plikiem danych i zawiera wszystkie obiekty, które w bazie zostaną utworzone, a także dane zawarte w tabelach. Domyślnie posiada on rozmiar 3 MB i jest automatycznie rozszerzany bez wskazania maksymalnego dopuszczalnego rozmiaru. Możliwe jest również wskazanie lokalizacji, w której zostanie utworzony. Drugi z nich, również o nazwie wywodzącej się z nazwy logicznej nowa_log.ldf, jest plikiem dziennika i przechowuje informacje o operacjach, jakie na bazie zostały wykonane, i jest wykorzystywany w procesie odtwarzania danych po wystąpieniu awarii. Ma domyślny rozmiar 1 MB i jest również automatycznie powiększany. Nazwy obu plików mogą być dowolnie zmienione przez użytkownika. Używając przycisku Add, możemy dodać kolejne pliki danych. Ponieważ najwolniejsze operacje wykonywane przez komputer to zawsze odczyt i zapis na nośniku fizycznym (dysku), dołożenie kolejnych plików danych umieszczonych na różnych dyskach spowoduje zrównoleglenie tych operacji (przynajmniej częściowe), co poprawia wydajność. Takie postępowanie nazywa się partycjonowaniem fizycznym i ma sens tylko dla dużych baz danych [8] [9] [10]. Jeśli utworzymy wiele plików danych, to domyślnie będą się one znajdować w domyślnej grupie plików PRIMARY. Na zakładce Filegroups (rysunek 2.19) możliwe jest utworzenie kolejnych grup plików. W momencie utworzenia grupa jest pusta. Możliwe jest ustalenie dowolnej z nich jako grupy domyślnej, a w przypadku grup plików nieposiadających tej cechy możliwe jest ustawienie właściwości tylko do odczytu (w takiej grupie nie można tworzyć żadnych nowych obiektów). Grupy plików mogą być usuwane (z wyjątkiem domyślnej), a przypisane do niej pliki fizyczne zostaną przeniesione do grupy domyślnej.
28
MS SQL Server. Zaawansowane metody programowania
Rysunek 2.18. Wizualne tworzenie nowej bazy w Microsoft SQL Server Management Studio — zakładka General
Rysunek 2.19. Wizualne tworzenie nowej bazy w Microsoft SQL Server Management Studio — zakładka Filegroups
Rozdział 2. Instalacja i konfiguracja środowiska
29
Jeśli utworzymy wiele plików danych w domyślnej grupie plików, o tym, do którego z nich trafią nowo tworzone obiekty, np. tabele, decyduje silnik bazy danych, opierając się na wewnętrznych algorytmach równoważenia obciążenia [11]. Jeśli przypiszemy plik do grupy plików (rysunek 2.20), możemy podczas tworzenia zdecydować, do której z nich tworzony obiekt trafi, poprzez dopisanie klauzuli ON NazwaGrupy na końcu zapytania, np. ON SECONDARY. Jeśli jednak do tej grupy należy więcej plików niż jeden, znów o przydziale wewnątrz niej decyduje silnik bazy danych.
Rysunek 2.20. Wizualne tworzenie nowej bazy w Microsoft SQL Server Management Studio — zakładka General z uwzględnieniem dodanej grupy plików
Poza dwoma poprzednio omawianymi zakładkami dostępna jest jeszcze zakładka Options — rysunek 2.21. Służy ona do ustawiania zaawansowanych właściwości bazy. Pozwala między innymi na ustawienie trybu zgodności z wersją SQL Server oraz trybu odzyskiwania po awarii. Pozostałe parametry są związane z automatyzacją procesów, domyślnymi ustawieniami kursorów, trybem zgodności z ANSI, ustawieniami przetwarzania, procesem powiadamiania oraz statusem bazy. W większości zastosowań ustawienia domyślne będą odpowiednie. Gdy zostaną zatwierdzone dane ustawione na wszystkich trzech zakładkach okna dialogowego (rysunek 2.19), powstanie nowa baza danych. Będzie ona widoczna jako ostatnia pozycja w strukturze hierarchicznej. Po odświeżeniu widoku serwera pojawi się już w miejscu wynikającym z porządku alfabetycznego. Utworzona na serwerze baza danych może zostać przeniesiona w dowolną lokalizację, również na inny komputer, aby mogła być obsługiwana za pomocą innego serwera SQL. Jednak dopóki jest dołączona do struktury logicznej baz obsługiwanych przez serwer, nie jest to możliwe, ponieważ zablokowane są wszystkie operacje dyskowe na
30
MS SQL Server. Zaawansowane metody programowania
Rysunek 2.21. Wizualne tworzenie nowej bazy w Microsoft SQL Server Management Studio — zakładka Options
Rozdział 2. Instalacja i konfiguracja środowiska
31
plikach bazy oraz dziennika. Aby umożliwić migrację bazy, należy ją najpierw odłączyć, wybierając z menu kontekstowego dla bazy danych (kliknięcie prawym przyciskiem myszy) pozycję Tasks, w której z kolei wybieramy polecenie Detach… („odłącz”) (rysunek 2.22). W odpowiedzi pojawia się okno dialogowe zawierające informacje o statusie bazy danych oraz pozwalające na wybranie opcji aktualizującej statystyki bazy oraz powodującej usunięcie wszystkich aktywnych połączeń z bazą. Kliknięcie przycisku OK powoduje rozpoczęcie procesu odłączania. Jeśli z jakichś powodów proces ten nie powiedzie się, informacje o nich będzie można odczytać w pozycji Message tego samego okna dialogowego. Najczęstszą przyczyną powstawania błędów podczas odłączania jest występowanie aktywnych połączeń z bazą. Błąd ten powinien zostać usunięty w przypadku zaznaczenia opcji Drop Connections.
Rysunek 2.22. Odłączanie bazy w Microsoft SQL Server Management Studio
32
MS SQL Server. Zaawansowane metody programowania
Jeśli uda nam się odłączyć bazę od struktury logicznej instancji serwera, możliwe będzie wykonanie operacji na jej plikach, np. przeniesienia ich w inne miejsce — na inny komputer. Powinniśmy taką operację przeprowadzać zarówno na pliku danych (*.mdf), jak i pliku dziennika (*.ldf). Po przeniesieniu w inne miejsce możemy bazę z powrotem przyłączyć do serwera (tego samego lub innego). W tym celu z menu kontekstowego dla węzła drzewa Databases wybieramy pozycję Attach… (rysunek 2.23). W rezultacie pojawia się okno dialogowe, w którym za pomocą przycisku Add… wskazujemy plik dołączanej bazy danych (*.mdf). Na skutek tego pojawia się informacja o dołączanej bazie: lokalizacja pliku, pierwotna nazwa logiczna, nowa nazwa logiczna (domyślnie taka sama jak stara) oraz właściciel bazy (dbo — database owner, „właściciel bazy”). Ponadto dociągany jest plik dziennika (*.ldf) wraz z pozostałymi plikami danych, o ile istnieją. Informacja o plikach składających się na bazę jest zawarta w dolnej części okna dialogowego. Po potwierdzeniu wyboru rozpoczyna się proces dołączania. Ewentualne błędy pojawią się w pozycji Message tego samego okna. Jedną z podstawowych przyczyn takiego stanu jest uszkodzenie któregoś z plików lub brak pliku dziennika. Dołączenie bazy bez pliku dziennika przy użyciu narzędzi wizualnych jest kłopotliwe. Należy usunąć plik dziennika z okienka …database details, a następnie wykonać dołączanie, co powoduje wygenerowanie pustego pliku dziennika. Dlatego należy bardzo dbać zarówno o plik dziennika, jak i plik danych. W przypadku uszkodzenia lub zagubienia pliku dziennika dołączenie bazy danych jest możliwe także za pomocą procedury systemowej: sp_attach_db [@dbname = ] 'NazwaLogiczna', [@filename1 = ] 'NazwaPlikuBazyDanych' [ ,...16 ]
Rysunek 2.23. Dołączanie bazy w Microsoft SQL Server Management Studio
Rozdział 2. Instalacja i konfiguracja środowiska
33
Obowiązkowe jest podanie jednego zbioru danych. Należy jednak pamiętać, że jeśli nie zostanie podany plik dziennika, to będzie utworzony jego pusty zamiennik, co może powodować problemy z odzyskiwaniem danych po awarii, gdy konieczne jest odwołanie się do czasu sprzed utworzenia tego pliku. Możliwe jest również użycie analogicznej procedury: sp_attach_single_file_db [@dbname = ] 'NazwaLogiczna', [@physname = ] 'NazwaPlikuBazyDanych'
Jest ona dedykowana dla baz danych składających się z tylko jednego pliku danych. Przedstawiony mechanizm odłączania i dołączania bazy danych może być stosowany do migrowania danych na inne serwery oraz jako narzędzie wspierające tworzenie kopii zapasowych. Omawiane mechanizmy powinny pozwolić na skuteczne zainstalowanie i skonfigurowanie MS SQL Server oraz na skorzystanie z przykładowej bazy danych dostępnej na stronie wydawnictwa, którą to bazę należy dołączyć do własnego serwera. Zasadniczym elementem bazy danych jest struktura składająca się z trzech tabel pokazanych w postaci diagramu relacyjnego na rysunku 2.24. Przedstawia on schemat zatrudnienia w małej firmie. Na strukturę organizacyjną składają się działy (tabela Dzialy). Każdy pracownik (tabela Osoby) może być przypisany do najwyżej jednego działu. Pracownik może mieć najwyżej jednego przełożonego (IdSzefa), który jest również pracownikiem firmy. Pracownik otrzymuje wypłaty (tabela Zarobki), każda z nich jest przypisana do pracownika. Przez wypłatę rozumie się każdą operację finansową wykonaną na rzecz pracownika — wynagrodzenie za kolejne miesiące, premie, diety itp. Ten fragment schematu będzie wykorzystywany do omówienia większości przykładów zawartych w książce.
Dzialy
Osoby *
Zarobki * IdZarobku
IdOsoby
IdOsoby
IdDzialu
IdDzialu
Nazwa
Brutto
Nazwisko Imie RokUrodz wzrost IdSzefa
Rysunek 2.24. Diagram zasadniczej części schematu relacyjnego przykładowej bazy danych
Opisane poprzednio tabele stanowią fragment większego schematu, pokazanego na rysunku 2.25. Przedstawia on proces sprzedaży realizowany przez firmę (hurtownia, sklep). Firma dysponuje towarami (tabela Towar), które pogrupowane są w kategorie (tabela Kategorie). Towary te były wyprodukowane przez firmy (tabela Producenci), które mają swoje siedziby w miastach (tabela Miasta), które znajdują się w województwach (tabela Wojewodztwa). Towary są kupowane przez klientów (tabela Klienci), którzy pochodzą z miast (tabela Miasta) znajdujących się w województwach (tabela Wojewodztwa). Dwie ostatnie tabele są wspólnymi słownikami opisującymi lokalizację zarówno producentów, jak i klientów. Na zakupione towary klienci mają wystawiane przez pracowników faktury (tabela Faktury), na których odnotowany jest fakt zakupu
34
MS SQL Server. Zaawansowane metody programowania
każdego z nich (tabela Transakcje). Omówiony schemat jest znany Czytelnikom moich poprzednich książek, ale uważam go za wystarczająco ogólny, by wykorzystać go do omawiania większości zagadnień występujących w bazach danych, niezależnie od platformy czy rodzaju przetwarzania (transakcyjne, analityczne). Ponadto wydaje mi się dość prosty (zwłaszcza jego podstawowa część), tak że jego zrozumienie nie wymaga specjalnego, dodatkowego wysiłku, co daje więcej czasu na przyswojenie sobie prezentowanych przykładów. W przyjętym nazewnictwie z premedytacją nie stosowano narodowych znaków diakrytycznych, co jest dopuszczalne, ale komplikuje później pisanie zapytań. Nazwy kluczy głównych zaczynają się od prefiksu Id, po którym następuje rzeczownik w liczbie pojedynczej określający zawartość tabeli lub tabeli nadrzędnej. Oznacza to, że pola kluczy głównych oraz kluczy obcych mają te same nazwy. Jedynym wyjątkiem jest pole klucza wewnętrznego IdSzefa, gdzie zastosowano ten sam prefiks, ale aby nie doprowadzić do dublowania nazw kolumn w tabeli, co jest zabronione, ciąg dalszy nie określa nazwy tabeli, lecz jedynie wskazuje na rolę pola.
Rysunek 2.25. Pełny diagram przykładowej bazy danych
Rozdział 3.
Język zapytań SQL w MS SQL Server 3.1. Zapytania wybierające Podstawową operacją wykonywaną na tabelach bazy danych jest wyświetlenie jej zawartości. Wykonujemy to zadanie za pomocą polecenia SELECT języka zapytań SQL. Należy zauważyć, że w języku tym nie jest uwzględniana wielkość liter. We wszystkich prezentowanych w tej książce przykładach będę się starał stosować jednak pewien formalny zapis, w którym nazwy poleceń, słowa kluczowe, dyrektywy, klauzule i inne „stałe” elementy składni będą zapisywane dużymi literami. Ta sama reguła dotyczyć będzie również operatorów oraz wbudowanych, systemowych funkcji. Pozostałe, „zmienne” elementy składni nie będą zapisywane w taki sposób. Formalizm taki ma za zadanie zapewnić lepszą czytelność kodu. Opis języka zgodny ze standardem SQL 1992 można również znaleźć w książce J. Harrington [6]. Jeśli chcemy wyświetlić wybrane pola z tabeli po słowie kluczowym SELECT, wymieniamy je w postaci listy separowanej przecinkami. Polecenie wyświetlające zawartość trzech wybranych pól z tabeli Osoby ma postać: SELECT Nazwisko, Imie, RokUrodz FROM Osoby;
Jak widać, polecenie może być zapisane w wielu liniach (tutaj: w dwóch) bez stosowania żadnego dodatkowego znaku przeniesienia czy kontynuacji. Podziału można dokonać w każdym sensownym miejscu kodu. Formalnie znakiem końca jest średnik, jednak może on zostać pominięty. Problemy mogą pojawić się wtedy, kiedy budujemy skrypt składający się z większej liczby poleceń, a parser nie może automatycznie zdecydować, czy w kolejnej linii mamy do czynienia z kontynuacją zapytania, czy też z nowym poleceniem. Takie problemy pojawiają się stosunkowo rzadko, głównie podczas tworzenia elementów proceduralnych. Można jednak żartobliwie stwierdzić, iż zapytania zakończone średnikiem działają lepiej.
36
MS SQL Server. Zaawansowane metody programowania
Przedstawiony poprzednio przykład wyświetlał dane zawarte w trzech kolumnach tabeli Osoby, we wszystkich jej wierszach. Jeśli chcemy wyświetlić wszystkie kolumny (pola) tabeli, to zamiast wypisywać pełną ich listę, możemy użyć symbolu specjalnego * (gwiazdki), który ją zastępuje. SELECT * FROM Osoby;
W obu poprzednich przykładach rekordy nie zostały posortowane, lecz wyprowadzone w kolejności ich wpisywania do tabeli (precyzyjnie w kolejności narzuconej przez indeksy, o czym będzie mowa w dalszych rozdziałach). Aby posortować wyprowadzany przez zapytanie zestaw rekordów, należy zastosować klauzulę ORDER BY, po której wskazujemy pole, względem którego odbędzie się ten proces. SELECT Nazwisko, Imie FROM Osoby ORDER BY RokUrodz;
Domyślnym kierunkiem sortowania jest sortowanie rosnące, od najmniejszych do największych wartości wskazanego pola. Jeśli chcemy takie sortowanie wskazać, jawnie używamy dyrektywy ASC. SELECT Nazwisko, Imie FROM Osoby ORDER BY RokUrodz ASC;
Jeśli chcemy zmienić kierunek sortowania na malejący, musimy jawnie użyć dyrektywy DESC. SELECT Nazwisko, Imie FROM Osoby ORDER BY RokUrodz DESC;
Jeżeli pole, względem którego wykonywaliśmy sortowanie, ma powtarzające się wartości, sensowne jest użycie kolejnej kolumny do sortowania rekordów w obrębie grupy o takich samych wartościach pierwszego. Kierunki sortowania dla każdego z pól są ustalane niezależnie. W przypadku sortowania rosnącego dyrektywę ASC można pominąć. SELECT Nazwisko, Imie FROM Osoby ORDER BY RokUrodz ASC, Wzrost DESC;
Na liście pól polecenia SELECT poza nazwami kolumn mogą pojawić się wyrażenia korzystające z wbudowanych operatorów (tabela 3.1) lub funkcji. W przykładzie pokazane zostało mnożenie wartości dwóch pól. SELECT RokUrodz*Wzrost FROM Osoby;
W wynikowym zestawie rekordów kolumna nie będzie miała nazwy (No column name). Możliwe jest nadanie jej przez użytkownika nazwy (aliasu) po słowie kluczowym AS. Ponadto można użyć tego wyrażenia do sortowania z możliwością ustalenia kierunku, jak pokazano w poprzednich przykładach. SELECT RokUrodz*Wzrost AS Iloczyn FROM Osoby ORDER BY RokUrodz*Wzrost;
Rozdział 3. Język zapytań SQL w MS SQL Server
37
Tabela 3.1. Wykaz operatorów w SQL
Algebraiczne
Logiczne Znakowe
Bitowe
+
dodawanie
–
odejmowanie
*
mnożenie
/
dzielenie
%
modulo
AND
iloczyn
OR
suma
NOT
przeczenie
+
konkatenacja łańcucha
&
iloczyn
|
suma
^
różnica symetryczna XOR
~
negacja
Zamiast pisać w klauzuli ORDER BY pełną postać wyrażenia (kiedy są one złożone, może to być kłopotliwe), możemy do określenia sposobu sortowania użyć aliasu. SELECT RokUrodz*Wzrost AS Iloczyn FROM Osoby ORDER BY Iloczyn;
W MS SQL możliwe jest aliasowanie bez użycia słowa kluczowego AS — po spacji. Jednak moim zdaniem jest to niezbyt czytelne i może prowadzić do nieporozumień. Jeśli wykonamy zapytanie: SELECT RokUrodz Wzrost FROM Osoby;
w którym po nazwie pierwszego pola „zapomnieliśmy” postawić przecinek, otrzymamy wyprowadzone wartości roku urodzenia w kolumnie o nazwie Wzrost. Czasami takie pomyłki są trudne do wykrycia, jeśli mamy dość długą listę kolumn, a wartości w polach są podobne co do zakresu przyjmowanych wartości. Przykłady stosowania operatorów algebraicznych przedstawione w trzech kolejnych zapytaniach pokazują, że mogą być one użyte zarówno w odniesieniu do wartości pól, jak i stałych. Użyty w przykładzie alias cos pokazuje, że możliwe jest w tym charakterze stosowanie również niektórych nazw zastrzeżonych (tutaj nazwa funkcji trygonometrycznej). Uwaga ta nie dotyczy poleceń oraz klauzul języka SQL. Trzeci przykład ilustruje zastosowanie operatora wyznaczającego resztę z dzielenia. SELECT Brutto, 0.8*Brutto AS Dochod FROM Zarobki; SELECT Brutto, IdOsoby*Brutto AS cos FROM Zarobki; SELECT RokUrodz, RokUrodz%2 AS ResztaZDzielenia FROM Osoby;
Dość ciekawą grupą są operatory bitowe, działające oddzielnie na każdym bicie binarnej notacji wartości. Najczęściej wykorzystywany jest operator iloczynu (koniunkcja), służący do weryfikacji, czy w dwóch danych są tak samo ustawione bity. W przykładzie
38
MS SQL Server. Zaawansowane metody programowania
posłużono się operacjami na stałych, gdzie pierwszy argument ma wartość 6 (binarnie 110), a drugi zmienia się od 0 do 6. Kolumny mają nazwy zgodne z wartością drugiego operatora. Dla iloczynu bitowego, w którym bit wyniku jest ustawiany na 1 wtedy, gdy odpowiednie bity w obu argumentach mają wartość 1 (w przeciwnym wypadku uzyskuje on wartość 0), przykładowe zapytanie ma postać przedstawioną poniżej, a wyniki zawarte są w tabeli 3.2. SELECT 6 & 0 AS '0', 6 & 1 AS '1', 6 & 2 AS '2', 6 & 3 AS '3', 6 & 4 AS '4', 6 & 5 AS '5', 6 & 6 AS '6'
Tabela 3.2. Wynik zapytania wyznaczającego iloczyn bitowy 0
1
2
3
4
5
6
0
0
2
2
4
4
6
Dla sumy bitowej (alternatywa), w której bit wyniku jest ustawiany na 1 wtedy, gdy odpowiednie bity w przynajmniej jednym argumencie mają wartość 1 (w przeciwnym wypadku uzyskuje on wartość 0), przykładowe zapytanie ma postać pokazaną poniżej, a wyniki zawarte są w tabeli 3.3. SELECT 6 | 0 AS '0', 6 | 1 AS '1', 6 | 2 AS '2', 6 | 3 AS '3', 6 | 4 AS '4', 6 | 5 AS '5', 6 | 6 AS '6'
Tabela 3.3. Wynik zapytania wyznaczającego sumę bitową 0
1
2
3
4
5
6
6
7
6
7
6
7
6
Dla różnicy symetrycznej (alternatywy wykluczającej), w której bit wyniku jest ustawiany na 1 wtedy, gdy odpowiednie bity argumentów mają różne wartości (w przeciwnym wypadku uzyskuje on wartość 0), przykładowe zapytanie ma postać pokazaną poniżej, a wyniki zawarte są w tabeli 3.4. SELECT 6 ^ 0 AS '0', 6 ^ 1 AS '1', 6 ^ 2 AS '2', 6 ^ 3 AS '3', 6 ^ 4 AS '4', 6 ^ 5 AS '5', 6 ^ 6 AS '6'
Tabela 3.4. Wynik zapytania wyznaczającego bitową różnice symetryczną 0
1
2
3
4
5
6
6
7
4
5
2
3
0
Dla przeczenia, w którym bit wyniku jest ustawiany na 1 wtedy, gdy bit argumentu ma wartość (w przeciwnym wypadku uzyskuje on wartość 0), przykładowe zapytanie ma postać, a wyniki zawarte są w tabeli 3.5. SELECT ~0 AS '0', ~1 AS '1', ~2 AS '2', ~3 AS '3', ~4 AS '4', ~5 AS '5', ~6 AS '6'
Tabela 3.5. Wynik zapytania wyznaczającego negację bitową 0
1
2
3
4
5
6
–1
–2
–3
–4
–5
–6
–7
Rozdział 3. Język zapytań SQL w MS SQL Server
39
Jednym z najpopularniejszych z punktu widzenia praktycznych zastosowań operacji jest konkatenacja, czyli łączenie dwóch lub więcej napisów w jeden ciąg. W MS SQL jest ona realizowana przez operator + (plus). Nieszczęśliwie jest on równoważny graficznie operatorowi dodawania, co prowadzi do różnego rodzaju nieporozumień. W zaprezentowanym przykładzie połączeniu podlegają pola zawierające nazwisko i imię. Ponieważ operacja taka nie wprowadza separatorów między łączone fragmenty, za ich dodanie odpowiada programista. W przykładzie jest nim łańcuch składający się ze spacji, wprowadzony pomiędzy łączone pola. Kolumnie wynikowej został nadany alias Osoba. SELECT Nazwisko + ' ' + Imie AS Osoba FROM Osoby;
Do łączenia można zastosować znaki specjalne, reprezentując je za pomocą odpowiednich kodów ASCII i dekodującej je funkcji char(). W zaprezentowanym przykładzie pokazano wstawienie między dwa łączone napisy znaku końca akapitu (enter) oraz tabulatora. Przy okazji pokazano, że w MS SQL możliwe jest wykonanie zapytania bez podania źródła (klauzula FROM). W takim przypadku będzie wyprowadzany jeden wiersz, a wyrażenia określające pola mogą zawierać stałe lub funkcje wbudowane tego środowiska. SELECT 'aaa' + char(13) + 'bbbbb' AS Nowa, char(9) + 'aaa' + char(9) + 'bbb' AS Tabulator;
Niestety, skutek ich działania nie będzie widoczny wtedy, kiedy wyniki zapytania wyprowadzane są do kontrolki tabelarycznej (grid). Skutek widoczny jest tylko na zakładce wyjścia tekstowego, jak pokazano niżej. Nowa Tabulator --------- --------aaa bbbbb aaa bbb (1 row(s) affected)
W dotychczasowych przykładach wyniki zawierały wszystkie rekordy odpytywanej tabeli. Aby ograniczyć ich liczbę, możemy dokonać filtrowania za pomocą klauzuli WHERE, po której definiowane jest wyrażenie zwracające wartość logiczną. Wyświetlone zostaną tylko te rekordy, dla których ma ono wartość TRUE. Możemy np. użyć operatora algebraicznego, aby wyświetlić osoby urodzone po roku 1970. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz >1970;
Do tworzenia filtrów poza operatorami algebraicznymi mogą zostać użyte operatory logiczne. W przykładzie zostaną wyświetlone osoby urodzone po roku 1970 i przed rokiem 1980. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz >1970 AND RokUrodz <1980;
Pełny wykaz operatorów, które mogą zostać użyte do budowania wyrażeń filtrujących, zawiera tabela 3.6.
40
MS SQL Server. Zaawansowane metody programowania
Tabela 3.6. Zestaw operatorów stosowanych do tworzenia filtrów
Relacyjne (algebraiczne)
Logiczne
Specjalne
=
równe
<
mniejsze niż
<=
mniejsze niż lub równe
>
większe niż
>=
większe niż lub równe
<>
różne
!=
nierówne
NOT
negacja
OR
suma logiczna
AND
iloczyn logiczny
IS NULL
ma wartość NULL
IS NOT NULL
ma wartość różną od NULL
BETWEEN
przedział dwustronnie domknięty
IN
lista
LIKE
podobny do wzorca
ANY
prawda, jeśli przynajmniej jedna pozycja na liście prawdziwa
SOME
prawda, jeśli przynajmniej jedna pozycja na liście prawdziwa (synonim ANY)
ALL
prawda, jeśli wszystkie pozycje na liście prawdziwe
EXISTS
prawda, jeśli zapytanie zwraca rekordy
Możliwe jest również zastosowanie aliasów nazw tabel umieszczonych w klauzuli FROM. W takim przypadku nazwa kwalifikowana pola składa się z aliasu tabeli, separatora w postaci kropki oraz nazwy tego pola. Nazwy kwalifikowane muszą być używane do filtrowania podobnie jak aliasy pól, co pokazano w przykładzie. SELECT o.RokUrodz AS Rok FROM Osoby AS o WHERE o.IdOsoby>3 AND Rok>1970;
Pomimo że zastosowano alias tabeli, możliwe jest odwoływanie się do pól bez użycia aliasu. Równoważnym rozwiązaniem poprzednio pokazanego przykładu jest zapytanie. SELECT RokUrodz AS Rok FROM Osoby AS o WHERE IdOsoby>3 AND Rok>1970;
W tabelach bazy danych nie wszystkie pola muszą być wypełnione. Może to być podyktowane wieloma względami: nie znamy wartości, nie chcemy lub nie możemy jej podać, nie ma ona sensu dla opisywanego w danym rekordzie bytu, etc. O takich pustych polach mówimy, że mają wartość NULL. Taki przypadek występuje również w odniesieniu do wartości logicznych. Poruszamy się zatem w logice trójwartościowej, w której oprócz dobrze znanych wartości TRUE (prawda) i FALSE (fałsz) występuje trzecia: NULL. Działania
Rozdział 3. Język zapytań SQL w MS SQL Server
41
podstawowych operatorów logicznych dla takiego zestawu wartości argumentów przedstawione są w: iloczynie logicznym (AND) (tabela 3.7), sumie logicznej (OR) (tabela 3.8), przeczeniu (NOT) (tabela 3.9). Tabela 3.7. Działanie operatora iloczynu dla logiki trójwartościowej A AND B
TRUE
FALSE
NULL
TRUE
TRUE
FALSE
NULL
FALSE
FALSE
FALSE
FALSE
NULL
NULL
FALSE
NULL
Tabela 3.8. Działanie operatora sumy dla logiki trójwartościowej A OR B
TRUE
FALSE
NULL
TRUE
TRUE
TRUE
TRUE
FALSE
TRUE
FALSE
NULL
NULL
TRUE
NULL
NULL
Tabela 3.9. Działanie operatora przeczenia dla logiki trójwartościowej A
NOT A
TRUE
FALSE
FALSE
TRUE
NULL
NULL
Napiszmy w postaci jawnej jedno z wyrażeń tabeli 3.7. NULL AND NULL = NULL
Możemy stwierdzić, że jest ono równoważne: (NULL=NULL)NULL
Wynika stąd, że jeśli napiszemy zapytanie o postaci: SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz = NULL;
to zawsze, bez względu na dane oraz pole przyrównane do wartości NULL, zwróci ono pusty zestaw rekordów. Wynika to z faktu, że takie wyrażenie filtrujące nigdy nie da wartości TRUE. Dlatego w celu wykrycia pustych pól stosujemy operator specjalny IS NULL. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz IS NULL;
Natomiast kiedy szukamy niepustych pól, będziemy używali operatora specjalnego IS NOT NULL. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz IS NOT NULL;
42
MS SQL Server. Zaawansowane metody programowania
Kolejnym operatorem specjalnym jest operator BETWEEN, który wyznacza przedział obustronnie domknięty (taki, który zawiera obie granice) dany dwoma wartościami. W przykładzie wyświetlone zostaną osoby urodzone pomiędzy 1970 a 1980 rokiem. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz BETWEEN 1970 AND 1980;
Należy pamiętać, że granice przedziału muszą być podawane, począwszy od wartości mniejszej. W przeciwnym razie zostanie zwrócony pusty zestaw rekordów. Równoważne zapytanie, w którym użyto operatorów relacyjnych i operatora logicznego, będzie miało postać. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz >=1970 AND RokUrodz <=1980;
W filtrze możemy odwołać się do listy. W tym celu używamy operatora IN, dla którego w nawiasach wymieniane są wartości separowane przecinkami. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz IN (1990, 1970, 1980);
Równoważne zapytanie wykorzystujące operator porównania oraz operatory logiczne ma postać. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz = 1990 OR RokUrodz = 1980 OR RokUrodz = 1970;
Analizując oba zapisy, możemy stwierdzić, że kolejność podawania wartości na liście nie ma wpływu na sposób działania zapytania. Tak samo będzie, jeśli wartości na liście zostaną powtórzone. Oczywiście w przypadku ręcznego definiowania listy raczej nie będziemy mieli do czynienia z taką sytuacją, ale kiedy jest ona generowana programistycznie, nie musimy weryfikować istnienia duplikatów. Minimalna zawartość listy musi składać się z jednego elementu. Lista nie może być pusta, gdyż prowadzi to do błędu składni. W praktyce oznacza to, że będzie się ona składała z wartości NULL, co daje poprawność składniową oraz pusty zestaw zwracanych rekordów. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz IN (NULL);
Tak jak poprzednio, w przypadku listy wpisywanej ręcznie ma to niewielkie znaczenie praktyczne. Jeśli jednak wartości będą generowane automatycznie, a nie jesteśmy pewni, czy zwrócony będzie przynajmniej jeden element, to warto jako pierwszy element wpisać wartość NULL, aby nie doprowadzić do przerwania przetwarzania kodu na skutek pojawienia się błędu. Operator listy jest dedykowany głównie do wartości numerycznych, jednak może działać z innymi typami pól. W przykładzie zaprezentowano wyświetlenie osób o nazwiskach Kowalski oraz Nowak. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko IN ('KOWALSKI', 'nowak');
Rozdział 3. Język zapytań SQL w MS SQL Server
43
Analizując zapytanie, możemy stwierdzić, że dane tekstowe są ograniczane znakiem apostrofu oraz że w środowisku MS SQL Server nie jest uwzględniana wielkość liter w stosunku do zawartości pól. Dlatego równoważne zapytanie może zostać zapisane w taki sposób, że tym razem nazwisko Kowalski będzie zapisane małymi literami, natomiast nazwisko Nowak dużymi. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko = 'kowalski' OR Nazwisko = 'NOWAK';
Naprawdę dla pól tekstowych dedykowany jest operator podobieństwa LIKE. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE 'KOW';
W podstawowej formie jest on równoważny operatorowi porównania, oczywiście z uwzględnieniem uwagi dotyczącej nierozróżniania wielkości liter. Powoduje to, że w obu przypadkach zostanie wyświetlona osoba o nazwisku Kow. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko = 'kow';
W operatorze tym możemy jednak używać znaków specjalnych, których zestaw jest zawarty w tabeli 3.10. Tabela 3.10. Znaki specjalne dla operatora podobieństwa LIKE Znak specjalny
Opis
%
Zastępuje dowolny ciąg znaków (w tym ciąg pusty)
_
Zastępuje dokładnie jeden znak
[]
Wielofunkcyjny („traktuj dosłownie”)
^
Negacja
Rozważmy teraz kilka praktycznych przykładów zastosowania znaków specjalnych w omawianym operatorze. W pierwszym wyświetlone zostaną nazwiska rozpoczynające się od frazy kow, ponieważ występujący po niej operator zastępuje dowolny ciąg znaków. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE 'KOW%';
Analogicznie możemy zapisać zapytanie wyświetlające nazwiska kończące się frazą kow. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '%KOW';
Ponieważ symbol % zastępuje również pusty ciąg znaków, w obu przypadkach zostanie wyświetlone nazwisko Kow, o ile jest w tabeli. Jeśli znak specjalny umieścimy po obu stronach frazy, otrzymamy nazwiska, które zawierają w środku fazę kow. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '%KOW%';
44
MS SQL Server. Zaawansowane metody programowania
Ponieważ każdy ze znaków % może reprezentować ciąg pusty, w zapytaniu tym wyświetlone zostaną również nazwiska rozpoczynające się tą frazą, kończące się nią, a także nazwisko Kow. Na podobnych zasadach możemy zbudować zapytanie, które wybiera te osoby, których nazwiska rozpoczynają się od litery k, a kończą się literą i. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE 'K%I';
W tym przypadku wyświetlone byłoby również nazwisko Ki, o ile istniałoby w tabeli. Ponieważ znak podkreślnika zastępuje dokładnie jeden znak, kolejny przykład wyświetla nazwiska, w których na trzeciej pozycji występuje litera w. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '__W%';
Ponieważ na końcu definicji filtra został użyty znak %, będą to nazwiska składające się z co najmniej trzech liter. Gdyby pominąć procent, wyświetlone zostałyby tylko nazwiska trzyliterowe (Kow, Lew, Paw). Do tej pory przedstawione znaki specjalne występują praktycznie we wszystkich komercyjnych serwerach baz danych, pozostałe są charakterystyczne dla serwera Microsoftu. Kolejny przykład wyświetla te nazwiska, w których pierwsza litera zawiera się w obustronnie domkniętym przedziale od k do n. Definicja przedziału powinna zaczynać się od znaku występującego wcześniej w alfabecie (zgodnie z sortowaniem zdefiniowanym dla używanej strony kodowej). SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '[k-n]%';
Jako znak specjalny może być również traktowany znak ^ (daszek). Gdy jest umieszczony w nawiasie kwadratowym jako pierwszy znak, reprezentuje przeczenie dla definicji przedziału. W przykładzie wyświetlane są nazwiska rozpoczynające się od litery spoza przedziału od k do n. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '[^k-n]%';
We wnętrzu nawiasu kwadratowego można również umieścić listę znaków. Jest ona specyficzna, ponieważ nie zawiera separatorów. Wynika to z faktu, że każdy znak umieszczony w nawiasie kwadratowym będzie traktowany dosłownie. W przykładzie wyświetlane są nazwiska rozpoczynające się od litery z listy znaków wymienionych w nawiasie kwadratowym. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '[knzaj]%';
Podobnie jak w przypadku przedziału znak ^ reprezentuje przeczenie. W przykładzie wyświetlane są nazwiska rozpoczynające się od litery spoza listy znaków wymienionych w nawiasie kwadratowym. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '[^knzaj]%';
Przeanalizujmy kilka przykładów filtrów, które można skonstruować na podstawie dostępnych znaków specjalnych. Wyświetlmy nazwiska, które rozpoczynają się od jednego z trzech znaków: k, –, n. Jeśli zachowamy kolejność wymieniania elementów
Rozdział 3. Język zapytań SQL w MS SQL Server
45
na liście, to zdefiniujemy przedział od k do n. Aby uzyskać wymaganą funkcjonalność, myślnik nie może znajdować się między jakimikolwiek symbolami. Może znaleźć się na początku albo na końcu listy. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '[kn-]%';
Podobna co do charakteru jest konstrukcja wyrażenia powodującego wyświetlenie nazwisk rozpoczynających się od cyfry (oczywiście w praktyce trudno spodziewać się takich danych, ale dla innych pól taki filtr może okazać się użyteczny). SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '[0-9]%';
Ponieważ znaki umieszczone w nawiasie kwadratowym są traktowane dosłownie, filtr pozwalający na wyświetlenie nazwisk rozpoczynających się od znaku procentu będzie miał postać: SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '[%]%';
Jeśli będziemy chcieli wyświetlić nazwiska, w których na pierwszym miejscu znajduje się jeden ze znaków: daszek, podkreślnik, myślnik lub procent, to myślnik nie może znaleźć się w środku listy, bo wtedy definiuje przedział, z kolei daszek nie może być pierwszy (gdyby został tam ustawiony, oznaczałby przeczenie), dlatego filtr realizujący to zadanie będzie miał postać: SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE '%[-%_^]%';
Jak widać, prosty w postaci domyślnej operator podobieństwa pozwala na bardzo złożone filtrowanie z wykorzystaniem znaków specjalnych. Jeśli stosujemy do filtrowania klauzulę WHERE, bez znajomości danych nie jesteśmy w stanie przewidzieć, ile rekordów zostanie zwróconych. Aby precyzyjnie ustalić liczebność wynikowego zestawu rekordów, możemy użyć dyrektywy TOP, po której określamy za pomocą stałej tę cechę. Gdyby nie został określony sposób sortowania, wyświetlonych zostałoby pierwszych pięć rekordów w kolejności ich wpisywania, a precyzyjniej decydują o tym indeksy utworzone dla tej tabeli. Jeśli zastosujemy sortowanie, o kolejności zadecyduje jego kierunek: SELECT TOP 5 Nazwisko, Imie FROM Osoby ORDER BY Wzrost;
Aby ustalić pierwsze pięć rekordów względem malejącego wzrostu, zmieniamy kierunek sortowania za pomocą sufiksu DESC. Wynikowy zestaw rekordów zawiera tabela 3.11. SELECT TOP 5 Nazwisko, Imie, Wzrost FROM Osoby ORDER BY Wzrost DESC;
Jak można zauważyć, pomimo że szósty wiersz charakteryzuje się takim samym wzrostem jak poprzedni, zostaje pominięty. O tym, który z wierszy o równej wartości cechy, względem której sortujemy, zostanie wyświetlony, a który będzie pominięty, decyduje silnik bazy danych. Tak jak poprzednio, możemy powiedzieć, że decyduje
46
MS SQL Server. Zaawansowane metody programowania
Tabela 3.11. Konsekwencja zastosowania dyrektywy TOP 5 Nazwisko
Imie
Wzrost
Kurek
TOMASZ
2.08
Konarski
STANISŁAW
2.08
Pogorzelski
GRZEGORZ
2.07
Miller
DANIEL
2.05
Rusnarczyk
WIESŁAW
2.05
Stępień
STANISŁAW
2.05
kolejność wpisywania rekordów, a precyzyjniej — indeksy utworzone dla tej tabeli. Poza ustaleniem liczby rekordów możliwe jest ustalenie ich procentu względem liczby rekordów w całej tabeli. SELECT TOP 5 PERCENT Nazwisko, Imie, Wzrost FROM Osoby ORDER BY Wzrost;
Należy pamiętać, że jeśli z obliczenia procentu wynika niecałkowita liczba rekordów, będzie ona zawsze zaokrąglana w górę. Ma to na celu zapewnienie, aby przy niewielkiej liczbie wierszy w tabeli oraz małej wartości procentu wyświetlany był przynajmniej jeden rekord. Oczywiście, jeśli jawnie wpiszemy TOP 0, to w obu przypadkach tej dyrektywy zostanie wyświetlony pusty zestaw rekordów. Do definiowania liczby rekordów może być zastosowany parametr zamieszczony w zapytaniu na zasadach zapytania skalarnego. W przykładzie wyświetlono w ten sposób 5 rekordów: DECLARE @a int SET @a=5 SELECT TOP (SELECT @a) Nazwisko, Wzrost FROM Osoby;
Można użyć również zapytania skalarnego, czyli takiego, które zwraca jedno pole w jednym wierszu. W przykładzie wyświetla się 1/20 liczby rekordów, które policzono za pomocą funkcji COUNT. SELECT TOP (SELECT COUNT(*)/20 FROM Osoby) Nazwisko, Wzrost FROM Osoby;
Niestety, ponieważ ściślejsze wyjaśnienie zarówno deklarowania i stosowania parametrów, jak i funkcji agregujących pojawia się później, nie będę w tym miejscu dokładnie opisywał zastosowanych mechanizmów. Chciałbym jednak pokazać ciekawy przypadek. Jeśli dzielenie przez 20 zastąpimy mnożeniem przez 0.05, co z punktu widzenia matematyki jest równoważne, to po wykonaniu zapytania pojawia się błąd: SELECT TOP (SELECT 0.05*COUNT(*) as Ile FROM Osoby) Nazwisko, Wzrost FROM Osoby; Msg 1060, Level 15, State 1, Line 1 The number of rows in the TOP clause must be an integer.
Wynika to z faktu, że SQL Server w przypadku wykonywania operacji algebraicznych prezentuje wynik w postaci najbardziej uniwersalnego typu wielkości występu-
Rozdział 3. Język zapytań SQL w MS SQL Server
47
jących w wyrażeniu, w naszym przypadku real, tak jak stała, przez którą mnożymy, nawet jeśli wynik jest całkowity. Należy dokonać konwersji wyniku do typu całkowitego, aby ominąć błąd, co pokazuje przykład. SELECT TOP (SELECT CAST(ile AS Int) FROM (SELECT 0.05*COUNT(*) as Ile FROM Osoby) as xxx) Nazwisko, Wzrost FROM Osoby;
Aby uzyskać listę niepowtarzających się rekordów, możemy użyć dyrektywy DISTINCT. SELECT DISTINCT Nazwisko FROM Osoby;
Zgodność jest ustalana na podstawie wszystkich wymienionych pól, stąd w kolejnym przykładzie mogą powtarzać się zarówno nazwiska, jak i imiona, ale ich pary muszą być różne. SELECT DISTINCT Nazwisko, Imie FROM Osoby;
W zapytaniu może pojawić się operator CASE, który przypisuje na podstawie warunku zawartego po słowie kluczowym WHEN wartość określoną po THEN. Lista warunków może być dowolnie długa, a do ich utworzenia możliwe jest zastosowanie wszystkich zdefiniowanych operatorów oraz funkcji. Wartość może być określona nie tylko jako stała, ale również jako wynik obliczenia funkcji. W zaprezentowanym przykładzie na podstawie wartości roku urodzenia przypisano etykiety określające wiek. W opcjonalnej sekcji ELSE zdefiniowano wartość w przypadku, kiedy żaden z wcześniej określonych warunków nie jest prawdziwy. Ponieważ warunki pokrywają pełny zakres liczb, etykieta 'Nie wiem' pojawi się w rekordach, w których rok urodzenia ma wartość NULL. SELECT Nazwisko, RokUrodz, Wiek= CASE WHEN RokUrodz >=1980 THEN 'Młody' WHEN RokUrodz >=1970 AND RokUrodz <1980 THEN 'Średni' WHEN RokUrodz <1970 THEN 'Stary' ELSE 'Nie wiem' END FROM Osoby;
Operator CASE działa w ten sposób, że jeśli pierwszy warunek jest prawdziwy, to nie są sprawdzane pozostałe. Jeśli jest fałszywy, sprawdzany jest drugi, a jeśli jest on prawdziwy, pomijane są kolejne itd. Dlatego jeżeli ustalimy właściwą kolejność, to nie zawsze musimy pisać pełną postać warunku. W przypadku przedziałów wystarczy podanie tylko jednej jego krawędzi. SELECT Nazwisko, RokUrodz, Wiek= CASE WHEN RokUrodz >=1980 THEN 'Młody' WHEN RokUrodz >=1970 THEN 'Średni' WHEN RokUrodz <1970 THEN 'Stary' ELSE 'Nie wiem' END FROM Osoby;
48
MS SQL Server. Zaawansowane metody programowania
Przedstawiona poprzednio składnia, w której następowało przypisanie rezultatu do pola za pomocą operatora =, jest rozwiązaniem starym, choć w dalszym ciągu stosowanym. Zgodnie ze standardem wprowadzonym przez ANSI obecnie (od wersji 2005) stosuje się alias, tak samo jak w przypadku innych pól obliczanych. SELECT Nazwisko, RokUrodz, CASE WHEN RokUrodz >=1980 THEN 'Młody' WHEN RokUrodz >=1970 THEN 'Średni' WHEN RokUrodz <1970 THEN 'Stary' ELSE 'Nie wiem' END AS Wiek FROM Osoby;
Jeśli wyrażenie polega na prostym porównaniu, możemy zastosować uproszczoną postać operatora. W tym przypadku po słowie kluczowym CASE występuje nazwa pola lub wyrażenie na zestawie pól, natomiast warunek redukuje się do wartości, z którą jest ono porównywane. SELECT Nazwisko, RokUrodz, CASE RokUrodz WHEN 1980 THEN 'Trzydzieści cztery' WHEN 1970 THEN 'Czterdzieści cztery' WHEN 1960 THEN 'Pięćdziesiąt cztery' ELSE 'Inny' END AS Wiek FROM Osoby;
Ta postać ma zastosowanie dla niewielkiej skończonej liczonej listy wartości, np. może być użyta do podania polskich nazw miesięcy na podstawie ich numeru porządkowego uzyskiwanego z daty. SELECT Nazwisko, DataZatr, CASE MONTH(DataZatr) WHEN 1 THEN 'styczeń' WHEN 2 THEN 'luty' WHEN 3 THEN 'marzec' WHEN 4 THEN 'kwiecień' WHEN 5 THEN 'maj' WHEN 6 THEN 'czerwiec' WHEN 7 THEN 'lipiec' WHEN 8 THEN 'sierpień' WHEN 9 THEN 'wrzesień' WHEN 10 THEN 'październik' WHEN 11 THEN 'listopad' WHEN 12 THEN 'grudzień' END AS Miesiac FROM Osoby;
Jednymi z najczęściej używanych funkcji są takie, które wyznaczają podsumowania, a szerzej agregaty. Podstawowa jest funkcja SUM, obliczająca sumę. Jeśli użyjemy jej w zapytaniu tylko ze wskazaniem na tabelę źródłową, to podsumowanie będzie dotyczyło wszystkich rekordów. W przykładzie obliczona została suma wszystkich wypłat, którą nazwano Razem:
Rozdział 3. Język zapytań SQL w MS SQL Server
49
SELECT SUM(Brutto) AS Razem FROM Zarobki;
Możliwe jest wyznaczenie podsumowania dla grup określonych polem wymienionym po klauzuli GROUP BY. Ponieważ grupowanie odbywa się dla zmieniającego się IdOsoby, podsumowanie będzie dotyczyło każdego z pracowników, który otrzymał jakąkolwiek wypłatę. SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki GROUP BY IdOsoby;
Należy zaznaczyć, że jeśli pole pojawi się po GROUP BY, to może zostać wymienione na liście wyświetlanych pól. Jeśli jednak na liście pól po SELECT pojawi się pole, na które nie działa funkcja agregująca, to musi się ono znaleźć na liście pól definiujących sposób grupowania. Pomimo tego, że zasada wydaje się bardzo prosta, praktyka uczy, że w przypadku bardziej złożonych zapytań właśnie niestosowanie się do niej jest źródłem bardzo dużej liczby błędów składniowych. Poza sumowaniem w MS SQL Server dostępne są inne funkcje agregujące, których wykaz zawiera tabela 3.12. Tabela 3.12. Wykaz funkcji agregujących Funkcja
Opis
AVG
wartość średnia
SUM
suma
MAX
maksimum
MIN
minimum
STDEV
odchylenie standardowe
VAR
wariancja
STDEVP
odchylenie populacji
VARP
wariancja populacji
COUNT
zlicz
W jednym zapytaniu możliwe jest wyznaczenie wielu funkcji agregujących dla tak samo zdefiniowanej grupy. Funkcje te mogą działać na dowolne pole z tabeli źródłowej. W przykładzie wyświetlone zostały sumy, wartości średnie oraz liczby wypłat dla każdego z pracowników, który otrzymał przynajmniej jedno wynagrodzenie. SELECT IdOsoby, SUM(Brutto) AS Razem, AVG(Brutto) AS Srednio, COUNT(IdOsoby) AS Ile FROM Zarobki GROUP BY IdOsoby;
W zapytaniach zawierających funkcje agregujące można wykorzystywać filtrowanie. Pierwszą z metod jest zastosowanie dobrze znanej klauzuli WHERE, która musi znajdować się przed opcją grupowania (o ile ta opcja występuje). Wyrażenie filtrujące musi dotyczyć pól tabeli, a nie wartości zagregowanych. W przykładzie dokonano filtrowania wyników w taki sposób, aby zostały wyświetlone rekordy dla pracowników wskazanych listą wartości ich identyfikatorów w operatorze IN.
50
MS SQL Server. Zaawansowane metody programowania SELECT IdOsoby, SUM(Brutto) AS Razem, AVG(Brutto) AS Srednio, COUNT(IdOsoby) AS Ile FROM Zarobki WHERE IdOsoby IN(1, 2, 7, 9) GROUP BY IdOsoby;
Drugą opcją jest filtrowanie wartości funkcji agregujących. W tym celu używamy klauzuli HAVING, która znajduje się po opcji grupowania, o ile ta opcja występuje. Wyrażenie filtrujące musi zawierać funkcję agregującą, chociaż niekoniecznie taką, która jest na liście wyświetlanych pól. W klauzuli HAVING nie jest dopuszczalne stosowanie aliasów. W porównaniu z poprzednim przykładem wyświetlone zostaną tylko te rekordy, dla których suma wypłat przekroczyła 1000. Wynikowy zestaw został posortowany względem malejących wartości tych sum. SELECT IdOsoby, SUM(Brutto) AS Razem, AVG(Brutto) AS Srednio, COUNT(IdOsoby) AS Ile FROM Zarobki WHERE IdOsoby IN(1, 2, 7, 9) GROUP BY IdOsoby HAVING SUM(Brutto)>1000 ORDER BY SUM(Brutto) DESC;
Należy zauważyć, że klauzula ORDER BY jest, poza jednym wyjątkiem, który zostanie przedstawiony później, ostatnim elementem zapytania. Ponadto w przeciwieństwie do HAVING możliwe jest używanie aliasów pól. Wszystkie dotychczasowe przykłady pobierały dane tylko z jednej tabeli źródłowej. Możemy spróbować wyświetlić informację pochodzącą z większej liczby tabel. W przykładzie wybrano pola Nazwisko i Brutto pochodzące z dwóch tabel Osoby i Zarobki, które zostały wymienione w postaci listy separowanej przecinkami po klauzuli FROM. SELECT Nazwisko, Brutto FROM Osoby, Zarobki;
Takie zapytanie jest poprawne składniowo i wyświetla iloczyn kartezjański (KROTKA), co oznacza, że dla każdego rekordu z pierwszej tabeli są wybierane wszystkie rekordy z drugiej. Jeśli chcemy jednak wyświetlać dla każdego pracownika tylko jego wypłaty, musimy określić warunek złączenia. Pierwszą metodą jest zastosowanie znanej już klauzuli WHERE, w której definiujemy wyrażenie łączące. Najczęściej do realizacji złączenia używamy pola klucza głównego tabeli nadrzędnej i pola klucza obcego tabeli podrzędnej. Ponieważ zgodnie ze zwyczajem oba pola mają taką samą nazwę, to aby umożliwić parserowi ich rozróżnienie, konieczne jest użycie nazw kwalifikowanych. Na taką nazwę składa się nazwa tabeli, separator w postaci kropki oraz nazwa pola. SELECT Nazwisko, Brutto FROM Osoby, Zarobki WHERE Osoby.IdOsoby=Zarobki.IdOsoby;
Jeśli chcemy dodatkowo wyświetlać dane pochodzące z trzeciej tabeli, to modyfikacja zapytania polega na: dodaniu pola z tej tabeli, dopisaniu nazwy tabeli do listy tabel oraz dodaniu kolejnego warunku złączenia, który będzie połączony z już istniejącym operatorem logicznym AND.
Rozdział 3. Język zapytań SQL w MS SQL Server
51
SELECT Nazwa, Nazwisko, Brutto FROM Osoby, Zarobki, Dzialy WHERE Osoby.IdOsoby = Zarobki.IdOsoby AND Dzialy.IdDzialu = Osoby.IdDzialu;
W tym rozwiązaniu ani kolejność wymieniania tabel w klauzuli FROM, ani kolejność definiowania warunków złączenia nie ma wpływu na uzyskany rezultat. Drugim wariantem realizacji złączenia jest zastosowanie operatora JOIN. Formalnie jest to rozwiązanie nowsze, ale w środowisku SQL Server istnieje praktycznie od początku. Tym razem między tabelami wstawiamy operator, a warunek złączenia wymieniamy po słowie kluczowym ON. W przykładzie przekreślono INNER, co ma oznaczać, że nie jest konieczne używanie pełnej nazwy operatora. SELECT Nazwisko, Brutto FROM Zarobki INNER JOIN Osoby ON Osoby.IdOsoby=Zarobki.IdOsoby;
Jeśli będziemy łączyli kolejną tabelę, to operator JOIN pojawia się po definicji pierwszego połączenia, a po słowie kluczowym ON definiuje się warunek połączenia tej tabeli z dwoma wcześniej połączonymi. SELECT Nazwa, Nazwisko, Brutto FROM Zarobki JOIN Osoby ON Osoby.IdOsoby = Zarobki.IdOsoby JOIN Dzialy ON Dzialy.IdDzialu = Osoby.IdDzialu;
W przeciwieństwie do realizacji złączeń za pomocą WHERE operator JOIN wymusza większy porządek. Łączenie powinno się odbywać, począwszy od tabeli leżącej najniżej w hierarchii (zawierającej najbardziej szczegółowe dane), w górę, tak jak to zostało pokazane w przykładzie, albo w kierunku przeciwnym. Oczywiście istnieje możliwość wymuszenia innej kolejności na skutek zastosowania nawiasów, ale z reguły prowadzi to do mniej czytelnej postaci. W przeszłości można było powiedzieć, że pomimo tego, iż obie realizacje złączeń zwracają te same wyniki, ich przetwarzanie było odmienne, co preferowało rozwiązanie nowsze. Jednak ze względu na to, że w każdym porządnym serwerze działają wewnętrzne optymalizatory zapytań, oba przypadki mają taką samą postać wewnętrzną. Można się o tym przekonać, zarówno analizując plan wykonania zapytania, jak i mierząc czas wykonania dla takich samych, dostatecznie licznych zestawów danych. Zwiększenie liczebności rekordów jest podyktowane tym, że w przypadku krótkich czasów wykonania pomiary są mało wiarygodne, ze względu na „zakłócenia” wprowadzane przez procesy tła. Rozróżnienie obu realizacji złączeń wynika z nieco innej cechy. Jeżeli łączyliśmy tabele Osoby i Zarobki za pomocą klauzuli WHERE, to jeśli któryś z pracowników nie otrzymał jeszcze wypłaty, informacja o nim „ginęła” w wynikowym zestawie rekordów. Jeśli natomiast używamy operatora JOIN, możemy określić kierunek złączenia. W przykładzie zastosowano złączenie prawe (RIGHT), w którym z tabeli stojącej po prawej stronie operatora JOIN wybierane są zawsze wszystkie rekordy, natomiast z tabeli występującej po stronie lewej tylko te, dla których warunek złączenia jest prawdziwy. Oznacza to,
52
MS SQL Server. Zaawansowane metody programowania
że w przypadku braku pary mogą pojawić się nazwiska, przy których wartość Brutto będzie wynosiła NULL. Przekreślone słowo kluczowe OUTER oznacza, że nie musimy używać pełnej nazwy operatora, tak samo jak to miało miejsce w przypadku INNER JOIN. SELECT Nazwisko, Brutto FROM Zarobki RIGHT OUTER JOIN Osoby ON Osoby.IdOsoby=Zarobki.IdOsoby;
Zmieniając kierunek złączenia na lewy (LEFT), uzyskamy zestaw rekordów, w którym uwzględnione zostaną wypłaty niemające właściciela, nieprzypisane do pracownika — „lewa kasa”. SELECT Nazwisko, Brutto FROM Zarobki LEFT OUTER JOIN Osoby ON Osoby.IdOsoby=Zarobki.IdOsoby;
Połączeniem funkcjonalności obu poprzednich operatorów jest FULL JOIN. Operator ten powoduje, że z obu tabel wyświetlane są wszystkie rekordy oraz te, dla których warunek złączenia jest prawdziwy. W tym przypadku wartości NULL mogą pojawić się dla pól pochodzących z każdej z łączonych tabel. SELECT Nazwisko, Brutto FROM Zarobki FULL OUTER JOIN Osoby ON Osoby.IdOsoby=Zarobki.IdOsoby;
W przypadku łączenia większej liczby tabel kierunek każdego ze złączeń określany jest oddzielnie. Można dowolnie mieszać kierunki, ale w praktyce z reguły łączymy wszystkie tabele w tę samą stronę. W przypadku dużej liczby łączonych tabel brak jednolitego kierunku łączenia może powodować duże problemy z interpretacją otrzymanego wynikowego zestawu rekordów. Oczywiście, jeśli mamy kłopot z ustaleniem, w którą stronę powinno odbywać się łączenie, stosujemy jako ostatnią deskę ratunku operator FULL JOIN. Aby nieco skrócić notację warunków złączenia, możemy używać aliasów tabel. Wtedy nazwy kwalifikowane pól będą zawierały zamiast nazwy tabeli jej alias, jak to pokazuje przykład. SELECT Nazwisko, Brutto FROM Zarobki AS Z JOIN Osoby AS O ON O.IdOsoby=Z.IdOsoby;
Aliasowanie tabel może być realizowane po słowie kluczowym AS (zalecane przez autora), jak również po spacji. Ta druga metoda jest jedyną w przypadku niektórych serwerów, takich jak ORACLE. SELECT Nazwisko, Brutto FROM Zarobki Z JOIN Osoby O ON O.IdOsoby=Z.IdOsoby;
Aliasowanie tabel jest niezbędne tylko wtedy, kiedy będziemy łączyć tabelę z samą sobą. Przykłady zastosowania takich zapytań będą przedstawione w dalszej części książki — patrz rozdział 4.
Rozdział 3. Język zapytań SQL w MS SQL Server
53
W standardzie SQL został wprowadzony również operator CROSS JOIN, który zwraca iloczyn kartezjański dwóch tabel. Nie jest on specjalnie użyteczny, ponieważ zastąpienie go przecinkiem realizuje dokładnie tę samą funkcjonalność. SELECT Nazwisko, Brutto FROM Zarobki CROSS JOIN Osoby;
Definiując zapytanie zawierające złączenie, nie musimy zdawać się na działanie wbudowanego optymalizatora, ale za pomocą dyrektyw dla kompilatora (ang. hints) możemy wymusić sposób ich realizacji. Możemy to prześledzić na podstawie zapytania wyświetlającego nazwy działów i nazwiska pracowników w nich pracujących. SELECT Nazwa, Nazwisko FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu = Osoby.IdDzialu;
Oprócz tego, że możemy zastosować JOIN, możemy użyć HASH JOIN oraz MERGE JOIN. Porównując plany wykonania dwóch pierwszych realizacji zapytania (rysunek 3.1), możemy zauważyć, że są one takie same. Oznacza to, że silnik bez jawnego wskazania sposobu realizacji złączenia proponuje właśnie taki jego typ. W przypadku wymuszenia złączenia typu MERGE uzyskany plan wykonania jest bardziej złożony. Po pobraniu danych pochodzących z każdej z tabel, a przed połączeniem ich z wynikowym zestawem rekordów konieczne jest wykonanie sortowania dla każdego składnika. Większa liczba operacji wskazuje na mniejszą wydajność takiej realizacji.
Rysunek 3.1. Porównanie planów wykonania zapytania ze złączeniem w zależności od dyrektyw dla kompilatora
54
MS SQL Server. Zaawansowane metody programowania
Podobną analizę możemy przeprowadzić, porównując plany wykonania zapytania zawierającego INNER JOIN oraz LEFT JOIN — rysunek 3.2. Analizując dwa pierwsze, łatwo zauważyć, że złączenie kierunkowe odpowiada NESTED LOOPS, co jest realizowane w ten sposób, że dla każdego rekordu jednej tabeli poszukujemy par, przeszukując po kolei wszystkie rekordy drugiej. Taką realizację złączenia INNER JOIN możemy uzyskać, stosując właściwą dyrektywę dla kompilatora. Takie same wnioski możemy wyciągnąć również dla złączenia typu RIGHT JOIN.
Rysunek 3.2. Porównanie planów wykonania zapytania o różnych rodzajach złączenia
W celu dopełnienia analizy wszystkich złączeń dokonano porównania złączenia typu INNER JOIN oraz FULL JOIN — rysunek 3.3. Ponieważ złączenie pełne jest połączeniem dwóch złączeń kierunkowych, w planie wykonania zapytania widoczne jest dwukrotne złączenie typu NESTED LOOPS. Oba zapytania łączą te same dwie tabele i odpowiadają dwóm kierunkom złączenia. Jedno z nich jest sprowadzane do skalara, a następnie wyniki są scalane. Ze stopnia złożoności planu wykonania zapytania zawierającego FULL JOIN wynika, że nie jest ono wydajniejsze, co oznacza, że jeśli nie jest to bezwzględnie konieczne, należy tego zapytania unikać i traktować je jedynie jako rozwiązanie ratunkowe, jak to już podkreślono wcześniej. Przyjmijmy, że mamy do dyspozycji tabelę zawierającą trzy pola: Nazwisko, Imie, RokUrodz. Tabela ta nie ma klucza podstawowego. Możemy przyjąć, że jest to lista pobrana z internetu czy zaimportowana z pliku tekstowego lub arkusza kalkulacyjnego. Strukturę tej tabeli przedstawia rysunek 3.4.
Rozdział 3. Język zapytań SQL w MS SQL Server
55
Rysunek 3.3. Porównanie planów wykonania zapytania o różnych rodzajach złączenia Rysunek 3.4. Struktura tabeli pomocniczej ttt
ttt Nazwisko Imie RokUrodz
Spróbujmy teraz wyświetlić te osoby, których nazwisko występuje w tabeli ttt. Możemy w tym celu użyć operatora IN, dla którego lista jest otrzymywana za pomocą zapytania wybierającego. Zapytanie takie musi zwracać dokładnie jedno pole. SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE Nazwisko IN (SELECT Nazwisko FROM ttt);
Spróbujmy teraz sprawdzić zgodność co najmniej dwóch cech. W tym celu dodajmy drugi warunek połączony z pierwszym operatorem logicznym AND i odwołujący się do listy — tym razem imion. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko IN (SELECT Nazwisko FROM ttt) AND Imie IN (SELECT Imie FROM ttt);
Zapytanie takie jest poprawne składniowo, ale rezultaty są różne od oczekiwanych. Przeanalizujmy to na przykładzie danych z rysunku 3.5.
56
MS SQL Server. Zaawansowane metody programowania OSOBY Kowalski
ttt Jan
Kowalski
Karol
Nowak
Jan
Rysunek 3.5. Ilustracja sprawdzania zgodności na dwóch listach
W tabeli Osoby jest rekord dotyczący Kowalskiego Jana. W tabeli ttt takiej osoby nie ma. Są natomiast Kowalski Karol oraz Nowak Jan. Połączone operatory IN działają w ten sposób, że sprawdzają, czy Kowalski jest na liście nazwisk (pola podkreślone) oraz czy imię Jan jest na liście imion (pola podwójnie podkreślone). Jak widać, Kowalski Jan został „złożony” z danych zawartych w dwóch rekordach. Połączenie sprawdzania na co najmniej dwóch listach operatorem AND nie powoduje zawężenia zakresu poszukiwań, a może prowadzić do jego rozszerzenia. Pierwsze rozwiązanie problemu polega na tym, żeby zamiast umieszczać w zapytaniu dla operatora listy pojedyncze pole tabeli, umieszczać pojedyncze pole, ale takie, które jest wynikiem obliczenia wyrażenia. W naszym przypadku pola są łączone operatorem konkatenacji. Ponieważ chcemy sprawdzać zgodność pod względem trzech atrybutów, z których ostatni jest liczbą, należy dokonać jego konwersji do postaci znakowej, np. za pomocą funkcji CAST (w starszych wersjach SQL Server radził sobie z łączeniem pól różnych typów, wykonując konwersję „w locie”; obecnie konwersja jest niezbędna). SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE Nazwisko + Imie + CAST(RokUrodz AS varchar) IN (SELECT Nazwisko + Imie + CAST(RokUrodz AS varchar) FROM ttt);
Takie rozwiązanie jest poprawne składniowo oraz zapewnia sprawdzanie zgodności dla danych z tego samego wiersza. Niestety, ma wadę, ponieważ porównaniu podlegają długie napisy i konieczne jest wykonywanie konwersji do jednego typu (praktycznie zawsze znakowego); rozwiązanie to jest mało wydajne. Przy okazji doświadczeń z listą zbudujmy pomocniczą tabelę Pusta, o jednym polu numerycznym Nr, i nie wpisujmy do niej żadnych danych. Pamiętamy, że w przypadku pustej listy statycznej próba użycia operatora IN kończyła się komunikatem o błędzie i aby temu zapobiec, musieliśmy dodawać jawnie wartość NULL. Natomiast odwołanie do listy dynamicznej, która nie zawiera elementów, kończy się powodzeniem. Oczywiście zwracany jest pusty zestaw rekordów. SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz IN (SELECT Nr FROM Pusta);
Powróćmy teraz do rozważań dotyczących sprawdzania zgodności danych w dwóch tabelach. Tym razem użyjemy złączenia z warunkiem dotyczącym porównywanego pola. SELECT Osoby.Nazwisko FROM Osoby JOIN ttt ON Osoby.Nazwisko=ttt.Nazwisko;
Zapytanie takie jest poprawne składniowo. Wniosek — możliwe jest dokonanie złączenia tabel na polach, które nie są kluczami, a jedynie są zgodne co do typu. Możemy teraz rozbudować warunek złączenia o sprawdzenie zgodności pozostałych atrybutów.
Rozdział 3. Język zapytań SQL w MS SQL Server
57
SELECT Osoby.Nazwisko FROM Osoby JOIN ttt ON Osoby.Nazwisko=ttt.Nazwisko AND Osoby.Imie=ttt.Imie AND Osoby.RokUrodz=ttt.RokUrodz;
Takie zapytanie jest również poprawne składniowo. Liczba warunków definiujących złączenie nie jest ograniczona, z czego możemy wyciągnąć wniosek, że możliwe jest dokonanie złączenia między tabelami z wykorzystaniem wielu pól, z których żadne nie musi być kluczem. Podsumujmy dotychczasowe doświadczenia, budując kilka zapytań wykorzystujących zarówno złączenia, jak i funkcje agregujące. Pierwsze z nich wyznacza wartości średnie wypłat dla każdego z działów firmy. Taki skutek otrzymamy, realizując odpowiednie złączenie między trzema tabelami oraz ustanawiając grupowanie względem pola Nazwa. SELECT Nazwa, AVG(Brutto) AS SredniaDzial FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY Nazwa;
Jeśli dodamy kolejne pole grupowania (Nazwisko), to średnia zostanie wyznaczona dla niższego z poziomów grupowania, czyli dla pracownika. Konieczność pozostawienia nadrzędnego pola grupowania Nazwa jest podyktowana względami składniowymi, jeśli oprócz nazwiska pracownika chcemy znać nazwę działu, w którym pracuje. Reguła mówi, że jeśli na liście pól do wyświetlenia występuje pole, na które nie działa funkcja agregująca, to musi się ono znaleźć w klauzuli GROUP BY. W przykładzie dodano w tej klauzuli pole IdOsoby, które pozwoli rozróżnić dwóch pracowników tego samego działu i o tym samym nazwisku. Załóżmy, że dwa byty mają takie same nazwy i chcemy mieć pewność, iż wskutek tego nie zostaną zgrupowane do jednego bytu. W takiej sytuacji należy oprócz pól, które są niezbędne z przyczyn składniowych, zastosować w opcjach grupowania klucze główne z poziomów tych bytów. SELECT Nazwa, Nazwisko, AVG(Brutto) AS SredniaPracownik FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY Nazwa, Nazwisko, Osoby.IdOsoby;
Dla tak przygotowanego zapytania możemy zastosować dwie metody filtrowania — dotyczącą wartości pól, którą definiujemy w klauzuli WHERE (lista osób, dla których wyznaczono średnie), i dotyczącą funkcji agregującej, która występuje w klauzuli HAVING, po definicji sposobu grupowania. Wynikowy zestaw rekordów został posortowany względem malejących wartości średniej. SELECT Nazwa, Nazwisko, AVG(Brutto) FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby WHERE Osoby.IdOsoby IN (1, 2, 5, 7, 3) GROUP BY Nazwa ,Nazwisko, Osoby.IdOsoby HAVING AVG(brutto)>300 ORDER BY AVG(Brutto) DESC;
58
MS SQL Server. Zaawansowane metody programowania
Należy przypomnieć, że w klauzuli HAVING nie może występować alias nazwy pola, natomiast może pojawić się w klauzuli ORDER BY. Możemy stosować również grupowanie w definicji zapytania dla operatora listy. W zaprezentowanym przykładzie będą to osoby, których suma zarobków jest większa niż 1000. Jak widać, nie została jawnie wyprowadzona wartość tej sumy, a w podzapytaniu zastosowano tylko filtr operujący na funkcji agregującej. SELECT Nazwisko FROM Osoby WHERE IdOsoby IN (SELECT IdOsoby FROM Zarobki GROUP BY IdOsoby HAVING SUM(Brutto)>1000);
Na podobnych zasadach możemy uzyskać dane o osobach, których nazwiska powtarzają się na liście. Podzapytanie dla operatora IN zwraca wykaz takich nazwisk na skutek zastosowania grupowania według nazwisku oraz filtra, który sprawdza, czy liczebność takiego zbioru jest większa niż 1. Wszystkie dodatkowe informacje o takich osobach uzyskujemy w zapytaniu nadrzędnym. Ponownie mamy do czynienia z podzapytaniem, w którym nie wybieramy funkcji agregującej, lecz jedynie stosujemy ją do zdefiniowania sposobu filtrowania. SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko IN (SELECT Nazwisko FROM Osoby GROUP BY Nazwisko HAVING COUNT(Nazwisko) >1);
Spróbujmy teraz wyświetlić osoby, które mają wzrost wyższy niż średnia wzrostu w firmie. Aby to wykonać, tworzymy podzapytanie korzystające z funkcji AVG, bez grupowania. Zapytanie to zwraca jedno pole w jednym wierszu. Taki typ zapytania nazywamy zapytaniem skalarnym. Może być ono użyte do tworzenia wyrażeń, w tym również do definiowania warunków. SELECT Nazwisko, Imie, Wzrost FROM Osoby WHERE Wzrost> (SELECT AVG(Wzrost) FROM Osoby);
Jednak z tak skonstruowanego zapytania nie wynika, ile wynosi wartość średniej. Zapytanie skalarne, poza tym, że może być wykorzystywane do definiowania wyrażeń, może także zostać użyte jako element na liście pól do wyświetlenia. SELECT Nazwisko, Imie, Wzrost, (SELECT AVG(Wzrost) FROM Osoby) AS SredniWzrost FROM Osoby WHERE Wzrost> (SELECT AVG(Wzrost) FROM Osoby);
Prosta zamiana funkcji agregującej na wyznaczającą maksimum MAX oraz zmiana operatora porównania na równości prowadzą do wyświetlenia najwyższych osób w firmie. W takim rozwiązaniu uwzględnia się fakt, że może pojawić się kilka osób, które mają ten sam maksymalny wzrost.
Rozdział 3. Język zapytań SQL w MS SQL Server
59
SELECT Nazwisko, Imie, Wzrost FROM Osoby WHERE Wzrost= (SELECT MAX(Wzrost) FROM Osoby);
Rozszerzeniem tego schematu jest zapytanie wyświetlające osoby o maksymalnej średniej wypłacie. W najbardziej zagnieżdżonym podzapytaniu wyznaczamy średnie wypłaty wszystkich pracowników, ustalając opcje grupowania na IdOsoby. Pole wyznaczające średnią zostało nazwane Sr. Zapytanie to zwraca jedną kolumnę i tyle wierszy, ilu pracowników otrzymało przynajmniej jedną wypłatę. Jeśli zostanie ono ujęte w nawiasy i nazwane po słowie kluczowym AS xxx, to może być traktowane jako źródło danych — tabela dynamiczna. Dlatego podzapytanie na wyższym poziomie w hierarchii wyznacza wartość maksymalną z wszystkich średnich Sr. Zapytanie to zwraca skalar — jedno pole w jednym wierszu. Pozostaje tylko wyświetlić w zapytaniu głównym te rekordy, dla których średnia jest równa tej wartości skalarnej, co realizowane jest za pomocą klauzuli HAVING. SELECT Nazwisko, Imie, AVG(Brutto) AS Srednia FROM Osoby RIGHT JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY Nazwisko, Imie HAVING AVG(Brutto)= (SELECT MAX(Sr) FROM (SELECT AVG(Brutto) AS Sr FROM Zarobki GROUP BY IdOsoby) AS xxx);
Jak widać, konstrukcję i analizę złożonych zapytań powinniśmy rozpocząć od dołu, od najbardziej zagnieżdżonego podzapytania. W przykładzie zastosowano złączenie RIGHT zamiast zwykłego INNER. Ma to na celu zapewnienie, że wyświetlona zostanie również wartość, jeśli najwyższa średnia wypłata pochodzi z wypłat nieprzypisanych do nikogo, np. w zarobkach IdOsoby ma wartość NULL albo wartość, która nie pojawia się na liście identyfikatorów w tabeli nadrzędnej. Kolejnym przykładem utworzonym według tego samego schematu jest zapytanie wyświetlające dział, w którym pracuje najwięcej osób. W najniżej stojącym w hierarchii podzapytaniu wyznaczamy liczebność każdego działu. Zapytanie ujęte w nawias i nazwane xxx stanowi źródło dla zapytania wyznaczającego maksimum liczebności. Zapytanie nadrzędne wyświetla te działy, w których liczba osób jest równa liczbie wyznaczonej przez zapytanie skalarne. Ponownie zastosowano złączenie prawe, aby uwzględnić sytuację, w której najwięcej pracowników nie jest przypisanych do żadnego działu. SELECT Nazwa, COUNT(IdOsoby) AS ile FROM Dzialy RIGHT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu GROUP BY Nazwa HAVING COUNT(IdOsoby)= (SELECT MAX(ile) FROM (SELECT COUNT(IdOsoby) AS ile FROM Osoby GROUP BY IdDzialu) AS xxx);
Kolejny schemat pozwala na wyświetlenie najwyższych pracowników w każdym dziale. W podzapytaniu wyświetlamy identyfikator działu oraz maksymalny wzrost, w każdym z nich nazwany maksi, używając grupowania według identyfikatora działu.
60
MS SQL Server. Zaawansowane metody programowania
Zapytanie to zostało nazwane xxx i może zostać potraktowane jako dynamiczna tabela. Dlatego jest dołączane do nadrzędnego zapytania, które wyświetla interesujące nas informacje. Warunek złączenia zawiera porównanie identyfikatorów z zapytania nadrzędnego oraz z tabeli dynamicznej. W klauzuli WHERE zawarto porównanie wartości maksymalnej wyliczonej w podzapytaniu (maksi) ze wzrostem każdego z pracowników, co realizuje postulat zawarty w pierwszym zdaniu akapitu. SELECT Nazwa, Nazwisko, Wzrost FROM Dzialy RIGHT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN (SELECT IdDzialu, MAX(Wzrost) AS maksi FROM Osoby GROUP BY IdDzialu) AS xxx ON Dzialy.IdDzialu=xxx.IdDzialu WHERE Wzrost=maksi;
Kolejny przykład zastosowania omawianego schematu budowy złożonych zapytań powoduje wyświetlenie listy osób o najdłuższym nazwisku w każdym dziale. Ponownie w podzapytaniu wyznaczamy długość nazwisk (funkcja LEN), z których dla każdego działu wybieramy wartość maksymalną. Dokonujemy złączenia zapytania nadrzędnego z podzapytaniem za pomocą identyfikatora działu. A w klauzuli WHERE następuje porównanie długości każdego z nazwisk z wartością maksymalną w dziale. SELECT Nazwa, Nazwisko, LEN(Nazwisko) FROM Dzialy RIGHT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN (SELECT IdDzialu, MAX(LEN(Nazwisko)) AS maksi FROM Osoby GROUP BY IdDzialu) AS xxx ON Dzialy.IdDzialu=xxx.IdDzialu WHERE LEN(Nazwisko)=maksi;
Należy zauważyć, że w obu przypadkach zamiast dodawać klauzulę WHERE, możemy rozbudować warunek złączenia, jak pokazuje przykład. Oczywiście skutek działania obu zapytań jest dokładnie taki sam. SELECT Nazwa, Nazwisko, LEN(Nazwisko) FROM Dzialy RIGHT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN (SELECT IdDzialu, MAX(LEN(Nazwisko)) AS maksi FROM Osoby GROUP BY IdDzialu) AS xxx ON Dzialy.IdDzialu=xxx.IdDzialu AND LEN(Nazwisko)=maksi;
Bardziej rozbudowaną postać tego samego schematu przedstawia zapytanie, które wyświetla listę pracowników o najwyższej średniej wypłacie brutto w każdym dziale. Tym razem rozpoczynamy od utworzenia zapytania wyświetlającego identyfikator działu i obliczającego średnią wypłatę każdego pracownika, co osiągamy, wykonując podwójne grupowanie względem IdDzialu oraz Osoby.IdOsoby. Podzapytanie to otrzymuje nazwę xxx i staje się źródłem danych dla nadrzędnego zapytania, w którym jest wyświetlany identyfikator oraz jest wyznaczana wartość maksymalna ze średnich na poziomie działu (grupowanie względem IdDzialu). Zewnętrzne podzapytanie otrzymuje nazwę yyy i jest połączone z nadrzędnym zapytaniem za pomocą identyfikatora działu.
Rozdział 3. Język zapytań SQL w MS SQL Server
61
Ponieważ warunek realizujący zadaną funkcjonalność dotyczy funkcji agregującej, został umieszczony w klauzuli HAVING. SELECT Nazwa, Nazwisko, AVG(Brutto) AS Srednio FROM Dzialy RIGHT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby JOIN (SELECT IdDzialu, MAX(SR) AS maksi FROM (SELECT IdDzialu, AVG(Brutto) AS SR FROM Osoby RIGHT JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY IdDzialu, Osoby.IdOsoby) AS xxx GROUP BY IdDzialu) AS yyy ON Dzialy.IdDzialu=yyy.IdDzialu GROUP BY Nazwa, Nazwisko, maksi HAVING AVG(Brutto)=maksi
Utwórzmy pomocniczą tabelę Stawki, która będzie zawierać wyrażoną w walucie definicję dolnego Od i górnego Do ograniczenia przedziału oraz przypisany do niego Opis. CREATE TABLE Stawki (Od money, Do money, Opis varchar(11));
Tabelę wypełniamy przykładowymi danymi, tak jak to pokazano w tabeli 3.13. Ostatni wpis w polu Do jest tylko próbą wyeliminowania sytuacji wyjątkowej, w której wypłata wykracza poza jakikolwiek zakres. Tabela 3.13. Zawartość tabeli Stawki Od
Do
Opis
0,00
500,00
Mało
500,00
1000,00
Średnio
1000,00
999999999999,00
Dużo
Utworzona tabela posłuży do zbudowania zapytania, które obok wartości brutto wyświetli etykietę określającą przynależność do przedziału określonego przez dane w niej zawarte. Rozwiązanie jest proste i polega na skonstruowaniu warunku złączenia za pomocą dwóch nierówności: ostrej i nieostrej, tak aby nie dublować opisów dla danych równych górnym ograniczeniom przedziałów. SELECT Brutto, Opis FROM Zarobki JOIN Stawki ON Brutto>= Od And Brutto< Do;
Jest to pierwszy pokazywany w tej książce przypadek uzasadnionego użycia definicji złączenia za pomocą nierówności. Uzasadnia to fakt, że w opisach złączeń nie używałem sformułowania, iż pola są równe, lecz tylko, że warunek jest prawdziwy, ponieważ w przypadku ogólnym nie musi zawierać operatora „równy”. W przypadku danych walutowych istnieje najmniejsza wartość przyrostu (0,01), zatem zmodyfikujmy dane w tabeli Stawki do postaci zawartej w tabeli 3.14.
62
MS SQL Server. Zaawansowane metody programowania
Tabela 3.14. Zmodyfikowana zawartość tabeli Stawki Od
Do
Opis
0,00
500,00
Mało
500,01
1000,00
Średnio
1000,01
999999999999,00
Dużo
Skoro określenie dolnego ograniczenia przedziału oraz dolnego przedziału bezpośrednio po nim występującego są różne, możemy przepisać poprzednie zapytanie do postaci, w której użyto dwóch nierówności nieostrych. SELECT Brutto, Opis FROM Zarobki JOIN Stawki ON Brutto>= Od And Brutto<= Do;
Taki zapis warunku złączenia jest równoważny zastosowaniu operatora specjalnego BETWEEN. SELECT Brutto, Opis FROM Zarobki JOIN Stawki ON Brutto BETWEEN Od AND Do;
Te doświadczenia wskazują, że do realizacji złączenia można użyć dowolnego spośród wszystkich zdefiniowanych dla serwera operatorów, w tym specjalnych. W naszym przykładzie możemy jednak zauważyć, że do wyznaczenia przedziału nie jest potrzebne użycie dwóch kolumn. Wręcz jest to kłopotliwe, ponieważ musimy uważać, aby dane zawarte w obu kolumnach były spójne. Łatwo można doprowadzić do sytuacji, gdy przedziały na siebie zachodzą. Możemy usunąć jedną z kolumn, a definicja przedziału będzie określona w ten sposób, że wartość Od i opis zawarty w wierszu będą określać dolne ograniczenie i etykietę, natomiast górnym ograniczeniem będzie kolejna co do wielkości wartość zawarta w tej samej kolumnie. Przykład zawartości uproszczonej tabeli Stawki zawiera tabela 3.15. Należy zaznaczyć, że uporządkowanie wierszy względem narastających wartości Od nie jest konieczne i że wynika tylko z chęci zachowania przejrzystości danych. Wiersze mogą być dowolnie zamienione miejscami. Tabela 3.15. Zawartość uproszczonej tabeli Stawki Od
Opis
0,00
Mało
500,00
Średnio
1000,00
Dużo
999999999999,00
NULL
Spróbujmy zrealizować takie samo zadanie jak w poprzednim przykładzie, wykorzystując uproszczoną postać tabeli. Ponieważ zadanie jest trudniejsze od dotychczasowych, postanowiłem rozbić je na kolejne elementarne kroki, które są prezentowane za pomocą rozbudowanych po kolei zapytań SQL oraz wyników, które one zwracają. Pierwszym krokiem jest wyznaczenie iloczynu kartezjańskiego wszystkich rekordów pochodzących z dwóch kopii tabeli Stawki. Ponieważ interesują nas tylko niepowta-
Rozdział 3. Język zapytań SQL w MS SQL Server
63
rzające się wartości par kolumny Do z obu tabel, takie, że pierwsza z nich będzie mniejsza od drugiej, zastosowano złączenie za pomocą operatora nierówności ostrej. Powstaje dzięki temu odpowiednik macierzy trójkątnej górnej, tzn. takiej, gdzie z macierzy wyjściowej wyeliminowano diagonalę oraz wszystkie elementy leżące poniżej niej — tabela 3.16. SELECT s1.Opis, s2.Opis, s1.Od, s2.Od FROM Stawki AS s1 JOIN Stawki As s2 ON s1.Od < s2.Od;
Tabela 3.16. Wynik zapytania tworzącego macierz trójkątną górną na podstawie uproszczonej tabeli Stawki Opis
Opis
Od
Od
Mało
Średnio
0,00
500,00
Mało
Dużo
0,00
1000,00
Średnio
Dużo
500,00
1000,00
Mało
NULL
0,00
999999999999,00
Średnio
NULL
500,00
999999999999,00
Dużo
NULL
1000,00
999999999999,00
Analizując otrzymane wyniki, widzimy, że w pierwszej kolumnie otrzymaliśmy interesujące nas etykiety przedziałów, ponieważ pierwsza z kolumn Od pokazuje dolne wartości przedziałów. Natomiast poprawna definicja górnego ograniczenia przedziału jest zawarta w tych rekordach, w których dla tej samej wartości pierwszej kolumny Od wartość w drugiej z nich jest najmniejsza. Taki zestaw rekordów uzyskujemy, stosując funkcję agregującą MIN oraz grupowanie względem pierwszej kolumny Opis. Natomiast druga kolumna Opis została wyeliminowana, ponieważ nie niesie istotnej informacji, a jednocześnie uniemożliwia ustanowienie właściwego grupowania. Dodatkowo obu kolumnom nadano aliasy pozwalające na określenie odgrywanej przez nie roli. Wyniki otrzymane na skutek uruchomienia tego zapytania przedstawia tabela 3.17. SELECT s1.Opis, s1.Od AS Minimum, MIN(s2.Od) AS Maksimum FROM Stawki AS s1 JOIN Stawki As s2 ON s1.Od < s2.Od GROUP BY s1.Opis, s1.Od;
Tabela 3.17. Wynik zapytania tworzącego przedziały na podstawie uproszczonej tabeli Stawki Opis
Minimum
Maksimum
Mało
0,00
500,00
Średnio
500,00
1000,00
Dużo
1000,00
999999999999,00
Pozostaje wykonanie ostatniego kroku, którym jest zastosowanie utworzonego zapytania jako tabeli dynamicznej w zapytaniu wyświetlającym interesujące nas dane. Podzapytanie o aliasie xxx jest odpowiednikiem wyjściowej tabeli Stawki, w której były dwie kolumny określające krańce przedziału. SELECT Brutto, Opis FROM Zarobki JOIN (SELECT s1.Opis, s1.Od AS Minimum, MIN(s2.Od) AS Maksimum FROM Stawki AS s1 JOIN Stawki As s2
64
MS SQL Server. Zaawansowane metody programowania ON s1.Od < s2.Od GROUP BY s1.Opis, s1.Od) AS xxx ON Brutto >= Minimum AND Brutto < Maksimum;
Rozwiązanie postawionego problemu pokazuje praktyczną użyteczność generowania iloczynu kartezjańskiego (macierzy trójkątnej górnej) dla kopii tabel. Podobny mechanizm postępowania możemy prześledzić dla przypadku określenia zależności koszyka zakupów. Interesuje nas informacja, jakie pary towarów występują jednocześnie w ramach tej samej faktury (te same zakupy). Wynik powinien zawierać zarówno dane o nazwach współwystępujących towarów, jak i o ich liczbie w danej transakcji. Takie zadanie możemy określić jako wyznaczenie quasi-korelacji towarów. Użyto prefiksu quasi, aby nie sugerować, że wyznaczany będzie znany ze statystyki współczynnik Pearsona. Pierwszym krokiem jest wyznaczenie iloczynu kartezjańskiego na dwóch kopiach tabeli Transakcje i wyświetlenie identyfikatora faktury i dwóch kopii identyfikatorów towarów (tabela 3.18). SELECT T1.IdFaktury, T1.IdTowaru,T2.IdTowaru FROM Transakcje AS T1 JOIN Transakcje T2 ON T1.IdFaktury=T2.IdFaktury ORDER BY T1.IdFaktury,T1.IdTowaru;
Tabela 3.18. Wynik zapytania generującego iloczyn kartezjański na dwóch kopiach tabeli Transakcje z zaznaczeniem wierszy do wyeliminowania w kolejnym kroku IdFaktury
IdTowaru
IdTowaru
1
25
25
3
17
17
3
17
41
3
41
41
3
41
17
…
…
…
Ponieważ interesuje nas relacja między różnymi towarami, należy wyeliminować autokorelacje, czyli zależności typu AA, BB. Aby nie prowadzić do redundancji zależności, trzeba również wyeliminować jedną z par, gdzie poprzednik zamienia się miejscami z następnikiem, czyli jedną z par typu AB, BA. Prowadzi to ponownie do wyznaczenia macierzy trójkątnej górnej, tym razem na podstawie relacji nierówności między identyfikatorami towaru. SELECT T1.IdFaktury, T1.IdTowaru,T2.IdTowaru FROM Transakcje AS T1 JOIN Transakcje T2 ON T1.IdFaktury=T2.IdFaktury AND T1.IdTowaru
Tak wyznaczona tabela dynamiczna, nazwana xxx, będzie źródłem danych dla nadrzędnego zapytania, w którym wyznaczona zostanie dla każdej pary towarów liczba transakcji, która je zawiera. Początek przykładowego, wynikowego zestawu rekordów zawiera tabela 3.19.
Rozdział 3. Język zapytań SQL w MS SQL Server
65
SELECT Tow1, Tow2, COUNT(*) AS Ile FROM (SELECT T1.IdFaktury, T1.IdTowaru AS Tow1, T2.IdTowaru AS Tow2 FROM Transakcje AS T1 JOIN Transakcje AS T2 ON T1.IdFaktury=T2.IdFaktury AND T1.IdTowaru
Tabela 3.19. Wynik zapytania wyznaczającego liczbę transakcji dla macierzy trójkątnej górnej wyznaczonej na podstawie iloczynu kartezjańskiego na tabeli Faktury Tow1
Tow2
Ile
1
5
1
1
7
1
…
…
…
17
22
10
17
30
3
…
…
…
Pełne rozwiązanie postawionego problemu jest jednak trochę bardziej złożone. Do wyznaczenia macierzy trójkątnej górnej nie zastosowano tym razem bezpośrednio dwóch kopii tabeli Transakcje, lecz zapytania korzystające dodatkowo z tabeli Towary, nazwane w przykładzie T1, T2. Taki zabieg pozwala na wyznaczenie poza identyfikatorem towaru również jego nazwy, a także wartości transakcji. Macierz trójkątna górna wynikająca z iloczynu kartezjańskiego obu zapytań ma ponownie nazwę xxx. Z niej wyznaczane są nazwy towarów oraz właściwe funkcje agregujące. SELECT Tow1, Tow2, COUNT(*) AS Ile , SUM(Szt1) AS Razem1, SUM(Szt2) AS Razem2, SUM(Wart1) AS Wartosc1, SUM(Wart2) AS Wartosc2 FROM (SELECT T1.IdFaktury, T1.NazwaTowaru AS Tow1,T2.NazwaTowaru AS Tow2, T1.szt AS Szt1, T2.szt AS Szt2, T1.Wartosc AS Wart1, T2.Wartosc AS Wart2 FROM (SELECT IdFaktury, Transakcje.IdTowaru, NazwaTowaru, szt, szt*cena AS Wartosc FROM Transakcje JOIN Towar ON Transakcje.IdTowaru = Towar.IdTowaru) AS T1 JOIN (SELECT IdFaktury, Transakcje.IdTowaru, NazwaTowaru, szt, szt*cena AS Wartosc FROM Transakcje JOIN Towar ON Transakcje.IdTowaru = Towar.IdTowaru) AS T2 ON T1.IdFaktury=T2.IdFaktury AND T1.IdTowaru
Ważnym zadaniem praktycznym jest wyznaczanie funkcji agregującej na wielu poziomach. Rozważmy to na przykładzie podsumowania wypłat na dwóch poziomach: dla każdego pracownika oraz dla każdego z działów. W takim przypadku konieczne jest zbudowanie podzapytania podsumowującego na poziomie nadrzędnym, w przykładzie nazwanym xxx, i połączenie z nadrzędnym zapytaniem wyznaczającym to podsumowanie na poziomie podrzędnym. Połączenie między nimi jest realizowane za pomocą identyfikatora działu występującego na obu poziomach.
66
MS SQL Server. Zaawansowane metody programowania SELECT Nazwa, Nazwisko, SUM(Brutto)As RazemO, RazemD FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby JOIN (SELECT IdDzialu, SUM(Brutto) AS RazemD FROM Osoby JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY IdDzialu) AS xxx ON Osoby.IdDzialu=xxx.IdDzialu GROUP BY Nazwa, Nazwisko, Osoby.IdOsoby, RazemD;
Jeśli mamy sumowanie na dwóch poziomach, konieczne jest zbudowanie zapytania i podzapytania dla trzech: zapytania i dwóch podzapytań. Czyli ze wzrostem liczby poziomów wyznaczania funkcji agregujących stopień złożoności budowanych zapytań znacznie rośnie. Rozumiejąc ten problem, twórcy MS SQL Server zaproponowali już w bardzo wczesnych wersjach produktu rozwiązanie w postaci dwóch opcji grupowania. Pierwszą z nich jest WITH ROLLUP, występująca po liście pól grupowania. Wyniki uzyskane za pomocą takiego zapytania przedstawia tabela 3.20. SELECT Nazwa, Nazwisko, SUM(Brutto) AS Razem FROM Dzialy JOIN Osoby ON Dzialy.Iddzialu=Osoby.Iddzialu JOIN Zarobki On Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY Nazwa, Nazwisko WITH ROLLUP;
Tabela 3.20. Wynik zapytania wyznaczającego podsumowanie z opcją WITH ROLLUP Nazwa
Nazwisko
Razem
Administracja
Janik
555,00
Administracja
Nowak
1332,00
Administracja
NULL
1887,00
Dyrekcja
Kowalski
2109,00
Dyrekcja
NULL
2109,00
Techniczny
Adamczyk
777,00
Techniczny
Kow
222,00
Techniczny
NULL
999,00
NULL
NULL
4995,00
Analizując wyniki, widzimy, że w rekordach, w których występują jednocześnie nazwy działów oraz nazwiska, wyznaczana jest suma wynagrodzenia dla pracownika. W rekordach, gdzie dla wymienionego działu pojawia się w polu Nazwisko wartość NULL, podsumowanie dotyczy działu. Jeśli oba pola są NULL, to obliczona suma dotyczy wszystkich wynagrodzeń w firmie. Możemy stwierdzić, że przy zachowaniu prostej postaci formalnej zrealizowane zostało podsumowanie na trzech poziomach hierarchii: całej firmy, każdego z działów i każdego z pracowników. Podobną funkcjonalność oferuje opcja WITH CUBE. Jak widać z wyników zawartych w tabeli 3.21, zastosowanie tej opcji powoduje, że do zestawu rekordów, który jest generowany na skutek zastosowania WITH ROLLUP, zostały dodane te, w których wartość NULL mają nazwy działów.
Rozdział 3. Język zapytań SQL w MS SQL Server
67
Możemy powiedzieć, że są to podsumowania dla pracowników z pominiętą nazwą działów. SELECT Nazwa, Nazwisko, SUM(Brutto) FROM Dzialy JOIN Osoby ON Dzialy.Iddzialu=Osoby.Iddzialu JOIN Zarobki On Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY Nazwa, Nazwisko WITH CUBE;
Tabela 3.21. Wynik zapytania wyznaczającego podsumowanie z opcją WITH CUBE Nazwa
Nazwisko
Razem
Administracja
Janik
555,00
Administracja
Nowak
1332,00
Administracja
NULL
1887,00
Dyrekcja
Kowalski
2109,00
Dyrekcja
NULL
2109,00
Techniczny
Adamczyk
777,00
Techniczny
Kow
222,00
Techniczny
NULL
999,00
NULL
NULL
4995,00
NULL
Adamczyk
777,00
NULL
Janik
555,00
NULL
Kow
222,00
NULL
Kowalski
2109,00
NULL
Nowak
1332,00
W przypadku stosowania obu opcji może pojawić się sytuacja, że w jednym dziale pracują osoby o tym samym nazwisku. Aby te osoby rozróżnić, możemy dodać kolejne pole grupowania, które będzie kluczem podstawowym. SELECT Nazwa, Nazwisko, SUM(Brutto)AS Suma FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY Nazwa,Nazwisko, Osoby.IdOsoby WITH ROLLUP;
Niestety, powoduje to, że rekordy są reprezentowane podwójnie, ponieważ oba pola wskazują na ten sam poziom w hierarchii tabel. Aby tego uniknąć, konieczne jest zastosowanie dyrektywy DISTINCT. SELECT DISTINCT Nazwa, Nazwisko, SUM(Brutto)AS Suma FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY Nazwa,Nazwisko, Osoby.IdOsoby WITH ROLLUP;
68
MS SQL Server. Zaawansowane metody programowania
Pomysł Microsoftu jednak nie przyjął się powszechnie, dodatkowo ANSI zaproponowała nieco inną składnię tego rozwiązania. Różnica polega na tym, że zrezygnowano ze słowa kluczowego WITH, nazwa opcji występuje bezpośrednio po klauzuli GROUP BY, a lista pól jest ujęta w nawiasy, co pokazuje przykład. Jedynym uzasadnieniem takich zmian jest fakt, że dyrektywa WITH odgrywa inną rolę — definiuje dynamiczną perspektywę, co zostanie pokazane dalej. Microsoft wprowadził do swojego produktu również i taką wersję składni, jednak dostępna jest ona dopiero od SQL Server 2008. Jeśli zaimportowaliśmy bazę w starszej wersji, konieczne jest ustalenie właściwego poziomu zgodności, co pokazuje pierwsza linia skryptu. Wartości ustawianego parametru mogą być następujące: 80 — SQL 2000, 90 — SQL 2005, 100 — SQL 2008 (łącznie z R2), 110 — SQL 2012. W skrypcie zablokowano jedną z opcji, stosując znak komentarza ––, który powinien być w celu przetestowania obu wersji przeniesiony o jedną linię do góry. ALTER DATABASE BazaRelacyjna SET COMPATIBILITY_LEVEL = 100; SELECT Nazwa, Nazwisko, SUM(Brutto) FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY CUBE(Nazwa, Nazwisko) --GROUP BY ROLLUP(Nazwa, Nazwisko)
Oczywiście zarówno stara, jak i nowa wersja składniowa dają w rezultacie dokładnie ten sam zestaw rekordów. Nową opcją wprowadzoną dla takiej notacji jest GROUPING SETS, która wyznacza podsumowania z uwzględnieniem danych tylko z jednego poziomu (tabela 3.22). Oznacza to, że na podstawie podsumowania dla pracownika nie możemy jednoznacznie przypisać go do działu. SELECT Nazwa, Nazwisko, SUM(Brutto) FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY GROUPING SETS(Nazwa, Nazwisko);
Tabela 3.22. Wynik zapytania wyznaczającego podsumowanie z opcją GROUPING SETS Nazwa
Nazwisko
Razem
Administracja
NULL
1887,00
Dyrekcja
NULL
2109,00
Techniczny
NULL
999,00
NULL
NULL
4995,00
NULL
Adamczyk
777,00
NULL
Janik
555,00
NULL
Kow
222,00
NULL
Kowalski
2109,00
NULL
Nowak
1332,00
Rozdział 3. Język zapytań SQL w MS SQL Server
69
Może się wydawać, że użyteczność opcji GROUPING SETS jest znikoma. O jej sile możemy się przekonać dopiero wtedy, kiedy mamy do czynienia z co najmniej trzema poziomami grupowania. Dlatego korzystając z tabel Transakcje, Towar, Producenci, Miasta, Wojewodztwa, utworzymy zapytanie o pięciu poziomach grupowania z zastosowaniem GROUPING SETS. Otrzymamy wówczas rozdzielne podsumowania dla każdej z grup. SELECT NazwaTowaru, NazwaKategorii, NazwaProducenta, Miasto, Województwo, SUM(Cena * szt) AS Wartosc FROM Transakcje INNER JOIN Towar ON Transakcje.IdTowaru = Towar.IdTowaru INNER JOIN Producenci ON Towar.IdProducenta = Producenci.IdProducenta INNER JOIN Miasta ON Producenci.IdMiasta = Miasta.IdMiasta INNER JOIN Wojewodztwa ON Miasta.IdWojewodztwa = Wojewodztwa.IdWojewodztwa INNER JOIN Kategorie ON Towar.IdKategorii = Kategorie.IdKategorii GROUP BY GROUPING SETS (NazwaTowaru, NazwaKategorii, NazwaProducenta, Miasto, Województwo);
W przypadku stosowania GROUPING SETS możliwe jest definiowanie wewnętrznych grup podsumowań za pomocą pozostałych dwóch opcji — ROLLUP i CUBE. Możliwe jest dowolne ich mieszanie oraz stosowanie wspólnych podzestawów atrybutów dla każdej z opcji. Uzyskujemy dzięki temu bardzo dużą elastyczność definiowania poziomów, na których wyznaczane są funkcje agregujące w przypadku zdefiniowania pojedynczego zapytania wybierającego. SELECT NazwaTowaru, NazwaKategorii, NazwaProducenta, Miasto, Województwo, SUM(Cena * szt) AS Wartosc FROM Transakcje INNER JOIN Towar ON Transakcje.IdTowaru = Towar.IdTowaru INNER JOIN Producenci ON Towar.IdProducenta = Producenci.IdProducenta INNER JOIN Miasta ON Producenci.IdMiasta = Miasta.IdMiasta INNER JOIN Wojewodztwa ON Miasta.IdWojewodztwa = Wojewodztwa.IdWojewodztwa INNER JOIN Kategorie ON Towar.IdKategorii = Kategorie.IdKategorii GROUP BY GROUPING SETS (CUBE(NazwaTowaru, NazwaKategorii), ROLLUP(NazwaProducenta, Miasto, Województwo));
Opisane poprzednio opcje grupowania znacznie ułatwiają budowanie zapytań analitycznych, jednak prowadzą do zmiany sposobu wyświetlania agregatów dla różnych poziomów, które są prezentowane w oddzielnych wierszach wynikowego zestawu rekordów. Taka forma wyświetlania znacznie utrudnia korzystanie z wyników na poziomie końcówki klienckiej. Możliwe jest wyświetlanie podsumowań w jednym wierszu, jeśli zastosujemy definicję okna. Zanim przejdziemy do tego zagadnienia, zapoznajmy się z funkcjami rangowymi wyznaczanymi nad oknem logicznym. Pierwszą z nich jest funkcja zwracająca numer wiersza ROW_NUMBER(). Minimalna definicja okna dla tego typu funkcji, występująca po słowie kluczowym OVER, składa się z określenia sposobu sortowania. SELECT IdOsoby, Brutto, ROW_NUMBER() OVER(ORDER BY Brutto DESC)AS RowNumber FROM Zarobki
Rekordy zostały ponumerowane, począwszy od tego, dla którego wartość Brutto była największa. Numeracja jest ciągła, a w przypadku równych wartości pola o numerze decyduje silnik bazy danych. Możliwe jest podzielenie zakresu, w którym realizowana
70
MS SQL Server. Zaawansowane metody programowania
jest numeracja na przedziały, za pomocą dyrektywy PARTITION BY. Jest to analogia do opcji grupowania w klasycznym zapytaniu wybierającym. W przykładzie rekordy zostały ponumerowane oddzielnie dla każdego z pracowników, a o numerze decydują malejące wartości wypłat. SELECT IdOsoby, Brutto, ROW_NUMBER() OVER(PARTITION BY IdOsoby ORDER BY Brutto DESC)AS RowNumber FROM Zarobki;
Przez analogię możemy skonstruować zapytanie, które ustala numer wiersza według wzrostu w obrębie każdego działu. Również tutaj dla równych wartości wzrostu o numerze wiersza decyduje silnik bazy danych. SELECT IdDzialu ,Wzrost, ROW_NUMBER() OVER(PARTITION BY IdDzialu order by Wzrost DESC) AS NUMER_WIERSZA FROM Osoby ORDER BY IdDzialu;
Według analogicznych zasad działa funkcja RANK(). Zasadnicza różnica polega na uwzględnieniu miejsc ex aequo, którym przypisywane są te same wartości, ale następna jest zwiększana o N – 1, gdzie N jest liczbą powtórzeń tej samej wartości. SELECT IdDzialu ,Wzrost, RANK() OVER(PARTITION BY IdDzialu ORDER BY Wzrost DESC) AS RANK FROM Osoby ORDER BY IdDzialu;
Podobnie działa gęsty ranking DENSE_RANK(), który również uwzględnia pozycje ex aequo, jednak kolejna wartość jest większa o 1; nie ma przeskoku w numeracji. SELECT IdDzialu ,Wzrost, DENSE_RANK() OVER(PARTITION BY IdDzialu ORDER BY Wzrost DESC) AS RANK FROM Osoby ORDER BY IDdzialu;
Kolejną funkcją tego rodzaju jest NTILE(), która wymaga podania parametru. Określa on głębokość kodowania. Można powiedzieć, że ustala liczbę koszyków, na które będzie podzielony cały zakres parametru określonego w definicji okna logicznego. Jeśli na krawędzi między dwoma przedziałami będą takie same wartości, to o ich przypisaniu do jednego z nich decyduje silnik serwera bazy danych. Jeśli liczba wierszy nie będzie bez reszty podzielna przez liczbę przedziałów, to zyskują na tym przedziały o niższych numerach. Różnica ich liczebności nie może być większa niż 1 element. SELECT IdDzialu ,Wzrost, NTILE(2) OVER(PARTITION BY IdDzialu ORDER BY Wzrost DESC) AS NUMER_KOSZYKA FROM Osoby ORDER BY IdDzialu;
Oczywiście liczba użytych w jednym zapytaniu funkcji rangowych nad oknem logicznym nie jest ograniczona. W przykładzie przedstawiono taką sytuację, najpierw numerując po kolei wszystkie wiersze, a następnie generując numery dla każdego z działów oddzielnie. Parametrami decydującymi o kolejności są identyfikator działu i malejąca wartość wzrostu.
Rozdział 3. Język zapytań SQL w MS SQL Server
71
SELECT IdDzialu ,Wzrost, ROW_NUMBER() OVER(ORDER BY Iddzialu,Wzrost DESC) AS NUMER_WIERSZA, ROW_NUMBER() OVER(PARTITION BY IdDzialu ORDER BY Iddzialu, Wzrost DESC) AS NUMER_W_Dziale FROM Osoby WHERE IdDzialu IS NOT NULL ORDER BY IdDzialu, Wzrost DESC;
Nie po raz pierwszy staje się bardzo widoczny nacisk producentów baz danych na przetwarzanie analityczne. Praktycznie z każdą nową realizacją produktu wprowadzane są nowe funkcjonalności. To samo dotyczy omawianego środowiska i funkcji rankingowych. W wersji 2008 R2 dodane zostały: FIRST_VALUE (parametr) — zwraca wartość parametru będącego argumentem
funkcji dla pierwszego rekordu okna wynikającego z zastosowanego sortowania; LAST_VALUE (parametr) — zwraca wartość parametru będącego argumentem
funkcji dla ostatniego rekordu okna wynikającego z zastosowanego sortowania; PERCENT_RANK() — wyraża ranking w skali procentowej, kodując go do obustronnie domkniętego przedziału <0, 1>; CUME_DIST() — wyraża ranking w skali procentowej, kodując go do jednostronnie domkniętego przedziału (0, 1> zgodnie z zależnością p/N, gdzie p jest liczbą rekordów, w których wartość jest nie większa niż argument sortowania, a N
jest liczbą wszystkich rekordów okna; LAG (parametr, przesunięcie, domyślna) — wyświetla wartość argumentu
sortowania z rekordu przesuniętego do tyłu o wartość drugiego parametru; gdy sięgamy poza zestaw rekordów, zwracana jest wartość domyślna; tylko pierwszy parametr jest obowiązkowy, domyślną wartością przesunięcia jest 1, a wartości domyślnej NULL; LEAD (parametr, przesunięcie, domyślna) — wyświetla wartość argumentu
z rekordu przesuniętego do przodu o wartość drugiego parametru; gdy sięgamy poza zestaw rekordów, zwracana jest wartość domyślna; tylko pierwszy parametr jest obowiązkowy, domyślną wartością przesunięcia jest 1, a wartości domyślnej NULL; Poniżej zaprezentowane zostało zastosowanie nowo wprowadzonych funkcji w zapytaniu wybierającym. SELECT Nazwa, Nazwisko, Wzrost, FIRST_VALUE(Nazwisko) OVER(PARTITION BY Nazwa ORDER BY Wzrost DESC) AS WyzszyWDziale, LAST_VALUE(Nazwisko) OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS NizszyWDziale, PERCENT_RANK() OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS RankingProcentowy, CUME_DIST() OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS RankingProcentowySkumulowany, LEAD(Wzrost) OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS DoPrzodu, LEAD(Wzrost, 2) OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS DoPrzoduODwa, LEAD(Wzrost, 2, 0) OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS DoPrzoduODwaZDomyslna, LAG(Wzrost) OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS DoTylu, LAG(Wzrost, 2) OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS DoTyluODwa,
72
MS SQL Server. Zaawansowane metody programowania LAG(Wzrost, 2, 0) OVER(PARTITION BY Nazwa ORDER BY Wzrost ASC) AS DoTyluODwaZDomyslna FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY Nazwa, Wzrost DESC;
Formalnie kolejne funkcje są traktowane jako funkcje analityczne, co wynika zarówno z ich zastosowania, jak i nieco odmiennej postaci składni. Jednak takie ich przypisanie jest tylko zabiegiem porządkowym, stąd omawiane są w tym samym miejscu. Zostały one wprowadzone w wersji 2012, więc jeśli posiadamy bazę danych utworzoną w starszej wersji serwera i baza ta została zaimportowana do serwera tej wersji, konieczne jest zmodyfikowanie bazy danych, tak aby była z tą wersją w pełni kompatybilna. Uzyskujemy to, wykonując zapytanie: ALTER DATABASE BazaRelacyjna SET COMPATIBILITY_LEVEL=110
Wartość flagi systemowej 110 oznacza zgodność z wersją 2012. Jeśli nie ustalimy właściwego trybu zgodności, pojawi się komunikat o błędzie o postaci: Msg 10762, Level 15, State 1, Line 6 The PERCENTILE_CONT function is not allowed in the current compatibility mode. It is only allowed in 110 mode or higher.
Omawiane funkcje obliczają percentyle (kwantyle rzędu p=n/100), co oznacza, że dla danego rozkładu zmiennej losowej znajdujemy taką liczbę, która reprezentuje pozycję p x <1–p w analizowanej grupie. PERCENTILE_DISC(n) odpowiada rozkładowi dyskretnemu, czyli wskazywana pozycja jest elementem grupy, natomiast PERCENTILE_ CONT(n) rozkładowi ciągłemu, co powoduje wyznaczenie wartości aproksymowanej, która nie musi pokrywać się z żadnym z elementów grupy. Składniowo parametr, względem którego określamy porządek rekordów, określamy w definicji WITHIN GROUP(), a podział na grupy tak jak w funkcjach rangowych w definicji partycji OVER(). O ile sposób porządkowania musi być określony, o tyle definicja partycji może być pusta, co odpowiada analizie całego zakresu rekordów. W przykładzie pokazano działanie obu funkcji do analizy rozkładu wzrostu w każdym z działów, w przypadku trzech wartości percentyli 0, 0.5, 1. SELECT Nazwa, Nazwisko, Wzrost, PERCENTILE_CONT(0) WITHIN GROUP(ORDER BY Wzrost DESC) OVER(PARTITION BY Nazwa), PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY Wzrost DESC) OVER(PARTITION BY Nazwa), PERCENTILE_CONT(1) WITHIN GROUP(ORDER BY Wzrost DESC) OVER(PARTITION BY Nazwa), PERCENTILE_DISC(0) WITHIN GROUP(ORDER BY Wzrost DESC) OVER(PARTITION BY Nazwa), PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY Wzrost DESC) OVER(PARTITION BY Nazwa), PERCENTILE_DISC(1) WITHIN GROUP(ORDER BY Wzrost DESC) OVER(PARTITION BY Nazwa) FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY Nazwa, Wzrost DESC;
Nad oknem logicznym (partycją) można również wyznaczać klasyczne funkcje agregujące. Muszą się one odnosić do pola wymienionego na liście po poleceniu SELECT. Definicja okna nie zawiera sposobu sortowania, lecz tylko sposób podziału. Minimalna definicja jest pusta, co powoduje, że funkcja agregująca jest wyznaczana dla wszystkich rekordów. Zastosowanie partycjonowania ogranicza zakres wyznaczania tej funkcji do wskazanych grup rekordów. Funkcje te mogą stanowić elementy wyrażeń, co pozwala na wyznaczanie udziałów procentowych:
Rozdział 3. Język zapytań SQL w MS SQL Server
73
SELECT IdDzialu, Osoby.IdOsoby, Brutto, SUM(Brutto) OVER() AS Suma, SUM(Brutto) OVER(PARTITION BY IdDzialu) AS Suma_dzial, Brutto / SUM(Brutto) OVER(PARTITION BY IdDzialu) AS Udzial FROM Osoby JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby;
Bardziej rozbudowany zestaw współczynników wyznaczanych za pomocą funkcji agregujących nad oknem logicznym zawiera kolejny przykład. SELECT IdDzialu, Osoby.IdOsoby, Brutto, SUM(Brutto) OVER () AS Suma, SUM(Brutto) OVER (PARTITION BY IdDzialu) AS Suma_Dzial, Brutto / SUM(Brutto) OVER(PARTITION BY IdDzialu) AS UdzialWyp_w_Dziale, SUM(Brutto) OVER (PARTITION BY Osoby.IdOsoby)/ SUM(Brutto) OVER(PARTITION BY IdDzialu) AS UdzialSUM_w_Dziale, Brutto / SUM(Brutto) OVER() AS UdzialWyp_w_Firmie, SUM(Brutto) OVER (PARTITION BY Osoby.IdOsoby)/SUM(Brutto) OVER() AS UdzialSUM_w_Firmie, SUM(Brutto) OVER (PARTITION BY IdDzialu)/SUM(Brutto) OVER() AS UdzialDzialu_w_Firmie FROM Osoby JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby ORDER BY IdDzialu;
Gdy funkcje agregujące są definiowane nad oknem logicznym, możliwe jest ich składanie. Pamiętać jednak należy, że wewnętrzna funkcja musi być jawnie obliczona w zapytaniu — w naszym przypadku suma wypłat nazwana Razem. Wymusza to również zastosowanie właściwych, zgodnych z wymaganiami składniowymi opcji grupowania — klauzula GROUP BY. SELECT IdDzialu, Osoby.IdOsoby, SUM(Brutto) AS Razem, AVG(SUM(Brutto)) OVER() AS SredniaSuma, AVG(SUM(Brutto)) OVER(PARTITION BY IdDzialu) AS SredniaSuma_dzial, SUM(Brutto) / AVG(SUM(Brutto)) OVER(PARTITION BY IdDzialu) AS Wspolczynnik FROM Osoby JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby GROUP BY IdDzialu, Osoby.IdOsoby;
W wersji 2012 w prowadzono rozszerzenia definicji okna logicznego. Możliwe jest oprócz grupowania zastosowanie sortowania. Powoduje to, że w obrębie partycji mamy zamiast podsumowania wyznaczaną bieżącą, kroczącą wartość funkcji agregującej, np. sumę bieżącą. Oznacza to, że w kolejności, w jakiej ustawiono rekordy w grupie, w pierwszym rekordzie pojawia się wartość pierwszego, w drugim suma pierwszego i drugiego rekordu, w trzecim suma od pierwszego do trzeciego itd. Możemy to interpretować jako zmienną definicję końca okna, która przesuwa się, począwszy od pierwszego do ostatniego rekordu okna, podczas gdy jego początek pozostaje stały i jest ustalony na pierwszym rekordzie grupy. SELECT Nazwa, Nazwisko, Brutto, SUM(Brutto) OVER() AS SumaFirma, SUM(Brutto) OVER(ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku) AS BiezacaFirma, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu) AS SumaDzial, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku) AS BiezacaDzial, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu, Osoby.IdOsoby) AS SumaPracownik,
74
MS SQL Server. Zaawansowane metody programowania SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu, Osoby.IdOsoby ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku) AS BiezacaPracownik FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby;
Do tej pory definicja położenia początku i końca okna nie była podawana jawnie, lecz tylko wynikała z przyjętej postaci składni. Jawne określenie jednego lub obu krańców daje dużo większe możliwości analizy. Określenie okna przez ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW wskazuje jawnie na sumę bieżącą liczoną od pierwszego do ostatniego wiersza grupy. W definicji UNBOUNDED PRECEDING oznacza niezwiązany początek, czyli pierwszy rekord, a CURRENT ROW oznacza bieżący rekord. Odwrotne określenie okna CURRENT ROW AND UNBOUNDED FOLLOWING oznacza, że suma bieżąca jest wyznaczana, począwszy od końca grupy do bieżącego rekordu, czyli w pierwszym rekordzie znajdziemy podsumowanie dla całego okna, a w kolejnych będą one zmniejszane. Można powiedzieć, że definicje te określają przeciwne kierunki wyznaczania bieżących podsumowań, a szerzej — bieżących funkcji agregujących. SELECT Nazwa, Nazwisko, Brutto, SUM(Brutto) OVER() AS SumaFirma, SUM(Brutto) OVER(ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacaFirma, SUM(Brutto) OVER(ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaFirma1, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu) AS SumaDzial, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacaDzial, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaDzial1, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu, Osoby.IdOsoby) AS SumaPracownik, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu, Osoby.IdOsoby ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacaPracownik, SUM(Brutto) OVER(PARTITION BY Dzialy.IdDzialu, Osoby.IdOsoby ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaPracownik1 FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby ORDER BY Dzialy.IdDzialu, Osoby.IdOsoby, IdZarobku;
W przypadku definicji zakresu wierszy do wyznaczenia okna możliwe jest zastosowanie dwóch ruchomych granic przedziału. Definicja 1 PRECEDING AND 1 FOLLOWING powoduje, że okno obejmuje jeden wiersz przed bieżącym, wiersz bieżący oraz jeden wiersz po nim. Oba przesunięcia względem pozycji aktualnej mogą być definiowane przez dowolną nieujemną liczbę, rozszerzając odpowiednio zakres wyznaczania agregatu. Oczywiście, jeśli istnieje taka potrzeba, N PRECEDING może zostać zastąpiony przez CURRENT ROW albo początek okna UNBOUNDED PRECEDING, natomiast N FOLLOWING przez CURRENT ROW lub koniec okna UNBOUNDED FOLLOWING. Zamiast definicji sumowania
Rozdział 3. Język zapytań SQL w MS SQL Server
75
przez słowo kluczowe ROWS możemy użyć RANGE. W składni ten drugi przypadek eliminuje możliwość używania przesunięcia z zastosowaniem omawianych opcji N PRECEDING oraz N FOLLOWING. SELECT Nazwisko, Brutto, SUM(Brutto) OVER(PARTITION BY Osoby.IdOsoby) AS SumaPracownik, SUM(Brutto) OVER(PARTITION BY Osoby.IdOsoby ORDER BY Osoby.IdOsoby, IdZarobku ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacaPracownik, SUM(Brutto) OVER(PARTITION BY Osoby.IdOsoby ORDER BY Osoby.IdOsoby, IdZarobku ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaPracownik1, SUM(Brutto) OVER(PARTITION BY Osoby.IdOsoby ORDER BY Osoby.IdOsoby, IdZarobku ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS RamkaPracownik1, SUM(Brutto) OVER(PARTITION BY Osoby.IdOsoby ORDER BY Osoby.IdOsoby, IdZarobku RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacaPracownik, SUM(Brutto) OVER(PARTITION BY Osoby.IdOsoby ORDER BY Osoby.IdOsoby, IdZarobku RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaPracownik1 FROM Osoby JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby ORDER BY Osoby.IdOsoby, IdZarobku;
W poprzednim przykładzie wyniki zwracane przez funkcję agregującą z definicją ruchomego okna z zastosowaniem ROWS oraz RANGE niczym się nie różnią. Dzieje się tak dlatego, że sortowanie wskazuje na niepowtarzające się wartości. Jeśli opcja sortowania wskazuje na powtarzające się wartości, sytuacja się zmienia. Jeśli do wyznaczenia grupowania oraz sortowania w oknie użyjemy roku wypłaty wyznaczonego za pomocą wyrażenia DATEPART(yy, DataWyplaty), to wszystkie wartości, względem których sortujemy, są równe. Powoduje to, że jeśli zastosujemy RANGE, suma zostanie wyznaczona dla wszystkich wartości okna i nie mamy do czynienia z sumą bieżącą. SELECT Brutto, DATEPART(yy,DataWyplaty), SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty)) AS SumaRok, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY DATEPART(yy,DataWyplaty) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacRok, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY DATEPART(yy,DataWyplaty) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaRok1, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY DATEPART(yy,DataWyplaty) ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS RamkaRok1, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY DATEPART(yy,DataWyplaty) RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacaRokA, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY DATEPART(yy,DataWyplaty) RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaRokkA1 FROM Zarobki ORDER BY DATEPART(yy,DataWyplaty);
Do wyznaczenia partycji roku zastosujmy tym razem wypłaty, natomiast do sortowania użyjemy dwóch części daty: roku i miesiąca uzyskiwanych przez konwersję do napisu i wybranie z niego pierwszych sześciu znaków zgodnie z wyrażeniem LEFT(CONVERT (varchar,DataWyplaty,112),6). W takim przypadku we wnętrzu partycji (pojedynczy rok) pojawią się grupy rekordów charakteryzujących się taką samą wartością roku
76
MS SQL Server. Zaawansowane metody programowania
i miesiąca. O ile zastosowanie ROWS we wszystkich przypadkach powoduje taki sam rezultat, działanie RANGE ponownie się zmienia. Tym razem suma bieżąca jest wyznaczana podgrupami, gdzie dla tej samej wartości rok, miesiąc sumy są takie same. Czyli do sumowania użyta została suma z wewnętrznej grupy rekordów charakteryzujących się taką samą wartością parametru sortowania. SELECT Brutto, LEFT(CONVERT(varchar,DataWyplaty,112),6) , SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty)) AS SumaRok, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY LEFT(CONVERT(varchar,DataWyplaty,112),6) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacRok, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY LEFT(CONVERT(varchar,DataWyplaty,112),6) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaRok1, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY LEFT(CONVERT(varchar,DataWyplaty,112),6) ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS RamkaRok1, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY LEFT(CONVERT(varchar,DataWyplaty,112),6) RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BiezacaRokA, SUM(Brutto) OVER(PARTITION BY DATEPART(yy,DataWyplaty) ORDER BY LEFT(CONVERT(varchar,DataWyplaty,112),6) RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS BiezacaRokkA1 FROM Zarobki ORDER BY LEFT(CONVERT(varchar,DataWyplaty,112),6);
Omówione powyżej zagadnienie budowania okna logicznego nazywane jest często partycjonowaniem logicznym. Należy odróżnić je od partycjonowania fizycznego, które polega na rozbiciu miejsc składowania danych na różnych urządzeniach fizycznych, dyskach. W przypadku MS SQL Server realizujemy to, przypisując tworzonej tabeli lub jej części różne grupy plików, które zostały pokazane w rozdziale 1. Takie działanie ma na celu poprawę wydajności wykonywania zapytań na skutek zrównoleglenia operacji I/O, które są najwolniejszymi procesami w przypadku każdego przetwarzania. Partycjonowanie fizyczne jest w zasadzie elementem administrowania serwerem i związanym z procesem strojenia (tuning). Ze względu na odmienność charakteru tych operacji od tematyki książki nie będą one ściślej omawiane. Zainteresowanych mogę odesłać do publikacji poświęconych tej tematyce [8] [9] [10] [11]. Powróćmy do rozwiązań sumowania wielopoziomowego obecnych już we wcześniejszych wersjach serwera. Możliwe jest zastosowanie operatora COMPUTE, po którym wymieniana jest funkcja agregująca, działająca na dowolne z pól występujących na liście po poleceniu SELECT, a następnie po słowie kluczowym BY definicja grupy, w której ta funkcja będzie obliczana. Niestety, nie jest on już wspierany przez wersję 2012, dlatego w celu sprawdzenia działania zapytań, które go dotyczą, należy użyć starszych realizacji MS SQL. W przeciwieństwie do stosowanych opcji grupowania, które niejawnie porządkują rekordy, to dla COMPUTE użytkownik musi zawsze zdefiniować ręcznie sposób sortowania za pomocą klauzuli ORDER BY. Klauzula sortująca musi poprzedzać wyznaczanie agregatu (jest to jedyny wyjątek, gdy ORDER BY nie jest ostatnią klauzulą zapytania wybierającego), a porządkowanie musi być zgodne z definicją grupy. SELECT Nazwa, Nazwisko, Brutto FROM Dzialy LEFT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu
Rozdział 3. Język zapytań SQL w MS SQL Server
77
LEFT JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby ORDER BY Nazwa COMPUTE SUM(Brutto) BY Nazwa;
Wynik tego zapytania (rysunek 3.6) może zostać zinterpretowany jako seria N zapytań wyświetlających wartości rekordów rozdzielanych jednowierszowymi zapytaniami wyznaczającymi sumę, a szerzej funkcję agregującą. Rysunek 3.6. Wynik zapytania wybierającego z operatorem COMPUTE
Dla tego typu zapytania właściwszą, czytelniejszą reprezentacją jest wyprowadzenie go do postaci tekstowej, co przedstawia poniższy listing. Nazwa Nazwisko Brutto --------------- --------------- --------------------Administracja Nowak 444.00 Administracja Nowak 888.00 Administracja Jan k 555.00 sum --------------------1887.00 Nazwa Nazwisko Brutto --------------- --------------- --------------------Dyrekcja Kowalski 111.00 Dyrekcja Kowalski 333.00 Dyrekcja Kowalski 666.00 Dyrekcja Kowalski 999.00 Dyrekcja Zięba 333.00 sum --------------------2442.00 Nazwa …
Nazwisko
Brutto
78
MS SQL Server. Zaawansowane metody programowania
Możliwe jest wielokrotne użycie operatora COMPUTE. Należy jednak pamiętać, że jako pierwsze muszą być wyznaczane podsumowania na najbardziej wewnętrznym, zagnieżdżonym poziomie, a następnie na kolejnych, wyższych. Wymagane jest zapewnienie sortowania zgodnego z definicją najniższego z nich. Na każdym poziomie grupowania można wyznaczać wiele różnych funkcji agregujących, pod warunkiem że działają na wyświetlane pole lub pola. SELECT Nazwa, Nazwisko, Brutto FROM Dzialy LEFT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu LEFT JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby ORDER BY Nazwa, Nazwisko COMPUTE SUM(Brutto), MAX(Brutto) BY Nazwa, Nazwisko COMPUTE SUM(Brutto), AVG(Brutto), COUNT(Brutto) BY Nazwa;
Wadami wykorzystywania operatora COMPUTE są przede wszystkim brak możliwości stosowania aliasów dla podsumowań oraz postać wyprowadzania wyników nienadająca się do przechwytywania przez końcówkę kliencką w celu dalszego przetworzenia. Sprawdza się jedynie jako łatwa postać raportowania, co stanowiło główny zamysł twórców w momencie jego wprowadzania. Na tym kończy się omówienie funkcjonalności niewspieranej przez najnowszą wersję serwera. Taki brak zgodności wstecznej jest wyjątkowym przypadkiem w historii MS SQL i nie pamiętam, aby jakikolwiek inny kiedyś wprowadzony element składniowy był pominięty w nowszych wersjach bazy. Wróćmy do prostszych problemów przetwarzania związanych z operacjami na listach. Możemy zastosować operator ANY, którego synonimem jest SOME, a którego argumentem jest lista (również dynamiczna, generowana zapytaniem wybierającym) i który musi być poprzedzony operatorem, np. relacyjnym. ANY zwraca wartość prawdziwą, jeśli wyrażenie jest prawdziwe dla dowolnego (przynajmniej jednego) elementu listy. W przykładzie wyświetlane są te rekordy, dla których wypłata jest wyższa od dowolnej średniej wypłaty dla pracownika. SELECT IdOsoby, Brutto FROM Zarobki WHERE Brutto > ANY (SELECT AVG(Brutto)FROM Zarobki GROUP BY IdOsoby);
Równoważnym rozwiązaniem jest zapytanie wykorzystujące SOME. SELECT IdOsoby, Brutto FROM Zarobki WHERE Brutto > SOME (SELECT AVG(Brutto)FROM Zarobki GROUP BY IdOsoby);
Ponieważ Brutto ma być większe od dowolnego elementu listy, wystarczy, aby było większe od najmniejszego z nich. Dlatego oba powyższe zapytania mogą być zastąpione zapytaniem z warunkiem wygenerowanym podzapytaniem skalarnym. Dla ułatwienia analizy wynik, jaki zwraca zapytanie skalarne, został dodany do listy wyświetlanych pól. SELECT IdOsoby, Brutto, (SELECT MIN(SR) FROM (SELECT Zarobki GROUP BY IdOsoby) AS FROM Zarobki WHERE Brutto > (SELECT MIN(SR) FROM (SELECT Zarobki GROUP BY IdOsoby) AS
AVG(Brutto)AS SR FROM xxx)AS MIN_SR AVG(Brutto)AS SR FROM xxx);
Rozdział 3. Język zapytań SQL w MS SQL Server
79
Podobne zasady obowiązują dla operatora ALL, jednak tym razem wartość TRUE jest zwracana wtedy, kiedy warunek jest prawdziwy dla wszystkich elementów listy. SELECT Brutto FROM Zarobki WHERE Brutto > ALL (SELECT AVG(Brutto)FROM Zarobki GROUP BY IdOsoby);
Dlatego równoważne jest zapytanie, w którym Brutto jest większe od największej wartości z listy. SELECT IdOsoby, Brutto, (SELECT MAX(SR) FROM (SELECT AVG(Brutto)AS SR FROM Zarobki GROUP BY IdOsoby) AS xxx)AS MAX_SR FROM Zarobki WHERE Brutto > (SELECT MAX(SR) FROM (SELECT AVG(Brutto)AS SR FROM Zarobki GROUP BY IdOsoby) AS xxx);
Nieco inaczej działa operator EXISTS, który zwraca TRUE, jeśli zapytanie będące jego atrybutem zwraca co najmniej jeden rekord. W przykładzie został on zastosowany do wyświetlenia danych tych osób, które miały co najmniej jedną wypłatę. W celu uzyskania takiej funkcjonalności konieczne było skorelowanie zapytania z podzapytaniem tworzącym listę za pomocą pola IdOsoby pochodzącego z obu źródeł. SELECT IdOsoby, Nazwisko FROM Osoby WHERE EXISTS (SELECT * FROM Zarobki WHERE IdOsoby=Osoby.IdOsoby);
Równoważnym rozwiązaniem jest zastosowanie złączenia wraz z eliminacją duplikatów za pomocą dyrektywy DISTINCT. SELECT DISTINCT Osoby.IdOsoby, Nazwisko FROM Osoby JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby;
Na podobnych zasadach tworzymy, korzystając z operatora EXISTS, zapytanie wyświetlające te osoby, które nie otrzymały żadnej wypłaty: SELECT IdOsoby, Nazwisko FROM Osoby WHERE NOT EXISTS (SELECT * FROM Zarobki WHERE IdOsoby=Osoby.IdOsoby);
dla którego równoważnym rozwiązaniem jest zastosowanie złączenia LEFT oraz eliminacja pól, w których identyfikator z tabeli Zarobki ma wartość NULL: SELECT Osoby.IdOsoby, Nazwisko FROM Osoby LEFT JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby WHERE Zarobki.IdOsoby IS NULL;
Ponieważ wszystkie przedstawione tutaj operatory listy dają się łatwo zastąpić innymi, wydajniejszymi rozwiązaniami, ich rola w budowaniu zapytań jest obecnie marginalna. Podobnie do Accessa w MS SQL jest możliwe wyznaczenie tabeli przestawnej za pomocą operatora PIVOT, którego atrybutem jest zapytanie wyznaczające wartości dla kolumn. Niestety, rozwiązanie to jest mało atrakcyjne, ponieważ nagłówki kolumn dane są statycznie za pomocą listy wartości wzbogaconej o ewentualne aliasy. Skutek działania przedstawionego zapytania zwraca tabela 3.23. SELECT kto, [1] AS D1, [2] AS D2, [3] AS D3, [4] AS D4, [5] AS D5 FROM (SELECT IdDzialu, Nazwisko+ ' '+ Imie as Kto, Brutto FROM Osoby JOIN Zarobki ON Osoby.IdOsoby=Zarobki.IdOsoby) ppp
80
MS SQL Server. Zaawansowane metody programowania PIVOT ( SUM (Brutto) FOR IdDzialu IN ([1], [2], [3], [4], [5] ) ) AS pvt ORDER BY Kto;
Tabela 3.23. Wynik zapytania wyznaczającego tabelę przestawną Kto
D1
D2
D3
D4
D5
1
Adamczyk Janusz
NULL
NULL
777,00
NULL
NULL
2
Janik Paweł
NULL
555,00
NULL
NULL
NULL
3
Kow Piotr
NULL
NULL
666,00
NULL
NULL
4
Kowalczyk Jarosław
NULL
NULL
777,00
NULL
NULL
5
Kowalski Jan
2109,00
NULL
NULL
NULL
NULL
6
Nowak Karol
NULL
1332,00
NULL
NULL
NULL
7
Nowicki Jan
NULL
NULL
NULL
2109,00
NULL
8
Zięba Andrzej
333,00
NULL
NULL
NULL
NULL
W SQL możliwe jest wykonywanie operacji na zbiorach rekordów. Pierwszą z nich jest wyznaczenie sumy zbiorów za pomocą operatora UNION użytego pomiędzy dwoma zapytaniami wybierającymi. Warunkiem poprawności składniowej jest to, aby oba zapytania zawierały taką samą liczbę pól, które są parami, są zgodne co do typu lub dają się skonwertować do wspólnego typu (z reguły tekstowego). Nie jest konieczna zgodność nazw pól. SELECT Nazwisko, Imie FROM Osoby UNION SELECT Nazwisko, Imie FROM ttt;
W podstawowej postaci składni ze zbioru wynikowego są eliminowane duplikaty rekordów bez względu na to, czy powtórka dotyczy danych pochodzących z tego samego źródła, czy z różnych źródeł (zapytań połączonych operatorem). Ubocznym skutkiem niejawnego zastosowania dyrektywy DISTINCT, która jest odpowiedzialna za usunięcie duplikatów, jest narastające posortowanie wyniku, tak jakby zastosowano klauzulę ORDER BY z listą wyświetlanych pól. Kiedy nazwy pól drugiego zapytania są różne od nazw pól pierwszego, o etykietach wynikowego zestawu decydują nazwy lub aliasy pierwszego zapytania. W związku z tym nie ma sensu nadawanie aliasów kolumnom zwracanym przez drugie zapytanie. Jeśli chcemy dokładnie dodać do siebie zbiór rekordów zwracanych przez dwa zapytania, musimy zastosować operator UNION ALL. W tym przypadku oba zestawy są wyprowadzane w kolejności łączonych zapytań i nie występuje sortowanie. Wszystkie pozostałe wymagania i uwagi są takie same jak w przypadku stosowania UNION. SELECT Nazwisko, Imie FROM Osoby UNION ALL SELECT Nazwisko, Imie FROM ttt;
Rozdział 3. Język zapytań SQL w MS SQL Server
81
Oba operatory mogą zostać użyte wielokrotnie i łączyć więcej niż dwa zapytania wybierające. Ponieważ suma zbiorów jest operacją przemienną, kolejność łączonych zapytań nie odgrywa roli. Poza sumą możliwe jest wyznaczenie za pomocą operatora INTERSECT części wspólnej (iloczynu) zbiorów rekordów. Wymagania dotyczące łączonych zapytań oraz uwagi dotyczące nazw są takie same jak w przypadku sumy zbiorów. SELECT Nazwisko, Imie FROM Osoby INTERSECT SELECT Nazwisko, Imie FROM ttt;
W starszych wersjach MS SQL Server możliwe było łączenie tym operatorem tylko dwóch zapytań wybierających. Obecnie może być używany wielokrotnie. Ponieważ wyznaczanie iloczynu zbiorów jest przemienne, to wynik nie jest zależny od kolejności łączonych zapytań. Ponadto możliwe jest wyznaczenie za pomocą operatora EXCEPT różnicy dwóch zestawów rekordów. SELECT Nazwisko, Imie FROM Osoby EXCEPT SELECT Nazwisko, Imie FROM ttt;
Operator odejmowania nie jest przemienny i kolejność zapytań ma wpływ na otrzymywany wynik. Podobnie jak w przypadku operatora INTERSECT, w starszych wersjach możliwe było tylko wykonanie odejmowania na dwóch zapytaniach. Obecnie możemy wykonywać tę operację na większej liczbie zapytań. SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz<1980 EXCEPT SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz<1970 EXCEPT SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz>1960;
Odejmowanie jest wykonywane, począwszy od pierwszej pary, a następnie odejmowane są po kolei następne zestawy rekordów. Oczywiście jawnie możemy ustalić kolejność operacji, ujmując pary w nawiasy. Stąd równoważne poprzedniemu jest zapytanie: (SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz<1980 EXCEPT SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz<1970) EXCEPT SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz>1960;
Natomiast odmienny wynik otrzymamy, ujmując w nawiasy dwa ostatnie zapytania, co warto sprawdzić praktycznie. SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz<1980 EXCEPT (SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz<1970 EXCEPT SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz>1960);
Ważnym elementem SQL jest możliwość predefiniowania zapytania za pomocą klauzuli WITH, po której definiujemy jego logiczną nazwę. W nawiasie możemy zdefiniować nazwy (aliasy) zwracanych przez nie pól, a po słowie kluczowym AS umieszczamy
82
MS SQL Server. Zaawansowane metody programowania
w nawiasie właściwe zapytanie wybierające, które zwraca tyle pól, ile nazw zdefiniowano po nazwie logicznej. Zapytanie takie może zostać użyte jako źródło danych w kolejnym zapytaniu wybierającym. W przykładzie zdefiniowano zapytanie o nazwie LiczbaWyplat, które następnie jest odpytywane; przykładowy wynik takiego skryptu zawiera tabela 3.24. WITH LiczbaWyplat (Kto, Ile) AS (SELECT IdOsoby, COUNT(*) FROM Zarobki GROUP BY IdOsoby) SELECT Kto, Ile FROM LiczbaWyplat ORDER BY Ile DESC;
Tabela 3.24. Wynik zapytania z klauzulą WITH Kto
Ile
1
4
6
4
11
2
2
2
3
2
4
1
...
...
Ponieważ predefiniowane zapytanie może być interpretowane jako dynamiczna tabela, podlega takim samym zasadom jak tabela zapisana na dysku. Taka tabela może być zastosowana w zapytaniu ze złączeniem, jak to pokazują następny przykład i wyniki zawarte w tabeli 3.25. WITH LiczbaWyplat (Kto, Ile) AS (SELECT IdOsoby, COUNT(*) FROM Zarobki GROUP BY IdOsoby) SELECT Nazwisko, Ile FROM Osoby JOIN LiczbaWyplat ON IdOsoby=kto ORDER BY Ile DESC;
Tabela 3.25. Wynik zapytania z klauzulą WITH i złączeniem Kto
Ile
Kowalski
4
Nowicki
4
Majewski
2
Nowak
2
Kow
2
Janik
1
...
...
Rozdział 3. Język zapytań SQL w MS SQL Server
83
Predefiniowane zapytanie nie musi mieć jawnie zdefiniowanych nazw kolumn. W takim przypadku nazwy są dziedziczone albo po nazwach wyświetlanych kolumn, albo po ich aliasach. Oczywiście, jeśli w zapytaniu pojawia się wyrażenie lub funkcja, pole to musi mieć alias, inaczej nie ma możliwości jawnego odwołania się do niego. Wynikowy zestaw rekordów pozostaje naturalnie bez zmian. WITH LiczbaWyplat AS (SELECT IdOsoby, COUNT(*) AS Ile FROM Zarobki GROUP BY IdOsoby) SELECT Nazwisko, Ile FROM Osoby JOIN LiczbaWyplat ON Osoby.IdOsoby=LiczbaWyplat.IdOsoby ORDER BY Ile DESC;
Kolejnym przykładem ilustrującym użyteczność klauzuli WITH jest wyznaczenie wartości średniej liczby wypłat w dziale; skutek jest przedstawiony w tabeli 3.26. Rozwiązanie to jest konkurencyjne w stosunku do budowania podzapytania, które pokazano wcześniej. WITH LiczbaWyplat (Kto, Ile) AS (SELECT IdOsoby, COUNT(*) FROM Zarobki GROUP BY IdOsoby) SELECT Nazwa, AVG(Ile) AS Srednio FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu JOIN LiczbaWyplat ON IdOsoby=kto GROUP BY Nazwa;
Tabela 3.26. Wynik zapytania z klauzulą WITH, złączeniem oraz wyznaczeniem funkcji agregującej Nazwa
Srednio
Administracja
1
Dyrekcja
2
Handlowy
4
Techniczny
1
Ciekawym przykładem zastosowania klauzuli WITH jest rekurencja. Ponieważ w definicji zapytania dla tej klauzuli może pojawić się dowolne zapytanie wybierające, możliwe jest również zastosowanie UNION ALL. Przy czym pierwsze zapytanie ustala wartość początkową zestawu rekordów, drugie natomiast odwołuje się przez nazwę do definicji predefiniowanego zapytania (do samego siebie). W nim dokonujemy modyfikacji pól, odwołując się do ich nazw w podstawowej definicji. Drugie z zapytań będzie wywoływane rekurencyjnie. W przykładzie oba pola mają wartość początkową 1, natomiast w drugim zapytaniu pierwsze jest zwiększane, a drugie zmniejszane o 1. Pierwsze z zapytań odwołuje się do tabeli o nazwie T1, która może mieć dowolną strukturę, ponieważ nie wybiera się z niego żadnych pól. Aby zapewnić zakończenie rekurencji, drugie z zapytań zawiera warunek zatrzymania, kiedy pole a osiągnie wartość 11, ustalony w klauzuli WHERE. Skutek działania zapytania pokazuje tabela 3.27. W definicji rekurencji w klauzuli WITH nie jest możliwe stosowanie operatora UNION, co w głównej mierze wynika z niejawnej realizacji dyrektywy DISTINCT.
84
MS SQL Server. Zaawansowane metody programowania WITH x (a, b) AS ( SELECT 1,1 FROM T1 UNION ALL SELECT a+1, b-1FROM x WHERE a<11 ) SELECT a, b FROM x;
Tabela 3.27. Wynik zapytania z klauzulą WITH wyznaczającą automatyczną inkrementację a
b
1
1
2
0
3
–1
...
...
Tak jak stwierdzono powyżej, tabela T1 może być dowolną tabelą, ale ponieważ w SQL Server w zapytaniu wybierającym można pominąć źródło, poprawne jest również rozwiązanie podane niżej. Oczywiście zestaw wynikowy jest dokładnie taki sam. WITH x (a, b) AS ( SELECT 1,1 UNION ALL SELECT a+1, b-1FROM x WHERE a<11 ) SELECT a, b FROM x;
Kolejny przykład pozwala na wygenerowanie kalendarza składającego się z pierwszych i ostatnich dni miesięcy z pewnego zakresu dat. Pierwsze zapytanie klauzuli WITH składa datę pierwszego dnia miesiąca z części składowych daty początkowej, w przykładzie arbitralnie ustalonej na 2006-10-5. Konkatenacji podlegają wyciągnięty z daty rok, napis zawierający łącznik (dywiz), wyciągnięty z daty miesiąc oraz napis zawierający –01. Cały łańcuch jest jawnie skonwertowany funkcją CAST do postaci zmiennej datetime. Druga kolumna pierwszego zapytania zawiera wartość początkową licznika 1. Z tym zapytaniem operatorem UNION ALL połączono zapytanie, które za pomocą funkcji DATEADD dodaje do pierwszej kolumny jeden miesiąc, a licznik zwiększa o 1. Warunkiem zatrzymania rekurencji jest osiągnięcie przez licznik liczby miesięcy dzielących od siebie datę początkową i końcową — w analizowanym przypadku 2008-1-5. W głównym zapytaniu wyświetlamy obie kolumny predefiniowanego zapytania, natomiast trzecia jest otrzymywana przez dodanie do pierwszej jednego miesiąca, a następnie odjęcie jednego dnia, co wyznacza ostatni dzień miesiąca. Wynik jest jawnie konwertowany do typu datetime. Pierwsze rekordy otrzymanego wyniku zawiera tabela 3.28. WITH x (dzien, licznik) AS (SELECT CAST(YEAR('2006-10-5') AS varchar) + '-' + CAST(MONTH('2006-10-5') AS varchar)+ CAST('-01' AS varchar) AS datetime),1 UNION ALL
Rozdział 3. Język zapytań SQL w MS SQL Server
85
SELECT DATEADD(m, 1, dzien), licznik+1 FROM x WHERE licznik < DATEDIFF(m,'2006-10-5','2008-1-5')) SELECT dzien AS Pierwszy, licznik, CAST(DATEDIFF(d,1,DATEADD(m, 1, dzien))AS datetime)AS Ostatni FROM x;
Tabela 3.28. Wynik zapytania z klauzulą WITH wyznaczający początki i końce miesięcy z określonego przedziału dat Pierwszy
licznik
Ostatni
2006-10-01 00:00:00.000
1
2006-10-31 00:00:00.000
2006-11-01 00:00:00.000
2
2006-11-30 00:00:00.000
2006-12-01 00:00:00.000
3
2006-12-31 00:00:00.000
Realizacja inkrementacji za pomocą predefiniowanych zapytań wydaje się mało konkurencyjna względem funkcji rangowych nad oknem logicznym. Ponieważ wyrażenie zmieniające wartość kolumny może być jednak bardzo złożone, w niektórych przypadkach taka konstrukcja wydaje się niezastąpiona. Zostanie to szerzej omówione podczas przedstawiania obsługi struktur hierarchicznych. W wersji 2012 zostały wprowadzone nowe metody ograniczające liczbę wyświetlanych wierszy. Rozważmy na początek proste zapytanie wyznaczające sumy wypłat pracowników, które posortujemy malejąco. SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki GROUP BY IdOsoby ORDER BY Razem DESC;
Nowa postać ograniczenia liczby wierszy jest definiowana na końcu zapytania, w naszym przypadku po sortowaniu. Składa się ona z definicji numeru początkowego rekordu N definiowanego za pomocą OFFSET N ROWS oraz z definicji liczby rekordów wyświetlanych. Jeżeli chcemy określić kolejnych M wierszy, stosujemy operator FETCH NEXT M ROWS ONLY. Obie wartości N i M powinny być liczbami całkowitymi dodatnimi. SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki GROUP BY IdOsoby ORDER BY Razem DESC OFFSET 5 ROWS FETCH NEXT 6 ROWS ONLY;
Jeśli chcemy określić liczbę rekordów znajdujących się przed wierszem początkowym, to po jego określeniu w klauzuli OFFSET używamy FETCH FIRST M ROWS ONLY, gdzie M jest liczbą całkowitą dodatnią. SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki GROUP BY IdOsoby ORDER BY Razem DESC OFFSET 15 ROWS FETCH FIRST 3 ROWS ONLY;
86
MS SQL Server. Zaawansowane metody programowania
Oprócz wyprowadzenia danych w postaci tabelarycznej możliwe jest sformatowanie wyniku do postaci zgodnej z formatem XML [4] [12]. Podstawową metodą jest zdefiniowanie na końcu zapytania dyrektywy FOR XML AUTO, co powoduje, że dane wyświetlane są w ten sposób, iż dla każdego znacznika o nazwie zgodnej z nazwą tabeli wyświetlane są wartości atrybutów, których nazwami są nazwy wyświetlanych kolumn. SELECT Nazwisko, Imie FROM Osoby FOR XML AUTO;
Wyniki wyświetlane przez zapytania generujące XML zostały przedstawione w postaci tekstowej Result to Text. Jeżeli zdecydujemy się na wyprowadzenie ich w domyślnej postaci tabelarycznej Result to Grid, to dwukrotne kliknięcie zawartości pola XML powoduje uruchomienie zakładki zawierającej edytor dla tego typu danych. XML_F52E2B61-18A1-11d1-B105-00805F49916B ------------------------------------------------------------------------------------------------
Jeśli zapytanie wybierające zawiera złączenia, to sformatowanie wyników do podstawowej postaci XML powoduje, że zewnętrzny znacznik jest nazwą tabeli nadrzędnej, a wewnętrzny podrzędnej. W obu pojawiają się atrybuty o nazwach zgodnych z nazwami pól. Wadą takiego rozwiązania jest to, że nadrzędne znaczniki są powtarzane dla każdego z wyprowadzanych wierszy. Nie ma grupowania znaczników wewnętrznych, co powoduje dużą redundancję danych. SELECT Nazwa, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Osoby.IdDzialu=Dzialy. IdDzialu FOR XML AUTO; XML_F52E2B61-18A1-11d1-B105-00805F49916B --------------------------------------------------------------------------------------------------- ....
Jeśli ustalimy kierunek realizacji złączenia na LEFT, pojawią się rekordy, w których działowi nie jest przypisany żaden pracownik. W takim przypadku w XML pojawia się pusty znacznik poziomu podrzędnego . SELECT Nazwa, Nazwisko, Imie FROM Dzialy LEFT JOIN Osoby ON Osoby.IdDzialu=Dzialy. IdDzialu FOR XML AUTO;
XML_F52E2B61-18A1-11d1-B105-00805F49916B ---------------------------------------------------------------------------------------------------
Jeśli w definicji wynikowego formatu XML dodamy atrybut ELEMENTS, to spowoduje to zmianę sposobu reprezentowania zawartości z atrybutowego na znacznikowy. Oznacza to, że zewnętrzny znacznik opisujący nazwę tabeli zawiera w sobie znaczniki odpowiadające nazwom pól tabeli, których zawartością są wartości pól. SELECT Nazwisko, Imie FROM Osoby FOR XML AUTO, ELEMENTS;
XML_F52E2B61-18A1-11d1-B105-00805F49916B --------------------------------------------------------------------------------------------------- Kowalski Jan Nowak Karol
Rozdział 3. Język zapytań SQL w MS SQL Server
87
Kow Piotr Janik Paweł Kowa...
Zastosowanie zapytania ze złączeniem do takiej formy wyprowadzania danych spowoduje, że najbardziej zewnętrznym znacznikiem będzie nazwa nadrzędnej tabeli; w jego wnętrzu pojawia się zarówno znacznik z polami tej tabeli, jak i znacznik reprezentujący nazwę tabeli podrzędnej. Dopiero we wnętrzu znacznika reprezentującego tabelę podrzędną pojawiają się znaczniki reprezentujące jej pola. Również w takiej formie wyprowadzania danych następuje powtarzanie się znaczników poziomów wyższych i redundancja danych. SELECT Nazwa, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Osoby.IdDzialu=Dzialy. IdDzialu FOR XML AUTO, ELEMENTS; XML_F52E2B61-18A1-11d1-B105-00805F49916B --------------------------------------------------------------------------------------------------- Dyrekcja Kowalski Jan Administracja Nowak Karol Techniczny Ko ....
Podobnie jak poprzednio, zastosowanie złączenia ze wskazaniem kierunku, np. LEFT, spowoduje pojawienie się pustych znaczników; w przykładzie reprezentuje to znacznik . SELECT Nazwa, Nazwisko, Imie FROM Dzialy LEFT JOIN Osoby ON Osoby.IdDzialu=Dzialy. IdDzialu FOR XML AUTO, ELEMENTS; XML_F52E2B61-18A1-11d1-B105-00805F49916B ---------------------------------------------------------------------------------------------------DyrekcjaAdministracja NowakKarolJanik PawełLisJanusz
Aby uzyskać postać XML bez zbędnych redundancji, należy zastosować składnię analogiczną do tej, która była prezentowana dla rekurencji w klauzuli WITH. Inne rozwiązania tego problemu można znaleźć w literaturze [13] [14] [15]. W skład zapytania wchodzą dwa elementy połączone operatorem UNION ALL. Nie można stosować skróconej wersji UNION, ponieważ zawiera w sobie niejawnie dyrektywę DISTINCT. Pierwsze z zapytań pobiera dane z tabeli nadrzędnej, a jego postać jest obwarowana wieloma wymaganiami formalnymi. Pierwsze pole musi zawierać wartość 1 i mieć alias Tag, drugie 0 i nazwę PARENT, kolejne pola zawierają interesujące nas atrybuty z tabeli nadrzędnej oraz wartości NULL przygotowane dla pól poziomu podrzędnego. W obu przypadkach aliasy są ujęte w nawias kwadratowy i mają ściśle określoną postać, na którą składają się rozdzielone wykrzyknikami: nazwa znacznika, w którym umieszczone zostaną atrybuty, numer poziomu hierarchii i nazwa pola z właściwej tabeli. W kolejnym składniku, wybierającym dane z połączenia obu tabel, pierwsze dwa pola mają wartość powiększoną o 1 i antysymetrycznie pola z tabeli nadrzędnej mają wartość NULL, a z podrzędnej wskazują na właściwe atrybuty. Dodatkowo na końcu zapytania zastosowano dyrektywę EXPLICIT. SELECT 1 AS Tag, 0 AS PARENT, IdDzialu AS [Dzial!1!IdDzialu], Nazwa AS [Dzial!1!Nazwa], NULL AS [Pracownik!2!Nazwisko], NULL AS [Pracownik!2!Imie] FROM Dzialy UNION ALL
88
MS SQL Server. Zaawansowane metody programowania SELECT 2 AS Tag, 1 AS PARENT, Osoby.IdDzialu AS Dzial, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu FOR XML EXPLICIT; ...
...
Niestety, ze względu na brak sortowania otrzymujemy niesatysfakcjonującą postać XML, w której najpierw pojawiają się wszystkie znaczniki poziomu nadrzędnego, a dopiero po nich znaczniki drugiego poziomu. Wskazywałoby to, że wszyscy pracownicy są przypisani do ostatniego działu. Wystarczy dodać porządkowanie według pojedynczego, nadrzędnego znacznika, aby uzyskać w pełni satysfakcjonującą postać wynikową. Przedstawiony w przykładzie drugi atrybut sortowania nie jest wymagany, lecz poprawia formę prezentacji. SELECT 1 AS Tag, 0 AS PARENT, IdDzialu AS [Dzial!1!IdDzialu], Nazwa AS [Dzial!1!Nazwa], NULL AS [Pracownik!2!Nazwisko], NULL AS [Pracownik!2!Imie] FROM Dzialy UNION ALL SELECT 2 AS Tag, 1 AS PARENT, Osoby.IdDzialu AS Dzial, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY [Dzial!1!IdDzialu], [Pracownik!2!Nazwisko] FOR XML EXPLICIT; ... ... ....
W przypadku wyświetlania danych z większej liczby tabel muszą się pojawić kolejne podzapytania połączone operatorami UNION ALL. Analogicznie pierwsze dwa pola będą powiększone o 1, wartości NULL będą dotyczyły pól z poziomów innych niż obsługiwany, a źródło będzie zawierać złączenie z kolejną tabelą. Sortowanie jest konieczne względem wszystkich poziomów, z wyjątkiem ostatniego. Jeżeli dla dowolnego pola zechcemy zmienić sposób prezentacji z atrybutowego na znacznikowy, wystarczy dodać jako trzeci element aliasu tego pola dyrektywę ELEMENT, jak to zrobiono dla pola Nazwisko. Pozostałe pola będą wyświetlane w dotychczasowej postaci. SELECT 1 AS Tag, 0 AS PARENT, IdDzialu AS [Dzial!1!IdDzialu], Nazwa AS [Dzial!1!Nazwa], NULL AS [Pracownik!2!Nazwisko!ELEMENT], NULL AS [Pracownik!2!Imie] FROM Dzialy
Rozdział 3. Język zapytań SQL w MS SQL Server
89
UNION ALL SELECT 2 AS Tag, 1 AS PARENT, Osoby.IdDzialu AS Dzial, NULL , Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY [Dzial!1!IdDzialu], [Pracownik!2!Nazwisko!ELEMENT] FOR XML EXPLICIT; Muras Piechowski ... ...
Zamiast dyrektywy ELEMENT możemy zastosować ELEMENTXSINIL, co powoduje określenie schematu dla pliku XML. Dodatkowo kiedy wskazany w ten sposób element ma wartość NULL, odpowiadający mu znacznik nie jest wyświetlany. SELECT 1 AS Tag, 0 AS PARENT, IdDzialu AS [Dzial!1!IdDzialu], Nazwa AS [Dzial!1!Nazwa], NULL AS [Pracownik!2!Nazwisko!ELEMENTXSINIL], NULL AS [Pracownik!2!Imie] FROM Dzialy UNION ALL SELECT 2 AS Tag, 1 AS PARENT, Osoby.IdDzialu AS Dzial, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY [Dzial!1!IdDzialu], [Pracownik!2!Nazwisko!ELEMENTXSINIL] FOR XML EXPLICIT; Muras Piechowski ... ...
Z kolei jeśli dla atrybutu użyjemy dyrektywy HIDE, nie będzie on wyświetlany w wynikowym pliku. W przykładzie został on zastosowany w celu ukrycia pól kluczy, które z punktu widzenia XML nie niosą ważnej informacji, ale są istotne dla uzyskania jego właściwej postaci formalnej. SELECT 1 AS Tag, 0 AS PARENT, IdDzialu AS [Dzial!1!IdDzialu!HIDE], Nazwa AS [Dzial!1!Nazwa], NULL AS [Pracownik!2!Nazwisko], NULL AS [Pracownik!2!Imie] FROM Dzialy UNION ALL SELECT 2 AS Tag, 1 AS PARENT, Osoby.IdDzialu AS Dzial, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY [Dzial!1!IdDzialu!HIDE], [Pracownik!2!Nazwisko] FOR XML EXPLICIT; ... ...
90
MS SQL Server. Zaawansowane metody programowania
Możliwe jest użycie dla całego zapytania dyrektywy PATH, co pozwala na wyświetlenie atrybutów poziomu podrzędnego bez znaczników będących ich nazwami. Kolejne zewnętrzne znaczniki otoczone są generowanymi automatycznie znacznikami . SELECT IdDzialu, Nazwa, NULL, NULL FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML PATH;
1 Dyrekcja
... 2RusnarczykWIESŁAW
...
Niestety, powoduje to sklejenie zawartości kolumn. Dlatego lepszym rozwiązaniem wydaje się zastosowanie na warstwie podrzędnej pojedynczej kolumny zawierającej wyrażenie łączące ich zawartość, ale przedzielone dodatkowym separatorem, np. spacją. SELECT IdDzialu, Nazwa, NULL FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko+ ' '+ Imie AS Pracownik FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML PATH;
Innym sposobem rozwiązania problemu z czytelnością wyniku jest zastosowanie aliasów. Na pierwszym poziomie mogą być one dowolne. Natomiast na niższych poziomach powinny definiować ścieżkę znaczników prowadzących do danego poziomu, zakończoną właściwym aliasem dla pola. Ponieważ separatorem kolejnych poziomów takiej ścieżki jest znak /, który nie może być użyty bezpośrednio w definicji nazwy, aliasy niższego poziomu muszą być zawarte w apostrofach. Należy zawsze pamiętać, że brak porządkowania po węźle poziomu pośredniego może doprowadzić do sytuacji, w której nazwa działu pojawi się dopiero dla n-tego węzła poziomu. SELECT IdDzialu AS Dzial, Nazwa AS Nazwa, NULL AS 'Nazwa/Nazwisko', NULL AS 'Nazwa/Imie' FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML PATH; Dyrekcja
Muras DAWID
... ...
Rozdział 3. Język zapytań SQL w MS SQL Server
91
Można jednak sprawdzić, że zastosowanie tylko wskazania nazwy węzła Nazwisko zamiast 'Nazwa/Nazwisko' nie zmienia sposobu działania. Wiele funkcji oferuje zastosowanie dyrektywy RAW, która wyświetla dane w postaci atrybutowego XML, za co odpowiada domyślna dyrektywa TYPE. Znaczniki zewnętrzne mają nazwę zamiast nazwy tabeli. SELECT IdDzialu, Nazwa, NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW;
...
…
Zamiana dyrektywy domyślnej na ELEMENTS zmienia sposób formatowania XML na postać znacznikową. SELECT IdDzialu, Nazwa, NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW, ELEMENTS; 1 Dyrekcja
... 2
Zastosowanie dyrektywy XMLDATA powoduje wygenerowanie nagłówka pliku XML określającego sposób formatowania jego zawartości, tak jak ma to miejsce w plikach XSD. Dyrektywa ta może być zastosowana zarówno po dyrektywie TYPE (ponieważ jest to stan domyślny, może zostać pominięty), jak i ELEMENTS. SELECT IdDzialu, Nazwa, NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL , Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW, XMLDATA;
92
MS SQL Server. Zaawansowane metody programowania
...
Natomiast użycie dyrektywy XMLSCHEMA powoduje wygenerowanie pełnej definicji schematu, zawierającej dodatkowo definicje wyjściowych typów po stronie serwera bazy danych. SELECT IdDzialu, Nazwa, NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL , Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW, XMLSCHEMA; …
|
Dyrektywa XMLSCHEMA pozwala również na jawne określenie położenia definicji schematu. W przykładzie wskazano co prawda na nieistniejącą lokalizację, co nie powoduje błędu przetwarzania, ale w praktyce sensowne jest wskazanie istniejącego miejsca. SELECT IdDzialu, Nazwa, NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW, XMLSCHEMA ('urn:Przyklad.com');
Rozdział 3. Język zapytań SQL w MS SQL Server
93
…
|
Można również zastosować BINARY BASE64, co spowoduje dekodowanie danych binarnych. SELECT IdDzialu, Nazwa, NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW, BINARY BASE64;
....
|
Irytujące może być to, że domyślną nazwą znacznika nadrzędnego jest . Można to zmienić, stosując alias umieszczony w apostrofach jako atrybut dyrektywy RAW. SELECT IdDzialu, Nazwa , NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW ('Dzialy_Pracownicy'); ... ...
Ponadto możliwe jest zdefiniowanie znacznika najbardziej zewnętrznego, obejmującego całość pliku, co jest uzyskiwane za pomocą atrybutu dyrektywy ROOT. SELECT IdDzialu, Nazwa, NULL AS Nazwisko, NULL AS Imie FROM Dzialy UNION ALL SELECT Osoby.IdDzialu, NULL, Nazwisko, Imie FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu ORDER BY IdDzialu FOR XML RAW ('Dzialy_Pracownicy'), ROOT('Calosc');
94
MS SQL Server. Zaawansowane metody programowania ... ...
Wyprowadzanie danych do postaci hierarchicznej struktury znacznikowej XML ma bardzo ważny aspekt praktyczny. Wynika to z podstawowej cechy tego formatu, jaką jest samoopisywalność. Jest on wykorzystywany w procesie migracji danych między różnymi środowiskami serwerów baz danych oraz do integrowania danych pochodzących ze źródeł rozproszonych, heterogenicznych, w jednorodną postać docelową [2] [4] [15] [16]. Dane o postaci XML można również bezpośrednio składować w bazie danych, co opisano w rozdziale 3.3 [17] [18]. Jednak taka metoda nie jest specjalnie zalecana ze względu na mało wydajne indeksowanie takich pól w porównaniu z indeksowaniem prostych pól schematu relacyjnego. Format XML ma również większość plików konfiguracyjnych różnego rodzaju oprogramowania komercyjnego.
3.2. Zapytania modyfikujące dane Pierwszym zadaniem związanym częściowo z modyfikowaniem danych, a częściowo z tworzeniem obiektów jest utworzenie tabeli na podstawie tabeli istniejącej w schemacie relacyjnym — „kopiowanie tabeli”. Wykonujemy je za pomocą polecenia SELECT, w którym poza listą pól i źródłem danych po klauzuli INTO wymieniamy nazwę nowej tabeli. SELECT Nazwisko, Imie, RokUrodz INTO Nowa FROM Osoby;
Powoduje to, że na podstawie nazw i typów wymienionych kolumn tabeli źródłowej tworzona jest tabela docelowa, a następnie przepisywane są do niej wszystkie rekordy. Jeśli chcemy ograniczyć liczbę przepisywanych wierszy, możemy zastosować klauzulę WHERE. Przepisaniu podlegają te rekordy, dla których wyrażenie filtrujące jest prawdziwe. Jednak jeśli ponownie spróbujemy utworzyć tę samą tabelę, pojawi się komunikat o błędzie, dlatego musimy najpierw jawnie ją usunąć poleceniem DROP. W przykładowym skrypcie zastosowano słowo kluczowe GO, które ten skrypt dzieli na dwie części. Druga z nich wykona się po zakończeniu przetwarzania wszystkich poleceń pierwszej. Jeśli po słowie GO umieścimy wartość całkowitą dodatnią, to poprzedzający je fragment wykona się właśnie wyrażą w niej liczbę razy. DROP TABLE Nowa GO SELECT Nazwisko, Imie, RokUrodz INTO Nowa FROM Osoby WHERE RokUrodz>1970;
Często istnieje konieczność utworzenia tylko struktury, tj. pustej tabeli. W takim przypadku możemy zmienić warunek, tak aby nigdy nie był prawdziwy.
Rozdział 3. Język zapytań SQL w MS SQL Server
95
SELECT Nazwisko, Imie, RokUrodz INTO Nowa FROM Osoby WHERE RokUrodz>19700000
Powiększenie wartości brzegowej dla pola RokUrodz może jednak okazać się nieskuteczne, jeśli nie ustanowiliśmy dla niego żadnych dodatkowych ograniczeń. Zawsze może pojawić się „szalony” operator, który wpisze większą wartość. Dlatego aby mieć pewność, że wynikowa tabela będzie pusta, możemy odwołać się do pola klucza głównego, które nie może być puste. SELECT Nazwisko, Imie, RokUrodz INTO Nowa FROM Osoby WHERE IdOsoby IS NULL;
Jeśli jednak tabela źródłowa nie ma klucza podstawowego, możemy użyć dowolnego wyrażenia, o którym wiemy, że jest zawsze fałszywe. SELECT Nazwisko, Imie, RokUrodz INTO Nowa FROM Osoby WHERE 1=2;
Jeśli mamy przygotowaną pustą tabelę, możemy rozpocząć wstawianie do niej danych. Wykonujemy to, używając polecenia INSERT INTO, po którym występuje nazwa zasilanej tabeli. Po słowie kluczowym VALUES wymieniamy w nawiasie kolejne wartości pól. Kolejność ta musi być zgodna z kolejnością definiowania pól w tabeli. INSERT INTO Nowa VALUES ('Kowal', 'Jan', 1970);
Jeśli z jakichś przyczyn nie chcemy lub nie umiemy podać wszystkich wartości albo zdecydowaliśmy się na podanie wartości pól w innej kolejności niż określona w definicji tabeli, po jej nazwie musi pojawić się ujęta w nawiasy lista zasilanych pól. Lista wartości musi być zgodna z kolejnością tej listy. INSERT INTO Nowa(Nazwisko, Imie) VALUES ('Nowak', 'Karol');
Ponieważ środowisko MS SQL Server dość dobrze radzi sobie z konwersją „w locie”, to możliwe jest bezpośrednie wstawienie do pól znakowych wartości numerycznych. W przykładzie do pola Imie wstawiono wartość 1980. INSERT INTO Nowa(Nazwisko, Imie) VALUES ('Janik', 1980);
Każde z przedstawionych zapytań powoduje wstawienie dokładnie jednego wiersza, dlatego zawartość tabeli po ich wykonaniu powinna być zgodna z przedstawioną w tabeli 3.29. Jeśli podczas wstawiania danych decydujemy się na pominięcie jakichś pól, to aby zapytanie zakończyło się sukcesem, muszą one spełniać przynajmniej jeden warunek:
96
MS SQL Server. Zaawansowane metody programowania
Tabela 3.29. Skutek wykonania trzech zapytań wstawiających wiersze Nazwisko
Imie
RokUrodz
Kowal
Jan
1970
Nowak
Karol
NULL
Janik
1980
NULL
pozwalać na przyjęcie wartości NULL; posiadać wartość domyślną; być automatycznie inkrementowane — co jest szczególnym przypadkiem
wartości domyślnej. Tabelę można również zasilić danymi zawartymi w innej tabeli. W tym przypadku zamiast słowa kluczowego VALUES używamy polecenia SELECT, w którym wymieniamy listę pól źródłowych. Liczba ich musi być zgodna z liczbą pól w tabeli docelowej, a podstawienie odbywa się pozycyjnie, a nie na podstawie nazwy. Wymagana jest zgodność co do typu odpowiednich par pól, a szerzej, możliwość automatycznej konwersji pola źródłowego na docelowe. W celu ograniczenia liczby wstawianych rekordów możliwe jest zastosowanie klauzuli WHERE z właściwym warunkiem. INSERT INTO Nowa SELECT Nazwisko, Imie, RokUrodz FROM Osoby WHERE RokUrodz > 1970 ORDER BY Nazwisko;
Podobnie jak poprzednio, jeśli chcemy ograniczyć liczbę zasilanych pól, po nazwie tabeli wymieniamy te pola, które chcemy zasilić. W przykładzie dodatkowo pokazano, że przed skopiowaniem źródłowy zestaw rekordów może zostać posortowany. Szerzej ujmując, możliwe jest zastosowanie dowolnie złożonego, poprawnego składniowo zapytania wybierającego. INSERT INTO Nowa (Imie, Nazwisko) SELECT Nazwisko, Imie FROM Osoby WHERE RokUrodz < 1970 ORDER BY Nazwisko DESC;
Kolejny przykład ilustruje omawianą wcześniej konwersję „w locie”, kiedy numeryczne pole źródłowe RokUrodz jest konwertowane na typ znakowy docelowego pola Nazwisko. Ponadto pokazano, że podstawienie jest pozycyjne, gdyż do pola Imie wstawiane są wartości pola Nazwisko tabeli źródłowej. INSERT INTO Nowa (Imie, Nazwisko) SELECT Nazwisko, RokUrodz FROM Osoby WHERE RokUrodz = 1970;
Od wersji 2008 możliwe jest blokowe wstawianie wartości. Zamiast wielokrotnie pisać polecenie INSERT INTO, możliwe jest po słowie VALUES wymienienie wartości dla kilku rekordów. Wartości dla każdego rekordu są ujęte w nawiasy, a lista rekordów jest separowana przecinkami. W przykładzie po wyczyszczeniu tabeli docelowej wstawione zostały dwie trójki rekordów. W pierwszym przypadku zawierające wszystkie
Rozdział 3. Język zapytań SQL w MS SQL Server
97
dane, a w drugim wartości tylko dla dwóch spośród trzech pól. Ponieważ w obu przypadkach dane są poprawne, po wykonaniu skryptu tabela będzie zawierała sześć rekordów, tak jak to pokazuje tabela 3.30. DELETE FROM Nowa INSERT INTO Nowa VALUES ('Kowal', 'Jan', 1970), ('Kowalik', 'Janusz', 1971), ('Kowalczyk', 'January', NULL); GO INSERT INTO Nowa(Nazwisko, Imie) VALUES ('Nowak', 'Karol'), ('Nowicki', '1970'), ('Nowacki', NULL); GO SELECT * FROM Nowa;
Tabela 3.30. Wyniki wykonania skryptu wstawiającego dwie trójki rekordów Nazwisko
Imie
RokUrodz
Kowal
Jan
1970
Kowalik
Janusz
1971
Kowalczyk
January
NULL
Nowak
Karol
NULL
Nowicki
1970
NULL
Nowacki
NULL
NULL
Jednak masowe wstawianie rekordów nie jest dokładnie równoważne wstawianiu rekord po rekordzie. Różnice pojawiają się wtedy, kiedy choć w jednym rekordzie dane są nieprawidłowe. Przy wstawianiu rekord po rekordzie tylko rekordy z niepoprawnymi danymi zostaną pominięte, przy wstawianiu masowym nie zostanie wstawiony żaden rekord. Można powiedzieć, że walidacji podlegają wszystkie wstawiane dane, lub inaczej, że transakcja obejmuje całą operację wstawiania masowego. Możemy się o tym przekonać, próbując wstawić jednocześnie dane, które wymagają i nie wymagają konwersji „w locie”. W przykładzie pierwsza grupa rekordów nie wymaga konwersji, w kolejnej wartości NULL oraz napis nie są konwertowane, ale konwersji wymaga wartość numeryczna wstawiana do pola Imie. Zawartość tabeli po wykonaniu skryptu jest zawarta w tabeli 3.31, po której przedstawiony został wygenerowany w zakładce Messages komunikat. INSERT INTO Nowa VALUES ('Kowal', 'Jan', 1970), ('Kowalik', 'Janusz', 1971), ('Kowalczyk', 'January', NULL); GO INSERT INTO Nowa(Nazwisko, Imie) VALUES ('Nowak', 'Karol'), ('Nowicki', 1970), ('Nowacki', NULL); GO SELECT * FROM Nowa;
(3 row(s) affected) Msg 245, Level 16, State 1, Line 1 Conversion failed when converting the varchar value 'Karol' to data type int.
98
MS SQL Server. Zaawansowane metody programowania
Tabela 3.31. Wynik wykonania skryptu masowego wstawiania danych z błędnymi wartościami Nazwisko
Imie
RokUrodz
Kowal
Jan
1970
Kowalik
Janusz
1971
Kowalczyk
January
NULL
Jak można zauważyć, komunikat może być mylący, gdyż dotyczy niepoprawnej konwersji typu znakowego, a nie, jak można by się spodziewać, wartości numerycznej. Zmiana kolejności wstawiania danych w drugiej trójce rekordów nie powoduje żadnej różnicy. Jeśli natomiast wszystkie dane wymagają takiej samej konwersji, skrypt zostanie wykonany bezbłędnie. Dotyczy to również sytuacji, gdy jedna lub kilka wartości jest NULL. Możemy to zaobserwować w rezultacie zawartym w tabeli 3.32. INSERT INTO Nowa VALUES ('Kowal', 'Jan', 1970), ('Kowalik', 'Janusz', 1971), ('Kowalczyk', 'January', NULL); GO INSERT INTO Nowa(Nazwisko, Imie) VALUES ('Nowak', 1980), ('Nowicki', 1970), ('Nowacki', 1971); GO SELECT * FROM Nowa;
Tabela 3.32. Wynik wykonania skryptu masowego wstawiania danych z jednakową konwersją Nazwisko
Imie
RokUrodz
Kowal
Jan
1970
Kowalik
Janusz
1971
Kowalczyk
January
NULL
Nowak
1980
NULL
Nowicki
1970
NULL
Nowacki
1971
NULL
W dotychczasowych przypadkach zasilaliśmy tabelę, w której nie zdefiniowano pola automatycznie inkrementowanego. Rozważmy teraz tabelę Dzialy, która ma klucz podstawowy zdefiniowany na kolumnie IdDzialu z funkcją generowania kolejnych wartości IDENTITY(1,1). Podczas wstawiania rekordów w takim przypadku na liście wartości musi być pomijana wartość dla takiej kolumny. W przypadku omawianej tabeli wystarczy podać tylko nazwę dodawanego działu. Wartość pola klucza zostanie wygenerowana automatycznie. INSERT INTO Dzialy VALUES ('Nowy')
Jeśli jednak chcemy (musimy) jawnie wstawiać wartość do pola inkrementowanego, konieczne jest przestawienie flagi systemowej IDENTITY_INSERT dla tej tabeli. Wówczas określenie listy zasilanych pól jest obowiązkowe, bez względu na to, czy zasilamy
Rozdział 3. Język zapytań SQL w MS SQL Server
99
wszystkie, czy też część z nich. Po skorzystaniu z funkcjonalności przestawionej flagi systemowej powinniśmy przywrócić jej stan domyślny. Kolejny wiersz będzie wstawiony z wartością wynikającą z automatycznej inkrementacji. Jeśli nie przestawimy flagi systemowej ręcznie, zostanie ona przywrócona z chwilą zamknięcia sesji, w której została zmieniona. Skutek wykonania skryptu został zawarty w tabeli 3.33. SET IDENTITY_INSERT Dzialy ON; GO INSERT INTO Dzialy (IdDzialu, Nazwa) VALUES (-13, 'Dodatkowy') GO SELECT * FROM Dzialy; GO SET IDENTITY_INSERT Dzialy OFF; INSERT INTO Dzialy VALUES ('Kolejny') GO SELECT * FROM Dzialy;
Tabela 3.33. Wynik wykonania skryptu z jawnym i niejawnym wstawianiem wartości do pola automatycznie inkrementowanego IdDzialu
Nazwa
–13
Dodatkowy
1
Dyrekcja
…
…
9
Nowy
10
Kolejny
Podobną sytuację uzyskamy, gdy wstawimy jawnie wartość pola IdDzialu wyższą niż dotąd uzyskiwane z automatycznej numeracji. Tym razem jednak po przestawieniu flagi kolejny rekord będzie miał wartość klucza wyższą niż ta, która została wstawiona ręcznie — tabela 3.34. Wynika to z kierunku przyrostu wartości pól ustawionego na wartość dodatnią 1. Dla ujemnych przyrostów m definicji funkcji IDENTITY(n, m) działanie będzie odwrotne. SET IDENTITY_INSERT Dzialy ON; GO INSERT INTO Dzialy (IdDzialu, Nazwa) VALUES (15, 'Dodany') GO SELECT * FROM Dzialy; GO SET IDENTITY_INSERT Dzialy OFF; INSERT INTO Dzialy VALUES ('Kolejny1') GO SELECT * FROM Dzialy;
100
MS SQL Server. Zaawansowane metody programowania
Tabela 3.34. Wynik wykonania skryptu z jawnym i niejawnym wstawianiem wartości do pola automatycznie inkrementowanego IdDzialu
Nazwa
–13
Dodatkowy
1
Dyrekcja
…
…
9
Nowy
10
Kolejny
15
Dodany
16
Kolejny1
Poza wstawianiem nowych wartości możliwe jest modyfikowanie istniejących danych. Wykonujemy to za pomocą polecenia UPDATE, po którym wymieniamy nazwę modyfikowanej tabeli, a po słowie kluczowym SET definiujemy wyrażenie modyfikujące. W przykładzie nadano wartość 0 polu RokUrodz. Zastosowana klauzula WHERE powoduje, że modyfikacja dotyczy tylko tych wierszy, w których to pole ma wartość NULL. Brak warunku spowodowałby modyfikację wszystkich wierszy tabeli. UPDATE Nowa SET RokUrodz = 0 WHERE RokUrodz IS NULL;
Możliwe jest modyfikowanie wielu pól jednocześnie. W tym celu wyrażenia modyfikujące rozdzielamy przecinkiem. W przykładzie dokonano przepisania zawartości pól Nazwisko i Imie na pisane wielkimi literami. Brak klauzuli WHERE świadczy, że modyfikowane są wszystkie rekordy. UPDATE Nowa SET Nazwisko = UPPER(Nazwisko), Imie = UPPER(Imie);
Ponieważ modyfikacja danych odbywa się dla całego wiersza, kolejne zapytanie z użyciem tabel pomocniczych spowoduje zamianę miejscami zawartości pól z dodatkową zmianą wielkości liter. UPDATE Nowa SET Nazwisko = UPPER(Imie), Imie = UPPER(Nazwisko);
Dane z tabeli możemy usuwać, stosując polecenie DELETE. Zastosowanie klauzuli WHERE ogranicza liczbę usuwanych wierszy w przykładzie do tych, w których wartość pola RokUrodz wynosi 0. DELETE FROM Nowa WHERE RokUrodz=0;
Brak filtrowania spowoduje, że usuwane są wszystkie rekordy. DELETE FROM Nowa;
Rozdział 3. Język zapytań SQL w MS SQL Server
101
Analogiczny rezultat uzyskamy, stosując polecenie TRUNCATE TABLE. TRUNCATE TABLE Nowa;
Polecenie to nie zezwala na użycie klauzuli WHERE, dlatego zawsze usuwane są wszystkie wiersze. Poza tym możemy powiedzieć, że o ile polecenie DELETE usuwa wiersz po wierszu, to TRUNCATE TABLE usuwa tylko wskaźnik do pierwszego wiersza w tabeli. W związku z tym wykonuje się szybciej. Dodatkową cechą tego polecenia jest to, że w przeciwieństwie do DELETE przywraca początkową wartość automatycznej inkrementacji wykorzystującej funkcję IDENTITY. Podczas wykonywania modyfikacji danych możemy zażyczyć sobie potwierdzenia ich wykonania. W tym celu posługujemy się klauzulą OUTPUT, po której wymieniamy interesujące nas wielkości, a która powoduje wyświetlenie ich na standardowym urządzeniu wejścia-wyjścia. W przypadku modyfikowania wierszy do dyspozycji mamy dwie tabele pomocnicze tworzone podczas każdej zmiany zawartości tabeli, o nazwach INSERTED i DELETED. Obie mają strukturę tabeli, na której wykonujemy zapytanie. W przypadku polecenia UPDATE tabela DELETED zawiera dane przed modyfikacją, natomiast INSERTED po modyfikacji. W przedstawionym przykładzie wyświetlane będą identyfikator pracownika, któremu zmieniono wypłatę, stara i nowa wartość brutto oraz za pomocą funkcji systemowej getdate() czas, w którym tej modyfikacji dokonano. UPDATE Zarobki SET Brutto=1.1 * Brutto OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto, getdate();
Możliwe jest przekierowanie zwracanej informacji ze standardowego wyjścia do tabeli trwałej lub tymczasowej albo zmiennej tabelarycznej. W przykładzie utworzono zmienną tabelaryczną o nazwie @test, która posiada cztery pola. Trzy pierwsze są numeryczne, pierwsze całkowite, dwa kolejne — rzeczywiste, a ostatnie jest typu datetime. Przekierowanie do zmiennej odbywa się w definicji wyjścia OUTPUT po klauzuli INTO na wzór zapytania wstawiającego dane. Dodatkowo zastosowano ograniczenie modyfikowanych wierszy do osób o identyfikatorze mniejszym niż 3. Przykładowy wynik działania zawiera tabela 3.35. Przy wykonywaniu skryptu należy pamiętać, że zmienna tabelaryczna żyje do końca skryptu, czyli odpytywanie jej po słowie kluczowym GO skończy się niepowodzeniem. DECLARE @test TABLE( Komu int NOT NULL, Stara real, Nowa real, Kiedy datetime); UPDATE Zarobki SET Brutto=1.1 * Brutto OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto, getdate() INTO @test WHERE IdOsoby<3 SELECT * FROM @test; GO
102
MS SQL Server. Zaawansowane metody programowania
Tabela 3.35. Wynik wykonania skryptu z potwierdzeniem modyfikacji danych i przekierowaniem ich do zmiennej tabelarycznej Komu
Stara
Nowa
Kiedy
1
3286,69
3615,359
2012-03-12 14:37:44.133
2
886,1
974,71
2012-03-12 14:37:44.133
1
4300,36
4730,396
2012-03-12 14:37:44.133
...
...
...
...
Analogicznie możemy uzyskać potwierdzenie wykonania operacji dla wstawiania wierszy poleceniem INSERT INTO. Należy jednak pamiętać, iż w tym przypadku niepusta jest tabela INSERTED, tabela DELETED nie zawiera wierszy. W prezentowanym przykładzie skorzystano z pomocniczej zmiennej tabelarycznej, a wynik wykonania skryptu jest zawarty w tabeli 3.36. DECLARE @test TABLE( Komu int NOT NULL, Nowa real, Kiedy datetime); INSERT INTO Zarobki (IdOsoby,Brutto) OUTPUT INSERTED.IdOsoby, INSERTED.Brutto, getdate() INTO @test VALUES(2, 111); SELECT * FROM @test; GO
Tabela 3.36. Wynik wykonania skryptu z potwierdzeniem wstawiania danych i przekierowaniem ich do zmiennej tabelarycznej Komu
Nowa
Kiedy
2
111
2010-07-09 13:59:44.937
Przy usuwaniu wierszy, odwrotnie niż w przypadku ich wstawiania, niepusta jest tabela DELETED, a INSERTED nie zawiera wpisów. W przykładzie nie odwołano się do pomocniczej tabeli, co powoduje, że wynik jest bezpośrednio kierowany na standardowe wyjście. DELETE FROM Zarobki OUTPUT DELETED.* WHERE Brutto < 200;
Taki sam przykład można również wykonać dla wstawiania danych do określonych pól. Należy zauważyć, że tabela INSERTED zawiera wszystkie pola tabeli docelowej, co spowoduje, że część pól będzie miała wartość wynikającą z automatycznej inkrementacji, a część będzie miała wartość NULL. INSERT INTO Zarobki (IdOsoby,Brutto) OUTPUT INSERTED.* VALUES(2, 111);
Rozdział 3. Język zapytań SQL w MS SQL Server
103
Dla modyfikacji danych oczywiście można również skorzystać z pełnej informacji zawartej w tabelach pomocniczych z wyświetleniem ich na ekranie. UPDATE Zarobki SET Brutto=1.1 * Brutto OUTPUT INSERTED.*, DELETED.* WHERE IdOsoby<3;
3.3. Tworzenie i modyfikacja tabel i perspektyw Poprzednio tworzyliśmy tabelę na podstawie obiektu już istniejącego. W praktyce musimy jednak zacząć od stanu, kiedy schemat jest pusty i musimy utworzyć tabelę od podstaw. W tym celu używamy polecenia CREATE TABLE, po którym podajemy nazwę tabeli, a w nawiasach w postaci listy separowanej przecinkami definicje jej pól. Minimalna definicja pola składa się z nazwy oraz typu. W przykładzie została utworzona tabela o dwóch polach numerycznych, która następnie została zasilona serią danych. Ponieważ wszystkie wartości są numeryczne, a na pola nie nałożono dodatkowych ograniczeń, to zapytanie wybierające zwróci pełny zestaw rekordów. CREATE TABLE Nowa (Nr1 int, Nr2 int); INSERT INTO Nowa VALUES (1,1), (1,2), (2,1), (2,2), (1,2), (NULL,NULL), (3,NULL), (NULL,4); SELECT * FROM Nowa;
Najważniejszym z ograniczeń, jakie możemy przypisać do kolumny, jest klucz podstawowy PRIMARY KEY. Mówi ono, że pole musi mieć wartości niepuste i unikalne. W przykładzie zostanie ono przypisane do pierwszego z pól. Ponieważ tabela o wskazanej nazwie już istnieje w bazie danych, w pierwszej linii skryptu zostanie usunięta poleceniem DROP TABLE. Zastosowane słowo kluczowe GO powoduje podział skryptu na mniejsze części, które są wykonywane sekwencyjnie i traktowane jako niezależne elementy. Konieczność takiego podziału wynika z tego, że polecenie CREATE TABLE, musi być pierwszym poleceniem skryptu. Następnie zasilamy tabelę zestawem danych takim samym jak poprzednio. DROP TABLE Nowa; GO CREATE TABLE Nowa (Nr1 int PRIMARY KEY, Nr2 int); INSERT INTO Nowa VALUES (1,1), (1,2),
104
MS SQL Server. Zaawansowane metody programowania (2,1), (2,2), (1,2), (null,null), (3,null), (null,4); SELECT * FROM Nowa;
Tym razem ze względu na wprowadzony klucz nie wszystkie dane są poprawne, ponieważ następuje dublowanie wartości oraz próba wpisania wartości NULL. Ponieważ przy wstawianiu masowym następuje walidacja wszystkich danych, to w przypadku pojawienia się choćby jednego błędu żaden rekord nie zostanie wstawiony, a na zakładce Messages pojawi się komunikat. Msg 2627, Level 14, State 1, Line 6 Violation of PRIMARY KEY constraint 'PK__Nowa__C7D1FE7164CCF2AE'. Cannot insert duplicate key in object 'dbo.Nowa'. The statement has been terminated. (0 row(s) affected)
Chcemy jednak, aby tylko niepoprawne dane zostały pominięte i by poprawne zostały wpisane do tabeli. W takim przypadku musimy zrezygnować z wpisywania masowego na rzecz wstawiania danych rekord po rekordzie. DROP TABLE Nowa; GO CREATE TABLE Nowa (Nr1 int PRIMARY KEY, Nr2 int); INSERT INTO Nowa VALUES(1,1) INSERT INTO Nowa VALUES(1,2) INSERT INTO Nowa VALUES(2,1) INSERT INTO Nowa VALUES(2,2) INSERT INTO Nowa VALUES(1,2) INSERT INTO Nowa VALUES(null,null) INSERT INTO Nowa VALUES(3,null) INSERT INTO Nowa VALUES(null,4) SELECT * FROM Nowa;
Ponieważ teraz walidacja następuje oddzielnie dla każdego ze wstawianych rekordów, to pomimo komunikatów o błędach rekordy z poprawnymi danymi zostaną wstawione. Fragment listy komunikatów wygenerowanych na skutek wykonania zapytania przedstawiono niżej, a wynik zapytania wybierającego zawiera tabela 3.37. (1 row(s) affected) Msg 2627, Level 14, State 1, Line 7 Violation of PRIMARY KEY constraint 'PK__Nowa__C7D1FE71689D8392'. Cannot insert duplicate key in object 'dbo.Nowa'. The statement has been terminated. (1 row(s) affected) … Msg 515, Level 16, State 2, Line 11 Cannot insert the value NULL into column 'Nr1', table 'master.dbo.Nowa'; column does not allow nulls. INSERT fails. The statement has been terminated. (1 row(s) affected)
Rozdział 3. Język zapytań SQL w MS SQL Server
105
Tabela 3.37. Wynik wykonania zapytania wybierającego do zasilonej tabeli z ograniczeniem PRIMARY KEY Nr1
Nr2
1
1
2
1
3
NULL
Podobnie jak klucz główny, ograniczenie UNIQUE nie pozwala na duplikowanie wartości. Jednak umożliwia wpisywanie do kolumny wartości NULL. Skutek odpytania tak utworzonej i zasilonej tym samym zestawem danych tabeli przedstawia tabela 3.38. DROP TABLE Nowa; GO CREATE TABLE Nowa (Nr1 int UNIQUE, Nr2 int);
Tabela 3.38. Wynik wykonania zapytania wybierającego do zasilonej tabeli z ograniczeniem UNIQUE Nr1
Nr2
1
1
2
1
NULL
NULL
3
NULL
Analizując wyniki, można stwierdzić, że ograniczenie UNIQUE działa niepoprawnie względem wartości pustych. Pomimo że (NULL=NULL)=> NULL, rekord (NULL, 4) nie został wpisany — bez względu na to, że z punktu widzenia matematyki dwie wartości NULL nie są równe. Takie działanie jest charakterystyczne dla nowszych wersji serwera MS SQL. Zarówno w starszych wersjach, jak i innych rozwiązaniach komercyjnych wiersz z danymi (NULL, 4) zostałby wstawiony. Specyfikacja ograniczenia nie musi pojawić się bezpośrednio w definicji pola tabeli. Może ono zostać utworzone w dowolnym miejscu zapytania tworzącego tabelę, ale po definicji używanej do utworzenia ograniczenia kolumny. W takim przypadku podajemy rodzaj więzów, a w nawiasie określamy pole, którego one dotyczą. W przykładzie przedstawiono definicję klucza podstawowego, która jest równoważna przypadkowi określenia go w definicji pola. W tym przypadku nazwa ograniczenia nadawana jest przez system. DROP TABLE Nowa; GO CREATE TABLE Nowa (Nr1 int, Nr2 int, PRIMARY KEY(Nr1));
Według takich samych zasad możemy zdefiniować ograniczenie, stosując słowo kluczowe CONSTRAINT, po którym pojawia się jego nazwa definiowana przez programistę.
106
MS SQL Server. Zaawansowane metody programowania CREATE TABLE Nowa ( Nr1 int, Nr2 int, CONSTRAINT pk PRIMARY KEY(Nr1));
W przypadku ograniczenia definiowanego poza definicją pola możliwe jest odwołanie się do większej liczby pól. W przykładzie pokazany został wielokrotny klucz podstawowy określony przez parę pól. Oznacza to, że unikalna i niepusta jest teraz para pól. W ogólnym przypadku może być to lista pól separowanych przecinkami. Należy pamiętać, że możemy odwoływać się do pól, które już zostały zdefiniowane i występują w poleceniu tworzącym tabelę przed miejscem określeniem ograniczenia. DROP TABLE Nowa; GO CREATE TABLE Nowa (Nr1 int , Nr2 int , PRIMARY KEY(Nr1, Nr2));
Analogiczny skutek możemy uzyskać, stosując słowo kluczowe CONSTRAINT, z tą różnicą, że nazwa jest teraz nadawana przez programistę. CREATE TABLE Nowa (Nr1 int , Nr2 int , CONSTRAINT pk PRIMARY KEY(Nr1, Nr2));
Takie same zasady dotyczą ograniczenia unikalności tworzonego poza definicją kolumny. Tym razem para pól jest unikalna, ale każde z nich może przyjmować wartość NULL. Również w takim przypadku MS SQL Server interpretuje dwie wartości puste jako równe sobie, co nie jest zgodne z zasadami algebry. DROP TABLE Nowa; GO CREATE TABLE Nowa (Nr1 int, Nr2 int, UNIQUE(Nr1, Nr2));
Przez analogię kolejny przykład pokazuje tworzenie złożonego ograniczenia unikalności za pomocą słowa kluczowego CONSTRAINT z nazwą nadaną przez operatora. CREATE TABLE Nowa (Nr1 int, Nr2 int, CONSTRAINT un UNIQUE(Nr1, Nr2));
Ponieważ ograniczenie UNIQUE jest różne od PRIMARY KEY tylko w zakresie stosunku do wartości pustych, pierwsze z nich możemy formalnie uczynić równoważne drugiemu przez dodanie ograniczenia NOT NULL w definicji właściwej kolumny lub kolumn. DROP TABLE Nowa; GO CREATE TABLE Nowa (Nr1 int NOT NULL, Nr2 int, CONSTRAINT un UNIQUE(Nr1));
Rozdział 3. Język zapytań SQL w MS SQL Server
107
W tabeli możliwe jest utworzenie tylko jednego klucza głównego. Jeśli chcemy określić dwa ograniczenia zachowujące się formalnie tak jak on, możemy zastosować kilka metod — definiując PRIMARY KEY i UNIQUE wzbogacone o NOT NULL bezpośrednio w definicji kolumny: CREATE TABLE Nowa (Nr1 int PRIMARY KEY, Nr2 int NOT NULL UNIQUE);
definiując je z zastosowaniem słowa kluczowego CONSTRAINT: CREATE TABLE Nowa (Nr1 int, Nr2 int NOT NULL, CONSTRAINT pk PRIMARY KEY(Nr1), CONSTRAINT un UNIQUE (Nr2));
poza definicją kolumny bez słowa kluczowego CONSTRAINT: CREATE TABLE Nowa (Nr1 int, Nr2 int NOT NULL, PRIMARY KEY(Nr1), UNIQUE (Nr2));
określając obie kolumny jako unikalne i niepozwalające na wartość NULL: CREATE TABLE Nowa (Nr1 int NOT NULL, Nr2 int NOT NULL, UNIQUE (Nr1), UNIQUE (Nr2));
W praktyce może to być przydatne, kiedy z przyczyn administracyjnych, prawnych musimy podać dwa pola, które mogą być identyfikatorami, np. NIP i PESEL. Tworzenie tabel jest właściwym miejscem, aby przedstawić typy danych dostępne na serwerze. W tabeli 3.39 przedstawione zostały typy całkowitoliczbowe w postaci: nazwy, zakresu dostępnych wartości oraz liczby bajtów przeznaczonych na ich zapisanie. Na uwagę zasługuje podstawowy typ int, który pozwala na zapisanie pełnego zakresu liczb o 9 znakach. Ma to szczególne znaczenie przy próbie reprezentowania za jego pomocą danych typu NIP czy PESEL, która w obu przypadkach zakończy się fiaskiem. Konieczna jest zatem reprezentacja za pomocą nadmiarowego bigint lub napisu angażującego co najmniej 10*2=20 bajtów dla krótszego z nich i prostego kodowania. Tabela 3.39. Typy danych — dane numeryczne, całkowite Typ
Zakres
Pamięć
bigint
–2^63 (–9,223,372,036,854,775,808) do 2^63–1 (9,223,372,036,854,775,807)
8 bajtów
int
–2^31 (–2,147,483,648) do 2^31–1 (2,147,483,647)
4 bajty
smallint
–2^15 (–32,768) do 2^15–1 (32,767)
2 bajty
tinyint
0 do 255
1 bajt
108
MS SQL Server. Zaawansowane metody programowania
Obecnie liczba ludzi na świecie wynosi około 7·109, co oznacza, że typ bigint 2631020 ze znacznym nadmiarem wystarczy do ich ponumerowania. Również jest znacznie większy od liczby wszystkich homo sapiens, którzy dotychczas żyli, szacowanej na 1011. Jest tylko o dwa rzędy mniejszy od szacowanej liczby gwiazd w obserwowalnym wszechświecie, wynoszącej 3·1022. Czytelnicy pamiętają zapewne, jak cesarz miał zapłacić twórcy gry w szachy. Na pierwszym polu miał położyć jedno ziarno pszenicy, na drugim dwa, trzecim cztery, etc., na ostatnim 263 ziaren — i wszystkie te ziarna miał dać wynalazcy. Ponieważ we wszystkich spichlerzach cesarza nie było tyle zboża (ba, od początku świata nie urosło tyle), twórca szachów stracił głowę. Jak można zauważyć, liczba ziaren na ostatnim szachowym polu to właśnie maksimum, które można zapisać przy użyciu typu bigint. To naprawdę bardzo dużo. Warto o tym pamiętać, planując typ danych dla pola klucza głównego. Podobnie dla liczb zmiennoprzecinkowych ich zakres zależy od przydzielonej pamięci, co ilustruje tabela 3.40. Tabela 3.40. Typy danych — dane numeryczne, zmiennoprzecinkowe Typ
Zakres
Pamięć
float (n)
–1.79E+308 do –2.23E–308 0 2.23E–308 do 1.79E+308
Zależy od n
real
–3.40E +38 do –1.18E –38 0 1.18E –38 do 3.40E +38
4 bajty
W przypadku typu float mamy możliwość sterowania dokładnością za pomocą parametru. Może on mieć wartość z przedziału <1, 53>, jednak gdy ustawimy wartość z zakresu <1, 24>, jest to równoznaczne n = 24, a powyżej n = 53, co odpowiada typowi double precision. Informacje o tym typie zawiera tabela 3.41. Tabela 3.41. Dokładność typu float(n) Wartość n
Dokładność
Pamięć
1 – 24
7 cyfr
4 Bajty
25 – 53
15 cyfr
8 Bajtów
W przypadku typów rzeczywistych mamy do dyspozycji również takie, gdzie możliwe jest określenie precyzji i skali: decimal[(p[, s])] oraz numeric[(p[, s])], w których: p (precision) oznacza maksymalną liczbę przechowywanych cyfr zarówno
przed, jak i po separatorze dziesiętnym; dopuszczalny jest zakres od 1 do 38 — domyślnie 18; s (scale) oznacza maksymalną liczbę cyfr po separatorze dziesiętnym — musi spełniać warunek 0 <= s <= p, domyślnie 0; skala może być podana tylko wtedy,
gdy podana została precyzja. Dla maksymalnej precyzji zakres danych jest zawarty w przedziale od –1038 + 1 do 1038 – 1. Powiązanie rozmiaru pamięci i precyzji dla obu typów przedstawia tabela 3.42.
Rozdział 3. Język zapytań SQL w MS SQL Server
109
Tabela 3.42. Zależność przydzielonej pamięci od precyzji dla typów decimal oraz numeric Precision
Pamięć w bajtach
1–9
5
10 – 19
9
20 – 28
13
29 – 38
17
Kolejnym przykładem numerycznego typu danych jest waluta. Przydziałem pamięci w tym przypadku rządzą zasady podobne do liczb rzeczywistych — tabela 3.43. Należy pamiętać, że ze względu na zaokrąglenia przechowywane są cztery cyfry dziesiętne, pomimo iż przy wyświetlaniu uwzględniane są tylko dwie. Tabela 3.43. Typy danych — dane numeryczne, walutowe Typ
Zakres
Pamięć
money
–922,337,203,685,477.5808 do 922,337,203,685,477.5807
8 bajtów
smallmoney
–214,748.3648 to 214,748.3647
4 bajty
Ostatnim rodzajem danych numerycznych jest typ przechowujący datę lub czas. Poszczególne typy tej grupy różnią się sposobem formatowania oraz zakresem, tak jak to pokazuje tabela 3.44. Należy zwrócić uwagę na dolny zakres każdego typu, ponieważ należy go traktować jako datę początkową kalendarza opisanego tym typem. Różnice w tym parametrze mogą być przyczyną potencjalnych błędów podczas konwersji czy też integracji danych. Ponadto warto zwrócić uwagę na to, że dla typów time, datetime2, datetimeoffset, możliwe jest definiowanie precyzji wyświetlania ułamków sekund, a dla typu datetimeoffset możliwe jest uwzględnienie zmian stref czasowych. Tabela 3.44. Typy danych — dane numeryczne, daty i czasu Typ
Format
Zakres
Dokładność
Pamięć (B)
time
hh:mm:ss[.nnnnnnn]
00:00:00.0000000 do 23:59:59.9999999
ns
3–5
date
YYYY-MM-DD
0001-01-01 do 9999-12-31
dzień
3
small datetime
YYYY-MM-DD hh:mm:ss
1900-01-01 do 2079-06-06
minuta
4
datetime
YYYY-MM-DD hh:mm:ss[.nnn]
1753-01-01 do 9999-12-31
0.00333 s
8
datetime2
YYYY-MM-DD hh:mm:ss[.nnnnnnn]
0001-01-01 00:00:00.0000000 do 9999-12-31 23:59:59.9999999
ns
6–8
datetime offset
hh:mm:ss[.nnnnnnn] [+|-]hh:mm
0001-01-01 00:00:00.0000000 do 9999-12-31 23:59:59.9999999 (w UTC)
ns
8 – 10
110
MS SQL Server. Zaawansowane metody programowania
Do danych numerycznych możemy zaliczyć bit, który może być traktowany jako reprezentacja wartości logicznej, która może przyjąć wartości 0, 1, NULL. Formalnie w bazie danych nie ma reprezentacji typu logicznego boolean. Kolejną grupą typów danych są te, które pozwalają na przechowywanie danych w postaci znakowej (łańcuchów, napisów). Mogą one przechowywać dane: char [(n)] — o stałej liczbie znaków określonej przez n z zakresu od 1 do 8000 — pamięć niezbędna do ich przechowania jest równa n bajtów; varchar [(n | max)] — o zmiennej liczbie znaków, których maksymalną liczbę określa n z zakresu od 1 do 8000 — pamięć niezbędna do ich przechowania jest równa n+2 bajtów; max oznacza, że na przechowanie jest zarezerwowane 231 – 1 bajtów, dane mogą mieć 0 znaków; nchar [(n)] — analogicznie do char, ale z zastosowaniem kodowania Unicode — narodowa strona kodowa, maksymalna wartość n wynosi 4000; nvarchar [(n | max)] — analogicznie do varchar, ale z zastosowaniem kodowania Unicode — narodowa strona kodowa, maksymalna wartość n wynosi 4000.
Kolejne typy danych możemy traktować jako złożone, chociaż nie zawsze ich postać na to by wskazywała. Pierwszym z nich jest uniqueidentyfier, który reprezentuje szesnastobitowy identyfikator. Zarówno zmienne, jak i kolumny tego typu muszą zostać zainicjowane na jeden z dwóch sposobów: przez zastosowanie funkcji NEWID; przez konwersję napisu o postaci xxxxxxxx–xxxx–xxxx–xxxx–xxxxxxxxxxxx, gdzie x jest cyfrą heksadecymalną z zakresu 0 – 9 oraz a – f, np. 6F9619FF– 8B86–D011–B42D–00C04FC964FF jest poprawną wartością typu uniqueidentifier.
W stosunku do danych tego typu możemy używać operatorów relacyjnych (=, <>, <, >, <=, >=) oraz sprawdzających wartość NULL (IS NULL | IS NOT NULL). Należy jednak pamiętać, że porządkowanie odbywa się na zasadzie porównania wzoru bitowego, a nie rzeczywistego porównania liczb. Powoduje to, że nie można używać operatorów arytmetycznych w definiowaniu kolumn tego typu. Możliwe jest stosowanie wszystkich ograniczeń z wyjątkiem definiowania automatycznej inkrementacji IDENTITY. Replikacje typu MERGE i TRANSACTIONAL wykorzystują ten typ danych podczas aktualizacji replik do unikalnej identyfikacji różnych kopii wierszy tabel [19] – [22]. Typ danych xml pozwala na przechowywanie danych w postaci dokumentów XML i zostanie opisany w dalszej części tego rozdziału. Dane geometry to metatyp pozwalający na przechowanie informacji w postaci definicji obiektów grafiki wektorowej. Analogicznie dane geography przechowują obiekty graficzne, z tym że zawierają rzutowanie na sferę reprezentującą Ziemię. Typ danych hierarchyid koduje logicznie informacje o miejscu pojedynczego węzła w acyklicznym grafie skierowanym (drzewie), wskazując drogę do korzenia [7]. Ścieżka jest wskazywana przez sekwencje etykiet, które opisują węzły, jakie należy
Rozdział 3. Język zapytań SQL w MS SQL Server
111
przejść od korzenia do wskazywanego węzła, etykiety każdego z poziomów są rozdzielane znakiem / (ukośnik). Korzeń drzewa jest opisywany pojedynczym znakiem /. Dla niższych poziomów węzły są opisywane przez serię wartości całkowitych separowanych kropką. Identyfikatory mogą mieć postać: / /1/ /0.3.-7/ /1/3/ /0.1/0.2/
Węzły mogą być dodawane w każdym miejscu drzewa, np. węzeł po węźle /1/2/ i przed /1/3/ może mieć postać /1/2.5/. Możliwe jest używanie jako etykiet wartości ujemnych. Etykiety węzłów nie mogą mieć nieinicjalnych zer, np. etykieta /1/1.1/ jest poprawna, /1/1.01/ nie jest poprawna. Natomiast możemy użyć 0 jako pojedynczej etykiety. Aby zminimalizować możliwość pojawienia się błędów, do wstawiania węzłów można użyć metody GetDescendant. Szczegółowe przykłady dla tego typu zostaną przedstawione w rozdziale 7.2. Danymi złożonymi są również typy LOB (Large Object Binary), charakteryzujące się tym, że w rekordzie zapisywany jest wskaźnik do miejsca w pliku danych, gdzie fizycznie występują dane tego rodzaju. Elementami tej klasy typów są: text — o zmiennej liczbie znaków, których maksymalna liczba może wynieść 231 - 1, czyli 2,147,483,647 znaków; rozmiar ten nie zmienia się nawet w przypadku kodowania znaków na dwóch bajtach; dane mogą mieć 0 znaków; ntext — o zmiennej liczbie znaków z kodowaniem Unicode, których maksymalna liczba może wynieść 230 - 1, czyli 1,073,741,823 znaków; pamięć potrzebna do
przechowywania jest dwukrotnie większa niż liczba znaków (kodowanie na dwóch bajtach); dane mogą mieć 0 znaków. image — o zmiennym rozmiarze 231 - 1, czyli 2,147,483,647 bajtów, zwykle
utożsamiany jest z obrazkiem, ale może zawierać dowolne dane binarne; binary [(n)] — o stałym rozmiarze n bajtów, gdzie n jest z zakresu <1, 8000>; varbinary [(n | max)] — o zmiennym rozmiarze do n bajtów, gdzie n jest z zakresu <1, 8000>, max Sql_variant to metatyp danych pozwalający dostosowywać się zmiennej do przekazywanej do niej wartości. Może obsługiwać typy skalarne, takie jak: varchar(max), varbinary(max), nvarchar(max), xml, text, ntext, image, timestamp, sql_variant, geography, hierarchyid, geometry oraz User-defined types. Typ ten jest niejawnie stosowany podczas tzw. konwersji „w locie”.
Typ danych table przechowuje zbiór danych w postaci tabelarycznej, z reguły jest stosowany do czasowego przechowywania zestawów rekordów zwracanych przez funkcję lub kursory. Można go traktować jako tabelę wirtualną. Definicję takiej zmiennej możemy zapisać jako: definicja_zmiennej_tabelarycznej ::= TABLE ( { definicja_kolumny | ograniczenie_globalne } [ ,...n ] )
112
MS SQL Server. Zaawansowane metody programowania
gdzie: definicja_kolumny ::= nazwa_kolumny skalarny_typ_danych [COLLATE definicja_kodowania] [[DEFAULT wyrażenie] | IDENTITY [(ziarno, przyrost)] ] [ROWGUIDCOL] [ograniczenie_kolumny] [...n]
oraz: ograniczenie_kolumny ::= {[NULL | NOT NULL] | [PRIMARY KEY | UNIQUE]| CHECK (wyrażenie_logiczne)}
oraz ograniczenie_globalne ::={{PRIMARY KEY | UNIQUE}(column_name [,...n ]) | CHECK(wyrażenie_logiczne)}
Przykłady wykorzystania takich zmiennych zostaną pokazane w rozdziale 7.1. Ważnym typem danych jest cursor, zwracający rekord z zadeklarowanego zestawu rekordów. Może on być wykorzystywany jako zmienna OUTPUT w procedurach składowanych. Deklaracje i podstawowe operacje otwarcia, zamknięcia i zwolnienia zasobów przedstawia skrypt, a szczegółowe przykłady pokazano w rozdziale 5.6. DECLARE Cur CURSOR FOR SELECT * FROM Osoby OPEN Cur … CLOSE Cur DEALLOCATE Cur
Powróćmy do przerwanego wątku opisującego tworzenie tabel. Wykorzystując opisane wcześniej typy proste, utwórzmy na wzór istniejącej tabeli Osoby tabelę Pracownicy. Pierwsza kolumna będzie automatycznie inkrementowanym polem klucza głównego, którego nazwa składa się z prefiksu Id oraz liczby mnogiej nazwy tabeli. Zastosowano w tym celu funkcję IDENTITY (n, m), która generuje kolejne wartości, począwszy od n, z przyrostem m. Domyślną wartością obu parametrów jest 1. Obie wielkości muszą być całkowite, mogą być ujemne; parametr m nie może być zerem. Dla kolumny klucza dodano dodatkowo ograniczenie NOT NULL, które nie musi być nadawane jawnie, ponieważ niejawnie jest zawarte w PRIMARY KEY. W tabeli 3.45 pokazane zostały wprowadzone do tabeli rekordy. CREATE TABLE Pracownicy (IdPracownika int IDENTITY(1, 1) NOT NULL PRIMARY KEY, Iddzialu int, Nazwisko varchar (15), Imie varchar (15), RokUrodz integer, DataZatr date, IdSzefa int); INSERT INTO Pracownicy (Nazwisko) VALUES ('Kowalski'), ('Nowak'); SELECT IdPracownika, Nazwisko FROM Pracownicy;
Tabela 3.45. Sprawdzenie działania automatycznej inkrementacji IdPracownika
Nazwisko
1
Kowalski
2
Nowak
Rozdział 3. Język zapytań SQL w MS SQL Server
113
Zdefiniujmy teraz tabelę, która będzie przechowywała zlecenia wystawione naszym pracownikom. Na definicję pól składają się: klucz podstawowy; pole przeznaczone na klucz obcy, którego nazwa jest taka sama jak nazwa klucza głównego w tabeli nadrzędnej; opis zlecenia, data jego wystawienia oraz dwa pola całkowite. Tabela ta zostanie zasilona przykładowymi danymi, a jej zawartość po wykonaniu skryptu przedstawia tabela 3.46. Możemy sprawdzić, czy wszystkie przykładowe rekordy zostały zapisane w tabeli. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, Opis varchar (15), DataZlec date, m_v int, mm_v int); INSERT INTO Zlecenia VALUES(1,'Zlecenie','2011-1-1',20,80); INSERT INTO Zlecenia VALUES(2,'Zlec','2011-10-21',30,70); INSERT INTO Zlecenia VALUES(99,'Zlec1','2011-10-11',40,70); INSERT INTO Zlecenia(m_v,mm_v) VALUES(40,60); INSERT INTO Zlecenia(m_v,mm_v) VALUES(5,60); INSERT INTO Zlecenia(m_v,mm_v) VALUES(20,160); INSERT INTO Zlecenia(m_v,mm_v) VALUES(80,20); SELECT * FROM Zlecenia;
Tabela 3.46. Przykładowe dane w tabeli Zlecenia IdZlecenia
IdPracownika
Opis
1
1
Zlecenie
2
2
Zlec
3
99
Zlec1
4
NULL
5
NULL
6 7
DataZlec
m_v
mm_v
2011-1-1
20
80
2011-10-21
30
70
2011-10-11
40
70
NULL
NULL
40
60
NULL
NULL
5
60
NULL
NULL
NULL
20
160
NULL
NULL
NULL
80
20
Dodajmy do definicji tabeli dla pola Opis ograniczenie zabraniające wpisywania wartości NULL. Modyfikacja jest realizowana przez usunięcie pierwotnej definicji tabeli poleceniem DROP, a następnie utworzenie jej ponownie z dodatkową opcją. Tabela jest zasilana takim samym jak poprzednio zestawem danych. DROP TABLE Zlecenia; GO CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, Opis varchar (15) NOT NULL, DataZlec date, m_v int, mm_v int);
Dla wierszy, w których do pola Opis próbowano wpisać NULL, pojawia się komunikat o błędzie o postaci:
114
MS SQL Server. Zaawansowane metody programowania Msg 515, Level 16, State 2, Line 11 Cannot insert the value NULL into column 'Opis', table 'test.dbo.Zlecenia'; column does not allow nulls. INSERT fails. The statement has been terminated.
Tylko pierwsze trzy rekordy zostały wpisane do tabeli docelowej. Możemy dla tego samego pola zdefiniować wartość domyślną, która będzie wstawiana w sytuacji, gdy nie podamy jawnie wartości pola Opis. W przykładzie wartość ta będzie miała postać napisu 'Brak'. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date, m_v int, mm_v int);
Teraz ponownie wszystkie rekordy zostaną zapisane w tabeli. Należy zaznaczyć, że ograniczenie wartości domyślnej działa tylko w przypadku wstawiania danych poleceniem INSERT i może być później zmieniana. Podobnie możemy zdefiniować wartość domyślną dla pola daty. Tym razem wykorzystamy funkcję systemową getdate(), która podaje bieżącą datę z zegara systemowego. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate(), m_v int, mm_v int);
Do zaprezentowania wprowadzonych wartości domyślnych zastosowano zapytanie wstawiające wiersz z dyrektywą DEFAULT VALUES. Skutek takiego zasilenia przedstawia tabela 3.47. INSERT INTO Zlecenia DEFAULT VALUES;
Tabela 3.47. Przykładowe dane w tabeli Zlecenia uzyskane za pomocą wartości domyślnych IdZlecenia
IdPracownika
Opis
DataZlec
m_v
mm_v
1
NULL
Brak
2011-09-28
NULL
NULL
W definicji tabeli możliwe jest ustawienie sprawdzenia wartości wprowadzanych danych z wyrażeniem. Realizujemy to przy użyciu ograniczenia CHECK. Jeżeli jest ono utworzone bezpośrednio w definicji pola, to do budowy wyrażenia można użyć nazwy tylko tej kolumny, dla której jest ono tworzone, dowolnych operatorów zdefiniowanych w środowisku, stałych oraz funkcji bez parametru lub z parametrami w postaci stałych albo wyrażeń odnoszących się do definiowanego pola. W przykładzie jako domyślną wartość daty wprowadzono datę bieżącą powiększoną o jeden dzień. W polu tym sprawdzamy, czy wprowadzona data jest późniejsza niż data bieżąca. W dwóch kolumnach numerycznych sprawdzamy, czy pierwsza z nich jest większa od 10, a druga mniejsza niż 100.
Rozdział 3. Język zapytań SQL w MS SQL Server
115
CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, Opis varchar(15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100));
Ponieważ nie wszystkie dane są zgodne z ograniczeniami, otrzymujemy serię komunikatów o błędach, z których pierwszy dotyczy zbyt małej wartości daty, a kolejne mówią o zbyt małej wartości m_w i zbyt dużej mm_v. Msg 547, Level 16, State 0, Line 8 The INSERT statement conflicted with the CHECK constraint "CK__Zlecenia__DataZl__6C190EBB". The conflict occurred in database "test", table "dbo.Zlecenia", column 'DataZlec'. The statement has been terminated. Msg 547, Level 16, State 0, Line 12 The INSERT statement conflicted with the CHECK constraint "CK__Zlecenia__m_v__6D0D32F4". The conflict occurred in database "test", table "dbo.Zlecenia", column 'm_v'. The statement has been terminated. Msg 547, Level 16, State 0, Line 13 The INSERT statement conflicted with the CHECK constraint "CK__Zlecenia__mm_v__6E01572D". The conflict occurred in database "test", table "dbo.Zlecenia", column 'mm_v'. The statement has been terminated.
Ponieważ w wyrażeniach sprawdzających, definiowanych przy kolumnie, nie możemy odwoływać się do innych pól, aby sprawdzić, czy pierwsze z pól numerycznych jest mniejsze niż drugie, musimy zastosować konstrukcje ze słowem kluczowym CONSTRAINT. W takim przypadku możemy odwołać się do każdego pola, które zostało zdefiniowane wyżej niż ta dyrektywa. Zastosowanie warunku, który pokazano w przykładzie, może być interpretowane tak, że m_v określa dolne ograniczenie, a mm_v górne przedziału jakiejś wielkości, np. czasu wykonania zlecenia, kwoty, jaka może być na nie przeznaczona, etc. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, Opis varchar(15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100) , CONSTRAINT spr CHECK(m_v < mm_v));
Ponieważ w ostatnim wprowadzanym wierszu mm_v jest mniejsze niż m_v, nowy komunikat o błędzie będzie miał postać: Msg 547, Level 16, State 0, Line 15 The INSERT statement conflicted with the CHECK constraint "spr". The conflict occurred in database "test", table "dbo.Zlecenia". The statement has been terminated.
Ponieważ użycie słowa kluczowego CONSTRAINT nie jest obowiązkowe, to równoważne zapytanie bez jego użycia będzie miało postać jak poniżej. Należy pamiętać, że w takim przypadku nazwę ograniczeniu nadaje system, co widać w przedstawionym komunikacie o błędzie, gdzie nazwa ta ma postać CK__Zlecenia__01142BA1.
116
MS SQL Server. Zaawansowane metody programowania CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, Opis varchar(15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); Msg 547, Level 16, State 0, Line 15 The INSERT statement conflicted with the CHECK constraint "CK__Zlecenia__01142BA1". The conflict occurred in database "test", table "dbo.Zlecenia". The statement has been terminated.
Ostatnim ograniczeniem, jakie zostanie przedstawione, jest klucz obcy FOREIGN KEY. Ograniczenie to jest bardzo ważne z punktu widzenia schematu relacyjnego, ponieważ określa, między jakimi polami następuje wymiana danych. Można powiedzieć, że wskazane pole tabeli nadrzędnej staje się słownikiem wartości dla pola klucza obcego. Dlatego w definicji poza określeniem nazwy, rodzaju ograniczenia oraz pola lub listy pól, których to ograniczenie dotyczy, konieczne jest zdefiniowanie referencji. Wskazuje się w niej na istniejącą tabelę oraz jej pole lub listę pól, zgodną co do liczby z listą pól wymienionych w FOREIGN KEY. Warunkiem wystarczającym jest to, aby pola lub pole w tabeli nadrzędnej definiowały jej klucz podstawowy. W praktyce jest to najczęściej wykorzystywany przypadek. W przykładzie został utworzony klucz obcy na polu IdPracownika, odwołujący się do klucza głównego tabeli Pracownicy, ustawionego na polu o tej samej nazwie. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika), Opis varchar(15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v));
Ponieważ w tabeli Pracownicy nie ma osoby o identyfikatorze 99, czyli próbowaliśmy przypisać zlecenie do nieistniejącego pracownika, pojawił się kolejny komunikat o błędzie o postaci: Msg 547, Level 16, State 0, Line 12 The INSERT statement conflicted with the FOREIGN KEY constraint "fk". The conflict occurred in database "test", table "dbo.Pracownicy", column 'IdPracown ka'. The statement has been terminated.
Klucz obcy nie pozwala na wstawienie wartości spoza listy dopuszczalnych wartości, jednak nie chroni przed wpisywaniem wartości pustej. Oznacza to, że zlecenie nie jest wystawione dla nikogo. Aby temu zapobiec, należy jawnie podać ograniczenie NOT NULL w definicji pola stanowiącego klucz obcy, jak pokazano w przykładzie. W przypadku wprowadzania przykładowego zestawu danych powoduje to wygenerowanie kolejnego komunikatu: CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int NOT NULL,
Rozdział 3. Język zapytań SQL w MS SQL Server
117
CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika), Opis varchar(15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); Msg 515, Level 16, State 2, Line 13 Cannot insert the value NULL into column 'IdPracownika', table 'test.dbo.Zlecenia'; column does not allow nulls. INSERT fails. The statement has been terminated.
Oczywiście nie zawsze musimy chcieć eliminować wstawianie pustych wartości do pola klucza obcego, ponieważ procedura wystawiania zleceń może wymagać najpierw jego zdefiniowania, a dopiero później, po jakichś dodatkowych czynnościach, określić, kto będzie je realizował. Utworzenie klucza obcego ma również inne konsekwencje. Jeśli do tabeli Zlecenia wstawimy wiersz, w którym wartość pola IdPracownika wskazuje na osobę o identyfikatorze 2, to próba usunięcia rekordu dotyczącego tej osoby z tabeli Pracownicy zakończy się niepowodzeniem. Zjawisko to przedstawia skrypt wraz z wygenerowanym na skutek jego wykonania komunikatem o błędzie. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int NOT NULL, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika), Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =2 SELECT * FROM Zlecenia Msg 547, Level 16, State 0, Line 1 The DELETE statement conflicted with the REFERENCE constraint "fk". The conflict occurred in database "test", table "dbo.Zlecenia", column 'IdPracown ka'. The statement has been terminated.
Rekordy tabeli nadrzędnej, powiązane kluczem obcym, dla których istnieją rekordy w tabeli podrzędnej, w stanie domyślnym są chronione przed usunięciem. Aby usunąć taki rekord, należałoby najpierw usunąć z tabeli podrzędnej wszystkie powiązane z nim rekordy i dopiero wówczas usuwać rekord z tabeli nadrzędnej. W jawnej postaci taką funkcjonalność realizuje dyrektywa ON DELETE NO ACTION. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int NOT NULL, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE NO ACTION, Opis varchar(15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100),
118
MS SQL Server. Zaawansowane metody programowania CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =2; SELECT * FROM Zlecenia;
Aby pokazać dalsze konsekwencje stosowania kluczy obcych, dokonajmy modyfikacji tabeli Pracownicy, tak aby klucz podstawowy nie był generowany automatycznie, a następnie wstawmy do niej dwa przykładowe rekordy. Skutek wykonania skryptu prezentuje tabela 3.48. CREATE TABLE Pracownicy (IdPracownika int NOT NULL PRIMARY KEY, Iddzialu int, Nazwisko varchar(15), Imie varchar(15), RokUrodz integer, DataZatr date, IdSzefa int); INSERT INTO Pracownicy (IdPracownika, Nazwisko) VALUES (1,'Kowalski'), (2,'Nowak'); SELECT IdPracownika, Nazwisko FROM Pracownicy;
Tabela 3.48. Przykładowe dane w tabeli Pracownicy IdPracownika
Nazwisko
1
Kowalski
2
Nowak
Jeśli w definicji tabeli jawnie ustawimy kolejną opcję ON UPDATE NO ACTION, która jest stanem domyślnym, to zarówno próba usunięcia, jak i zmodyfikowania rekordu w tabeli Pracownicy, do którego odwołują się rekordy z tabeli podrzędnej, zakończy się niepowodzeniem. W przypadku usuwania działa dyrektywa ON DELETE, a w przypadku modyfikacji — ON UPDATE. Po przykładowym skrypcie SQL przedstawiono komunikaty informujące o błędach podczas modyfikacji danych. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int NOT NULL, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE NO ACTION ON UPDATE NO ACTION, Opis varchar(15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(1,'Zlecenie','2010-11-1',20,80); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =1; UPDATE Pracownicy SET IdPracownika=3 WHERE IdPracownika =2; SELECT * FROM Zlecenia; Msg 547, Level 16, State 0, Line 1
Rozdział 3. Język zapytań SQL w MS SQL Server
119
The DELETE statement conflicted with the REFERENCE constraint "fk". The conflict occurred in database "test", table "dbo.Zlecenia", column 'IdPracown ka'. The statement has been terminated. Msg 547, Level 16, State 0, Line 2 The UPDATE statement conflicted with the REFERENCE constraint "fk". The conflict occurred in database "test", table "dbo.Zlecenia", column 'IdPracown ka'. The statement has been terminated.
Aby umożliwić modyfikacje klucza głównego pracownika, który miał wystawione chociaż jedno zlecenie, należy najpierw dodać nowy rekord reprezentujący pracownika z nowym identyfikatorem, następnie przepisać wszystkie jego zlecenia, wstawiając nową wartość klucza obcego, a na koniec usunąć stary rekord reprezentujący modyfikowaną osobę. W przypadku wielu tabel podrzędnych połączonych kluczem obcym operacje przepisywania rekordów z nowym identyfikatorem jako kluczem obcym należy powtórzyć dla każdej z nich. Jak widać, jest to operacja bardzo pracochłonna. Zarówno dla usuwania, jak i modyfikacji można ten proces zautomatyzować, ustanawiając dla dyrektyw ON DELETE oraz ON UPDATE opcję CASCADE, tak jak to pokazano w skrypcie. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE CASCADE ON UPDATE CASCADE, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(1,'Zlecenie','2010-11-1',20,80); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =1; UPDATE Pracownicy SET IdPracownika=3 WHERE IdPracownika =2; SELECT * FROM Zlecenia;
Jak wynika z analizy danych zawartych w tabeli 3.49, obie modyfikacje w tabeli Pracownicy zostały wykonane, a ich skutek przeniósł się kaskadowo do tabeli Zlecenia. Jeden z rekordów został automatycznie usunięty, a drugi ma zmieniony identyfikator pracownika. Tabela 3.49. Dane w tabeli Zlecenia po wykonaniu modyfikacji tabeli Pracownicy w przypadku zastosowania opcji CASCADE IdZlecenia
IdPracownika
Opis
DataZlec
m_v
mm_v
2
3
Zlec
2010-10-21
30
70
Choć pozornie mechanizm ten wydaje się bardzo atrakcyjny, niesie za sobą potencjalnie bardzo duże niebezpieczeństwo. Jeśli mamy wielopoziomową strukturę relacyjną z tabelami na każdym poziomie połączonymi kluczami obcymi z ustawioną opcją automatycznego, kaskadowego usuwania wierszy, to przypadkowe, pomyłkowe wykasowanie rekordów na najwyższym poziomie pociąga za sobą hierarchie operacji kasowania w tabelach znajdujących się na wszystkich poziomach podrzędnych. Można powiedzieć o lawinie usuwania danych. Oczywiście nie można również wykluczyć
120
MS SQL Server. Zaawansowane metody programowania
celowego działania, sabotażu, w przypadku którego ta opcja działa na korzyść intruza. Mniej kłopotliwa jest opcja kaskadowej modyfikacji rekordów, ale wobec powszechnego stosowania automatycznie inkrementowanych kluczy głównych jej rola jest niewielka. Innym rozwiązaniem automatyzacji procesu usuwania i modyfikowania danych w tabeli nadrzędnej jest użycie w definicji klucza obcego opcji SET NULL. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int NOT NULL, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE SET NULL ON UPDATE SET NULL, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v));
Dokonana w przykładowym skrypcie modyfikacja spowoduje jednak podczas tworzenia tabeli wygenerowanie komunikatu o błędzie, ponieważ ograniczenie NOT NULL zastosowane w definicji pola jest sprzeczne z opcjami automatyzacji reakcji na zmiany rekordów w tabeli Zlecenia. Msg 1761, Level 16, State 0, Line 1 Cannot create the foreign key "fk" with the SET NULL referential action, because one or more referencing columns are not nullable. Msg 1750, Level 16, State 0, Line 1 Could not create constraint. See previous errors.
Aby umożliwić zastosowanie opcji SET NULL, w kolejnym skrypcie usunięto ograniczenie NOT NULL. Po zasileniu tabeli i wykonaniu zapytań modyfikujących pokazano jej wynikową zawartość w tabeli 3.50. Jak można zaobserwować, w obu rekordach pole kluczy obcych ma wartość NULL. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY (1, 1) NOT NULL PRIMARY KEY, IdPracownika int, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE SET NULL ON UPDATE SET NULL, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(1,'Zlecenie','2010-11-1',20,80); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =1; UPDATE Pracownicy SET IdPracownika=3 WHERE IdPracownika =2; SELECT * FROM Zlecenia;
Tabela 3.50. Dane w tabeli Zlecenia po wykonaniu modyfikacji tabeli Pracownicy w przypadku zastosowania opcji SET NULL IdZlecenia
IdPracownika
Opis
DataZlec
m_v
mm_v
1
NULL
Zlecenie
2010-11-01
20
80
2
NULL
Zlec
2010-10-21
30
70
Rozdział 3. Język zapytań SQL w MS SQL Server
121
Kolejna opcja automatyzacji modyfikacji danych SET DEFAULT wymaga określenia wartości domyślnej dla kolumny klucza obcego. W przykładzie ustawiono ją na wartość 1. Należy pamiętać, że powinna ona być zgodna z istniejącą wartością w tabeli nadrzędnej. Następnie spróbowano wykasować pracownika o identyfikatorze 1 oraz zmienić wartość identyfikatora pracownika z 2 na 3. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int DEFAULT 1, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE SET DEFAULT ON UPDATE SET DEFAULT, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(1,'Zlecenie','2010-11-1',20,80); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =1; UPDATE Pracownicy SET IdPracownika=3 WHERE IdPracownika =2; SELECT * FROM Zlecenia;
W pierwszym przypadku pojawił się komunikat o błędzie, ponieważ kasowanie dotyczyło pracownika, którego identyfikator został użyty jako wartość domyślna. Drugi rekord został pomyślnie zmodyfikowany, a zlecenie zostało przypisane pracownikowi o numerze 1. Potwierdza to wynikowy zestaw rekordów przedstawiony w tabeli 3.51. (1 row(s) affected) Msg 547, Level 16, State 0, Line 14 The DELETE statement conflicted with the FOREIGN KEY constraint "fk". The conflict occurred in database "test", table "dbo.Pracownicy", column 'IdPracown ka'. The statement has been terminated.
Tabela 3.51. Dane w tabeli Zlecenia po wykonaniu modyfikacji tabeli Pracownicy w przypadku zastosowania opcji SET DEFAULT IdZlecenia
IdPracownika
Opis
DataZlec
m_v
mm_v
1
1
Zlecenie
2010-11-01
20
80
2
1
Zlec
2010-10-21
30
70
Analogiczne działanie uzyskamy w przypadku ustawienia 2 jako wartości domyślnej klucza obcego. Tym razem usuwanie zakończy się sukcesem, a modyfikacja identyfikatora spowoduje wygenerowanie komunikatu. Potwierdza to również zawartość tabeli 3.52. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY(1, 1) NOT NULL PRIMARY KEY, IdPracownika int DEFAULT 2, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE SET DEFAULT ON UPDATE SET DEFAULT, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()),
122
MS SQL Server. Zaawansowane metody programowania m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(1,'Zlecenie','2010-11-1',20,80); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =1; UPDATE Pracownicy SET IdPracownika=3 WHERE IdPracownika =2; SELECT * FROM Zlecenia; Msg 547, Level 16, State 0, Line 15 The UPDATE statement conflicted with the FOREIGN KEY constraint "fk". The conflict occurred in database "test", table "dbo.Pracownicy", column 'IdPracown ka'. The statement has been terminated.
Tabela 3.52. Dane w tabeli Zlecenia po wykonaniu modyfikacji tabeli Pracownicy w przypadku zastosowania opcji SET DEFAULT IdZlecenia
IdPracownika
Opis
DataZlec
m_v
mm_v
1
2
Zlecenie
2010-11-01
20
80
2
2
Zlec
2010-10-21
30
70
Ustanowienie wartości domyślnej klucza obcego na NULL spowoduje, że opcja SET DEFAULT zachowa się tak samo jak SET NULL. Obie modyfikacje zakończą się sukcesem, co potwierdza zawartość tabeli 3.53. CREATE TABLE Zlecenia (IdZlecenia int IDENTITY (1, 1) NOT NULL PRIMARY KEY, IdPracownika int DEFAULT NULL, CONSTRAINT fk FOREIGN KEY(IdPracownika) REFERENCES Pracownicy(IdPracownika) ON DELETE SET DEFAULT ON UPDATE SET DEFAULT, Opis varchar (15) NOT NULL DEFAULT 'Brak', DataZlec date DEFAULT getdate()+1 CHECK (DataZlec >= getdate()), m_v int CHECK(m_v>10), mm_v int CHECK(mm_v<100), CHECK(m_v < mm_v)); INSERT INTO Zlecenia VALUES(1,'Zlecenie','2010-11-1',20,80); INSERT INTO Zlecenia VALUES(2,'Zlec','2010-10-21',30,70); DELETE FROM Pracownicy WHERE IdPracownika =1; UPDATE Pracownicy SET IdPracownika=3 WHERE IdPracownika =2; SELECT * FROM Zlecenia;
Tabela 3.53. Dane w tabeli Zlecenia po wykonaniu modyfikacji tabeli Pracownicy w przypadku zastosowania opcji SET DEFAULT IdZlecenia
IdPracownika
Opis
1
NULL
Zlecenie
2
NULL
Zlec
DataZlec
m_v
mm_v
2010-11-01
20
80
2010-10-21
30
70
Stwierdzono poprzednio, że klucz obcy może mieć referencje do istniejącej tabeli. Jest jeden wyjątek od tej reguły. W firmie istnieje hierarchia pracowników, każdy z nich ma szefa, który jest również pracownikiem tej firmy. W tym celu do definicji tabeli Pracownicy dodajemy kolejne pole, IdSzefa, na którym tworzymy klucz obcy, który
Rozdział 3. Język zapytań SQL w MS SQL Server
123
odwołuje się do pola IdPracownika z tej samej tabeli. Ponieważ tabela jest tworzona ponownie, trudno powiedzieć, że w momencie wykonywania skryptu już istnieje. Parser niejako antycypuje sytuację, która nastąpi za chwilę, czyli zakłada, że polecenie zakończy się sukcesem. Taki klucz obcy możemy nazwać wewnętrznym. W przykładowym skrypcie po utworzeniu tabeli następuje zasilenie danymi, co przedstawia tabela 3.54. Naczelny szef ma w polu IdSzefa wpisaną wartość NULL. CREATE TABLE Pracownicy (IdPracownika int IDENTITY(1, 1) NOT NULL PRIMARY KEY, Iddzialu int, Nazwisko varchar(15), Imie varchar(15), RokUrodz integer, DataZatr date, IdSzefa int CONSTRAINT fk FOREIGN KEY (IdSzefa) REFERENCES Pracownicy(IdPracownika)); INSERT INTO Pracownicy (Nazwisko, IdSzefa) VALUES ('Kowalski', NULL), ('Nowak', 1), ('Janik', 1), ('Wilk', 2); SELECT IdPracownika, Nazwisko, IdSzefa FROM Pracownicy;
Tabela 3.54. Dane w tabeli Pracownicy ze zdefiniowanym wewnętrznym kluczem obcym IdPracownika
Nazwisko
IdSzefa
1
Kowalski
NULL
2
Nowak
1
3
Janik
1
4
Wilk
2
Tak samo zadziała skrypt, w którym nie użyto słowa kluczowego CONSTRAINT, czyli nazwa ograniczenia zostanie nadana przez system. CREATE TABLE Pracownicy (IdPracownika int IDENTITY (1, 1) NOT NULL PRIMARY KEY, Iddzialu int, Nazwisko varchar (15), Imie varchar (15), RokUrodz integer, DataZatr date, IdSzefa int, FOREIGN KEY (IdSzefa) REFERENCES Pracownicy(IdPracownika));
Dla wewnętrznego klucza obcego dla dyrektyw ON DELETE i ON UPDATE opcje różne od NO ACTION są niedopuszczalne. Wynika to z tego, że akcje CASCADE i SET NULL mogłyby doprowadzić do cyklicznego wykonywania zapytań modyfikujących dane — rekurencji. To oraz wygenerowane komunikaty można sprawdzić, wykonując kolejny skrypt. CREATE TABLE Pracownicy (IdPracownika int PRIMARY KEY, Iddzialu int, Nazwisko varchar(15), Imie varchar(15), RokUrodz integer, DataZatr date,
124
MS SQL Server. Zaawansowane metody programowania IdSzefa int, CONSTRAINT fk FOREIGN KEY (IdSzefa) REFERENCES Pracownicy(IdPracownika) ON DELETE CASCADE ON UPDATE CASCADE --ON DELETE SET NULL ON UPDATE SET NULL );
Msg 1785, Level 16, State 0, Line 1 Introducing FOREIGN KEY constraint 'fk' on table 'Pracownicy' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints. Msg 1750, Level 16, State 0, Line 1 Could not create constraint. See previous errors.
Referencje możemy również zdefiniować w stosunku do klucza głównego. Tworzona jest dzięki temu relacja typu 1:1. W przykładowym skrypcie tworzymy tabelę Pracownicy_1, która wymienia dane z tabelą Pracownicy w ten sposób, że każdemu rekordowi w jednej z nich odpowiada najwyżej jeden w drugiej. Pole klucza w nowej tabeli nie powinno być automatycznie inkrementowane. CREATE TABLE Pracownicy_1 (IdPracownika int PRIMARY KEY REFERENCES Pracownicy(IdPracownika), Napis varchar(15), Liczba real); INSERT INSERT INSERT SELECT
INTO Pracownicy_1 VALUES (1,'Tekst1',1.1); INTO Pracownicy_1 VALUES (9,'Tekst2',1.2); INTO Pracownicy_1 VALUES (NULL,'Tekst3',1.3); * FROM Pracownicy_1;
W skrypcie zaproponowano zasilenie danymi, gdzie w pole klucza tabeli Pracownicy_1 oprócz jednego poprawnego rekordu próbowano wprowadzić taki, w którym wartość klucza podstawowego przekraczała wartość w tabeli Pracownicy, a w drugim przypadku była NULL. Komunikaty wynikające z tych prób zamieszczono niżej, a wynikową zawartość pokazano w tabeli 3.55. Msg 547, Level 16, State 0, Line 8 The INSERT statement conflicted with the FOREIGN KEY constraint "FK__Pracownic__IdPra__04459E07". The conflict occurred in database "master", table "dbo.Pracownicy", column 'IdPracownika'. The statement has been terminated. Msg 515, Level 16, State 2, Line 9 Cannot insert the value NULL into column 'IdPracownika', table 'master.dbo.Pracownicy_1'; column does not allow nulls. INSERT fails. The statement has been terminated
Tabela 3.55. Dane w tabeli Pracownicy_1 ze zdefiniowanym referencyjnym kluczem głównym IdPracownika
Napis
Liczba
1
Tekst1
1.1
Jeśli tworzymy referencyjny klucz podstawowy, to musimy to zrobić bezpośrednio w definicji pola tabeli. Stosowanie składni z użyciem słowa kluczowego CONSTRAINT jest niepoprawne. CREATE TABLE Pracownicy_1 (IdPracownika int, CONSTRAINT pk PRIMARY KEY (idPracownika)REFERENCES Pracownicy(IdPracownika), Napis varchar(15), Liczba real);
Rozdział 3. Język zapytań SQL w MS SQL Server
125
Gdyby w skrypcie wystąpiła przekreślona linia, to komunikat o błędzie miałby postać: Msg 156, Level 15, State 1, Line 4 Incorrect syntax near the keyword 'REFERENCES'.
Usuwanie tabeli jest wykonywane za pomocą polecenia DROP TABLE. Jeżeli jednak zastosujemy je do tabeli, do której za pomocą klucza obcego odwołuje się choć jedna tabela podrzędna, przetwarzanie zakończy się fiaskiem i wyświetli się komunikat o błędzie. DROP TABLE Pracownicy;
Msg 3726, Level 16, State 1, Line 1 Could not drop object 'Pracownicy' because it is referenced by a FOREIGN KEY constraint.
Możemy jednocześnie usuwać wiele tabel. Ponieważ usuwanie odbywa się zgodnie z kolejnością wymienienia na liście, tabela Pracownicy nie zostanie usunięta, ponieważ jest z nią powiązana tabela podrzędna. Natomiast tabela Zlecenia będzie usunięta, ponieważ nie ma takich powiązań. DROP TABLE Pracownicy, Zlecenia;
Jeśli zamienimy kolejność usuwania, jako pierwsza zostanie usunięta tabela Zlecenia. Spowoduje to, że nie będzie już obiektów odwołujących się do tabeli Pracownicy. Dlatego tym razem tabela ta zostanie skutecznie usunięta i całe polecenie zakończy się sukcesem. DROP TABLE Zlecenia, Pracownicy;
Zobaczmy teraz w praktyce zastosowanie identyfikatorów typu uniqueidentifier. W przykładowym skrypcie tworzona jest tabela z polem tego typu oraz polem automatycznie inkrementowanym za pomocą funkcji IDENTITY. Zasilona ona została dwoma rekordami, gdzie w pierwszym zastosowano funkcję NEWID() generującą identyfikator, a w drugim wymuszono wstawienie wartości domyślnej. CREATE TABLE T1 (nr1 int IDENTITY, nr2 uniqueidentifier); GO INSERT INTO T1 (nr2) VALUES (NEWID()); INSERT INTO T1 DEFAULT VALUES; GO SELECT * FROM T1;
Analizując wyniki zawarte w tabeli 3.56, widzimy szesnastkowy kod identyfikatora, który został sformatowany według zasady 8-4-4-4-12 cyfr heksadecymalnych, co oznacza, że funkcja NEWID() zamiast do pola uniqueidentifier może wpisać wartość do pola znakowego o minimalnym rozmiarze 36. Można to sprawdzić, zmieniając definicje kolumny w skrypcie na nr2 varchar(36). Przy okazji można potwierdzić, że w stanie domyślnym funkcja IDENTITY generuje wartości, począwszy od 1 ze skokiem 1. Tabela 3.56. Przykład zastosowania pola uniqueidentifier nr1
nr2
1
DAA7C652-101F-443B-9378-4F55AE7BA3CD
2
NULL
126
MS SQL Server. Zaawansowane metody programowania
Aby wzbogacić nasz schemat, spróbujmy dodać kolejne tabele podrzędne względem tabeli Osoby. Tabele te znajdują się na tym samym poziomie co tabela Zlecenia. Można powiedzieć, że są do niej równoległe. Pierwsza tabela przechowuje informacje o nagrodach przyznanych pracownikom i składa się z: ręcznie uzupełnianego klucza głównego; klucza obcego wiążącego tabele z ustawionymi opcjami automatycznego usuwania i modyfikacji pól; pola znakowego. Druga opisuje urlopy i składa się analogicznie do poprzedniej z: klucza głównego o wartościach generowanych automatycznie; klucza obcego, w którym ustawiono referencje bez podania typu i nazwy klucza, pominięto również nazwę pola w tabeli nadrzędnej, ponieważ nazywa się ono tak samo jak definiowane; z dwóch pól określających daty początku i końca urlopu, w pierwszym ustalono wartość domyślną na datę bieżącą, dodatkowo określono, że data końca nie może być mniejsza niż data początku. CREATE TABLE Nagrody (IdNagrody int NOT NULL PRIMARY KEY, IdOsoby int NOT NULL FOREIGN KEY REFERENCES Osoby(IdOsoby) ON DELETE CASCADE ON UPDATE CASCADE, Nagroda varchar(15)); GO CREATE TABLE Urlopy (IdUrlopu int IDENTITY PRIMARY KEY, IdOsoby int REFERENCES Osoby, Od date DEFAULT getdate(), Do date, CONSTRAINT SprU CHECK(Do>=Od));
Na rysunku 3.7 przedstawiono diagram pokazujący utworzone skryptem powiązanie między tabelami Osoby i Urlopy. Tak jak pokazano w skrypcie, przy łączeniu pól o takiej samej nazwie nie jest konieczne wskazywanie pola odniesienia, natomiast to, że referencja jest elementem klucza obcego, silnik bazy danych wykrywa na podstawie tego, że w tabeli podrzędnej jest inne pole klucza podstawowego, a połączenie wskazuje na klucz podstawowy tabeli nadrzędnej. Na rysunku 3.7 widać również reprezentacje wewnętrznego klucza obcego zdefiniowanego na tabeli Osoby. Rysunek 3.7. Diagram prezentujący tabele Osoby i Urlopy oraz ich powiązania
Osoby IdOsoby IdDzialu Nazwisko Imie RokUrodz Wzrost
Urlopy IdUrlopu IdOsoby Od Do
DataZatr IdSzefa
Wybierając jedną z tabel, a następnie klikając prawym przyciskiem myszy, wyświetlamy menu podręczne, z którego możemy wybrać pozycję Relationships. Dla tabeli Urlopy pojawi się okno dialogowe, a w nim definicja jednego klucza obcego (rysunek
Rozdział 3. Język zapytań SQL w MS SQL Server
127
3.8). Rozwijając pozycję Tables And Columns Specification, możemy sprawdzić, na jakich polach określono połączenie. W tym samym miejscu przez przycisk z … możemy dostać się do lokalizacji, w której mogą być one edytowane.
Rysunek 3.8. Okno edytowania kluczy obcych
W definicji tabeli możliwe jest tworzenie kolumn obliczanych. Co prawda jest to sprzeczne z zasadami normalizacji, a konkretnie z trzecią postacią normalną, jednak ponieważ taka funkcjonalność występuje, wypadałoby powiedzieć o niej kilka słów. W praktyce komercyjnej często rezygnuje się z wyższych postaci normalnych, godząc się z wieloma niedogodnościami stąd wynikającymi, szczególnie z wystąpieniem redundancji czy brakiem spójności danych. Robi się to po to, aby zredukować liczbę złączeń w zapytaniach, a przez to poprawić wydajność. Uważam, że jest to mimo wszystko zabieg ryzykowny, a poprawę szybkości przetwarzania można uzyskać, stosując inne mechanizmy. Występowanie w bazie anomalii wynikających z niepełnej normalizacji należy uważać za stan niepożądany i sięgać do niego tylko w absolutnie wyjątkowych sytuacjach. W przykładzie utworzono tabelę o trzech polach całkowitych. Dwa pierwsze są wprowadzane ręcznie, natomiast trzecie jest ich ilorazem. Wyrażenie opisujące taką kolumnę określamy po słowie kluczowym AS i możemy w nim użyć wszystkich pól występujących przed polem obliczanym, wszystkich operatorów algebraicznych oraz funkcji środowiska. Bezpośrednio w definicji nie podajemy typu tej kolumny. W skrypcie występują cztery instrukcje zasilające tabelę danymi, w których wartości podawane są tylko dla kolumn niezdefiniowanych wyrażeniem. DROP TABLE kol_obl; GO CREATE TABLE kol_obl (a int, b int, c AS a/b); GO INSERT INTO kol_obl VALUES(1,2); INSERT INTO kol_obl VALUES(6,2); INSERT INTO kol_obl VALUES(1,NULL); INSERT INTO kol_obl VALUES(1,0); SELECT * FROM kol_obl;
W czwartym wierszu wprowadzania danych próbowano wstawić do dzielnej wartość 0, więc pojawił się komunikat o błędzie.
128
MS SQL Server. Zaawansowane metody programowania Msg 8134, Level 16, State 1, Line 6 Divide by zero error encountered.
Jak widać z zawartości tabeli 3.57, w kolumnie c następuje obcięcie wyniku. Dzieje się tak, ponieważ typ wyniku jest konsekwencją typów kolumn definiujących wyrażenie. Tabela 3.57. Przykład konsekwencji zastosowania pola obliczanego w definicji tabeli a
b
c
1
2
0
6
2
3
1
NULL
NULL
Jeżeli teraz w definicji tabeli choć jedna z kolumn wprowadzanych manualnie będzie miała typ rzeczywisty, to i typ kolumny wynikowej będzie taki sam. Prowadzi to do poprawnego wyznaczenia ilorazu, co widać w tabeli 3.58. Oczywiście nie zmienia to konsekwencji dzielenia przez zero. DROP TABLE kol_obl; GO CREATE TABLE kol_obl (a real, b int, c AS a/b ); GO INSERT INTO kol_obl VALUES(1,2); INSERT INTO kol_obl VALUES(6,2); INSERT INTO kol_obl VALUES(1,NULL); INSERT INTO kol_obl VALUES(1,0); SELECT * FROM kol_obl;
Tabela 3.58. Przykład konsekwencji zastosowania pola obliczanego w definicji tabeli a
b
c
1
2
0.5
6
2
3
1
NULL
NULL
Jak już powiedziano wcześniej, do definiowania wyrażeń możemy wykorzystać wbudowane funkcje, również niedeterministyczne. W kolejnym przykładzie kolumna jest obliczana za pomocą generatora liczb pseudolosowych — funkcji RAND() generującej wartości z przedziału <0, 1). Tym razem typ wyniku jest konsekwencją typu wartości zwracanej przez funkcję. DROP TABLE kol_obl; GO CREATE TABLE kol_obl (a real, b int, c AS RAND(b));
Rozdział 3. Język zapytań SQL w MS SQL Server
129
Do definiowania wyrażeń możemy korzystać z funkcji użytkownika. Ponieważ precyzyjne omówienie sposobu tworzenia funkcji pojawi się w dalszej części książki (rozdział 5.3), oprzyjmy się na intuicji wynikającej z programowania funkcji w innych środowiskach. W przykładowym skrypcie utworzona została bezparametrowa funkcja maks, która zwraca wartość rzeczywistą. W jej ciele obliczane jest maksimum pola b, które stanowi wartość zwracaną do chwili wywołania funkcji. CREATE FUNCTION maks () RETURNS REAL AS BEGIN DECLARE @wyn real SELECT @wyn =MAX(b)FROM kol_obl RETURN @wyn END
Utworzona poprzednio funkcja definiuje wartość, jaka będzie automatycznie wpisywana do pola c tabeli. W skrypcie zasilono tabelę czterema wierszami danych. Po każdej instrukcji wstawiającej rekord wykonano zapytanie wybierające, sprawdzające aktualną zawartość zasilanej tabeli. DROP TABLE kol_obl; GO CREATE TABLE kol_obl (a real, b int, c AS dbo.maks()); GO INSERT INTO kol_obl VALUES(1,2); SELECT * FROM kol_obl; INSERT INTO kol_obl VALUES(6,0); SELECT * FROM kol_obl; INSERT INTO kol_obl VALUES(1,NULL); SELECT * FROM kol_obl; INSERT INTO kol_obl VALUES(1,6); SELECT * FROM kol_obl;
Wyniki w postaci tekstowej wygenerowane dzięki wykonaniu przykładowego skryptu zaprezentowano poniżej. (1 row(s) affected) a b c ------------- ----------- ------------1 2 2 (1 row(s) affected) (1 row(s) affected) a b c ------------- ----------- ------------1 2 2 6 0 2 (2 row(s) affected) (1 row(s) affected) a b c ------------- ----------- ------------1 2 2 6 0 2 1 NULL 2 (3 row(s) affected)
130
MS SQL Server. Zaawansowane metody programowania (1 row(s) affected) a b c ------------- ----------- ------------1 2 6 6 0 6 1 NULL 6 1 6 6 (4 row(s) affected)
Z analizy zawartości tabeli uzyskiwanej w każdym kroku oraz komunikatów możemy wnioskować, że przeliczenie zawartości następuje po wprowadzeniu każdego rekordu i ze względu na zastosowaną funkcję zmiana dotyczy wszystkich wierszy. Zmodyfikujmy teraz funkcję maks tak, aby posiadała parametr, który będzie określał modyfikowane wiersze. W naszym przypadku będą to te, w których wartość kolumny a tabeli jest równa wartości podanej parametrem. CREATE FUNCTION maks (@grupa int) RETURNS REAL AS BEGIN DECLARE @wyn real SELECT @wyn =MAX(b)FROM kol_obl WHERE a=@grupa RETURN @wyn END
W skrypcie tworzącym tabelę zmieniono sposób wywołania, podstawiając za parametr wartość pola a. Idea skryptu zasilającego i testującego zawartość tabeli pozostała bez zmian. Zmodyfikowano nieco wstawiane wartości. CREATE TABLE kol_obl (a real, b int, c AS dbo.maks(a)); GO INSERT INTO kol_obl VALUES(1,2); SELECT * FROM kol_obl; INSERT INTO kol_obl VALUES(2,0); SELECT * FROM kol_obl; INSERT INTO kol_obl VALUES(1,3); SELECT * FROM kol_obl; INSERT INTO kol_obl VALUES(2,6); SELECT * FROM kol_obl;
Poniżej przedstawiono rezultaty otrzymane przez uruchomienie skryptu. (1 row(s) affected) a b c ------------- ----------- ------------1 2 2 (1 row(s) affected) (1 row(s) affected) a b c ------------- ----------- ------------1 2 2 2 0 0 (2 row(s) affected)
Rozdział 3. Język zapytań SQL w MS SQL Server
131
(1 row(s) affected) a b c ------------- ----------- ------------1 2 3 2 0 0 1 3 3 (3 row(s) affected) (1 row(s) affected) a b c ------------- ----------- ------------1 2 3 2 0 6 1 3 3 2 6 6 (4 row(s) affected)
Analiza wyników prowadzi do wniosku, że przeliczeniu podlegają wszystkie wiersze tabeli należące do grupy rekordów charakteryzujących się tą samą wartością kolumny a. Takie rozwiązanie wydaje się bardzo atrakcyjne, ponieważ wymusza automatyczne przeliczenie i odświeżenie wszystkich rekordów, na które ma wpływ wyrażenie definiujące kolumnę wyliczaną. Jednak jeśli mamy do wykonania skomplikowane obliczenia prowadzące do wyznaczenia kolumny albo jeśli liczba modyfikowanych wyrażeniem rekordów jest duża, musimy zastanowić się nad sensownością takiego rozwiązania. Każde wstawienie rekordu albo modyfikacja pól użytych w wyrażeniu spowoduje obciążenie silnika bazy danych długotrwałymi przeliczeniami, a to prowadzi do drastycznego spadku wydajności przy zmianie zawartości tabeli. W praktyce komercyjnej trudno wyobrazić sobie zastosowanie warte takich komplikacji. Tworzone do tej pory tabele były obiektami trwałymi, istniejącymi do momentu jawnego ich usunięcia. Możliwe jest również tworzenie tabel o skończonym czasie życia. Jeżeli nazwa tabeli zaczyna się od pojedynczego znaku #, to tabela jest tabelą tymczasową lokalną. DROP TABLE #MyTempTable; GO CREATE TABLE #MyTempTable (nr INT PRIMARY KEY); GO INSERT INTO #MyTempTable VALUES (1); SELECT * FROM #MyTempTable;
Taka tabela nie jest widoczna z innych sesji, nawet jeśli zostały one otwarte przez tego samego użytkownika. Nadaje się ona na czasowy magazyn danych do późniejszego przetwarzania w tej samej sesji. Można ją jawnie usunąć, wykorzystując polecenie DROP TABLE, jednak najdłuższym czasem jej życia jest zakończenie sesji, w której została utworzona, bez względu na to, czy takie zakończenie było działaniem celowym, czy też wynikało z awarii. Jeżeli nazwa tabeli rozpoczyna się od dwóch znaków ##, to tabela jest tabelą tymczasową globalną. DROP TABLE ##MyTempTable; GO CREATE TABLE ##MyTempTable (nr INT PRIMARY KEY);
132
MS SQL Server. Zaawansowane metody programowania GO INSERT INTO ##MyTempTable VALUES (2); SELECT * FROM ##MyTempTable;
Tymczasowa tabela globalna jest widoczna z sesji otwartych przez jej twórcę, różnych od tej, w której została utworzona, a przez nazwę kwalifikowaną jest widoczna również z sesji innych użytkowników, którzy mają do niej przypisane prawa. Nadaje się do przekazywania danych między sesjami tego samego użytkownika lub różnych użytkowników. Maksymalny czas jej życia wyznacza również koniec sesji, w której ją utworzono. Załóżmy, że tabela globalna została utworzona w sesji A, w której jest cyklicznie zasilana wartościami od 1 do 9. Aby przedłużyć czas wykonywania skryptu, po wstawieniu każdego z rekordów wymuszono przerwę trwającą 1s. Na zakończenie tabela została usunięta, ponieważ użytkownik jawnie odłączył sesję. Podobny rezultat uzyskalibyśmy przez jawne usunięcie tabeli. --SESJA A CREATE TABLE ##MyTempTable (nr INT PRIMARY KEY); GO DECLARE @licz int SET @licz=1 WHILE @licz <10 BEGIN INSERT INTO ##MyTempTable VALUES (@Licz) WAITFOR DELAY '00:00:01' SET @licz=@licz+1 END --DROP TABLE ##MyTempTable DISCONNECT
W trakcie trwania sesji A został uruchomiony w sesji B skrypt odczytujący zawartość globalnej tabeli tymczasowej. Odczyty są wykonywane dziewięciokrotnie w odstępach 2s wymuszonych poleceniem WAITFOR DELAY. --SESJA B DECLARE @licz int SET @licz=1 WHILE @licz <10 BEGIN SELECT * FROM ##MyTempTable WAITFOR DELAY '00:00:02' SET @licz=@licz+1 END
Obserwując przetwarzanie skryptu w sesji B, możemy zauważyć, iż tabela jest usuwana, pomimo że w innej sesji pobierane są z niej dane. Taki sam efekt zaobserwowalibyśmy również przy wymuszeniu modyfikowania danych w tabeli z poziomu drugiej sesji. Co więcej, nawet jawne zastosowanie transakcji nie blokuje automatycznego usuwania tabeli z końcem sesji, w której ją utworzono. Potwierdza to przedstawiony komunikat o błędzie pojawiający się w sesji B. Msg 208, Level 16, State 0, Line 5 Invalid object name '##MyTempTable'.
Rozdział 3. Język zapytań SQL w MS SQL Server
133
W definicji tabel może pojawić się kolumna typu XML [16] [17]. Spróbujmy przeanalizować operacje, które można na niej wykonać. Zbudujemy tabelę o dwóch kolumnach: automatycznie inkrementowanej klucza głównego oraz interesującego nas typu znacznikowego. Do tabeli wstawiamy wartość zawierającą znacznik nadrzędny i zawarty w nim znacznik podrzędny. DROP TABLE TXML; GO CREATE TABLE TXML( ID int IDENTITY(1,1), KolumnaXml XML); GO INSERT INTO TXML VALUES(''); SELECT * FROM TXML;
W rezultacie otrzymujemy zawartość kolumny o postaci:
Widoczne jest na poziomie podrzędnym zastąpienie dwóch znaczników, otwierającego i zamykającego, znacznikiem pustym. Podobnie jak przy wykonywaniu zapytań wyprowadzających zawartość tabel o typach prostych do postaci XML, dwukrotne kliknięcie w obrębie kontrolki Grid, w polu zawierającym ten typ, przenosi nas do edytora, w którym otrzymujemy czytelniejszą postać pliku. Możemy również wprowadzać zmiany oraz utrwalić zawartość na dysku. Zawartość pola XML możemy zmodyfikować, stosując metodę modify tej kolumny przez dodanie poleceniem insert kolejnego poziomu znaczników, posiadającego wartość Nazwa1, a wskazanego w klauzuli into. Atrybut przekazywany do metody modify jest łańcuchem. Należy zwrócić szczególną uwagę na to, że stosowane są metody opracowane po stronie języka obiektowego, z zastosowaniem mechanizmu CLR, co można zweryfikować, analizując komunikaty o błędach. Powoduje to, że w nazwach metod, poleceń i klauzul jest uwzględniana wielkość liter, co wymusiło odejście od stosowanej do tej pory konsekwentnie w SQL formy zapisu. Szczegółowe omówienie tego sposobu tworzenia obiektów MS SQL Server znajdzie Czytelnik w rozdziale 7.4. Poniżej skryptu pokazana została zawartość kolumny po modyfikacji. UPDATE TXML SET KolumnaXml.modify( 'insert Nazwa1 into (/Rodzic/Dziecko)[1]')WHERE ID=1; SELECT * FROM TXML; Nazwa1
Podobnie możemy dodać znacznik, używając opcji as first into, co spowoduje, że zostanie on wstawiony jako pierwszy element wskazanego poziomu. Tym razem w znaczniku został zdefiniowany atrybut ID o wartości 11. UPDATE TXML SET KolumnaXml.modify( 'insert Nazwa0 as first into (/Rodzic/Dziecko)[1]')WHERE ID=1; SELECT * FROM TXML Nazwa0 Nazwa1
134
MS SQL Server. Zaawansowane metody programowania
Według takich samych zasad możemy wstawić ostatni znacznik wskazanego poziomu, używając dyrektywy as last into. UPDATE TXML SET KolumnaXml.modify( 'insert Nazwa2 as last into (/Rodzic/Dziecko)[1]')WHERE ID=1; SELECT * FROM TXML Nazwa0Nazwa1 Nazwa2
Możliwe jest zastosowanie do modyfikacji danych XML pomocniczej zmiennej tego typu, która zasilana jest zawartością dodawanego znacznika. Przy modyfikacji używana jest dyrektywa sql:variable("@a"), wskazująca na tę zmienną. W przykładzie pole uzupełniono o kolejny znacznik poziomu Dziecko, który dodany zostanie na końcu. DECLARE @a as xml SET @a= 'Kolejne'; UPDATE TXML SET KolumnaXml.modify( 'insert sql:variable("@a") as last into (/Rodzic)[1]')WHERE ID=1; SELECT * FROM TXML; Nazwa0 Nazwa1Nazwa2Kolejne
Do tej pory do określenia miejsca wstawienia stosowano opcję insert. Możliwe jest jego wskazanie przy wykorzystaniu opcji after. Użyty po definicji poziomu wskaźnik [1] określa, za którym elementem dokonane zostanie wstawienie. UPDATE TXML SET KolumnaXml.modify( 'insert Trzecie after (/Rodzic/Dziecko)[1]')WHERE ID=1; SELECT * FROM TXML; Nazwa0 Nazwa1Nazwa2 TrzecieKolejne
Przez analogię możemy zastosować opcję before, dla której wskazano drugi element poziomu. UPDATE TXML SET KolumnaXml.modify( 'insert Drugie before (/Rodzic/Dziecko)[2]')WHERE ID=1; SELECT * FROM TXML; Nazwa0Nazwa1 Nazwa2Drugie TrzecieKolejne
Takie same zasady jak w przypadku wstawiania znaczników obowiązują przy dodawaniu innych elementów dozwolonych przez standard XML. W kolejnym przykładzie pole zostało uzupełnione o komentarz, wstawiany na poziomie Dziecko, po jego drugim elemencie. UPDATE TXML SET KolumnaXml.modify( 'insert after (/Rodzic/Dziecko)[2]')WHERE ID=1; SELECT * FROM TXML;
Rozdział 3. Język zapytań SQL w MS SQL Server
135
Nazwa0Nazwa1 Nazwa2Drugie TrzecieKolejne
Kolejny komentarz dodano, stosując we wskazaniu miejsca odwołania się do wartości atrybutu ID=11 zdefiniowanego na poziomie Wnuczek. UPDATE TXML SET KolumnaXml.modify( 'insert before (/Rodzic/Dziecko/Wnuczek[@ID=11])[1]')WHERE ID=1; SELECT * FROM TXML; Nazwa0Nazwa1 Nazwa2Drugie TrzecieKolejne
Oprócz komentarzy możliwe jest dodawanie do XML programów. W przykładzie użyto wywołania programu uruchamiającego konsolę. UPDATE TXML SET KolumnaXml.modify( 'insert before(/Rodzic)[1]')WHERE ID=1; SELECT * FROM TXML; Nazwa0Nazwa1Nazwa2Drugie TrzecieKolejne
Również zamieszczenie skryptu Java zawierającego funkcję odbywa się na omawianych wcześniej zasadach. Pokazana w przykładzie funkcja ma dla dwóch argumentów zwracać wartość 1, kiedy pierwszy z nich jest większy. W przeciwnym razie ma zwrócić 0. Analizując wynik, należy zauważyć, że jest to pierwszy element, w którym pamiętane są formatowanie i znaki zmiany linii. UPDATE TXML SET KolumnaXml.modify( 'insert before (/Rodzic/Dziecko)[1]')WHERE ID=1; SELECT * FROM TXML; Nazwa0Nazwa1Nazwa2Drugie TrzecieKolejne
Innym uzupełnieniem danych XML może być ustalenie sposobu formatowania i stosowanych stylów. W przykładzie pokazano wstawienie informacji z osadzonym obrazem tła. Podobnie jak w przypadku skryptu, ten element zachowuje formatowanie i łamanie linii.
136
MS SQL Server. Zaawansowane metody programowania UPDATE TXML SET KolumnaXml.modify( 'insert before (/Rodzic/Dziecko)[1]')WHERE ID=1; SELECT * FROM TXML;
Nazwa0Nazwa1Nazwa2Drugie TrzecieKolejne
Oprócz operacji wstawiania metoda modify daje możliwość modyfikowania wpisów. Realizujemy to, stosując polecenie replace value of, którego atrybut określa miejsce i rodzaj modyfikowanej cechy. W przykładzie wskazano drugi element poziomu Wnuczek, dla którego modyfikowana ma być zawartość znacznika text(). Nowa wartość jest określana po słowie kluczowym with. UPDATE TXML SET KolumnaXml.modify( 'replace value of (/Rodzic/Dziecko/Wnuczek[2]/text())[1] with "Drugi"')WHERE ID=1; SELECT * FROM TXML;
Nazwa0DrugiNazwa2Drugie TrzecieKolejne
Według analogicznych zasad możemy modyfikować wartości atrybutów zdefiniowanych dla znacznika. Przykład pokazuje zamianę wartości ID pierwszego elementu poziomu Wnuczek. UPDATE TXML SET KolumnaXml.modify( 'replace value of (/Rodzic/Dziecko/Wnuczek/@ID)[1] with "10"')WHERE ID=1; SELECT * FROM TXML; Nazwa0DrugiNazwa2Drugie TrzecieKolejne
Rozdział 3. Język zapytań SQL w MS SQL Server
137
Do zmiany danych możemy zastosować instrukcję warunkową if, która jest definiowana jako element polecenia modyfikującego zawartość lub wstawiającego wiersze. W przykładzie do zbudowania wyrażenia testującego zastosowano funkcję count zliczającą elementy wskazanego poziomu. Gdy jest ona większa niż 3, wartość identyfikatora zostanie ustalona na 30. W opcjonalnej sekcji else wskazano, że w przeciwnym wypadku identyfikator ten ma być równy 20. UPDATE TXML SET KolumnaXml.modify( 'replace value of (/Rodzic/Dziecko/Wnuczek[3]/@ID)[1] with if (count(/Rodzic/Dziecko/Wnuczek) > 3) then "30" else "20"')WHERE ID=1; SELECT * FROM TXML; Nazwa0DrugiNazwa2Drugie TrzecieKolejne
Zarówno w przypadku wstawiania, jak i aktualizacji wpisów w kolumnie XML możliwe jest odwołanie się do pomocniczej zmiennej tego typu. Operacja odbywa się wtedy w trzech krokach. W pierwszym zmienna jest zasilana wybranym polem tabeli. Drugi krok to wykorzystanie metody modyfikującej tej zmiennej, zgodnie z zasadami, jakie pokazano przy bezpośrednich operacjach na polu. Ostatni krok to wykonanie zapytania modyfikującego, w którym stara wartość pola jest zastępowana nową. W przykładzie pokazano wstawienie nowego znacznika na poziomie Wnuczek dla drugiego elementu poziomu Dziecko. DECLARE @a as xml SELECT @a=KolumnaXml FROM TXML WHERE ID=1; SET @a.modify( 'insert Nowy into (/Rodzic/Dziecko)[2]') UPDATE TXML SET KolumnaXml=@a WHERE ID=1; SELECT * FROM TXML; Nazwa0DrugiNazwa2DrugieNowy TrzecieKolejne
Uzasadnieniem zastosowania takiego sposobu modyfikacji danych XML jest to, że możliwe jest wielokrotne modyfikowanie pomocniczej zmiennej, natomiast aktualizacja pola wykona się tylko raz. Ponadto istnieje możliwość sprawdzenia poprawności
138
MS SQL Server. Zaawansowane metody programowania
dokonanych modyfikacji i przeprowadzenia ich walidacji przed utrwaleniem danych w tabeli. Poleceniem delete możemy usuwać elementy z danych typu XML. W przypadku elementów pomocniczych możemy stosować wbudowane metody — dla programów /processing-instruction(), a dla komentarzy /comment(). Pierwszy znak / (ukośnik) jest stałym elementem wskazującym na hierarchię XML. UPDATE TXML SET KolumnaXml.modify( 'delete //processing-instruction()')WHERE ID=1; UPDATE TXML SET KolumnaXml.modify( 'delete //comment()')WHERE ID=1; SELECT * FROM TXML;
Nazwa0DrugiNazwa2DrugieNowy TrzecieKolejne
Ponieważ elementy formatujące i skrypty rozpoczynają się znacznikiem, a różnica polega tylko na tym, że mają ustalone nazwy, ich usuwanie polega na odwołaniu się do niego na właściwym poziomie hierarchii XML. UPDATE TXML SET KolumnaXml.modify( 'delete /Rodzic/script')WHERE ID=1; UPDATE TXML SET KolumnaXml.modify( 'delete /Rodzic/style')WHERE ID=1; SELECT * FROM TXML; Nazwa0DrugiNazwa2DrugieNowy TrzecieKolejne
Usuwanie jednego spośród powtarzających się znaczników poziomu może być zrealizowane przez podanie hierarchii oraz indeksu pozycji do usunięcia. W przykładzie usuwany jest drugi element poziomu Wnuczek. UPDATE TXML SET KolumnaXml.modify( 'delete /Rodzic/Dziecko/Wnuczek[2]')WHERE ID=1; SELECT * FROM TXML; Nazwa0Nazwa2DrugieNowy TrzecieKolejne
Usuwanie atrybutów jest wykonywane dla wszystkich znaczników poziomu, które je posiadają. Wskazania dokonujemy przez podanie poziomu oraz nazwy atrybutu poprzedzonego znakiem @. UPDATE TXML SET KolumnaXml.modify( 'delete(/Rodzic/Dziecko/Wnuczek/@ID)')WHERE ID=1; SELECT * FROM TXML Nazwa0Nazwa2 DrugieNowyTrzecie Kolejne
Rozdział 3. Język zapytań SQL w MS SQL Server
139
Wskazanie indeksu do poziomu podrzędnego może zostać wykonane przez dodanie do hierarchii wskazującej poziom nadrzędny indeksu poprzedzonego znakiem gwiazdki. UPDATE TXML SET KolumnaXml.modify( 'delete /Rodzic/Dziecko/*[2]')WHERE ID=1; SELECT * FROM TXML; Nazwa0DrugieNowy TrzecieKolejne
Wskazanie zawartości usuwanej ze znacznika jest dokonywane przez wskazanie poziomu, zastosowanie metody /text() oraz określenie jego numeru. UPDATE TXML SET KolumnaXml.modify( 'delete /Rodzic/Dziecko/Wnuczek/text()[1]')WHERE ID=1; SELECT * FROM TXML; Drugie TrzecieKolejne
Możliwe jest również zastosowanie metod wybierających z kolumn XML określony zakres znaczników [17] [18]. Aby zrealizować testy, zasilmy tabelę dwoma wierszami zawierającymi przykładowe dane o postaci: INSERT INTO TXML VALUES ('Nazwa0Nazwa1Nazwa2DrugieNazwa0Nazwa11Nazwa22Drugie');
Najprostszym zadaniem jest wybranie z kolumny typu XML danych zapisanych na wskazanym poziomie. Do tego celu zastosujemy metodę query, której parametrem jest hierarchia określająca poziom. W zestawie wynikowym przedstawiony został tylko jeden rekord, drugi będzie pusty. SELECT KolumnaXml.query('/Rodzic/Dziecko/Wnuczek') FROM TXML; Nazwa0Nazwa1Nazwa2
Aby wyeliminować pusty wiersz, zastosujemy klauzulę WHERE, w której odwołamy się do metody exist. Zwraca ona wartość 1, jeśli wskazany parametrem element istnieje w polu, na rzecz którego działa ta metoda. W przykładzie sprawdzono, czy istnieje wskazany poziom hierarchii, co dało pożądany rezultat. SELECT KolumnaXml.query('/Rodzic/Dziecko/Wnuczek') FROM TXML WHERE KolumnaXml.exist('/Rodzic/Dziecko/Wnuczek')=1; Nazwa0Nazwa1Nazwa2
Możliwe jest również wskazanie do wyświetlenia wybranego atrybutu na wskazanym poziomie dzięki określeniu go indeksem umieszczonym w nawiasie kwadratowym. SELECT KolumnaXml.query('/Rodzic/Dziecko/Wnuczek[1]') FROM TXML WHERE KolumnaXml.exist('/Rodzic/Dziecko/Wnuczek')=1; Nazwa0
140
MS SQL Server. Zaawansowane metody programowania
Również w metodzie exist możliwe jest odwołanie się do określonego elementu. W przykładzie sprawdzono, czy istnieje dziecko o indeksie 3, ale wyświetlono informacje dotyczące dziecka o indeksie 2. SELECT KolumnaXml.query('/Rodzic/Dziecko[2]') FROM TXML WHERE KolumnaXml.exist('/Rodzic/Dziecko[3]')=1; Drugie
Stosując funkcję /text(), możemy wyświetlić zawartość znaczników wybranego poziomu. W przykładzie wyświetlono dane zawarte we wszystkich elementach na poziomie Wnuczek dla tych rekordów, gdzie istnieje dziecko o indeksie 3. SELECT KolumnaXml.query('/Rodzic/Dziecko/Wnuczek/text()') FROM TXML WHERE KolumnaXml.exist('/Rodzic/Dziecko[3]')=1; Nazwa0Nazwa1Nazwa2
Poza wartościami znaczników możemy wyświetlać znaczniki wraz ze zdefiniowanymi w nich atrybutami. W tym celu po wskazaniu poziomu stosujemy ujętą w nawias kwadratowy, a poprzedzoną znakiem @ nazwę. Zauważmy, że w wynikowym zestawie rekordów wyświetlą się tylko te znaczniki, w których atrybut został zastosowany, a pozostałe, chociaż występują na tym poziomie, będą pominięte. SELECT KolumnaXml.query('/Rodzic/Dziecko/Wnuczek[@ID]') FROM TXML WHERE KolumnaXml.exist('/Rodzic/Dziecko[3]')=1 Nazwa0Nazwa2
Jeśli parametr metody query stanowi porównanie wskazania poziomu z wartością, to rezultatem będzie wartość logiczna. Wartość true uzyskamy wtedy, kiedy chociaż jeden znacznik poziomu będzie równy tej wartości. W przeciwnym wypadku otrzymamy false. SELECT KolumnaXml.query('/Rodzic/Dziecko/Wnuczek="Nazwa0"') FROM TXML WHERE KolumnaXml.exist('/Rodzic/Dziecko')=1; true false
Metoda value pozwala na wyświetlenie wartości atrybutu wskazanego pierwszym parametrem. Drugi parametr wskazuje, jakiego typu dane ma zwracać ta metoda. SELECT KolumnaXml.value('(/Rodzic/Dziecko/Wnuczek/@ID)[1]', 'int') FROM TXML WHERE KolumnaXml.exist('/Rodzic/Dziecko')=1; 11 NULL
Metoda nodes, która generuje zestaw rekordów, począwszy od poziomu danego parametrem, może zostać wykorzystana do wygenerowania za pomocą operatora CROSS APPLY zestawu rekordów zawierającego pełny poziom wyjściowy oraz wszystkie poziomy pochodne. Na liście pól umieszczamy wtedy wywołanie metody query dla danych uzyskanych z metody nodes ze wskazaniem początku za pomocą parametru . (kropka) bieżącego poziomu. SELECT T2.Loc.query('.') FROM TXML CROSS APPLY KolumnaXml.nodes('/Rodzic/Dziecko') as T2(Loc); Nazwa0Nazwa1Nazwa2
Rozdział 3. Język zapytań SQL w MS SQL Server
141
Drugie Trzecie Kolejne Nazwa0Nazwa11Nazwa22 Drugie
Jeśli jako parametr dla query wskażemy dwie kropki, początkiem będzie rodzic wskazanego poziomu, czyli w pokazanym przykładzie wierzchołek hierarchii. SELECT T2.Loc.query('..') FROM TXML CROSS APPLY KolumnaXml.nodes('/Rodzic/Dziecko') as T2(Loc); Nazwa0Nazwa1Nazwa2Drugie TrzecieKolejne Nazwa0Nazwa1Nazwa2DrugieNazwa0Nazwa1Nazwa2DrugieNazwa0Nazwa1Nazwa2DrugieNazwa0Nazwa11Nazwa22Drugie Nazwa0Nazwa11Nazwa22Drugie
Pomimo dużych możliwości manipulowania danymi zapisanymi w postaci kolumn XML w tabelach schematu relacyjnego, uważam, że format ten powinien być raczej wykorzystywany do przekazywania danych między różnymi środowiskami niż do bezpośredniego ich składowania [4].
3.4. Modyfikowanie tabel Do tej pory zmiany w strukturze wcześniej utworzonych tabel uzyskiwaliśmy przez ich usunięcie i ponowne wygenerowanie w nowej postaci. Prowadzi to do utraty danych zawartych w tak modyfikowanym obiekcie. Oczywiście możliwe jest działanie polegające na zmianie nazwy starej tabeli, utworzenie nowej struktury pod starą nazwą, a następnie przeniesienie danych. Jednak taki proces nie jest ani wygodny, ani wydajny. Spróbujmy dokonać modyfikacji struktury bez ingerencji w dane — użyjemy w tym celu polecenia ALTER TABLE. Rozważmy tabelę Dzialy, do której dodamy kolumnę dyrektywą ADD. Kolumna będzie przechowywała dane całkowite i będzie miała nazwę kod. ALTER TABLE Dzialy ADD kod integer; SELECT * FROM Dzialy;
142
MS SQL Server. Zaawansowane metody programowania
Utworzona kolumna będzie pusta. Wprowadźmy za pomocą polecenia UPDATE lub narzędzi wizualnych kilka wartości do pola kod, aby uzyskać rezultat pokazany w tabeli 3.59. Tabela 3.59. Tabela Dzialy z przykładowym zestawem danych w kolumnie kod IdDzialu
Nazwa
kod
1
Dyrekcja
1
2
Administracja
23
3
Techniczny
345
4
Handlowy
4567
5
Pomocniczy
NULL
Po tej operacji spróbujmy zmienić typ kolumny kod na znakową o maksymalnej długości 2, a następnie wprowadźmy nowy wiersz z daną znakową wprowadzaną do tego pola. Jednocześnie pamiętamy, że pośród danych początkowych dwie wymagały do zapisu co najmniej trzech znaków. ALTER TABLE Dzialy ALTER COLUMN kod varchar(2); INSERT INTO Dzialy (kod) VALUES('aa'); SELECT * FROM Dzialy;
Wykonanie skryptu nie spowodowało wygenerowania komunikatu o błędzie lub ostrzeżenia. Zbyt długie napisy reprezentujące liczby zostały zastąpione gwiazdką — tabela 3.60. Pozostałe liczby zostały skonwertowane do docelowego typu znakowego. Tabela 3.60. Tabela Dzialy z zestawem danych po wykonaniu konwersji kolumny kod IdDzialu
Nazwa
kod
1
Dyrekcja
1
2
Administracja
23
3
Techniczny
*
4
Handlowy
*
5
Pomocniczy
NULL
6
aa
Zachowanie serwera świadczy o braku walidacji danych zawartych w kolumnie przy konwersji do typów znakowych. Pociąga to za sobą konieczność wykonania przez programistę sprawdzenia zgodności danych źródłowych z typem wynikowym przed wykonaniem tego rodzaju operacji. Spróbujmy teraz dokonać konwersji zwrotnej do typu całkowitego oraz wstawić przykładowy wiersz. W tym przypadku następuje walidacja danych i dopóki nie zmienimy ostatniego złego wpisu w kolumnie, nie jest możliwa zmiana jej typu, o czym świadczy komunikat pokazany po skrypcie SQL. ALTER TABLE Dzialy ALTER COLUMN kod int; INSERT INTO Dzialy (kod) VALUES(4321);
Rozdział 3. Język zapytań SQL w MS SQL Server
143
SELECT * FROM Dzialy; Msg 245, Level 16, State 1, Line 1 Conversion failed when converting the varchar value '*' to data type int.
Prześledźmy inny przykład konwersji danych, tworząc przykładową tabelę Test, która poza automatycznie inkrementowanym kluczem podstawowym zawiera pole rzeczywiste. Do tak przygotowanego obiektu wpiszmy przykładowy zestaw danych, co pokazuje tabela 3.61. DROP TABLE Test; GO CREATE TABLE Test (id int IDENTITY(1,1) PRIMARY KEY, nr real); GO INSERT INTO Test VALUES(1),(1.5),(2),(2.3);
Tabela 3.61. Tabela Test z przykładowym zestawem danych id
nr
1
1
2
1.5
3
2
4
2.3
W tabeli skonwertujmy pole nr do typu całkowitego. Podobnie jak w przypadku zmiany typu na znakowy, nie następuje walidacja danych, a wartości podlegają obcięciu do części całkowitych, co potwierdza wynik przedstawiony w tabeli 3.62. Tak jak poprzednio, nie został wygenerowany komunikat o błędzie ani nie pojawiła się żadna forma ostrzeżenia. ALTER TABLE Test ALTER COLUMN nr int; GO SELECT * FROM Test;
Tabela 3.62. Tabela Test z zestawem danych po wykonaniu konwersji kolumny nr id
nr
1
1
2
1
3
2
4
2
Powróćmy do tabeli Dzialy, w której dla kolumny kod ustanowimy blokadę wpisywania wartości NULL. W tym przypadku wykonywane jest sprawdzenie zgodności danych z ograniczeniem, a ponieważ jedna z komórek jest pusta, generowany jest komunikat pokazany poniżej skryptu. ALTER TABLE Dzialy ALTER COLUMN kod int NOT NULL; Msg 515, Level 16, State 2, Line 1
144
MS SQL Server. Zaawansowane metody programowania Cannot insert the value NULL into column 'kod', table 'test.dbo.Dzialy'; column does not allow nulls. UPDATE fails. The statement has been terminated.
Dopiero po zmianie ostatniego złego wpisu możliwa jest zmiana ograniczenia na NOT NULL dla kolumny kod. Na podobnych zasadach, za pomocą opcji ADD CONSTRAINT polecenia ALTER TABLE, możemy dodawać do tabeli ograniczenia. W tabeli Dzialy dodajmy ograniczenie unikalności dla kolumny kod. ALTER TABLE Dzialy ADD CONSTRAINT un UNIQUE (kod);
Ponieważ w przypadku ograniczeń jest sprawdzana zgodność danych z warunkami z niego wynikającymi, pojawi się komunikat o błędzie, a ograniczenie nie zostanie ustanowione. Msg 1505, Level 16, State 1, Line 1 The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.Dzialy' and the index name 'un'. The duplicate key value is (). Msg 1750, Level 16, State 0, Line 1 Could not create constraint. See previous errors. The statement has been terminated.
W przypadku ograniczenia UNIQUE obowiązuje nas niepoprawna z punktu widzenia algebry interpretacja porównania wartości NULL — są one sobie równe (NULL=NULL=>TRUE?!), dlatego komunikat dotyczy tych wartości. Można powiedzieć, że są one traktowane jak napisy, pomimo że reprezentować mogą wszystkie typy danych, również typy numeryczne. Takie działanie budzi u mnie duże opory. Niestety, jako użytkownik serwera muszę to przyjąć z dobrodziejstwem inwentarza. Na takich samych zasadach możemy dodawać do tabeli ograniczenia dowolnego typu spośród tych, które mogą być definiowane po słowie kluczowym CONSTRAINT. Spróbujmy teraz, stosując opcję DROP COLUMN, usunąć kolumnę, na której zdefiniowano ograniczenie. Próba ta zakończy się niepowodzeniem dla przypadku ograniczeń integralnościowych — UNIQUE i PRIMARY KEY. Jest to potwierdzone przez komunikat zamieszczony po skrypcie. ALTER TABLE Dzialy DROP COLUMN kod;
Msg 5074, Level 16, State 1, Line 1 The object 'un' is dependent on column 'kod'. Msg 4922, Level 16, State 9, Line 1 ALTER TABLE DROP COLUMN kod failed because one or more objects access this column.
Dopiero usunięcie ograniczenia integralnościowego za pomocą opcji DROP CONSTRAINT pozwala na usunięcie kolumny. W poleceniu tym odwołujemy się do nazwy ograniczenia, co jest proste w przypadku nazw nadanych przez użytkownika. Jeśli nazwa została nadana przez system, wymaga to przeszukania odpowiednich perspektyw słownikowych, co zostanie omówione później. ALTER TABLE Dzialy DROP CONSTRAINT un; ALTER TABLE Dzialy DROP COLUMN kod;
Rozdział 3. Język zapytań SQL w MS SQL Server
145
Jak już zaznaczono, blokadę usuwania kolumny wprowadza również ustanowienie klucza głównego. Można to sprawdzić, wykonując przedstawiony skrypt, w którym tworzona jest tabela nnn z polem nr będącym kluczem głównym. Do tabeli wprowadzane są dane przykładowe, a następnie zostaje podjęta próba usunięcia kolumny z ograniczeniem. W zapytaniu tworzącym tabelę zastosowano jawnie słowo CONSTRAINT, aby możliwe było proste odwołanie się do nazwy ograniczenia. DROP TABLE nnn; CREATE table nnn (nr integer, Nazwisko varchar(15) NULL, CONSTRAINT kl PRIMARY KEY(nr)); GO INSERT INTO nnn(nr,nazwisko) VALUES(1,'KOWAL'); INSERT INTO nnn(nr) values(2); GO ALTER TABLE nnn DROP COLUMN nr; SELECT * FROM nnn;
Na nieco innej zasadzie blokadę usuwania kolumny wprowadza klucz obcy. Utwórzmy ponownie tabelę nnn, która tym razem poza kluczem głównym zawiera definicję klucza obcego na polu IdOsoby, odwołującą się do pola klucza głównego tabeli Osoby. Skrypt zawiera zasilanie nowej tabeli danymi. Może on zostać pominięty bez zmiany sposobu działania. Próba usunięcia kolumny IdOsoby powoduje wygenerowanie komunikatu, który stwierdza, że do kolumny odwołuje się wiele obiektów. Komunikat jest niezależny od tego, czy w tabeli zawarte są dane, czy też ich nie ma. DROP TABLE nnn; CREATE table nnn (ID integer, IdOsoby integer, CONSTRAINT fk FOREIGN KEY(IdOsoby) REFERENCES Osoby(IdOsoby), Nazwisko varchar(15) NULL, CONSTRAINT kl PRIMARY KEY(ID)); GO INSERT INTO nnn(ID,nazwisko) VALUES(1,'KOWAL'); INSERT INTO nnn(ID) values(2); GO ALTER TABLE nnn DROP COLUMN IdOsoby; GO SELECT * FROM nnn; Msg 5074, Level 16, State 1, Line 1 The object 'fk' is dependent on column 'IdOsoby'. Msg 4922, Level 16, State 9, Line 1 ALTER TABLE DROP COLUMN IdOsoby failed because one or more objects access this column.
Spróbujmy usunąć tabelę o nazwie Nowa. DROP TABLE Nowa;
Jeśli tabela nie istnieje w schemacie, pojawia się komunikat o błędzie. Aby temu zapobiec, możemy usuwanie tabeli wykonywać warunkowo, odwołując się do jednej z perspektyw słownikowych INFORMATION_SCHEMA.TABLES. W instrukcji warunkowej IF
146
MS SQL Server. Zaawansowane metody programowania
zastosowano operator EXISTS, który zwraca wartość true, jeśli zapytanie będące jego argumentem zwraca przynajmniej jeden wiersz. Jeśli tabela istnieje, zapytanie zawiera jeden wiersz, operator EXISTS daje wartość true i polecenie usuwania jest wykonywane. W przeciwnym wypadku nie jest podejmowana żadna akcja. IF EXISTS (SELECT Table_Name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Nowa') DROP TABLE Nowa
Niestety, w starszych wersjach SQL Server nie jest możliwe ustanowienie wartości domyślnej na skutek modyfikacji tabeli. Taka możliwość pojawiła się dopiero w wersji 2008 R2. Utwórzmy tabelę test o dwóch polach, z których pierwsze jest numeryczne, a drugie znakowe. Następnie za pomocą opcji ADD CONSTRAINT dodajmy ograniczenie wartości domyślnej DEFAULT. Składnia w tym przypadku różni się od dotychczas spotykanych tym, że po wartości stosujemy słowo kluczowe FOR, po którym wskazujemy pole, którego ograniczenie dotyczy. Dodatkowo w skrypcie dodano ograniczenie zabraniające polu ID przyjmować wartość NULL. DROP TABLE test; GO CREATE TABLE test (Id int, Opis varchar(11)); GO ALTER TABLE test ADD CONSTRAINT Def DEFAULT 'aaa' FOR Opis; ALTER TABLE Test ALTER COLUMN ID int NOT NULL ; GO INSERT INTO TEST VALUES(1, 'xxx'); INSERT INTO TEST VALUES(2, NULL); INSERT INTO test(Id) VALUES(3); SELECT * FROM Test;
Analizując dane zawarte w tabeli 3.63, możemy powiedzieć, że dodane ograniczenie działa poprawnie w zmodyfikowanej tabeli. Widać również wyraźnie, że działa tylko w przypadku pominięcia, podczas wstawiania wartości dla kolumny, na której jest ustanowione. Jawne wstawienie wartości NULL nadpisuje wartość domyślną. Tabela 3.63. Tabela Test z zestawem danych po dodaniu ograniczenia DEFAULT Id
Opis
1
xxx
2
NULL
3
aaa
Przy okazji tworzenia tabel i modyfikacji danych omówmy funkcjonalność wprowadzoną w wersji 2012, jaką jest sekwencja. Jest to obiekt generujący kolejne wartości liczb całkowitych zgodnie z regułami określonymi parametrami. Podstawowe polecenie, które tworzy ten obiekt z parametrami o wartościach domyślnych, ma postać: CREATE SEQUENCE licznik
Rozdział 3. Język zapytań SQL w MS SQL Server
147
Podstawowe parametry sekwencji to wartość startowa oraz przyrost. Obie wartości muszą być liczbami całkowitymi, dopuszczalne są wartości ujemne, przyrost nie może być zerem. Jeśli chcemy utworzyć sekwencję o takiej samej nazwie, musimy najpierw usunąć starą definicję. DROP SEQUENCE licznik; GO CREATE SEQUENCE licznik START WITH 1 INCREMENT BY 1;
Praktyczne zastosowanie sekwencji można przedstawić na przykładzie pomocniczej zmiennej tabelarycznej, do której wstawiamy kolejne generowane wartości pierwszego pola przez zastosowanie NEXT VALUE FOR licznik. DECLARE @Pracownicy TABLE (ID int NOT NULL PRIMARY KEY, Nazwisko varchar(20) NOT NULL); INSERT @Pracownicy VALUES (NEXT VALUE FOR licznik, 'Kowalski'), (NEXT VALUE FOR licznik, 'Nowak'), (NEXT VALUE FOR licznik, 'Janik'); SELECT * FROM @Pracownicy;
Zamiast usuwać definicje istniejącego obiektu, można go zmodyfikować. W przykładzie za pomocą parametru RESTART przestawiono wartość początkową sekwencji, a następnie dodano dwa kolejne rekordy. ALTER SEQUENCE Licznik RESTART WITH 11; INSERT @Pracownicy VALUES (NEXT VALUE FOR licznik, 'Kowal'), (NEXT VALUE FOR licznik, 'Nowy'); SELECT * FROM @Pracownicy;
Podczas tworzenia sekwencji możliwe jest określenie następujących parametrów: START WITH stała — definiuje pierwszą generowaną wartość; INCREMENT BY stała — definiuje przyrost, z jakim generowane są kolejne wartości; MINVALUE stała albo NO MINVALUE — definiuje minimalną wartość, do której
wraca sekwencja po uzyskaniu wartości maksymalnej w przypadku generowania cyklicznego, lub wartość, na której zakończy się generowanie, jeśli przyrost był ujemny; MAXVALUE stała albo NO MAXVALUE — definiuje maksymalną wartość, na której
zakończy się generowanie, lub wartość, do której wraca sekwencja po uzyskaniu wartości minimalnej w przypadku generowania cyklicznego, jeśli przyrost był ujemny; CYCLE albo NO CYCLE — definiuje, czy generowanie będzie cykliczne; w stanie
domyślnym jest niecykliczne; CACHE stała albo NO CACHE — definiuje liczbę jednocześnie generowanych wartości
sekwencji, które będą przechowywane w pamięci podręcznej i wykorzystywane pojedynczo przy każdym kolejnym odwołaniu do obiektu; wartość stałej musi
148
MS SQL Server. Zaawansowane metody programowania
być większa niż różnica parametrów MAXVALUE-START WITH, gdy przyrost jest dodatni, lub START WITH-MINVALUE, gdy przyrost jest ujemny; NO CACHE wskazuje, że wartości będą generowane pojedynczo. Gdy sekwencja jest modyfikowana poza opcją START WITH, można określić ponownie wszystkie pozostałe parametry. Dodatkowo możemy określić parametr RESTART, który powoduje rozpoczęcie generowania wartości od początku, albo RESTART WITH stała, co sprawi, że generowanie rozpocznie się od wskazanej wartości.
3.5. Perspektywy (widoki) Tabele są elementami zawierającymi utrwalone na nośniku dane. W serwerach baz danych istnieje możliwość utworzenia dynamicznej ich reprezentacji za pomocą perspektyw, niekiedy nazywanych widokami. Perspektywy tworzymy z wykorzystaniem polecenia CREATE VIEW, po którym następuje nazwa obiektu, a po słowie kluczowym AS dowolne poprawne składniowo zapytanie wybierające. Bardzo często, tak jak w przykładzie, zapytanie to wybiera kolumny z jednej tabeli. Za pomocą perspektywy możliwe jest wstawianie wierszy, aktualizowanie danych oraz usuwanie wierszy. Odbywa się to na zasadach takich samych jak wtedy, kiedy dotyczy to tabel. CREATE SELECT GO INSERT INSERT UPDATE DELETE GO SELECT SELECT
VIEW Dane AS Nazwisko, Imie FROM Osoby; INTO INTO Dane FROM
Dane VALUES ('Kowalski','Wiesław'); Dane VALUES ('Nowak','Jan'); SET Imie='Piotr' WHERE Imie= 'Wiesław'; Dane WHERE Nazwisko= 'Nowak' AND Imie='Jan';
* FROM Dane; * FROM Osoby;
Należy zaznaczyć, że wszystkie modyfikacje dokonywane za pośrednictwem perspektywy mają swoje odbicie w danych zapisanych w tabeli źródłowej. Można to zaobserwować, porównując tabele 3.64 i 3.65. Trudno mówić o modyfikowaniu danych zawartych w perspektywie, ponieważ nie przechowuje ona danych, lecz jedynie stanowi ich dynamiczną kopię. Tabela 3.64. Przykładowe rekordy perspektywy Dane Nazwisko
Imie
Kowalski
Jan
…
…
Kowalski
Piotr
Tabela 3.65. Przykładowe rekordy tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
1
1
Kowalski
Jan
1976
1.67
…
…
…
…
…
…
15
NULL
Kowalski
Piotr
NULL
NULL
Rozdział 3. Język zapytań SQL w MS SQL Server
149
Ponieważ w definicji perspektywy może znaleźć się dowolne zapytanie wybierające niezawierające sortowania, to dopuszczalne jest filtrowanie rekordów za pomocą klauzuli WHERE. W przykładzie utworzono widok oparty na pojedynczej tabeli Osoby, z której wyświetlono dane Nazwisko, Imie i Wzrost osób, których wzrost przekraczał 1.7. Następnie za pośrednictwem tej perspektywy wpisano dane, w których drugi rekord nie spełnia kryterium filtrowania. Ponadto zmieniono wzrost w pierwszym ze wstawianych rekordów, tak aby był niższy od wartości progowej. Całość uzupełnia polecenie usunięcia jednego z rekordów oraz odpytanie o zawartość perspektywy i tabeli. CREATE VIEW Dane_check AS SELECT Nazwisko, Imie, Wzrost FROM Osoby WHERE Wzrost > 1.70; GO INSERT INTO Dane_check VALUES ('Kowalski','Karol',1.8); INSERT INTO Dane_check VALUES ('Nowak','Jan', 1.6); INSERT INTO Dane_check VALUES ('Kowal','Piotr',1.8); UPDATE Dane_check SET Wzrost=1.6 WHERE Nazwisko = 'Kowalski' AND Imie= 'Karol'; DELETE FROM Dane_check WHERE Nazwisko= 'Nowak' AND Imie='Jan'; GO SELECT * FROM Dane_check; SELECT * FROM Osoby;
Analizując dane widziane z poziomu perspektywy (tabela 3.66), widzimy, że pojawił się tylko jeden nowy rekord, co jest gwarantowane przez filtr. Natomiast zawartość tabeli Osoby (tabela 3.67) pokazuje, że wszystkie rekordy zostały dodane oraz poprawnie wykonała się modyfikacja danych, pomimo że docelowe wartości wzrostu nie spełniają kryterium. Oznacza to, iż pomimo że istnieje klauzula WHERE, w stanie domyślnym wartości nie są walidowane zarówno podczas wstawiania, jak i modyfikowania. Można zatem wstawiać wartości niezgodne z wyrażeniem filtrującym. Polecenie DELETE nie spowodowało usunięcia rekordu, ponieważ nie był on widziany przez perspektywę. Tabela 3.66. Przykładowe rekordy perspektywy Dane_check Nazwisko
Imie
Wzrost
Nowak
Karol
1.72
…
…
…
Nowak
Edward
1.93
Kowal
Piotr
1.80
Tabela 3.67. Przykładowe rekordy tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
1
1
Kowalski
Jan
1976
1.67
…
…
…
…
…
…
15
NULL
Kowalski
Piotr
NULL
NULL
17
NULL
Kowalski
Karol
NULL
1.60
18
NULL
Nowak
Jan
NULL
1.60
19
NULL
Kowal
Piotr
NULL
1.80
150
MS SQL Server. Zaawansowane metody programowania
Jeśli chcemy wymusić walidację danych podczas zasilania perspektywy, musimy ją zmodyfikować, dodając dyrektywę WITH CHECK OPTION. Należy zauważyć, że w tym przypadku nie są możliwe inne sposoby ograniczania liczby wierszy, np. za pomocą dyrektywy TOP. Ponieważ perspektywa o danej nazwie istnieje, musimy ją najpierw usunąć poleceniem DROP VIEW. Alternatywą mogłoby być zastosowanie zamiast CREATE VIEW polecenia ALTER VIEW. Reszta elementów skryptu pozostała bez zmian. DROP VIEW Dane_check; GO CREATE VIEW Dane_check AS SELECT Nazwisko, Imie, Wzrost FROM Osoby WHERE Wzrost > 1.70 WITH CHECK OPTION; GO INSERT INTO Dane_check VALUES ('Kowalski','Karol',1.8); INSERT INTO Dane_check VALUES ('Nowak','Jan', 1.6); INSERT INTO Dane_check VALUES ('Kowal','Piotr',1.8); UPDATE Dane_check SET Wzrost=1.6 WHERE Nazwisko = 'Kowalski' AND Imie= 'Karol'; DELETE FROM Dane_check WHERE Nazwisko= 'Nowak' AND Imie='Jan'; GO SELECT * FROM Dane_check; SELECT * FROM Osoby;
Ponieważ w tym wypadku następuje sprawdzenie poprawności wprowadzanych danych względem wyrażenia filtrującego, przy próbie wprowadzenia nieodpowiednich wartości otrzymujemy komunikat o błędzie, a wiersze zawierające niepoprawne dane nie zostaną zapisane. Msg 550, Level 16, State 1, Line 2 The attempted insert or update failed because the target view either specifies WITH CHECK OPTION or spans a view that specifies WITH CHECK OPTION and one or more rows resulting from the operation did not qualify under the CHECK OPTION constraint. The statement has been terminated.
Taki sam komunikat otrzymamy podczas próby modyfikacji danych prowadzącej do pojawienia się wartości niezgodnych z zastosowanym filtrem. Gdybyśmy zamiast zasilania serią pojedynczych wierszy zastosowali wstawianie masowe (lista list wartości ujętych w nawiasy, które są oddzielane przecinkami — pojedyncza instrukcja INSERT), to ze względu na istniejące w jednym rekordzie błędne dane żaden z wierszy nie trafi do źródłowej tabeli, tak samo jak to miało miejsce przy zasilaniu w ten sposób tabel. W definicji perspektywy ograniczyliśmy się do części pól tabeli. Aby wpisywanie danych zakończyło się sukcesem, pomijane w definicji widoku, lecz występujące w tabeli pola muszą spełniać przynajmniej jeden z warunków: zezwalać na przyjmowanie NULL, mieć wartość domyślną albo być automatycznie inkrementowane. Warunki te są takie same jak przy jawnym wpisywaniu do tabeli wartości tylko do wybranych pól. Nie dla wszystkich perspektyw modyfikacja danych jest dopuszczalna. Jeśli na liście pól w jej definicji pojawi się jakiekolwiek wyrażenie czy funkcja, takie akcje są niemożliwe. Wynika to z tego, że silnik bazy danych musiałby znać sposób rozdzielenia wartości na elementarne atrybuty definiujące takie wyrażenie. Podobnie sprawa wygląda, gdy perspektywa w swojej definicji zawiera operacje na zbiorach UNION, INTERSECT, MINUS oraz gdy zawiera złączenie. Ten ostatni przypadek jest zilustrowany skryptem oraz komunikatem o błędzie będącym rezultatem jego wykonania.
Rozdział 3. Język zapytań SQL w MS SQL Server
151
CREATE VIEW zlaczenie AS SELECT Nazwa, Nazwisko FROM Dzialy JOIN Osoby ON Dzialy.IdDzialu= Osoby.IdDzialu; GO INSERT INTO zlaczenie VALUES('Nowy', 'Nowak'); Msg 4405, Level 16, State 1, Line 1 View or function 'zlaczenie' is not updatable because the modification affects multiple base tables.
W definicji perspektywy można dwojako określić aliasy kolumn. Pierwszy sposób to nadanie ich bezpośrednio w definicji zapytania przy określeniu wyświetlanych kolumn, po słowie kluczowym AS. W takim przypadku nowe nazwy mogą zostać określone tylko dla części pól. Drugi sposób polega na wymienieniu listy nazw pól ujętych w nawias i separowanych przecinkami bezpośrednio po nazwie tworzonej lub modyfikowanej perspektywy. W tym przypadku musimy wymienić nazwy wszystkich pól, nawet kiedy pokrywają się z nazwami kolumn źródłowych lub ich aliasami zdefiniowanymi w zapytaniu. Jeśli tak jak w przykładzie użyjemy obu metod jednocześnie, to nazwy zdefiniowane w nagłówku są nadpisywane na te, których użyto w ciele perspektywy. Obowiązkowe jest aliasowanie pól obliczanych, takich, które zawierają wyrażenia lub funkcje. CREATE VIEW aliasy(NazwiskoPrac, ImiePrac, RokUrodz) AS SELECT Nazwisko AS N, Imie AS I, RokUrodz AS R FROM Osoby; GO INSERT INTO aliasy(NazwiskoPrac, ImiePrac) VALUES('Nowak', 'Jan');
W definicji perspektywy można stosować klauzulę WITH. W przykładzie została ona użyta do wyznaczenia liczby wypłat dla każdego z pracowników, a w zasadniczej definicji perspektywy została złączona z tabelą Osoby, z której pobrano nazwisko pracownika. W przykładzie wykorzystano taką samą nazwę perspektywy jak zapytania zawartego w WITH. Na zewnątrz widoczna jest tylko nazwa perspektywy, a w ciele tylko nazwa określająca podzapytanie. Oczywiście możliwe jest, aby te nazwy były różne, co nie zmienia sposobu działania widoku. CREATE VIEW LiczbaWyplat AS WITH LiczbaWyplat (Kto, Ile) AS (SELECT IdOsoby, COUNT(*) FROM Zarobki GROUP BY IdOsoby); SELECT Nazwisko, Ile FROM Osoby JOIN LiczbaWyplat ON IdOsoby=kto; GO SELECT * FROM LiczbaWyplat;
Podczas tworzenia perspektyw dostępne są dyrektywy, których opis szczegółowy prezentuje tabela 3.68. Dotyczą one zachowania perspektyw podczas zawansowanego przetwarzania, blokady modyfikacji obiektów źródłowych, etc.
152
MS SQL Server. Zaawansowane metody programowania
Tabela 3.68. Dyrektywy dla tworzenia perspektyw Dyrektywa
Opis
ENCRYPTION
Koduje obiekt zawierający ciało perspektywy z zastosowaniem sys.syscomments; powoduje to zablokowanie publikacji definicji podczas replikacji.
SCHEMABINDING
Dowiązuje perspektywę do schematu, w którym znajduje się definiująca ją tabela (tabele). Zastosowanie tej dyrektywy powoduje zablokowanie możliwości modyfikacji oraz usunięcia wszystkich definiujących ją tabel. Konieczne jest jej usunięcie przed modyfikacją (usunięciem) obiektu źródłowego lub zmianą (usunięciem) dyrektywy. Wymaga używania nazw kwalifikowanych (schemat.tabela np. dbo.Dzialy) tabel, widoków oraz zastosowanych w definicji funkcji użytkownika (ważne w przypadku funkcji zwracających tabelę, gdzie podczas wywołania z poziomu skryptu TSQL nie ma takiego obowiązku).
VIEW_METADATA
Wskazuje, że silnik bazy danych będzie zwracał do DB-Library, ODBC oraz OLE DB API informację o metadanych zamiast o tabeli (tabelach) bazowej podczas żądania przesłania takich danych ze strony końcówki klienckiej.
Przykład skryptu tworzącego perspektywę z zastosowaniem dyrektywy WITH ENCRYPTION, powodującej kodowanie jej ciała oraz blokowanie publikacji, został przedstawiony poniżej. CREATE VIEW Dane_check WITH ENCRYPTION AS SELECT Nazwisko, Imie, Wzrost FROM Osoby WHERE Wzrost > 1.70; GO SELECT * FROM Dane_check;
Według podobnych zasad możemy użyć opcji WITH SCHEMABINDING, wiążącej definicję perspektywy z obiektami źródłowymi. Należy zwrócić szczególną uwagę na konieczność stosowania nazw kwalifikowanych źródeł danych. DROP VIEW Dane_check GO CREATE VIEW Dane_check WITH SCHEMABINDING AS SELECT Nazwisko, Imie, Wzrost FROM dbo.Osoby WHERE Wzrost > 1.70; GO SELECT * FROM Dane_check;
Kiedy obiektem źródłowym dla perspektywy z opcją SCHEMABINDING jest inna perspektywa, konieczne jest, aby była ona również utworzona z opcją SCHEMABINDING. W przeciwnym wypadku wystąpi błąd przetwarzania i perspektywa podrzędna nie zostanie utworzona. Takie powiązanie wymusza również kolejność usuwania obiektów, począwszy od najniższego poziomu podrzędności. DROP VIEW Dane_check; GO DROP VIEW Dane; GO CREATE VIEW Dane
Rozdział 3. Język zapytań SQL w MS SQL Server
153
WITH SCHEMABINDING AS SELECT Nazwisko, Imie, Wzrost FROM dbo.Osoby; GO CREATE VIEW Dane_check WITH SCHEMABINDING AS SELECT Nazwisko, Imie, Wzrost FROM dbo.Dane WHERE Wzrost > 1.70; GO SELECT * FROM Dane_check;
Ostatnia z dyrektyw VIEW_METADATA nie zmienia sposobu zachowania perspektywy, kiedy używamy jej z poziomu wbudowanej końcówki klienckiej, np. SQL Server Management Studio. CREATE VIEW Dane1 WITH VIEW_METADATA AS SELECT Nazwisko, Imie, Wzrost FROM Osoby WHERE Wzrost > 1.70; GO SELECT * FROM Dane1;
Aby pokazać wpływ tej dyrektywy na informację przez nią zwracaną, należy zbudować prostą aplikację kliencką. W przykładzie pokazano realizację tej aplikacji w postaci formularza Windows, napisanej w języku C#. W definicji okna utworzono kontrolki typu Button (przycisk) oraz DataGridView (tabela). Obsługa zdarzenia kliknięcia przycisku zawiera utworzenie łańcucha połączeniowego oraz instancji połączenia SqlConnection, która jest otwierana. Następnie tworzona jest instancja DataTable zasilana z zastosowaniem metody GetSchema obiektu połączenia. Zastosowano alternatywnie dwa sposoby definiowania parametru za pomocą metod obiektów SqlClientMetaData CollectionNames oraz OleDbMetaDataCollectionNames. Obiekt DataTable stanowi źródło danych dla kontrolki DataGridView. Prezentowana klasa zawiera standardową obsługę wyjątków. using System; using System.Data; using System.Data.OleDb; using System.Data.SqlClient; using System.Windows.Forms; namespace WindowsFormsApplication { public partial class Form1 :Form { public Form1() {InitializeComponent();} private void button1_Click(object sender, EventArgs e){ string con_str = "Data Source=.;Initial Catalog=BazaRelacyjna;User ID=sa;Password=haslo"; try{ SqlConnection conn = new SqlConnection(con_str); conn.Open(); //DataTable tbl = conn.GetSchema(SqlClientMetaData CollectionNames.ViewColumns);
154
MS SQL Server. Zaawansowane metody programowania DataTable tbl = conn.GetSchema(OleDbMetaDataCollectionNames.Columns); dataGridView1.DataSource = tbl; } catch (Exception ex){MessageBox.Show(ex.Message);} } } }
Widok aplikacji po wykonaniu obsługi zdarzenia kliknięcia przycisku pokazuje rysunek 3.9. Jak widać, zawartość kontrolki przedstawia nie rekordy, jak podczas wykonania zapytania wybierającego, ale precyzyjną informację o elementach definiujących perspektywę. Należy zaznaczyć, że zastosowanie alternatywnego sposobu zasilania kontrolki, zablokowanego w prezentowanej aplikacji, przyniesie dokładnie taki sam skutek.
Rysunek 3.9. Widok okna aplikacji testującej działanie perspektywy utworzonej z dyrektywą VIEW_METADATA
Aby sprawdzić, czy w schemacie istnieje wskazana perspektywa, na przykład w celu późniejszego jej usunięcia, możemy odwołać się do tabel systemowych. W prezentowanym przykładzie za pomocą operatora EXISTS sprawdzono, czy zapytanie będące jego atrybutem zwraca rekord. Odwołuje się ono do perspektywy SYSOBJECTS, a wskazanie konkretnego obiektu odbywa się za pomocą identyfikatora ID. Dodatkowo przez odwołanie się do funkcji OBJECTPROPERTY zawężono zakres poszukiwań do perspektyw. Drugi z warunków jest nadmiarowy i może być bez zmiany funkcjonalności usunięty. IF EXISTS (SELECT * FROM SYSOBJECTS WHERE Id = Object_Id(N'Dane_check') AND OBJECTPROPERTY(Id, N'IsView') = 1) DROP VIEW Dane_check;
Takie samo działanie będzie miał skrypt, w którym odwołujemy się do tej samej tabeli systemowej, a jako filtr zastosujemy sprawdzenie nazwy oraz typu obiektu. IF EXISTS (SELECT * FROM SYSOBJECTS WHERE name = 'Dane_check' AND xtype = 'V' ) DROP VIEW Dane_check;
Zamiast odwoływać się bezpośrednio do perspektyw systemowych, możliwe jest skorzystanie z perspektyw słownikowych należących do INFORMATION_SCHEMA. W przykładzie skorzystano ze słownika VIEWS, a wskazanie obiektu następuje przez sprawdzenie nazwy.
Rozdział 3. Język zapytań SQL w MS SQL Server
155
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME ='Dane_check') DROP VIEW Dane_check;
Ważne informacje o zawartości schematu relacyjnego możemy uzyskać, odpytując perspektywy należące do schematu sys. W przedstawionym skrypcie przedstawiono trzy, w kolejności od najogólniejszej, zwracającej największą ilość informacji, do najbardziej zwartej. SELECT * FROM sys.all_objects; SELECT * FROM sysobjects; SELECT * FROM sys.objects;
Aby wyświetlić wszystkie perspektywy systemowe właściciela sys, możemy wykonać zapytanie jak poniżej. SELECT name FROM sys.all_objects WHERE type= 'V' AND schema_id =4 ORDER BY name;
Ze względu na liczbę zwracanych rekordów nie przedstawiono ich wynikowego zestawu, pozostawiając zapoznanie się z nim dociekliwemu Czytelnikowi. Pośród wielu interesujących perspektyw schematu sys znajduje się taka, która opisuje zdarzenia obsługiwane przez triggery (wyzwalacze); jej zawartość będzie szczegółowo omawiana w rozdziale 5.5. SELECT * FROM sys.trigger_event_types;
Najważniejszy zestaw perspektyw słownikowych należy do schematu INFORMATION_ SCHEMA. Jest on dedykowany do uzyskiwania najistotniejszych danych opisujących najważniejsze, podstawowe elementy bazy danych. Aby obejrzeć ich wykaz, możemy się odwołać do jednej z wcześniej omawianych perspektyw schematu sys, tak jak to pokazano w przykładzie, którego wynik zawiera tabela 3.69. SELECT name FROM sys.all_objects WHERE type= 'V' AND schema_id =3 ORDER BY name;
Aby uzyskać informacje o omawianych poprzednio obiektach — tabelach, występujących w nich ograniczeniach oraz perspektywach — możemy odpytać wybrane perspektywy słownikowe, jak pokazuje to przykład. SELECT * FROM INFORMATION_SCHEMA.TABLES; SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS; SELECT * FROM INFORMATION_SCHEMA.VIEWS;
Należy zauważyć, że definicje omawianych słowników są zapisane w systemowej bazie model, a w związku z tym są kopiowane do każdej nowo tworzonej bazy. Należy zastanowić się nad korzyściami płynącymi ze stosowania perspektyw. Waga tych obiektów jest nie do przecenienia. Przede wszystkim są wykorzystywane jako element poprawiający poziom bezpieczeństwa w bazie [23] [24] [25] [26]. Przypisując
156
MS SQL Server. Zaawansowane metody programowania
Tabela 3.69. Wykaz perspektyw słownikowych w schemacie INFORMATION_SCHEMA Name CHECK_CONSTRAINTS COLUMN_DOMAIN_USAGE COLUMN_PRIVILEGES COLUMNS CONSTRAINT_COLUMN_USAGE CONSTRAINT_TABLE_USAGE DOMAIN_CONSTRAINTS DOMAINS KEY_COLUMN_USAGE PARAMETERS REFERENTIAL_CONSTRAINTS ROUTINE_COLUMNS ROUTINES SCHEMATA TABLE_CONSTRAINTS TABLE_PRIVILEGES TABLES VIEW_COLUMN_USAGE VIEW_TABLE_USAGE VIEWS
uprawnienia do perspektywy zamiast do źródłowej tabeli, możemy ograniczyć liczbę zarówno dostępnych kolumn, jak i wierszy. Pozwalają one również na zastąpienie oryginalnych nazw pól tabeli bardziej przyjaznymi dla użytkownika. Ponieważ w momencie ich tworzenia generowany jest również plan wykonania definiującego je zapytania, pozwalają na poprawę wydajności przetwarzania, zwłaszcza gdy odpytywane są jako pojedyncze źródła danych. Kiedy musimy zbudować złożone zapytanie, możliwe jest rozłożenie go na prostsze elementy, które definiują perspektywy, a te z kolei są wykorzystywane jako źródła obiektów. Ten mechanizm prześledzimy na przykładzie struktury opisującej rozgrywki piłkarskie. Na początku zdefiniujmy potrzebną do przechowywania danych strukturę tabelaryczną. Pozwoli to równocześnie na pokazanie złożonych zależności i ograniczeń, jakie można napotkać podczas tworzenia skryptów generujących schematy relacyjne. Przykładowy diagram pokazujący docelowy zestaw tabel pokazano na rysunku 3.10 Pomimo że nie mamy jeszcze utworzonych elementów schematu, skrypt rozpocznijmy od poleceń usuwających tabele. Jest to istotne ze względu na wymuszoną kluczami obcymi kolejność wykonywania tych poleceń. Będą one użyteczne w momencie testowania całości skryptu. Uważny Czytelnik może uzupełnić ten fragment o warunki, które będą powodować, że usuwanie będzie wykonywane tylko wtedy, gdy obiekt będzie już istniał. Takie rozwiązanie przedstawiono już w tym rozdziale.
Rozdział 3. Język zapytań SQL w MS SQL Server
157
BRAMKI IDBRAMKI
ZAWODNICY
IDMECZU
IDZAWODNIKA
IDZAWODNIKA
IDDRUZYNY
MINUTA
IMIE
STATUS
NAZWISKO
MECZE IDMECZU SEZON IDGOSC IDGOSP
DRUZYNY
IDSEDZIEGOG
IDDRUZYNY
IDSEDZIEGOL1
NAZWA
IDSEDZIEGOL2 IDSEDZIEGOT
SEDZIOWIE IDSEDZIEGO IMIE NAZWISKO
DATAM
Rysunek 3.10. Diagram relacyjny bazy danych opisującej rozgrywki piłkarskie DROP DROP DROP DROP DROP GO
TABLE TABLE TABLE TABLE TABLE
BRAMKI; MECZE; ZAWODNICY ; DRUZYNY; SEDZIOWIE;
W następnej kolejności utwórzmy tabele, ograniczając się do określenia typów pól, a spośród ograniczeń zdefiniujmy jedynie automatycznie inkrementowane klucze podstawowe. Postępujemy tak, aby możliwe było ich tworzenie w dowolnej kolejności, ponieważ klucze obce wymusiłyby, aby w pierwszej kolejności tworzone były te tabele, do których nie ma żadnych referencji. W rozgrywkach biorą udział drużyny, w których występują zawodnicy. Takie przywiązanie na stałe jest znacznym uproszczeniem, ponieważ w trakcie sezonu zawodnik może zmienić barwy klubowe, zarówno na stałe, w czasie trwania okna transferowego między rundami jesienną i wiosenną, jak i czasowo, na skutek wypożyczenia. Pełne rozwiązanie problemu wymagałoby utworzenia dodatkowej tabeli łączącej drużyny i zawodników, zawierającej informacje o czasie trwania przynależności do klubu. Ponieważ uproszczone zadanie nie jest trywialne, pozostańmy przy takim problemie. Drużyny parami rozgrywają między sobą mecze, które sędziują sędziowie. Dla meczów ustalono, że domyślną datą rozgrywania meczu jest dzień dodania rekordu. Oczywiście informację tę można zmienić. Mimo że do ustalenia tabeli informacja o sędziach nie jest konieczna, podajmy ją, aby pokazać interesujące powiązania między tabelami oraz dość złożoną postać ograniczeń. Na boisku jest zgodnie z obowiązującymi przepisami czterech sędziów: główny, dwóch liniowych oraz techniczny. Zgodnie z przepisami, które mają wejść wkrótce w życie, będzie ich sześciu. Dotychczasowy zestaw zostanie uzupełniony o dwóch sędziów bramkowych. W meczach padają bramki, które są zdobywane przez zawodników. Jeśli założymy, że w meczach mogą padać gole samobójcze, możemy wprowadzić pole statusu, określające rodzaj bramki. Można jednak przyjąć, że w przypadku gola samobójczego przypisuje się go graczowi drużyny przeciwnej, który ostatni dotknął piłki. Jeśli nie można tego rozstrzygnąć, ponieważ na przykład po wznowieniu gry bramkarz wrzucił sobie piłkę do bramki, to gol jest przypisywany kapitanowi drużyny przeciwnej. Takie założenie przyjęto w pokazanym dalej skrypcie generującym tabelę wyników.
158
MS SQL Server. Zaawansowane metody programowania CREATE TABLE Bramki (IdBramki int IDENTITY(1,1) PRIMARY KEY, IdMeczu int, IdZawodnika int, Minuta int, Status int); GO CREATE TABLE Druzyny (IdDruzyny int IDENTITY(1,1) PRIMARY KEY, Nazwa VARCHAR(20)); GO CREATE TABLE Mecze (IdMeczu int IDENTITY(1,1) PRIMARY KEY, Sezon int, IdGosc int, IdGosp int, IdSedziegoG int, IdSedziegoL1 int, IdSedziegoL2 int, IdSedziegoT int, DATAM DATE DEFAULT getdate()); GO CREATE TABLE Sedziowie (IdSedziego int IDENTITY(1,1) PRIMARY KEY, Imie VARCHAR(20), Nazwisko VARCHAR(20)); GO CREATE TABLE Zawodnicy (IdZawodnika int IDENTITY(1,1) PRIMARY KEY, IdDruzyny int, Imie VARCHAR(20), Nazwisko VARCHAR(20)); GO
Przejdźmy teraz do modyfikowania tabel, dodając do nich kolejne ograniczenia. Pierwszymi niech będą klucze obce. W tabeli Zawodnicy dodane zostanie jedno takie ograniczenie z referencją do tabeli Druzyny. Natomiast w tabeli Bramki potrzebne będą dwa — pierwsze, określające mecz, w którym zdobyto bramkę (referencja do Mecze), i drugie, wskazujące na strzelca (referencja do Zawodnicy). ALTER TABLE Zawodnicy ADD FOREIGN KEY(IdDruzyny) REFERENCES Druzyny(IdDruzyny); GO ALTER TABLE Bramki ADD FOREIGN KEY (IdMeczu) REFERENCES Mecze (IdMeczu); GO ALTER TABLE Bramki ADD FOREIGN KEY (IdZawodnika) REFERENCES Zawodnicy (IdZawodnika); GO
Bardziej interesujące są klucze obce tworzone w tabeli Mecze. Obie drużyny reprezentowane przez pola IdGosc oraz IdGosp odwołują się do tego samego pola IdDruzyny w nadrzędnej tabeli Druzyny. Analogicznie cztery identyfikatory opisujące sędziów mają referencje do jednego pola IdSedziego w tabeli Sedziowie. Połączenia między tabelami są dobrze widoczne na rysunku 3.10.
Rozdział 3. Język zapytań SQL w MS SQL Server
159
ALTER TABLE Mecze ADD FOREIGN KEY (IdGosc) REFERENCES Druzyny(IdDruzyny); GO ALTER TABLE Mecze ADD FOREIGN KEY (IdGosp) REFERENCES Druzyny(IdDruzyny); GO ALTER TABLE Mecze ADD FOREIGN KEY(IdSedziegoG) REFERENCES Sedziowie(IdSedziego); GO ALTER TABLE Mecze ADD FOREIGN KEY(IdSedziegoL1) REFERENCES Sedziowie(IdSedziego); GO ALTER TABLE Mecze ADD FOREIGN KEY(IdSedziegoL2) REFERENCES Sedziowie(IdSedziego); GO ALTER TABLE Mecze ADD FOREIGN KEY(IdSedziegoT) REFERENCES Sedziowie(IdSedziego); GO
Kolejny etap stanowi dodanie warunków sprawdzających CHECK. W przypadku tabeli Bramki ustalono, że minuta strzelenia bramki musi być dodatnia. Nie określono górnego zakresu ze względu na możliwość przedłużenia czasu gry. Ponadto przyjęto, że status może przyjąć jedną z dwóch wartości: 0 lub 1. W przypadku meczów zablokowano sytuacje, w której drużyna gra sama ze sobą, oraz ustalono, aby każdy z sędziów był różny — dla czterech osób generuje to (3 + 2 + 1) = 6 zależności. Jeśli będzie ich sześciu, będziemy musieli sprawdzić 15 warunków. Oczywiście każdy z elementarnych warunków może być sprawdzany oddzielnie. Tym razem zdecydowano się zbudować jedno wyrażenie walidujące, łącząc elementy operatorem AND. Na koniec, aby niemożliwe było rozegranie meczów między tymi samymi drużynami więcej niż raz na sezon, ustanowiono ograniczenie unikalności dla listy trzech pól. ALTER TABLE Bramki ADD CONSTRAINT SPRMINUTA CHECK (Minuta > 0); GO ALTER TABLE Bramki ADD CONSTRAINT SPRSTATUS CHECK ((Status = 0) OR (Status = 1)); GO ALTER TABLE Mecze ADD CONSTRAINT SPRSEDZIOWIE CHECK ( (IdGosc<>IdGosp)AND (IdSedziegoG<>IdSedziegoL1)AND (IdSedziegoG<>IdSedziegoL2)AND (IdSedziegoG<>IdSedziegoT)AND (IdSedziegoL1<>IdSedziegoL2)AND (IdSedziegoL1<>IdSedziegoT)AND (IdSedziegoL2<>IdSedziegoT)); GO ALTER TABLE Mecze ADD CONSTRAINT sssUNIQUE(Sezon, IdGosc, IdGosp); GO
Po utworzeniu struktury i zdefiniowaniu ograniczeń tabele zostały zasilone danymi. Pomimo tego, że idea przetwarzania nie jest zależna od danych, pozwoliłem je sobie przytoczyć, aby Czytelnik mógł lepiej śledzić na podstawie wyników częściowych proces dochodzenia do pełnego rozwiązania. Z premedytacją ograniczona została liczba wpisanych wierszy.
160
MS SQL Server. Zaawansowane metody programowania INSERT INSERT INSERT GO INSERT INSERT INSERT INSERT GO INSERT INSERT INSERT GO INSERT INSERT INSERT INSERT INSERT INSERT INSERT INSERT GO INSERT INSERT INSERT INSERT GO
INTO Druzyny VALUES ('Nasi'); INTO Druzyny VALUES ('Inni'); INTO Druzyny VALUES ('Nowi'); INTO INTO INTO INTO
Sedziowie Sedziowie Sedziowie Sedziowie
VALUES VALUES VALUES VALUES
('Jan', 'Kowal'); ('Piotr','Niewidomy'); ('Janusz', 'Demon'); ('Kazimierz', 'Tama');
INTO Mecze VALUES (1, 1, 2, 1, 2, 3, 4, '2011-11-23'); INTO Mecze VALUES (2, 2, 1, 1, 3, 2, 4,'2011-11-23'); INTO Mecze VALUES (1, 3, 2, 2, 4, 3, 1,'2011-12-12'); INTO INTO INTO INTO INTO INTO INTO INTO
Zawodnicy Zawodnicy Zawodnicy Zawodnicy Zawodnicy Zawodnicy Zawodnicy Zawodnicy
INTO INTO INTO INTO
Bramki Bramki Bramki Bramki
VALUES VALUES VALUES VALUES VALUES VALUES VALUES VALUES
VALUES VALUES VALUES VALUES
(1, (2, (1, (3,
(1, 'Wiktor', 'Koster'); (2, 'Michal', 'Piatek'); (1, 'Piotr', 'Madrala'); (2, 'Jakub', 'Stoczynski'); (2, 'Jarosław', 'Swiat'); (1, 'Jakub','Noga'); (1,'Dariusz', 'Piegat'); (2, 'Paweł', 'Borsuk'); 2, 4, 5, 1,
21, 0); 5, 1); 9, 0); 1, 1);
Poniżej przedstawiono elementarne zapytania wybierające istotne kolumny z utworzonych tabel; skutki tych zapytań są zawarte w tabelach 3.70 – 3.73, aby ułatwić analizę wyników uzyskanych za pomocą kolejnych, bardziej złożonych zapytań budujących perspektywy. SELECT IdMeczu, IdGosp, IdGosc FROM Mecze;
Tabela 3.70. Dane dotyczące rozegranych meczów IdMeczu
IdGosp
IdGosc
1
2
1
3
2
3
2
1
2
SELECT IdBramki, IdMeczu, IdZawodnika FROM Bramki;
Tabela 3.71. Dane dotyczące strzelonych bramek IdBramki
IdMeczu
IdZawodnika
1
1
2
2
2
4
3
1
5
4
3
1
SELECT IdZawodnika, IdDruzyny FROM Zawodnicy;
Rozdział 3. Język zapytań SQL w MS SQL Server
161
Tabela 3.72. Dane dotyczące przynależności klubowej zawodnika IdZawodnika
IdDruzyny
1
1
2
2
3
1
4
2
5
2
6
1
7
1
8
2 SELECT * FROM Druzyny;
Tabela 3.73. Dane dotyczące drużyn występujących w rozgrywkach IdDruzyny
Nazwa
1
Nasi
2
Inni
3
Nowi
Postawmy sobie zadanie utworzenia zapytania, które utworzy tabelę wyników, wyznaczając sumę punktów (3 — zwycięstwo, 1 — remis, 0 — porażka) oraz sumy bramek strzelonych i straconych we wszystkich rozegranych meczach. Początek skryptu stanowi usunięcie perspektyw, co nie jest konieczne, a zostało pokazane, aby ustalić obowiązkową kolejność wykonywania tych poleceń, począwszy od najogólniejszej, idąc w kierunku najbardziej szczegółowych. DROP DROP DROP DROP DROP DROP GO
VIEW VIEW VIEW VIEW VIEW VIEW
DruzynyWMeczu; PunktyWMeczu; SumaBramek; WszystkieBramki; BramkiGosci; BramkiGospodarzy;
Realizację rozwiązania rozpocznijmy od utworzenia perspektywy BramkiGospodarzy, zliczającej w każdym meczu bramki strzelone przez drużynę występującą w roli gospodarza. W tym celu połączone zostały za pomocą kluczy głównych i obcych o zgodnych nazwach tabele Bramki, Zawodnicy, Druzyny i Mecze, co pozwala na określenie klubu strzelca bramki. Dodatkowo połączono Mecze z Druzyny, przypisując do IdDruzyny pole IdGosp, co wskazuje, że poszukujemy bramek drużyny będącej gospodarzem. W tym złączeniu określono kierunek na RIGHT, aby w przypadku, kiedy nie istnieją wpisy dotyczące bramek zdobytych przez wskazaną drużynę, funkcja agregująca COUNT zwróciła 0. Poza polem zawierającym obliczenie dodano pole BGosc, reprezentujące bramki gości, którego wartość ustalono na 0, przez co należy rozumieć, że skoro drużyna jest gospodarzem, to nie uzyskuje bramek jako gość. Potrzebę istnienia takiego pola pokażę w dalszej części budowania rozwiązania. Skrypt został uzupełniony o zapytanie
162
MS SQL Server. Zaawansowane metody programowania
wybierające, wyświetlające dane zwracane przez perspektywę, którego wyniki dla przykładowych danych zawiera tabela 3.74. CREATE VIEW BramkiGospodarzy AS SELECT Mecze.IdMeczu, Mecze.IdGosp AS Druzyna, COUNT(IdBramki) AS BGosp, 0 AS BGosc FROM Bramki JOIN Zawodnicy ON Zawodnicy.IdZawodnika=Bramki.IdZawodnika JOIN Druzyny ON Druzyny.IdDruzyny=Zawodnicy.IdDruzyny RIGHT JOIN Mecze ON Mecze.IdGosp= Druzyny.IdDruzyny AND Mecze.IdMeczu=Bramki.IdMeczu GROUP BY Mecze.IdMeczu, Mecze.IdGosp; GO SELECT * FROM BramkiGospodarzy;
Tabela 3.74. Bramki zdobyte przez drużynę gospodarzy IdMeczu
Druzyna
BGosp
BGosc
1
2
2
0
2
1
0
0
3
2
0
0
Antysymetryczna względem poprzedniej jest perspektywa BramkiGosci. Tym razem zliczane są bramki dla drużyny będącej gościem, a bramki gospodarzy ustawiane są na 0. Ponadto połączenie pomiędzy meczami i drużynami korzysta z pola IdGosc. Skutek odpytania tej perspektywy przedstawiono w tabeli 3.75. CREATE VIEW BramkiGosci AS SELECT Mecze.IdMeczu, Mecze.IdGosc AS Druzyna, 0 AS BGosp, COUNT(IdBramki) AS BGosc FROM Bramki JOIN Zawodnicy ON Zawodnicy.IdZawodnika=Bramki.IdZawodnika JOIN Druzyny ON Druzyny.IdDruzyny=Zawodnicy.IdDruzyny RIGHT JOIN Mecze ON Mecze.IdGosc= Druzyny.IdDruzyny AND Mecze.IdMeczu=Bramki.IdMeczu GROUP BY Mecze.IdMeczu, Mecze.IdGosc; GO SELECT * FROM BramkiGosci;
Tabela 3.75. Bramki zdobyte przez drużynę gości IdMeczu
Druzyna
BGosp
BGosc
1
1
0
0
2
2
0
1
3
3
0
0
Połączenie operatorem UNION ALL zapytań wybierających wszystkie pola i wszystkie rekordy z obu przedstawionych perspektyw powoduje, że w dwóch rekordach mamy dostępne dane o bramkach zdobytych w meczu przez drużynę gości i gospodarzy, co pokazuje dla przykładowych danych tabela 3.76.
Rozdział 3. Język zapytań SQL w MS SQL Server
163
CREATE VIEW WszystkieBramki AS SELECT IdMeczu,Druzyna,BGosp,BGosc FROM BramkiGospodarzy UNION ALL SELECT IdMeczu,Druzyna,BGosp,BGosc FROM BramkiGosci; GO SELECT * FROM WszystkieBramki;
Tabela 3.76. Bramki zdobyte przez drużyny gospodarzy i gości IdMeczu
Druzyna
BGosp
BGosc
1
1
0
0
1
2
2
0
2
1
0
0
2
2
0
1
3
2
0
0
3
3
0
0
Zsumowanie danych, zawartych w obu rekordach dla każdego meczu, powoduje spłaszczenie wyników do postaci, jaką znamy z codziennej praktyki. Skutek odpytania tej perspektywy przedstawia tabela 3.77. CREATE VIEW SumaBramek AS SELECT IdMeczu, SUM(BGosp) AS ALLBGOSP, SUM(BGosc) AS ALLBGOSC FROM WszystkieBramki GROUP BY IdMeczu; GO SELECT * FROM SumaBramek;
Tabela 3.77. Bramki zdobyte przez drużyny gospodarzy i gości IdMeczu
ALLBGOSP
ALLBGOSC
1
2
0
2
0
1
3
0
0
Porównanie liczby bramek zdobytych w każdym meczu pozwala na przypisanie punktów każdej z drużyn występujących w meczu. Wykonywane jest to niezależnie w każdym rekordzie dla drużyny gości i gospodarzy, z zastosowaniem operatora CASE, zawierającego trzy warunki odpowiadające zdobyciu odpowiedniej liczby punktów: 3, 1 lub 0. Pierwszy i ostatni warunek dla obu wyliczanych pól są przeciwne, warunek środkowy opisujący remis jest taki sam. Rekordy zwracane przez tę perspektywę przedstawia tabela 3.78. CREATE VIEW PunktyWMeczu AS SELECT IdMeczu, ALLBGOSC, ALLBGOSP, CASE WHEN ALLBGOSC > ALLBGOSP THEN 3 WHEN ALLBGOSC = ALLBGOSP THEN 1 WHEN ALLBGOSC < ALLBGOSP THEN 0 END AS PKTGOSC, CASE WHEN ALLBGOSP > ALLBGOSC THEN 3
164
MS SQL Server. Zaawansowane metody programowania WHEN ALLBGOSP = ALLBGOSC THEN 1 WHEN ALLBGOSP < ALLBGOSC THEN 0 END AS PKTGOSP FROM SumaBramek; GO SELECT * from PunktyWMeczu;
Tabela 3.78. Punkty zdobyte przez drużyny gości i gospodarzy IdMeczu
ALLBGOSC
ALLBGOSP
PKTGOSC
PKTGOSP
1
0
2
0
3
2
1
0
3
0
3
0
0
1
1
W następnym kroku rozdzielamy punkty przypisane do drużyn gościa i gospodarza. Perspektywa zawiera dwa zestawy rekordów połączone operatorem UNION. Obydwa pobierają dane z tabeli Mecze oraz perspektywy PunktyWMeczu, połączonych za pomocą pól o nazwie IdMeczu. Zestawy te różnią się tym, że pierwszy z nich wyświetla identyfikator gospodarzy i punkty przez nich uzyskane, drugi — identyfikator gości i punkty tej drużyny. Należy zauważyć, że zamieniono miejscami pola ALLBGOSP i ALLBGOSC, tak aby w obu zestawach rekordów pierwsze pole reprezentowało bramki zdobyte, a drugie — stracone przez wskazaną drużynę. Rekordy zwrócone przez tę perspektywę są zawarte w tabeli 3.79. CREATE VIEW DruzynyWMeczu AS SELECT Mecze.IdMeczu, IdGosp, PKTGosp, ALLBGOSP, ALLBGOSC FROM Mecze JOIN PunktyWMeczu ON Mecze.IdMeczu=PunktyWMeczu.IdMeczu UNION SELECT Mecze.IdMeczu, IdGosc, PKTGosc, ALLBGOSC, ALLBGOSP FROM Mecze JOIN PunktyWMeczu ON Mecze.IdMeczu=PunktyWMeczu.IdMeczu; GO SELECT * FROM DruzynyWMeczu;
Tabela 3.79. Punkty, bramki zdobyte i stracone przez drużyny w każdym z meczów IdMeczu
IdGosp
PKTGosp
ALLBGOSP
ALLBGOSC
1
1
0
0
2
1
2
3
2
0
2
1
0
0
1
2
2
3
1
0
3
2
1
0
0
3
3
1
0
0
Aby uzyskać końcową postać tabeli, połączono polami o nazwach IdGosp oraz IdDruzyny perspektywę DruzynyWMeczu z tabelą Druzyny, z której wyświetlono nazwę. Pola pochodzące z perspektywy zsumowano, tak aby uzyskać całkowitą liczbę punktów, bramek zdobytych i straconych. Zestaw rekordów przedstawiony w tabeli 3.80 został posortowany malejąco względem sumy punktów oraz różnicy bramek. SELECT Nazwa, COUNT(IdGosp) AS IleSpotkan, SUM(PKTGosp) AS Punkty, SUM(ALLBGOSP) AS BStrzelone, SUM(ALLBGOSC) AS BStracone
Rozdział 3. Język zapytań SQL w MS SQL Server
165
FROM Druzyny JOIN DruzynyWMeczu ON IdDruzyny=IdGosp GROUP BY Nazwa, IdGosp ORDER BY Punkty DESC, BStrzelone-BStracone DESC;
Tabela 3.80. Końcowa postać tabeli rozgrywek Nazwa
IleSpotkan
Punkty
BStrzelone
BStracone
Inni
3
7
3
0
Nowi
1
1
0
0
Nasi
2
0
0
3
Jeśli po kolei, rozpoczynając od ciała ostatniej perspektywy, jako źródła danych podamy zamiast perspektyw ich definicję, to otrzymamy pojedyncze zapytanie stanowiące równoważne rozwiązanie problemu. Aby pokazać dużą złożoność, przytoczmy pełną jego postać. SELECT Nazwa, COUNT(IdGosp) AS IleSpotkan, SUM(PKTGosp) AS Punkty, SUM(ALLBGOSP) AS BStrzelone, SUM(ALLBGOSc) AS BStracone FROM Druzyny JOIN (SELECT MECZE.IdMeczu, IdGosp, PKTGosp ,ALLBGOSP, ALLBGOSC FROM Mecze JOIN (SELECT IdMeczu, ALLBGOSC, ALLBGOSP, CASE WHEN ALLBGOSC > ALLBGOSP THEN 3 WHEN ALLBGOSC = ALLBGOSP THEN 1 WHEN ALLBGOSC < ALLBGOSP THEN 0 END AS PKTGosc, CASE WHEN ALLBGOSP > ALLBGOSC THEN 3 WHEN ALLBGOSP = ALLBGOSC THEN 1 WHEN ALLBGOSP < ALLBGOSC THEN 0 END AS PKTGosp FROM (SELECT IdMeczu, SUM(BGosp) AS ALLBGOSP, SUM(BGosc) AS ALLBGOSC FROM (SELECT IdMeczu, Druzyna, BGosp, BGosc FROM (SELECT Mecze.IdMeczu,Mecze.IdGosp AS Druzyna , COUNT(IdBramki) AS BGosp, 0 AS BGosc FROM Bramki JOIN Zawodnicy ON Zawodnicy.IdZawodnika=Bramki.IdZawodnika JOIN Druzyny ON Druzyny.IdDruzyny=Zawodnicy.IdDruzyny RIGHT JOIN Mecze ON Mecze.IdGosp= Druzyny.IdDruzyny AND Mecze.IdMeczu=Bramki.IdMeczu GROUP BY Mecze.IdMeczu,Mecze.IdGosp ) AS BramkiGospodarzy UNION SELECT IdMeczu, Druzyna, BGosp, BGosc FROM (SELECT Mecze.IdMeczu, Mecze.IdGosc AS Druzyna, 0 AS BGosp, COUNT(IdBramki) AS BGosc FROM BRAMKI JOIN Zawodnicy ON Zawodnicy.IdZawodnika=Bramki.IdZawodnika JOIN Druzyny ON Druzyny.IdDruzyny=Zawodnicy.IdDruzyny RIGHT JOIN Mecze ON Mecze.IdGosc= Druzyny.IdDruzyny AND Mecze.IdMeczu=Bramki.IdMeczu GROUP BY Mecze.IdMeczu, Mecze.IdGosc) AS BramkiGosci)
166
MS SQL Server. Zaawansowane metody programowania AS WszystkieBramki GROUP BY IdMeczu) AS SumaBramek) AS PunktyWMeczu ON Mecze.IdMeczu=PunktyWMeczu.IdMeczu UNION SELECT Mecze.IdMeczu, IdGosc, PKTGosc ,ALLBGOSC, ALLBGOSP FROM Mecze JOIN (SELECT IdMeczu, ALLBGOSC, ALLBGOSP, CASE WHEN ALLBGOSC > ALLBGOSP THEN 3 WHEN ALLBGOSC = ALLBGOSP THEN 1 WHEN ALLBGOSC < ALLBGOSP THEN 0 END AS PKTGosc, CASE WHEN ALLBGOSP > ALLBGOSC THEN 3 WHEN ALLBGOSP = ALLBGOSC THEN 1 WHEN ALLBGOSP < ALLBGOSC THEN 0 END AS PKTGosp FROM (SELECT IdMeczu, SUM(BGosp) AS ALLBGOSP, SUM(BGosc) AS ALLBGOSC FROM (SELECT IdMeczu, Druzyna,BGOSP,BGOSC FROM (SELECT Mecze.IdMeczu, Mecze.IdGosp AS Druzyna, COUNT(IdBramki) AS BGosp, 0 AS BGosc FROM Bramki JOIN Zawodnicy ON Zawodnicy.IdZawodnika=Bramki.IdZawodnika JOIN Druzyny ON Druzyny.IdDruzyny=Zawodnicy.IdDruzyny RIGHT JOIN Mecze ON Mecze.IdGosp=Druzyny.IdDruzyny AND Mecze.IdMeczu=Bramki.IdMeczu GROUP BY Mecze.IdMeczu, Mecze.IdGosp) AS BramkiGospodarzy UNION ALL SELECT IdMeczu, Druzyna, BGosp, BGosc FROM (SELECT Mecze.IdMeczu, Mecze.IdGosc AS Druzyna, 0 AS BGosp, COUNT(IdBramki) AS BGosc FROM Bramki JOIN Zawodnicy ON Zawodnicy.IdZawodnika=Bramki.IdZawodnika JOIN Druzyny ON Druzyny.IdDruzyny=Zawodnicy.IdDruzyny RIGHT JOIN Mecze ON Mecze.IdGosc= Druzyny.IdDruzyny AND Mecze.IdMeczu=Bramki.IdMeczu GROUP BY Mecze.IdMeczu, Mecze.IdGosc) AS BramkiGosci) AS WszystkieBramki GROUP BY IdMeczu) AS SumaBramek) AS PunktyWMeczu ON Mecze.IdMeczu=PunktyWMeczu.IdMeczu )AS DruzynyWMeczu ON IdDruzyny=IdGosp GROUP BY Nazwa, IdGosp ORDER BY Punkty DESC, BStrzelone-BStracone DESC;
Jak widać ze złożoności i objętości przedstawionego kodu, zbudowanie takiego rozwiązania bezpośrednio, bez budowania pomocniczych perspektyw, wymaga naprawdę bardzo dobrej znajomości składni SQL oraz dużej biegłości w posługiwaniu się nią. Oczywiście podstawą jest wyobrażenie sobie pełnego rozwiązania i wszystkich kroków pośrednich. Należy przypomnieć, że zaprezentowane zostało rozwiązanie problemu uproszczonego, w którym pominięto transfery zawodników między klubami w trakcie trwania sezonu rozgrywek. Rozwiązanie pełnego problemu wymaga utworzenia tabeli pośredniczącej łączącej tabele Zawodnicy i Druzyny, która poza kluczem głównym oraz kluczami obcymi do wymienionych tabel będzie zawierać informacje o początku
Rozdział 3. Język zapytań SQL w MS SQL Server
167
i końcu kontraktu. Oczywiście zapytanie będzie bardziej złożone, ale co łatwo zauważyć, zmiany pojawią się jedynie w podstawowym zapytaniu (perspektywie), ustalającym na podstawie identyfikatora zawodnika, dla której z drużyn bramka została zdobyta. W rozwiązaniu pełnym należy uwzględnić daty początku i końca kontraktu oraz datę rozgrywania meczu. Praktyczną realizację takiego rozwiązania pozostawiam uważnemu Czytelnikowi. Jednym z głównych orędowników rozwiązywania złożonych problemów przetwarzania w bazach za pomocą pomocniczych perspektyw jest Joe Celko, członek ANSI X3H2 Database Standards Committee, współtwórca standardów SQL 89 i 92 oraz autor licznych publikacji i książek z tej dziedziny [7]. Można się o tym przekonać, analizując zawarte w jego opracowaniach rozwiązania złożonych problemów przetwarzania z zastosowaniem SQL [27] – [30]. Jeśli chodzi o mnie, nie jestem ortodoksyjnie nastawiony do żadnej z form rozwiązywania zadań za pomocą SQL. Równie wartościowe są rozwiązania wykorzystujące pośrednie perspektywy, a także te, w których utworzono jedno zapytanie rozwiązujące problem w jednym kroku. Jednak z uwagi na wartość edukacyjną postulowałbym, aby dążyć do rozwiązań z pojedynczym zapytaniem albo niewielką liczbą perspektyw, ponieważ pozwala to na lepsze opanowanie sztuki posługiwania się SQL, co jest naprawdę złożoną umiejętnością.
3.6. Tworzenie typu użytkownika Środowisko MS SQL pozwala na tworzenie typów użytkownika. Możemy to uzyskać, stosując wprowadzone w wersji 2008 zapytanie tworzące typ o nazwie litery: CREATE TYPE litery FROM char(1) NULL
lub działającą również w starszych wersjach procedurę systemową: EXEC sp_addtype litery, 'char(1)', 'NULL'
W obu przypadkach możemy podać dwa parametry — pierwszy, określający źródłowy typ danych, wraz z ewentualnym określeniem długości, drugi, wskazujący na stosunek do wartości NULL. Należy zauważyć, że w drugim przypadku parametry podawane są jako zmienne znakowe. Konsekwencją utworzenia takiego obiektu jest zapisanie jego definicji w bazie danych, co pokazuje rysunek 3.11. Z poziomu hierarchicznej struktury obiektów możemy również dodać nowy typ, a dla już istniejącego obejrzeć i zmodyfikować dane. Jednakże modyfikacje te nie mogą dotyczyć parametrów podstawowych nazwy, typu wyjściowego i jego wymiaru oraz reakcji na wartość NULL. Możliwe jest natomiast przypisanie lub usunięcie wartości domyślnej oraz reguły. Wartość domyślną tworzymy za pomocą zapytania: CREATE DEFAULT A AS 'A'
w którym po słowie kluczowym DEFAULT podawana jest nazwa, a po AS wartość obiektu. Umiejscowienie tego obiektu w hierarchicznej strukturze obiektów bazy danych przedstawia rysunek 3.12.
168
MS SQL Server. Zaawansowane metody programowania
Rysunek 3.11. Typ użytkownika zapisany w bazie danych i okno jego właściwości Rysunek 3.12. Miejsce wartości domyślnej w hierarchicznej strukturze obiektów bazy danych
Kolejnym przykładem tworzenia wartości domyślnej jest taki, w którym utworzono taki obiekt o nazwie jeden i wartości 1. CREATE DEFAULT Jeden AS 1
Podobny schemat obowiązuje przy tworzeniu reguły, gdzie po słowie kluczowym RULE podajemy nazwę, a po AS wyrażenie sprawdzające, do którego budowania używamy parametrów o nazwach rozpoczynających się od znaku @. W przykładzie pokazano
Rozdział 3. Język zapytań SQL w MS SQL Server
169
skrypt tworzący trzy reguły, w których pierwsza sprawdza, czy wartość jest różna od zera, druga, czy znak jest literą, a ostatnia, czy napis jest równy jednemu z elementów listy dopuszczalnych wartości. Konsekwencje wykonania tego przykładu przedstawia rysunek 3.13. CREATE RULE IsNotZero AS @value <> 0 GO CREATE RULE lit AS @range >= 'A' and @range<='Z' GO CREATE RULE Waluta AS @list IN ('PLN', 'EUR', 'USD')
Rysunek 3.13. Miejsce reguły w hierarchicznej strukturze obiektów bazy danych
Wszystkie utworzone w tym podrozdziale obiekty nie znalazły (w prezentowanych do tej pory przykładach) zastosowania w tworzeniu elementów schematu relacyjnego. Typ użytkownika może zostać wykorzystany podczas tworzenia lub modyfikowania tabel. Ten drugi przypadek jest zilustrowany przykładem. ALTER TABLE Dzialy ADD kod litery
Utworzone ograniczenia (wartość domyślna i reguła) mogą być przywiązane zarówno bezpośrednio do typu (rysunek 3.14), jak i do wskazanej kolumny tabeli. Kolumna nie musi być typu użytkownika, ale przypisane ograniczenie powinno być sensowne dla określonego do niej typu podstawowego. Należy zauważyć, że nazwa kolumny musi być nazwą kwalifikowaną, podaną w postaci napisu lub zmiennej znakowej.
170
MS SQL Server. Zaawansowane metody programowania sp_bindefault A,litery GO sp_bindrule Lit,litery GO sp_bindefault A,'Dzialy.kod' GO sp_bindrule Lit,'Dzialy.kod'
Rysunek 3.14. Typ użytkownika z przypisanymi ograniczeniami
Na analogicznych zasadach następuje odwiązanie ograniczeń. Ponieważ zarówno typ użytkownika, jak i typ kolumny mogą mieć przypisane tylko jedno ograniczenie każdego rodzaju, to do odwiązania ograniczenia wystarczyło wskazanie jego rodzaju oraz nazwy obiektu, z którego jest ono zdejmowane. sp_unbindefault 'Dzialy.kod' GO sp_unbindefault litery GO sp_unbindrule 'Dzialy.kod' GO sp_unbindrule litery
Należy zauważyć, że odwiązując ograniczenia od kolumny, która jest typu, do którego przypisano te ograniczenia, zachowujemy jej przypisanie do typu. Możemy powiedzieć, że kolumna jest typu użytkownika ze zdjętymi ograniczeniami. Aby można było zachować porządek, nie powinno się wykonywać takich operacji. Jednak aby śledzić zależności między ograniczeniami, typami i kolumnami, możemy skorzystać z narzędzi wizualnych, tak jak to pokazuje rysunek 3.15. Używanie tak definiowanych typów i ograniczeń może stanowić pewne rozwinięcie funkcjonalności serwera, nie jest ono jednak znaczące. Dopiero zastosowanie typów użytkowników tworzonych za pomocą klas języków wyższego rzędu CLR, które pozwala na wprowadzenie obiektowości do relacyjnego serwera baz danych, stanowi rewolucyjne rozszerzenie możliwości bazy danych. Ponieważ do tworzenia tych typów konieczne jest zapoznanie się z rozszerzeniem proceduralnym, przedstawione zostaną one w rozdziale 7.4.
Rozdział 3. Język zapytań SQL w MS SQL Server
171
Rysunek 3.15. Zależności między ograniczeniami a typami i kolumnami tabel
3.7. Tworzenie indeksów Aby wyjaśnić sposób tworzenia i wykorzystania indeksów, utwórzmy tabelę pomocniczą składającą się z trzech kolumn, Nazwisko, Imie, RokUrodz, z tabeli Osoby i zawierającą 14 pierwszych jej wierszy. DROP TABLE Prac GO SELECT Nazwisko, Imie, RokUrodz INTO Prac FROM Osoby WHERE IdOsoby<15 GO SELECT * FROM Prac GO
Po odpytaniu tabeli Prac prostym zapytaniem wybierającym, niezawierającym sortowania, otrzymamy zestaw rekordów, którego fragment zawiera tabela 3.81. Możemy zauważyć, że rekordy zostały wyświetlone w takiej kolejności, w jakiej były wpisywane do tabeli. Tabela 3.81. Skutek wybierania danych z tabeli Prac bez założonego indeksu Nazwisko
Imie
RokUrodz
Kowalski
Jan
1976
Nowak
Karol
1979
…
…
…
Kowalski
Zenon
NULL
Kowalski
Adam
NULL
%procent
NULL
NULL
172
MS SQL Server. Zaawansowane metody programowania
Na tabeli możemy utworzyć różne indeksy. Rozpocznijmy od typu CLUSTERED (grupujący), dla którego określamy nazwę oraz wskazujemy tabelę oraz pole w niej występujące. Ubocznym skutkiem takiej operacji jest to, że zestaw rekordów, wybierany prostym zapytaniem, jest posortowany względem pola definiującego indeks, co widać w tabeli 3.82. CREATE CLUSTERED INDEX Ix_name ON Prac(Nazwisko) GO SELECT * FROM Prac
Tabela 3.82. Skutek wybierania danych z tabeli Prac po założeniu indeksu CLUSTERED na polu Nazwisko Nazwisko
Imie
RokUrodz
%procent
NULL
NULL
…
…
…
Kowalczyk
Jarosław
1982
Kowalski
Jan
1976
Kowalski
Piotr
0
Kowalski
Zenon
NULL
Kowalski
Adam
NULL
…
…
…
Zięba
Andrzej
1970
Aby usunąć indeks, musimy użyć polecenia DROP, wskazując go przez nazwę kwalifikowaną. Obowiązek stosowania nazwy kwalifikowanej mógłby wskazywać na to, że nazwy indeksów muszą być unikalne jedynie w obrębie tabeli, jednak unikalność nazwy dotyczy całej bazy danych. Jeśli po usunięciu indeksu grupującego utworzymy inny, to proste zapytanie wybierające wyświetli rekordy posortowane względem pola nowego indeksu, jak to pokazuje tabela 3.83 DROP INDEX Prac.Ix_name GO CREATE CLUSTERED INDEX Ix_imie ON Prac (imie) GO SELECT * FROM Prac
Tabela 3.83. Skutek wybierania danych z tabeli Prac po założeniu indeksu CLUSTERED na polu Imie Nazwisko
Imie
RokUrodz
%procent
NULL
NULL
Kowalski
Adam
NULL
…
…
…
Nowicki
Jan
1972
Kowalski
Jan
1976
Adamczyk
Janusz
1976
…
…
…
Kowalski
Zenon
NULL
Rozdział 3. Język zapytań SQL w MS SQL Server
173
Jeśli nie usuniemy indeksu grupującego, co symbolizuje zablokowana linia w przykładowym skrypcie, to próba utworzenia kolejnego takiego obiektu dla tej samej tabeli wywoła komunikat o błędzie, świadczący o tym, że na tabeli może zostać utworzony co najwyżej jeden indeks tego rodzaju. --DROP INDEX Prac.Ix_imie GO CREATE CLUSTERED INDEX Ix_imienazwisko ON Prac (imie, nazwisko) GO SELECT * FROM Prac Msg 1902, Level 16, State 3, Line 1 Cannot create more than one clustered index on table 'Prac'. Drop the existing clustered index 'Ix_imie' before creating another.
Jeśli odblokujemy pierwszą z linii kodu, to stwierdzimy, że możliwe jest tworzenie indeksów na dowolnej liście pól tabeli. Oczywiście w tym stanie proste zapytanie wybierające zachowa się tak, jakby została zastosowana klauzula ORDER BY z listą zawierającą wszystkie pola indeksu. Analizując dane zwracane przez zapytania zawarte w tabelach 3.82 i 3.83, możemy ponadto zauważyć, że indeks typu CLUSTERED nie wymaga unikalnych danych w definiującej go kolumnie. Utworzenie indeksu CLUSTERED powoduje fizyczne uporządkowanie rekordów tabeli względem wskazanego pola lub listy pól. Ponieważ taki rodzaj uporządkowania można zrealizować jednocześnie tylko na jeden sposób, powoduje to ograniczenie liczby indeksów grupujących na tabeli do najwyżej jednego. Inne rodzaje indeksów są zorganizowane w postaci hierarchicznej struktury drzewiastej. Zawsze zawierają element początkowy ROOT, wskazujący na elementy podrzędne, i kilka warstw obiektów potomnych, które wskazują położenie obiektów znajdujących się na kolejnym poziomie. Najniższy poziom liści zawiera wskaźniki do rekordów uporządkowane względem cechy definiującej indeks. Można powiedzieć, że na najniższym poziomie mamy dostępną dwukierunkową listę, porządkującą rekordy według pól definiujących indeks. Usuwanie indeksów niegrupujących NONCLUSTERED jest realizowane na takich samych zasadach jak pokazane dla indeksów grupujących. To samo dotyczy ich tworzenia. Należy zaznaczyć, że słowo kluczowe NONCLUSTERED może zostać pominięte w poleceniu tworzącym taki indeks. Ten rodzaj indeksu jest traktowany jako domyślny. Na tabeli można utworzyć co najwyżej 250 indeksów niegrupujących. DROP INDEX Prac.Ux_imienazwisko DROP INDEX Prac.Ix_imienazwisko GO CREATE INDEX Ix_imienazwisko ON Prac (imie, nazwisko) GO SELECT * FROM Prac GO CREATE UNIQUE INDEX Ux_imienazwisko ON Prac (imie, nazwisko) GO SELECT * FROM Prac GO CREATE INDEX Ix_imienazw ON Prac (imie, nazwisko) GO
174
MS SQL Server. Zaawansowane metody programowania
W przypadku indeksów niegrupujących możliwe jest utworzenie na jednej tabeli wielu indeksów. Również możliwe jest wielokrotne utworzenie indeksu na tym samym zestawie pól. W przypadku prezentowanego kodu para kolumn została trzykrotnie zaindeksowana. Kolejnym rodzajem indeksu jest indeks unikalny UNIQUE. Aby możliwe było jego utworzenie, wartości pól, które go definiują, nie mogą się powtarzać, tak samo jak to miało miejsce podczas definiowania ograniczenia tego samego rodzaju. Sprawdzenie tego faktu odbywa się w momencie tworzenia indeksu i jeśli nie jest on prawdziwy, generowany jest komunikat o błędzie. CREATE GO SELECT GO CREATE GO SELECT
INDEX Ix_im ON Prac (imie) * FROM Prac UNIQUE INDEX Ux_im ON Prac (imie) * FROM Prac
Msg 1505, Level 16, State 1, Line 1 The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.Prac' and the index name 'Ux_im'. The duplicate key value is (Jan). The statement has been terminated.
Indeksy unikalne mogą być tworzone zarówno jako grupujące CLUSTERED, jak i niegrupujące NONCLUSTERED. Utworzone definicje indeksów są przechowywane w hierarchicznej strukturze bazy danych, na poziomie dzieci tabeli, na rzecz której działają (rysunek 3.16). Z tego miejsca możliwe jest wyświetlenie informacji o ich właściwościach. Rysunek 3.16. Położenie definicji indeksów w strukturze bazy danych
Podstawową zakładką okna właściwości jest zakładka General, pokazana na rysunku 3.17, na której możemy dodać lub usunąć kolumny z definicji indeksu oraz ustalić dla każdej porządek sortowania. Możemy również ustalić rodzaj indeksu, a także określić,
Rozdział 3. Język zapytań SQL w MS SQL Server
175
Rysunek 3.17. Właściwości indeksu, zakładka General
czy jest on unikalny. Należy zauważyć, że poza omówionymi rodzajami CLUSTERED i NONCLUSTERED pojawił się Primary XML, stosowany na takich samych zasadach jak indeks unikalny, a dedykowany do pól zawierających dane w postaci XML. Indeks typu Spatial jest stosowany do danych opisujących grafikę wektorową, co zostanie omówione później. Kolejna zakładka, przedstawiona na rysunku 3.18, pozwala na ustalenie dodatkowych opcji związanych z wymuszeniem przebudowania indeksu, automatycznym przeliczeniem statystyk, blokowaniem wierszy tabeli oraz stron indeksu. Możliwe jest ustawienie maksymalnego współczynnika zapełnienia filtra. Po przekroczeniu tego współczynnika są dodawane kolejne liście oraz następuje czasowe zablokowanie używania indeksu.
Rysunek 3.18. Właściwości indeksu, zakładka Options
Zakładka Included Columns, pokazana na rysunku 3.19, jest obsługiwana tak samo jak zakładka General, ale tym razem kolumny nie tworzą klucza, nie stanowią pól indeksu, lecz są wykorzystywane w przypadku odwoływania się do niego.
176
MS SQL Server. Zaawansowane metody programowania
Rysunek 3.19. Właściwości indeksu, zakładka Included Columns
Pozostałe zakładki służą do ustalenia miejsca składowania indeksu we wskazanym Filegroup oraz ewentualnego jego partycjonowania fizycznego (rysunek 3.20), dodatkowych opcji dla danych wektorowych, filtra ograniczającego zakres indeksowania, poziomu fragmentacji wymuszającej odświeżenie oraz opcji zdefiniowanych przez użytkownika.
Rysunek 3.20. Właściwości indeksu, zakładka Storage
W zasadzie pominąłem najważniejszą informację dotyczącą indeksów — jakie korzyści płyną z ich tworzenia. Skoro można powiedzieć, że indeksy numerują wiersze względem wskazanej listy cech, to w przypadku wykonywania zapytania wymagającego sortowania według cechy (atrybutu) posiadającej swój indeks silnik bazy danych nie musi przeprowadzić tej operacji, lecz może skorzystać z porządku ustalonego przez indeks. Podobnie w przypadku filtrowania konieczne jest tylko ustalenie indeksów rekordów wyznaczających granice, a pełny zestaw rekordów będzie ustalany na podstawie indeksu. Wynika stąd wniosek, że stosowanie indeksów poprawia wydajność przetwarzania zapytań [6] [7] [31] – [33]. Należy jednak pamiętać, że przyspieszenie to dotyczy
Rozdział 3. Język zapytań SQL w MS SQL Server
177
tylko zapytań wybierających. W przypadku modyfikacji danych stosowanie indeksów powoduje pogorszenie wydajności związane z koniecznością zmodyfikowania indeksów. Dla wstawiania do struktury drzewiastej należy dopisać kolejny wskaźnik, a jeśli nie ma dla niego miejsca, trzeba przesunąć wpisy w liściach, tak aby to było możliwe. Dla aktualizacji należy usunąć wpis ze starego miejsca i wstawić go w nowe, z zastrzeżeniem takim jak dla operacji INSERT. Najmniejsze opóźnienie towarzyszy kasowaniu rekordów, ponieważ wymaga tylko usunięcia wpisu w indeksie. Jak widać, po wykonaniu wielu modyfikacji może się okazać, że na poziomie liści mogą pojawić się „dziury”. Ponadto każdy indeks zajmuje przestrzeń na dysku. Oba fakty prowadzą do wniosku, że należy tworzyć tylko takie indeksy, z których będziemy często korzystać, a ich liczba musi być stosunkowo niewielka. Łatwo wskazać na te pola, dla których indeksy powinny być zawsze tworzone. Są to klucze podstawowe oraz pola z ograniczeniem UNIQUE. W środowisku MS SQL Server, podobnie jak w większości komercyjnych środowisk, na tych polach indeksy są tworzone automatycznie podczas tworzenia ograniczenia [1] [6] [7]. Na PRIMARY KEY tworzony jest unikalny indeks grupujący, a na polach UNIQUE są tworzone unikalne indeksy niegrupujące, co można zobaczyć, analizując rysunek 3.21. Rysunek 3.21. Indeksy tworzone automatycznie
Jak łatwo zauważyć, nie są tworzone automatycznie indeksy na polach kluczy obcych, a w przypadku powszechnie realizowanych złączeń należałoby rozważyć ręczne ich dodanie. Dla każdego z indeksów, które są tworzone ręcznie lub automatycznie, tworzone są statystyki ich wykorzystania (rysunek 3.21), użyteczne przy ocenie poprawności stosowania indeksów oraz ich wpływu na przetwarzanie [34] [35]. Należy zauważyć, że poza indeksami statystyki są generowane dla każdej kolumny tabeli, jednak takie statystyki są mniej szczegółowe. Wróćmy do problematyki tworzenia kluczy obcych. W tym celu utwórzmy dwie tabele Nad i Pod. Obie zawierają dwa pola: pierwsze typu całkowitego, a drugie znakowe. Żadna
z tych tabel nie zawiera ograniczenia klucza podstawowego. Dodatkowo w drugiej z tabel dodano drugie pole całkowite, na którym ustanowiono ograniczenie klucza obcego, które odwołuje się do pierwszej tabeli. Niestety, takie działanie kończy się fiaskiem, o czym świadczy komunikat zamieszczony po skrypcie.
178
MS SQL Server. Zaawansowane metody programowania CREATE TABLE Nad (IdNad int, NadOpis varchar(11)) GO CREATE TABLE Pod (IdPod int, IdNad int, CONSTRAINT FkPod FOREIGN KEY(IdPod) REFERENCES Nad(IdNad), PodOpis varchar(11)) Msg 1776, Level 16, State 0, Line 1 There are no primary or candidate keys in the referenced table 'Nad' that match the referencing column list in the foreign key 'FkPod'. Msg 1750, Level 16, State 0, Line 1 Could not create constraint. See previous errors.
Analizując komunikat, można dojść do prostego rozwiązania, polegającego na utworzeniu pola klucza podstawowego na pierwszym polu tabeli Nad. Takie działanie zapewnia spełnienie warunku wystarczającego dla pola, do którego odwołuje się klucz obcy. Istnieje jednak rozwiązanie alternatywne, polegające na tym, że po utworzeniu tabeli Nad tworzymy dla niej indeks unikalny opierający się na polu IdNad. Takie działanie powoduje spełnienie warunku koniecznego utworzenia klucza obcego. Zdefiniowanie klucza podstawowego jest warunkiem wystarczającym, gdyż jego utworzenie powoduje automatyczne utworzenie indeksu UNIQUE na tym samym polu. DROP TABLE Nad GO CREATE TABLE Nad (IdNad int, NadOpis varchar(11)) GO CREATE UNIQUE INDEX Cx_Nad ON Nad(IdNad) GO CREATE TABLE Pod (IdPod int, IdNad int, CONSTRAINT FkPod FOREIGN KEY(IdPod) REFERENCES Nad(IdNad), PodOpis varchar(11))
Należy pamiętać, że do skutecznego utworzenia klucza musimy zastosować indeks typu UNIQUE, samo określenie indeksu jako grupujący CLUSTERED nie jest wystarczające. Rozważmy teraz ciekawy przykład tworzenia kluczy obcych. Ponownie tworzone są dwie tabele Nad i Pod. Tym razem każda z nich ma trzy pola dwa numeryczne i jedno znakowe. Zmodyfikujmy obie tabele tak, aby na pierwszych polach obu tabel zostały ustanowione pola klucza podstawowego. W kolejnym kroku możemy dodać do definicji każdej z tabel klucz obcy zbudowany na drugim z pól numerycznych, a odwołujący się do tabeli przeciwnej. Otrzymujemy w ten sposób klucze obce cykliczne typu A-B i B-A, co w postaci diagramu przedstawia rysunek 3.22. Rysunek 3.22. Diagram relacyjny dla dwóch tabel połączonych cyklicznie
Pod
Nad
IdPod
IdNad
IdNad
IdPod
PodOpis
NadOpis
Rozdział 3. Język zapytań SQL w MS SQL Server DROP TABLE Pod DROP TABLE Nad GO CREATE TABLE Nad (IdNad int NOT NULL, IdPod int, NadOpis varchar(11)) GO CREATE TABLE Pod (IdPod int NOT NULL, IdNad int, PodOpis varchar(11)) GO ALTER TABLE Nad ADD CONSTRAINT ALTER TABLE Pod ADD CONSTRAINT GO ALTER TABLE Nad ADD CONSTRAINT REFERENCES Pod(IdPod) ALTER TABLE Pod ADD CONSTRAINT REFERENCES Nad(IdNad)
179
pk_Nad PRIMARY KEY(IdNad) pk_Pod PRIMARY KEY(IdPod) fk_Nad FOREIGN KEY(IdPod) fk_Pod FOREIGN KEY(IdNad)
Jeśli spróbujemy teraz usunąć tabele, to w przypadku zastosowania kolejności wykonywania operacji przedstawionej w skrypcie otrzymamy dwa komunikaty o błędach. DROP TABLE Pod DROP TABLE Nad Msg 3726, Level 16, State 1, Line 1 Could not drop object 'Pod' because it is referenced by a FOREIGN KEY constraint. Msg 3726, Level 16, State 1, Line 2 Could not drop object 'Nad' because it is referenced by a FOREIGN KEY constraint.
Zmiana kolejności usuwania nie ma na nic wpływu; komunikaty pojawią się tylko w odwrotnej kolejności. DROP TABLE Nad DROP TABLE Pod
Jak widać, klucze cykliczne skutecznie bronią tabel przed usunięciem. Aby wykonać taką operację, należy najpierw usunąć ograniczenia kluczy obcych, a w drugim kroku skasować tabele. ALTER TABLE Nad DROP CONSTRAINT fk_Nad ALTER TABLE Pod DROP CONSTRAINT fk_Pod GO DROP TABLE Nad DROP TABLE Pod
Zamiast wykorzystywać ograniczenia klucza podstawowego do realizacji klucza obcego, można użyć indeksów typu UNIQUE utworzonych na obu pierwszych polach tabel. Tak jak poprzednio, w kolejnym kroku możemy dodać do definicji każdej z tabel klucz obcy odwołujący się do tabeli przeciwnej. ALTER TABLE Nad DROP CONSTRAINT fk_Nad ALTER TABLE Pod DROP CONSTRAINT fk_Pod GO DROP TABLE Nad DROP TABLE Pod
180
MS SQL Server. Zaawansowane metody programowania GO CREATE TABLE Nad (IdNad int NOT NULL, IdPod int, NadOpis varchar(11)) GO CREATE TABLE Pod (IdPod int NOT NULL, IdNad int, PodOpis varchar(11)) GO CREATE UNIQUE INDEX Cx_Nad ON Nad(IdNad) CREATE UNIQUE INDEX Cx_Pod ON Pod(IdPod) GO ALTER TABLE Nad ADD CONSTRAINT fk_Nad FOREIGN KEY(IdPod) REFERENCES Pod(IdPod) ALTER TABLE Pod ADD CONSTRAINT fk_Pod FOREIGN KEY(IdNad) REFERENCES Nad(IdNad)
Na tle tych rozważań rysuje się schemat uniwersalnego skryptu generującego tabele schematu relacyjnego. Główne założenie polega na tym, że ustalenie kolejności tworzenia tabel wynikającego z referencji kluczy obcych jest zawsze pracochłonne, a czasami niemożliwe. Powoduje to, że automat do generowania musiałby być wyposażony w dodatkową logikę. Dlatego o wiele łatwiej jest w pierwszym fragmencie skryptu utworzyć proste tabele, zawierające tylko definicje nazw i typów pól, ewentualnie wzbogacone o ustalenie stosunku do wartości NULL oraz zdefiniowanie wartości domyślnych. Kolejny fragment zawierałby dodanie ograniczeń kluczy podstawowych lub indeksów unikalnych lub ograniczeń UNIQUE. Później następowałoby dodanie kluczy obcych. Całość kończyłoby ustanowienie ograniczeń sprawdzających CHECK. Struktura takiego skryptu została zaprezentowana poniżej. CREATE TABLE T1 (IdT1 int NOT NULL, T1_1 typ NULL | NOT NULL DEFAULT xxx, ... T1_N ....) GO ... CREATE TABLE TN(....) GO ALTER TABLE T1 ADD CONSTRAINT pk_T1 PRIMARY KEY(T1_1) ... GO --Zamiast lub oprócz CREATE UNIQUE INDEX Ux_T1 ON T1(T1_1) --W tym miejscu również mogą się pojawić inne indeksy --(CLUSTERED, o ile to możliwe, lub NONCLUSTERED) ... GO ALTER TABLE T1 ADD CONSTRAINT fk_T1 FOREIGN KEY(T1_k) REFERENCES TL(TL_m) ... GO ALTER TABLE T1 ADD CONSTRAINT ck_T1 CHECK(...)
Rozdział 3. Język zapytań SQL w MS SQL Server
181
Powróćmy do głównego nurtu poświęconego możliwościom tworzenia indeksów i ich łączenia. Przede wszystkim możliwe jest utworzenie unikalnego indeksu grupującego. Liczba indeksów unikalnych na tabeli nie jest ograniczona. CREATE TABLE Nad (IdNad int NOT NULL, IdPod int, NadOpis varchar(11)) GO CREATE TABLE Pod (IdPod int NOT NULL, IdNad int, PodOpis varchar(11)) GO CREATE UNIQUE CLUSTERED INDEX CREATE UNIQUE INDEX Ux_Nad ON CREATE UNIQUE CLUSTERED INDEX CREATE UNIQUE INDEX Ux_Pod ON
Cx_Nad ON Nad(IdNad) Nad(NadOpis) Cx_Pod ON Pod(IdPod) Pod(PodOpis)
Możliwe jest utworzenie unikalnego indeksu niegrupującego, jednak indeks typy CLUSTERED nie wymaga unikalnych wartości pól. DROP TABLE Nad DROP TABLE Pod GO CREATE TABLE Nad (IdNad int NOT NULL, IdPod int, NadOpis varchar(11)) GO CREATE TABLE Pod (IdPod int NOT NULL, IdNad int, PodOpis varchar(11)) GO CREATE CLUSTERED INDEX Cx_Nad ON CREATE UNIQUE NONCLUSTERED INDEX CREATE CLUSTERED INDEX Cx_Pod ON CREATE UNIQUE NONCLUSTERED INDEX
Nad(IdNad) Ux_Nad ON Nad(NadOpis) Pod(IdPod) Ux_Pod ON Pod(PodOpis)
Dla każdego z tworzonych indeksów możliwe jest ustalenie kierunku sortowania, niezależnie dla każdej z kolumn ten indeks definiujących. Możemy to sprawdzić, tworząc indeks o sortowaniu malejącym na polu Imie tabeli Prac, a następnie wykonując zapytanie wybierające, którego rezultat pokazuje tabela 3.84. DROP INDEX Prac.Ix_im GO CREATE CLUSTERED INDEX Ix_im ON Prac (Imie DESC) GO SELECT * FROM Prac
Utworzenie indeksu na parze pól Nazwisko, Imie z przeciwnymi kierunkami sortowania spowoduje, że dane zostaną wyświetlone zgodnie z zawartością tabeli 3.85. Należy pamiętać, że ponieważ poprzedni indeks był grupujący, konieczne jest usunięcie jego pierwotnej definicji.
182
MS SQL Server. Zaawansowane metody programowania
Tabela 3.84. Skutek wybierania danych z tabeli Prac po założeniu indeksu grupującego z sortowaniem malejącym na polu Imie Nazwisko
Imie
RokUrodz
KOW
Piotr
1967
KOWALSKI
Piotr
0
JANIK
Paweł
1971
...
...
...
ZIĘBA
Andrzej
1972
KOWALSKI
Adam
1980
DROP INDEX Prac.Ix_im GO CREATE CLUSTERED INDEX Ix_im ON Prac (Nazwisko ASC, Imie DESC) GO SELECT * FROM Prac
Tabela 3.85. Skutek wybierania danych z tabeli Prac po założeniu indeksu grupującego z sortowaniem rosnącym na polu Nazwisko oraz malejącym na polu Imie Nazwisko
Imie
RokUrodz
ADAMCZYK
Janusz
1976
…
…
…
KOWALSKI
Piotr
0
KOWALSKI
Jerzy
1970
KOWALSKI
Jan
1976
KOWALSKI
Adam
1980
…
…
…
ZIĘBA
Andrzej
1972
Aby ominąć ograniczenia związane z liczbą kolumn (nie więcej niż 16) lub rozmiarem indeksu (do 900 bajtów na wiersz), możliwe jest dodanie do indeksu typu NONCLUSTERED kolumn niekluczowych. Liczba kolumn niekluczowych nie może być większa niż 1023, a ich rozmiar nie może przekroczyć 2GB. W celu poznania tego mechanizmu utwórzmy tabelę o trzech polach: IdPliku int — 4B Nazwa varchar(20) — 20B Opis varchar(1000) — 1000B
Jak widać, sama trzecia kolumna przekracza dopuszczalny rozmiar, a łączna objętość danych jednego wiersza wynosi 1024. Dla takiej tabeli spróbujmy utworzyć indeks zawierający wszystkie trzy kolumny. Powoduje to wygenerowanie komunikatu z ostrzeżeniem. DROP TABLE Plik GO CREATE TABLE Plik
Rozdział 3. Język zapytań SQL w MS SQL Server
183
(IdPliku int, Nazwa varchar(20), Opis varchar(1000)) GO CREATE INDEX Ix_plik ON Plik(IdPliku, Nazwa, Opis) Warning! The maximum key length is 900 bytes. The index 'Ix_pl k' has maximum length of 1024 bytes. For some combination of large values, the insert/update operation will fail.
Pomimo wszystko indeks został utworzony, a informacja dotyczy tylko tego, że dla niektórych kombinacji dużych danych (w tym przypadku pole Opis) operacje wstawiania, modyfikacji mogą zakończyć się niepowodzeniem. Aby temu zaradzić, możemy utworzyć indeks na dwóch pierwszych polach, a pole powodujące przekroczenie rozmiaru dołączyć w klauzuli INCLUDE. CREATE INDEX Ix_plik ON Plik(IdPliku,Nazwa) INCLUDE (Opis)
Przy takiej definicji indeks utworzony został bez ostrzeżeń. Takie samo postępowanie należy przeprowadzić, kiedy wszystkie kolumny są mniejsze od ograniczenia, ale ich suma już je przekracza. Z reguły dołączane są pola o największym rozmiarze. Podczas tworzenia indeksu typu NONCLUSTERED z dołączanymi kolumnami obowiązują następujące zasady: indeks musi zawierać przynajmniej jedną kolumnę klucza; kolumny dołączone mogą być definiowane dla indeksów tworzonych na
tabelach i perspektywach; dołączać można kolumny wszystkich typów, z wyjątkiem text, ntext oraz image; w definicji pól znakowych nie można stosować ograniczenia długości o postaci (max); kolumny obliczane muszą być deterministyczne; kolumna nie może występować równocześnie jako kluczowa i niekluczowa; kolumna nie może zostać powtórzona na liście INCLUDE.
Rozważmy na przykładzie ograniczenia dotyczące kolumn obliczanych. W tym celu tworzymy tabelę o trzech polach całkowitych oraz dwóch polach wyznaczanych na podstawie iloczynu dwóch pól prostych oraz pola zasilanego datą systemową. DROP TABLE Oblicz GO CREATE TABLE Oblicz (IdOblicz int, a int, b int, c AS a*b, data AS getdate()) GO
184
MS SQL Server. Zaawansowane metody programowania
Utworzenie zarówno indeksu zawierającego bezpośrednio w definicji pole iloczynu, jak i takiego, gdzie to pole jest dołączane, kończy się powodzeniem. CREATE INDEX Ix_Oblicz1 ON Oblicz(IdOblicz,c) GO CREATE INDEX Ix_Oblicz2 ON Oblicz(IdOblicz) INCLUDE (c)
Natomiast próba dołączenia do indeksu pola zawierającego datę systemową kończy się niepowodzeniem, co potwierdza pojawiający się komunikat. CREATE INDEX Ix_Oblicz3 ON Oblicz(IdOblicz) INCLUDE (data) Msg 2729, Level 16, State 1, Line 1 Column 'data' in table 'Oblicz' cannot be used in an index or statistics or as a partition key because it is non-deterministic.
Taki sam skutek pojawi się również, kiedy kolumna niedeterministyczna będzie elementem indeksu. Zestaw rekordów zawartych w tabeli nie musi podlegać indeksowaniu w całości. Możemy ograniczyć go na skutek zastosowania klauzuli WHERE w definicji indeksu. W przykładzie zrealizowano to dla kolumny daty, gdzie proces ten jest wykonywany dla dat po 01-01-2005. Jest to uzasadnione praktycznie, jeśli będziemy przetwarzać tylko dane uznane za bieżące i nie będziemy się odwoływać do danych historycznych. Podział na takie części jest oczywiście kwestią umowy. DROP TABLE filtr GO CREATE TABLE filtr (IdFiltr int, a int, data date DEFAULT getdate()) GO CREATE INDEX Ix_filtr1 ON Filtr(data) GO CREATE INDEX Ix_filtr2 ON Filtr(data) WHERE data>'20050101'
Jeśli jednak spróbujemy określić zmieniany dynamicznie warunek, ustalający, że dane historyczne, niepodlegające indeksowaniu, są odległe o rok od daty bieżącej, to otrzymamy komunikat o błędzie. CREATE INDEX Ix_filtr3 ON Filtr(data) WHERE DATEDIFF(year, data, getdate())<1 Msg 10735, Level 15, State 1, Line 3 Incorrect WHERE clause for filtered index 'Ix_filtr3' on table 'Filtr'.
W klauzuli WHERE indeksu mogą pojawić się warunki proste, praktycznie ograniczające się do porównania ze stałą.
Rozdział 3. Język zapytań SQL w MS SQL Server
185
CREATE INDEX Ix_filtr3 ON Filtr(data) WHERE data IS NOT NULL
Poza tabelami indeksowaniu mogą podlegać również perspektywy. Jeśli jednak utworzymy obiekt tej klasy bez żadnych dodatkowych dyrektyw, to próba indeksowania zakończy się błędem. DROP VIEW Prac GO CREATE VIEW Prac AS SELECT IdOsoby, Nazwisko, RokUrodz FROM Osoby GO CREATE INDEX Ix_ID ON Prac(IdOsoby) GO CREATE INDEX Ix_Nazw ON Prac(Nazwisko, RokUrodz) Msg 1939, Level 16, State 1, Line 1 Cannot create index on view 'Prac' because the view is not schema bound.
Analizując komunikat, widzimy, że konieczne jest zastosowanie dyrektywy SCHEMABINDING wiążącej w sposób sztywny definicje kolumn perspektywy z kolumnami tabeli. Dyrektywa ta wymusza stosowanie kwalifikowanych nazw obiektów źródłowych w zapytaniu wybierającym, określającym obsługiwany zestaw rekordów. W tym przypadku indeksowanie kończy się sukcesem. DROP VIEW Prac GO CREATE VIEW Prac WITH SCHEMABINDING AS SELECT IdOsoby, Nazwisko, RokUrodz FROM dbo.Osoby GO CREATE UNIQUE CLUSTERED INDEX Ix_ID ON Prac(IdOsoby) GO CREATE INDEX Ix_Nazw ON Prac(Nazwisko, RokUrodz)
Należy pamiętać, że pierwszym utworzonym dla perspektywy indeksem musi być UNIQUE CLUSTERED, co praktycznie wymusza konieczność wyświetlania przez nią kolumny klucza podstawowego. Miejsce indeksów w hierarchicznej strukturze bazy danych pokazuje rysunek 2.23. Poza tymi, które utworzono przedstawionym skryptem, zawiera on kilka innych, aby podkreślić, że może być ich wiele i mogą mieć różne cechy. Aby można było skutecznie tworzyć indeksy dla perspektywy, muszą one spełniać kilka warunków: nie mogą zawierać funkcji agregujących, użytkownika, CLR i należących do grupy FullTextSearch; muszą być deterministyczne;
186
MS SQL Server. Zaawansowane metody programowania
Rysunek 3.23. Indeksy dla perspektywy Prac
nie mogą zawierać samozłączeń, podzapytań, operatorów UNION; nie mogą obsługiwać typów LOB; nie mogą zawierać dyrektyw TOP i DISTINCT.
Na utworzonych wcześniej indeksach możemy wykonywać kilka operacji modyfikujących ich stan. ALTER INDEX {nazwa_indeksu | ALL} ON REBUILD | DISABLE | REORGANIZE
Dostępne opcje odpowiadają za: REBUILD — wskazuje, że indeks zostanie przebudowany z zastosowaniem
wszystkich ustawień parametrów, jakich dokonano podczas jego tworzenia; jest to równoważne zastosowaniu DBCC DBREINDEX, powoduje, że nieaktywny DISABLE indeks (indeksy) staje się aktywny ENABLE, przebudowa indeksu grupującego CLUSTERED nie pociąga za sobą przebudowy skojarzonych z nim indeksów niegrupujących, chyba że zastosowano opcję ALL; DISABLE — oznacza indeks jako nieaktywny, nieużywany przez silnik bazy,
pozostawia niezmienioną jego definicję; zmiana na nieaktywny dla indeksu typu CLUSTERED powoduje zablokowanie dostępu przez użytkownika do tabeli, na której został ustanowiony; aby odblokować indeks, można zastosować polecenia ALTER INDEX REBUILD lub CREATE INDEX WITH DROP_EXISTING;
REORGANIZE — wskazuje, że najniższy poziom indeksu (liście) zostaną przeorganizowane, jest to równoważne zastosowaniu DBCC INDEXDEFRAG;
taka modyfikacja indeksu zawsze odbywa się w trybie online; oznacza to, że długoterminowe blokady tabeli nie są utrzymywane, co pozwala na modyfikowanie danych podczas wykonywania reorganizacji. Przykładowy skrypt pokazuje sekwencje operacji wykonanych na indeksie: pełną przebudowę, czasowe wyłączenie oraz reorganizację liści. Ostatnie z poleceń powoduje pojawienie się komunikatu o błędzie, ponieważ proces reorganizacji wymaga, aby przetwarzany indeks był aktywny — w stanie ENABLE. Możemy tego dokonać, wykonując przebudowę indeksu, która poza zasadniczym zadaniem powoduje uaktywnienie indeksu.
Rozdział 3. Język zapytań SQL w MS SQL Server
187
ALTER INDEX Ix_im ON Prac REBUILD GO ALTER INDEX Ix_im ON Prac DISABLE GO ALTER INDEX Ix_im ON Prac REORGANIZE Msg 1973, Level 16, State 1, Line 1 Cannot perform the specified operation on disabled index 'Ix_im' on table 'Prac'.
Należy pamiętać, że proces reorganizacji, polegający na komasowaniu wpisów na poziomie liści, tak aby je możliwie zapełnić, nie powoduje wyeliminowania dziur. Można wręcz powiedzieć, że proces ten powoduje, że będą one stanowiły, o ile to możliwe, pełne bloki najniższego poziomu drzewa, a jeśli nie jest to możliwe, to będą do nich jak najbardziej zbliżone. Jeżeli proces blokowania zostanie wykonany na wszystkich indeksach perspektywy lub tabeli, pojawi się seria ostrzeżeń. Występują one tylko dlatego, że pierwszym blokowanym indeksem jest indeks grupujący CLUSTERED, co pociąga za sobą sukcesywne blokowanie pozostałych. ALTER INDEX ALL ON Prac REBUILD GO ALTER INDEX ALL ON Prac DISABLE GO Warning: Index 'Ix_im' on table 'Prac' was disabled as a result of disabling the clustered index on the table. Warning: Index 'Ux_im' on table 'Prac' was disabled as a result of disabling the clustered index on the table. Warning: Index 'Ix_imienazwisko' on table 'Prac' was disabled as a result of disabling the clustered index on the table. Warning: Index 'Ux_imienazwisko' on table 'Prac' was disabled as a result of disabling the clustered index on the table. Warning: Index 'Ix_imienazw' on table 'Prac' was disabled as a result of disabling the clustered index on the table.
Utworzenie i konserwowanie zestawu indeksów jest czynnością bardzo ważną, mającą bardzo duży, w większości przypadków decydujący wpływ na wydajność przetwarzania. Pomimo że są one bardzo dobrze opisane w publikacjach dotyczących zarówno programowania [7] [35], jak i administrowania bazą danych [31], to poprawne posługiwanie się nimi jest w dalszym ciągu bliższe sztuce niż pracy inżynierskiej. Programiście i administratorowi pozostaje tylko śledzenie statystyk i reagowanie na zawarte w nich informacje lub nieprzewidziane spadki wydajności.
188
MS SQL Server. Zaawansowane metody programowania
3.8. Inne narzędzia klienckie MS SQL Server Ważnym elementem jest możliwość stosowania zamiast bardzo rozbudowanej końcówki klienta SQL Server Management Studio lekkich końcówek klienckich uruchamianych z linii poleceń. Skojarzoną z aktualnymi wersjami SQL Server aplikacją jest SQLCMD. Okienko, w którym została ona uruchomiona i w którym następnie wykonane zostało zapytanie odpytujące słownik systemowy INFORMATION_SCHEMA.TABLES, jest przedstawione na rysunku 3.24. Należy zauważyć, że znakiem do zamknięcia polecenia, a jednocześnie sygnałem do rozpoczęcia przetwarzania zapytania lub skryptu SQL jest słowo kluczowe GO. Rysunek 3.24. Skutek wykonania zapytania za pomocą końcówki klienckiej SQLCMD
Aplikacja ta zawiera szereg opcji, których znaczenie przedstawiono w postaci metanotacji. Sqlcmd [-U użytkownik] [-P hasło] [-S serwer] [-H komputer_lub_IP] [-E użyj_zaufanego_połączenia] [-d baza_danych] [-l opóźnienie_logowania] [-t opóźnienie_zapytania] [-h nagłówek] [-s separator_dla_konkatenacji] [-w szerokość_okna] [-a wielkość_pakietu] [-e wartość_wejściowa ] [-I dostępne_identyfikatory_ujęte_w_cudzysłów] [-c słowo_kończące_polecenie] [-L[c] lista_serwerów[czyszczenie_wyjścia]] [-q "zapytanie_lub_skrypt_SQL"] [-Q "zapytanie_lub_skrypt_SQL_i_wyjście"] [-m poziom_błędów] [-V poziom_błędu] [-W pomiń_końcowe_spacje] [-u wyjście_unicode] [-r[0|1] komunikaty_dla_standardowego_błędu_użytkownika] [-i zbiór_wejściowy] [-o zbiór_wyjściowy] [-z nowe_hasło] [-f | i:[,o:]] [-Z nowe_hasło_i_wyjście] [-k[1|2] usuń_[zamień]_znaki_sterujące] [-y zmienna_ilość znaków_na_szerokość] [-Y ustalona_ilość_znaków_na_szerokość] [-p[1] wyświetl_statystyki [, format]] [-R zastosuj_regionalne_ustawienia_klienta] [-b przy_błędzie_zakończ_przetwarzanie] [-v zmienna = "wartość"...] [-A użyj_dedykowanego_połączenia_administratora] [-X[1] zablokowane_polecenia, skrypt_startowy, zmienne_środowiskowe [i_wyjście]] [-x podstaw_za_zablokowane_zmienne] [-? Wyświetla_pokazaną_informację]
Rozdział 3. Język zapytań SQL w MS SQL Server
189
Starszą, wciąż aktywną aplikacją kliencką SQL Server jest OSQL, który jest następcą już niewspieranej aplikacji ISQL, pochodzącej z wersji 2000. Pomimo utrzymania wsparcia aplikacja ta jest schyłkowa i należy się spodziewać rychłego jej wycofania. Działanie jej jest analogiczne do SQLCMD, co widać na rysunku 3.25. Rysunek 3.25. Skutek wykonania zapytania za pomocą końcówki klienckiej OSQL
Zestaw parametrów tej aplikacji jest bardzo podobny do SQLCMD, co przedstawia zamieszczony poniżej metakod. OSQL [-?] | [-L] | [{{-U użytkownik [-P hasło]} | –E} [-S nazwa_serwera[\nazwa_instancji]] [-H komputer] [-d nazwa_bazy] [-l opóźnienie_logowania] [-t opóźnienie_zapytania] [-h nagłówek_po_liczbie_linii] [-s separator_kolumn] [-w szerokość_kolum] [-a rozmiar_pakietu] [-e] [-I] [-D nazwa_źródła_danych] [-c słowo_kończące_polecenie] [-q "zapytanie"] [-Q "zapytanie_i_wyjście"] [-n] [-m poziom_błędu] [-r {0 | 1}] [-i zbiór_wejściowy] [-o zbiór_wyjściowy] [-p] [-b] [-u] [-R] [-O] [-X[1]]]
Znaczenie wybranych parametrów obu aplikacji można dokładniej przedstawić w postaci listy, gdzie kursywą zapisano parametry, które należy ustawić dla opcji: ? — wyświetla parametry programu; L — wyświetla listę serwerów, na których zainstalowano MS SQL (lokalny,
grupa robocza, domena); E — używa do połączenia autoryzacji dziedziczonej po systemie; l opóźnienie_logowania — określa w sekundach czas na zrealizowanie
połączenia z serwerem (domyślnie 8 s); t opóźnienie_zapytania — określa w sekundach czas na zrealizowanie
polecenia (domyślnie nieograniczony); h nagłówek_po_liczbie_linii — określa liczbę linii zestawu wynikowego, po których wyświetlany jest ponownie nagłówek; -h-1 (bez spacji) określa
brak nagłówków;
190
MS SQL Server. Zaawansowane metody programowania s separator_kolumn — określa znak separatora kolumn (domyślnie pusty); w szerokość_kolum — określa liczbę znaków w linii na wyświetlaczu (domyślnie 80); a rozmiar_pakietu — określa wielkość pakietu danych z przedziału od 512b do 65535b (domyślnie taka jak w ustawieniach serwera); dla kopiowania masowego zalecana 8192b; e — przesyła dane wejściowe na wyjście (echo); I — ustanawia opcję połączenia jako QUOTED_IDENTIFIER; D nazwa_źródła_danych — pozwala na połączenie za pomocą definicji źródła ODBC (działa tylko ze źródłami MS SQL); c słowo_kończące_polecenie — określa słowo kluczowe kończące polecenie (domyślnie GO); q "zapytanie" — wykonuje zapytanie dane parametrem, ale nie kończy aplikacji (skrypt nie powinien zawierać słowa GO); możliwe jest stosowanie zmiennych %variable lub zmiennych środowiska %variable%, np.: SET table = sysobjectsosql /q "SELECT * FROM %table%";
Q "zapytanie" — wykonuje zapytanie dane parametrem i kończy pracę aplikacji; n — usuwa numerację i symbol zachęty > z wyników; m poziom_błędu — parametryzuje sposób wyświetlania komunikatów o błędzie,
wyświetlane są komunikaty należące do grupy o wskazanym lub wyższym numerze, blokuje informacje dla błędów o niższym poziomie; dla -m-1 (bez spacji) blokowane są wszystkie komunikaty; r {0 | 1} — przekierowuje komunikaty o błędach na standardowe wyjście (stderr); dla 0 tylko błędy o poziomie od 11 lub wyższych; dla 1 są wyświetlane wszystkie komunikaty, łącznie z generowanym poleceniem PRINT; i zbiór_wejściowy — wskazuje plik zawierający skrypt SQL, synonimem jest znak <; o zbiór_wyjściowy — wskazuje plik, w którym pojawią się wyniki przetwarzania skryptu SQL, synonimem jest znak >; p — wyświetla statystyki wydajności; b — wskazuje, że po wystąpieniu błędu aplikacja ma zakończyć działanie i powrócić do systemu operacyjnego, zwracając DOS ERRORLEVEL; wartość ta wynosi 1 dla poziomu błędu powyżej 10, w przeciwnym wypadku 0; u — wskazuje plik, w którym pojawią się wyniki przetwarzania skryptu SQL w formacie UNICODE; R — wskazuje, że sterownik SQL Server ODBC używa ustawień klienta
w przypadku konwersji typów daty i czasu do łańcucha; O — wskazuje, że aby zachować zgodność ze starszym ISQL, są blokowane określone cechy OSQL:
Rozdział 3. Język zapytań SQL w MS SQL Server
191
EOF w przetwarzaniu wsadowym; automatyczne skalowanie szerokości okna konsoli; rozbudowane komunikaty; wskazywanie poziomu błędu przez ustawienie stałej wartości dla DOS ERRORLEVEL wynoszącej -1; X[1] — blokuje polecenia ED oraz !!, kiedy aplikacja jest wykonywana z pliku
wsadowego (batch). Posługiwanie się aplikacjami klienckimi uruchamianymi z linii poleceń ma duże znaczenie praktyczne i nie jest, jak wiele osób sądzi, popisem konserwatywnego programisty, który lekceważy narzędzia wizualne jako przeznaczone dla laików. Jest ważnym elementem pozwalającym na wykonywanie zadań administracyjnych, szczególnie gdy korzystamy z systemowego terminarza Scheduler. Również wtedy, gdy pracę tę chcemy wykonać w trybie cichym lub wsadowym. Nie bez znaczenia jest fakt, że użycie dedykowanego połączenia administratora pozwala na pominięcie elementów uruchamianych podczas logowania, co w przypadku ich błędnego działania blokuje możliwość połączenia się z serwerem inną drogą.
192
MS SQL Server. Zaawansowane metody programowania
Rozdział 4.
Problemy rozwiązywane z wykorzystaniem SQL Od wielu lat w trakcie zajęć przedstawiam studentom zadania do rozwiązania w ramach stałego konkursu związanego z SQL. Chcę w ten sposób pokazać studentom zakres problemów, których rozwiązaniem jest pojedyncze zapytanie wybierające. Przede wszystkim chcę, aby zobaczyli, że zadania te mogą być naprawdę złożone i że ich rozwiązanie wymaga dużej biegłości w budowaniu zapytań i przestawienia sposobu myślenia na hierarchiczny. Ponadto z reguły wyobrażamy sobie, że skomplikowane zapytanie musi dotyczyć struktury złożonej z wielu tabel, które należy połączyć, tymczasem w konkursie dominują zadania dotyczące bardzo prostych diagramów, nierzadko ograniczonych do jednej tabeli. Chciałbym, aby teraz Czytelnik mógł poznać część tych zadań. Prezentowane problemy były rozwiązywane przez studentów w czasie kilku edycji konkursu. Aby podkreślić stopień trudności zadań, powiem, że od mniej więcej 100-osobowej grupy wykładowej otrzymywałem zaledwie dwa lub trzy rozwiązania, przy czym w jednym konkursie były do rozwiązania 3 lub 4 problemy i rzadko kiedy jeden student rozwiązywał więcej niż jeden problem. Na potrzeby tej książki rozwiązania zostały przeze mnie skorygowane i przedstawione w formie zgodnej z przyjętymi tutaj założeniami formalnymi. Pierwszy przykład to zadanie kinowe. Wyobraźmy sobie rząd krzeseł, dla ułatwienia — nieograniczony. Przychodzący widzowie wykupują bilety, co jest odnotowywane w postaci wpisu do tabeli, który reprezentuje numer zajętego siedzenia. Po jakimś czasie część miejsc jest zajęta, ale pozostają między nimi luki, które mogą być zajęte. W tym momencie przychodzi grupa składająca się z n osób — przyjaciele, dobrzy znajomi, wycieczka — do wyboru. Życzą sobie, aby ich miejsca sąsiadowały ze sobą. Nam jako właścicielom kina zależy na jak najściślejszym zapełnieniu miejsc, dlatego chcemy, aby ta rezerwacja jak najściślej zapełniała nasze luki w numeracji. Jak widać, do opisania tego problemu wystarczy jedna tabela, zawierająca jedną kolumnę typu całkowitoliczbowego. Skrypt ją tworzący i uzupełniający przykładowymi wpisami pokazano poniżej. DROP TABLE krzeslo; GO CREATE TABLE krzeslo (nr int); GO
194
MS SQL Server. Zaawansowane metody programowania INSERT INSERT INSERT INSERT INSERT INSERT INSERT …
INTO INTO INTO INTO INTO INTO INTO
krzeslo krzeslo krzeslo krzeslo krzeslo krzeslo krzeslo
VALUES VALUES VALUES VALUES VALUES VALUES VALUES
(5); (150); (215); (54); (58); (23); (42);
Zadanie można postawić w następujący sposób. Należy znaleźć najmniejszą lukę w numeracji, w którą zmieszczą się wszyscy członkowie grupy, której liczebność wynosi n. Spróbujmy rozbić ten problem na elementarne kroki. W pierwszym musimy wyznaczyć liczebność wszystkich luk w numeracji. W tym celu łączymy tabelę samą ze sobą (samozłączenie), co wymusza stosowanie aliasowania tabeli przez k1 i k2 oraz stosowanie kwalifikowanej nazwy pola. Gdybyśmy nie podali warunku, otrzymalibyśmy iloczyn kartezjański, każdy element z każdym, symetryczną macierz kwadratową. Możemy się ograniczyć tylko do luk liczonych od krzesła o niższym numerze w górę. W tym celu wprowadzamy warunek nierównościowy k2.nr>k1.nr, co jest równoważne wyznaczeniu macierzy trójkątnej górnej, czyli wszystkich elementów leżących powyżej diagonali. Taki mechanizm jest bardzo często stosowany w złożonych zadaniach analitycznych [1] [2] [7] [36] – [41]. Ponieważ musimy wyeliminować luki w numeracji odwołujące się do krzeseł, między którymi nie ma zajętych miejsc, wyznaczmy minimalną wartość większą od wskazanego numeru. W takim przypadku krzesło początkowe pocz jest określone przez k1.nr, a koniec przedziału kon wyznacza funkcja agregująca MIN. Użycie funkcji agregującej wymusza stosowanie grupowania względem pola, na którym nie jest ona wyznaczana. SELECT k1.nr AS pocz, MIN(k2.nr) AS kon FROM krzeslo AS k1 JOIN krzeslo AS k2 ON k2.nr>k1.nr GROUP BY k1.nr;
Takie zapytanie staje się teraz źródłem dla nadrzędnego zapytania, w którym wskazujemy krzesło początkowe oraz przedział numeracji. Dodatkowo uzupełniamy je o warunek filtrujący tylko takie przedziały, które są większe niż liczebność grupy; w przykładzie przyjęto 8. Skutek działania tego zapytania zawiera tabela 4.1. SELECT pocz, kon-pocz AS liczba FROM (SELECT k1.nr AS pocz, MIN(k2.nr) AS kon FROM krzeslo AS k1 JOIN krzeslo AS k2 ON k2.nr>k1.nr GROUP BY k1.nr)AS xxx WHERE kon-pocz > 8;
Tabela 4.1. Skutek wykonania zapytania wyznaczającego wszystkie przedziały większe od liczebności grupy pocz
liczba
6
17
23
19
44
10
58
21
79
10
92
58
…
…
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
195
Ponieważ interesuje nas najlepsze dopasowanie grupy do wolnych miejsc, musimy teraz wyznaczyć wartość minimalną spośród wszystkich większych niż parametr. Prowadzi to do sformułowania zapytania skalarnego, które zostanie użyte w klauzuli filtrującej WHERE nadrzędnego zapytania. Powoduje ono ostateczne sformatowanie wyświetlanych danych, na które składa się numer krzesła wyznaczającego początek przedziału oraz liczba wolnych miejsc po nim występujących. Zapytanie to jako źródło wykorzystuje ponownie już omówione zapytanie wyznaczające macierz trójkątną górną. Ostateczny wynik, będący rozwiązaniem problemu, pokazuje tabela 4.2. Należy zauważyć, że zmiana liczebności grupy poszukującej wolnych miejsc wymaga tylko zmiany tej wartości w jednym miejscu zapytania. SELECT pocz, kon-pocz AS liczba FROM (SELECT k1.nr AS pocz, MIN(k2.nr) AS kon FROM krzeslo AS k1 JOIN krzeslo AS k2 ON k2.nr>k1.nr GROUP BY k1.nr)AS xxx WHERE kon-pocz= (SELECT MIN(kon-pocz) AS liczba FROM (SELECT k1.nr AS pocz, MIN(k2.nr) AS kon FROM krzeslo AS k1 JOIN krzeslo AS k2 ON k2.nr>k1.nr GROUP BY k1.nr)AS xxx WHERE kon-pocz > 8)
Tabela 4.2. Skutek wykonania zapytania rozwiązującego zadanie kinowe pocz
liczba
44
10
79
10
Rozpatrzmy problem wymagający nieco bardziej złożonego schematu relacyjnego. Wyobraźmy sobie producenta urządzeń, do których wykonania potrzebny jest pewien zestaw części, przy czym różne urządzenia mogą zawierać jakąś liczbę takich samych elementów. Części składowe są produkowane przez różnych dostawców; różni dostawcy mogą też produkować tę samą część. Wstępnie załóżmy, że części reprezentowane są przez dwie tabele — jedną dla urządzeń, drugą dla dostawców — a wspólną informacją jest ich nazwa (w praktyce będzie to raczej symbol). W wyniku takich założeń otrzymujemy diagram składający się z dwóch niezależnych struktur zawierających po trzy tabele rysunek 4.1.
Rysunek 4.1. Schemat relacyjny opisujący urządzenia i dostawców części
Utworzenie tabel schematu może być skutkiem uruchomienia poniższego skryptu SQL.
196
MS SQL Server. Zaawansowane metody programowania DROP TABLE UrzadzenieCzesci GO DROP TABLE Urzadzenie GO DROP TABLE CzesciU GO DROP TABLE DostawcaCzesci GO DROP TABLE Dostawca GO DROP TABLE CzesciD GO CREATE TABLE Urzadzenie (IdUrz int IDENTITY(1,1) PRIMARY KEY, Nazwa varchar(30)) GO CREATE TABLE CzesciU (IdCz int IDENTITY(1,1) PRIMARY KEY, Nazwa varchar(30)) GO CREATE TABLE UrzadzenieCzesci (Id int IDENTITY(1,1) PRIMARY KEY, IdUrz int FOREIGN KEY REFERENCES Urzadzenie(IdUrz), IdCz int FOREIGN KEY REFERENCES CzesciU(IdCz)) GO CREATE TABLE Dostawca (IdDostawcy int IDENTITY(1,1) PRIMARY KEY, Nazwa varchar(30)) GO CREATE TABLE CzesciD (IdCz int IDENTITY(1,1) PRIMARY KEY, Nazwa varchar(30)) GO CREATE TABLE DostawcaCzesci (Id int IDENTITY(1,1) PRIMARY KEY, IdDostawcy int FOREIGN KEY REFERENCES Dostawca(IdDostawcy), IdCz int FOREIGN KEY REFERENCES CzesciD(IdCz)) GO
Tak utworzony schemat może zostać zasilony przykładowymi danymi. W przykładzie ograniczono się tylko do początkowych rekordów tabel. INSERT INSERT … GO INSERT INSERT INSERT … GO INSERT INSERT … INSERT INSERT …
Urzadzenie VALUES ('Komputer') Urzadzenie VALUES ('Szpadel') CzesciU VALUES ('Drzwi tylne') CzesciU VALUES ('Szyba') CzesciU VALUES ('Dach') UrzadzenieCzesci VALUES (1, 6) UrzadzenieCzesci VALUES (1, 7) UrzadzenieCzesci VALUES (5, 1) UrzadzenieCzesci VALUES (5, 2)
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL GO INSERT INSERT … GO INSERT INSERT INSERT … GO INSERT INSERT … INSERT INSERT …
197
Dostawca VALUES ('Komputery SA') Dostawca VALUES ('RTV i AGD') CzesciD VALUES ('Drzwi tylne') CzesciD VALUES ('Szyba') CzesciD VALUES ('Dach') DostawcaCzesci VALUES (1, 8) DostawcaCzesci VALUES (1, 9) DostawcaCzesci VALUES (2, 9) DostawcaCzesci VALUES (2, 10)
Pierwsze zadanie dla tego schematu polega na wyznaczeniu wszystkich części niezbędnych do wykonania urządzenia. Rozwiązanie jest raczej trywialne i ogranicza się do zrealizowania dwóch złączeń opisujących połączenia między tabelami dolnego wariantu diagramu z rysunku 4.1. SELECT Urzadzenie.Nazwa AS Urzadzenie, CzesciU.Nazwa AS Czesci FROM CzesciU INNER JOIN UrzadzenieCzesci ON CzesciU.IdCz = UrzadzenieCzesci.IdCz INNER JOIN Urzadzenie ON UrzadzenieCzesci.IdUrz = Urzadzenie.IdUrz
Kolejnym krokiem jest ustalenie dla każdego z dostawców, ile spośród części niezbędnych do wykonania każdego z urządzeń jest w jego ofercie. Również w tym przypadku nie mamy do czynienia z bardzo złożonym zapytaniem, łączymy tylko po trzy tabele diagramu według poprzedniego schematu, a dwie gałęzie łączymy za pomocą pola Nazwa tabel opisujących części widziane od strony urządzenia CzesciU i dostawcy CzesciD. Ponieważ zliczamy części z tabeli CzesciU, to występujące na liście wyświetlanych pól nazwy urządzenia i dostawcy muszą pojawić się w klauzuli GROUP BY. SELECT Dostawca.Nazwa AS Dostawca, Urzadzenie.Nazwa, COUNT(CzesciU.Nazwa) AS IloscCzesci FROM CzesciU INNER JOIN CzesciD ON CzesciU.Nazwa = CzesciD.Nazwa INNER JOIN Dostawca ON CzesciD.IdCz = DostawcaCzesci.IdCz INNER JOIN DostawcaCzesci ON Dostawca.IdDostawcy = DostawcaCzesci.IdDostawcy INNER JOIN UrzadzenieCzesci ON CzesciU.IdCz = UrzadzenieCzesci.IdCz INNER JOIN Urzadzenie ON UrzadzenieCzesci.IdUrz = Urzadzenie.IdUrz GROUP BY Dostawca.Nazwa,Urzadzenie.Nazwa
198
MS SQL Server. Zaawansowane metody programowania
Docelowym zadaniem jest wyświetlenie listy wszystkich dostawców, którzy oferują wszystkie części niezbędne do wyprodukowania każdego z urządzeń. W rozwiązaniu zostanie wykorzystane poprzednie zapytanie stanowiące „najniższe piętro”, najbardziej zagnieżdżone podzapytanie tab2, z którego mamy informacje o nazwie dostawcy, nazwie urządzenia i liczbie części, które do tego urządzenia oferuje dostawca. Pozostaje zbudowanie zapytania „równoległego” tab1, które dla każdej nazwy urządzenia zlicza części niezbędne do jego wykonania. Obydwa elementy składowe są połączone nazwą urządzenia, a ponieważ liczba części, z których to urządzenie się składa, i liczba części oferowanych przez dostawcę mają być równe (dostawca oferuje wszystkie części), dodajemy drugi warunek, porównujący te wielkości. W nadrzędnym zapytaniu z połączonych elementów składowych wybieramy interesujące nas pola, co kończy tworzenie zapytania. SELECT tab1.Urzadzenie, tab2.Dostawca, tab1.IloscCzesci FROM (SELECT Urzadzenie.Nazwa AS Urzadzenie, COUNT(CzesciU.Nazwa) AS IloscCzesci FROM Urzadzenie INNER JOIN UrzadzenieCzesci ON Urzadzenie.IdUrz = UrzadzenieCzesci.IdUrz INNER JOIN CzesciU ON UrzadzenieCzesci.IdCz = CzesciU.IdCz GROUP BY Urzadzenie.Nazwa ) AS tab1 INNER JOIN (SELECT Dostawca.Nazwa AS Dostawca, Urzadzenie.Nazwa, COUNT(CzesciU.Nazwa) AS IloscCzesci FROM CzesciU INNER JOIN CzesciD ON CzesciU.Nazwa = CzesciD.Nazwa INNER JOIN Dostawca ON CzesciD.IdCz = DostawcaCzesci.IdCz INNER JOIN DostawcaCzesci ON Dostawca.IdDostawcy = DostawcaCzesci.IdDostawcy INNER JOIN UrzadzenieCzesci ON CzesciU.IdCz = UrzadzenieCzesci.IdCz INNER JOIN Urzadzenie ON UrzadzenieCzesci.IdUrz = Urzadzenie.IdUrz GROUP BY Dostawca.Nazwa,Urzadzenie.Nazwa ) AS tab2 ON tab2.IloscCzesci=tab1.IloscCzesci AND tab2.Nazwa=tab1.Urzadzenie
Tak postawione zadanie określa się mianem dzielenia relacyjnego bez reszty [7] [29] [30] [42] – [45]. Należy zaznaczyć, że w wyniku wykonania zapytania może się okazać, że nie dla wszystkich urządzeń istnieją dostawcy oferujący pełny zestaw części. W skrajnym przypadku może się okazać, że takie urządzenie nie istnieje, i otrzymamy pusty zestaw rekordów. Możemy teraz uprościć schemat relacyjny, wychodząc z założenia, że w bazie danych części urządzenia i oferowane przez dostawców będą określane tym samym kluczem, co pozwala na wyeliminowanie jednej z tabel. W przykładzie zdecydowano się na pozostawienie tabeli CzesciU i usunięcie CzesciD rysunek 4.2.
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
199
Rysunek 4.2. Uproszczony schemat relacyjny opisujący urządzenia i dostawców części
Taka modyfikacja schematu nie powoduje istotnych zmian w sposobie rozwiązania problemu, eliminuje jedynie złączenie między tabelami reprezentującymi części teraz zapisywane w jednym miejscu. SELECT tab1.Urzadzenie, tab2.Dostawca, tab1.IloscCzesci FROM (SELECT Urzadzenie.IdUrz, Urzadzenie.Nazwa AS Urzadzenie, COUNT(CzesciU.Nazwa) AS IloscCzesci FROM Urzadzenie INNER JOIN UrzadzenieCzesci ON Urzadzenie.IdUrz = UrzadzenieCzesci.IdUrz INNER JOIN CzesciU ON UrzadzenieCzesci.IdCz = CzesciU.IdCz GROUP BY Urzadzenie.IdUrz, Urzadzenie.Nazwa ) AS tab1 INNER JOIN (SELECT Dostawca.Nazwa AS Dostawca, Urzadzenie.IdUrz, COUNT(CzesciU.Nazwa) AS IloscCzesci FROM CzesciU INNER JOIN Dostawca ON CzesciU.IdCz = DostawcaCzesci.IdCz INNER JOIN DostawcaCzesci ON Dostawca.IdDostawcy = DostawcaCzesci.IdDostawcy INNER JOIN UrzadzenieCzesci ON CzesciU.IdCz = UrzadzenieCzesci.IdCz INNER JOIN Urzadzenie ON UrzadzenieCzesci.IdUrz = Urzadzenie.IdUrz GROUP BY Dostawca.Nazwa,Urzadzenie.IdUrz ) AS tab2 ON tab2.IloscCzesci=tab1.IloscCzesci AND tab2.IdUrz=tab1.IdUrz
Możliwe jest kolejne uproszczenie składni, polegające na wyeliminowaniu najbardziej zewnętrznego zapytania, najwyższego „piętra”, zbierającego informację z dwóch złączonych podzapytań. Realizujemy to przez modyfikacje wyświetlanych pól dla elementu, który poprzednio stanowił podzapytanie o nazwie tab1. SELECT Urzadzenie.Nazwa AS Urzadzenie, tab2.Dostawca AS Dostawca, COUNT(UrzadzenieCzesci.IdUrz) AS IloscCzesci, tab2.IloscCzesci AS IloscCzesciD
200
MS SQL Server. Zaawansowane metody programowania FROM Urzadzenie INNER JOIN UrzadzenieCzesci ON Urzadzenie.IdUrz = UrzadzenieCzesci.IdUrz INNER JOIN (SELECT Dostawca.Nazwa AS Dostawca, UrzadzenieCzesci.IdUrz, COUNT(DostawcaCzesci.IdCz) AS IloscCzesci FROM Dostawca INNER JOIN DostawcaCzesci ON Dostawca.IdDostawcy = DostawcaCzesci.IdDostawcy INNER JOIN UrzadzenieCzesci ON DostawcaCzesci.IdCz = UrzadzenieCzesci.IdCz GROUP BY Dostawca.Nazwa,UrzadzenieCzesci.IdUrz ) AS tab2 ON tab2.IdUrz=Urzadzenie.IdUrz GROUP BY Urzadzenie.Nazwa, tab2.Dostawca, tab2.IloscCzesci HAVING COUNT(UrzadzenieCzesci.IdUrz) = tab2.IloscCzesci
Podobnym problemem jest dzielenie relacyjne z resztą [1] [7] [46]. Tym razem poszukujemy dostawcy, który produkuje największą liczbę części potrzebnych do wykonania urządzenia. W rozwiązaniu tego zadania pojawią się oczywiście również i ci dostawcy, którzy oferują pełny asortyment, ale dla niektórych urządzeń, które poprzednio nie pojawiały się w wynikowym zestawie rekordów, pojawią się informacje o niepełnej ofercie, ale takiej, która daje maksymalną liczbę niezbędnych elementów. Brakujące do wykonania urządzenia elementy są traktowane jako reszta z takiego dzielenia. W pierwszym kroku rozwiązania wyprowadzimy identyfikator urządzenia oraz zduplikowaną informację o maksimum części oferowanych przez dostawcę, mającą postać wyniku ze zliczenia identyfikatora części dokonanego w zapytaniu głównym oraz wyniku takiej operacji wyciągniętego z podzapytania. Równość tych wartości możemy uważać za weryfikację poprawności przetwarzania. W realizacji wykorzystujemy znane już podzapytanie tab2, z którego wyznaczamy maksimum, otrzymując zapytanie tab3. W nadrzędnym zapytaniu wybieramy interesujące nas pola, pamiętając o właściwym grupowaniu — które musi zawierać wszystkie wyprowadzane pola, na które nie działa funkcja agregująca, również pobierane z podzapytania pole MaksymalnaIlosc — oraz o warunku w klauzuli HAVING. SELECT Dostawca.Nazwa, UrzadzenieCzesci.IdUrz, COUNT(UrzadzenieCzesci.IdCz) AS IloscCzesci, MaksymalnaIlosc FROM Dostawca JOIN DostawcaCzesci ON Dostawca.IdDostawcy=DostawcaCzesci.IdDostawcy INNER JOIN UrzadzenieCzesci ON DostawcaCzesci.IdCz = UrzadzenieCzesci.IdCz JOIN (SELECT IdUrz, MAX(IloscCzesci) AS MaksymalnaIlosc FROM (SELECT DostawcaCzesci.IdDostawcy, UrzadzenieCzesci.IdUrz, COUNT(DostawcaCzesci.IdCz) AS IloscCzesci FROM DostawcaCzesci JOIN UrzadzenieCzesci ON DostawcaCzesci.IdCz = UrzadzenieCzesci.IdCz GROUP BY DostawcaCzesci.IdDostawcy,UrzadzenieCzesci.IdUrz )AS tab2
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
201
GROUP BY IdUrz) AS tab3 ON UrzadzenieCzesci.IdUrz=tab3.IdUrz GROUP BY Dostawca.Nazwa, UrzadzenieCzesci.IdUrz, MaksymalnaIlosc HAVING COUNT(UrzadzenieCzesci.IdCz)=MaksymalnaIlosc ORDER BY UrzadzenieCzesci.IdUrz
Ostatni krok jest związany z uzupełnieniem wyprowadzanych informacji o nazwę urządzenia oraz liczbę części, które się na nie składają. Pierwsza informacja wymaga dołączenia jako źródła tabeli Urzadzenie. Druga informacja musi zostać uzyskana z podzapytania tab1, które korzystając z UrzadzenieCzesci, zlicza właściwą wartość. SELECT Dostawca.Nazwa, UrzadzenieCzesci.IdUrz, Urzadzenie.Nazwa, COUNT(UrzadzenieCzesci.IdCz) AS IloscCzesci, MaksymalnaIlosc, IloscCzesciU FROM Dostawca JOIN DostawcaCzesci ON Dostawca.IdDostawcy=DostawcaCzesci.IdDostawcy JOIN UrzadzenieCzesci ON DostawcaCzesci.IdCz = UrzadzenieCzesci.IdCz INNER JOIN (SELECT IdUrz, COUNT(IdUrz) AS IloscCzesciU FROM UrzadzenieCzesci GROUP BY IdUrz) AS tab1 ON tab1.IdUrz=UrzadzenieCzesci.IdUrz JOIN Urzadzenie ON UrzadzenieCzesci.IdUrz = Urzadzenie.IdUrz JOIN (SELECT IdUrz, MAX(IloscCzesci) AS MaksymalnaIlosc FROM (SELECT DostawcaCzesci.IdDostawcy, UrzadzenieCzesci.IdUrz, COUNT(DostawcaCzesci.IdCz) AS IloscCzesci FROM DostawcaCzesci JOIN UrzadzenieCzesci ON DostawcaCzesci.IdCz = UrzadzenieCzesci.IdCz GROUP BY DostawcaCzesci.IdDostawcy,UrzadzenieCzesci.IdUrz ) AS tab2 GROUP BY IdUrz) AS tab3 ON UrzadzenieCzesci.IdUrz=tab3.IdUrz GROUP BY Dostawca.Nazwa,Urzadzenie.Nazwa, UrzadzenieCzesci.IdUrz, MaksymalnaIlosc, IloscCzesciU HAVING COUNT(UrzadzenieCzesci.IdCz)=MaksymalnaIlosc ORDER BY UrzadzenieCzesci.IdUrz
Realizacja dzielenia relacyjnego z resztą może mieć wiele zastosowań praktycznych, związanych z poszukiwaniem najlepszego dopasowania pomiędzy elementami lub atrybutami dwóch obiektów bądź stron. Przykładem może być kolejne zadanie, nazywane przeze mnie „swatką”. Wyobraźmy sobie pewną grupę panów, z których każdy posiada pewien zestaw najlepiej określających go cech, przy czym ich liczba może być dla każdej osoby inna. Z drugiej strony mamy grupę pań, które określają zestaw cech, których oczekują u potencjalnych partnerów. Również w tym przypadku cechy określane przez każdą z pań mogą być różne, a ich liczba nie jest ściśle określona. Można sobie wyobrazić zarówno taką panią, która oczekuje spełnienia tylko jednego wymagania, jak i taką, która oczekuje o wiele liczniejszego ich zestawu. Diagram relacyjny
202
MS SQL Server. Zaawansowane metody programowania
opisujący to zadanie przedstawia rysunek 4.3. Problemem, który należy rozwiązać za pomocą jednego zapytania wybierającego, jest określenie dla każdej z pań takiego pana, który spełnia maksymalną liczbę jej wymagań. Należy przy tym zauważyć, że istnieje możliwość, iż takich panów może być więcej niż jeden, oraz że może zajść sytuacja, w której któraś z kobiet będzie miała takie wymagania, którym nie sprosta żaden mężczyzna. Nie rozróżniamy przy tym wagi poszczególnych wymagań, które traktujemy jako tak samo ważne. Może również zdarzyć się sytuacja, w której kobieta nie poda żadnej poszukiwanej cechy, i wtedy także należy wyświetlić odpowiedni komunikat. Rysunek 4.3. Uproszczony schemat relacyjny do zadania dzielenia relacyjnego z resztą
Poniżej przedstawiony został skrypt tworzący diagram z rysunku 4.3. W skrypcie tym przy usuwaniu tabel zastosowano odwołanie do perspektywy słownikowej użytkownika INFORMATION_SCHEMA w celu sprawdzenia, czy taki obiekt istnieje w schemacie. IF EXISTS (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Kobiety') DROP TABLE Kobiety; GO CREATE TABLE Kobiety (IdKobiety integer IDENTITY(1,1) PRIMARY KEY, Nazwisko varchar(25) NOT NULL); GO IF EXISTS (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Mezczyzni') DROP TABLE Mezczyzni; GO CREATE TABLE Mezczyzni (IdMezczyzny integer IDENTITY(1,1) PRIMARY KEY, Nazwisko varchar(25) NOT NULL); GO IF EXISTS (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'CechyPoszukiwane') DROP TABLE CechyPoszukiwane; GO CREATE TABLE CechyPoszukiwane (IdCechyPoszukiwanej integer IDENTITY(1,1) PRIMARY KEY, IdKobiety int NOT NULL FOREIGN KEY REFERENCES Kobiety(IdKobiety), Cecha varchar(25) NOT NULL); GO IF EXISTS (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'CechyMezczyzny') DROP TABLE CechyMezczyzny; GO CREATE TABLE CechyMezczyzny (IdCechyMezczyzny integer IDENTITY(1,1) PRIMARY KEY, IdMezczyzny int NOT NULL FOREIGN KEY REFERENCES Mezczyzni(IdMezczyzny), Cecha varchar(25) NOT NULL);
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
203
Do tak utworzonych tabel należy wpisać dane za pomocą skryptu, którego idea jest pokazana niżej. INSERT INSERT … GO INSERT INSERT … GO INSERT INSERT INSERT INSERT … GO INSERT INSERT INSERT INSERT … GO
INTO Kobiety VALUES('Kowalska'); INTO Kobiety VALUES('Malinowska'); INTO Mezczyzni VALUES('Kowalski'); INTO Mezczyzni VALUES('Nowak'); INTO INTO INTO INTO
CechyPoszukiwane CechyPoszukiwane CechyPoszukiwane CechyPoszukiwane
INTO INTO INTO INTO
CechyMezczyzny CechyMezczyzny CechyMezczyzny CechyMezczyzny
VALUES(1,'przystojny'); VALUES(1,'mądry'); VALUES(1,'zaradny'); VALUES(2,'przystojny');
VALUES(1,'przystojny'); VALUES(1,'bogaty'); VALUES(1,'ciepły'); VALUES(2,'uczciwy');
Pierwszym sposobem rozwiązania będzie odwołanie się do wyznaczania funkcji agregujących nad oknem logicznym OVER(PARTITION BY …) [1] [2]. Podstawą jest podzapytanie nazwane x, które wybiera ze złączonych tabel CechyPoszukiwane i Cechy Mezczyzny pola zawierające informacje o identyfikatorze kobiety i identyfikatorze mężczyzny oraz oblicza z zastosowaniem operatora CASE, ile jest cech wspólnych. Zastosowano złączenie lewe LEFT JOIN, aby w przypadku braku pana spełniającego jakiekolwiek z kryteriów pani otrzymać rekord zawierający w identyfikatorze mężczyzny NULL. Stąd dla takiego warunku liczba cech wspólnych wynosi 0, a kiedy istnieją rekordy z wartościami różnymi od NULL, są one zliczane w grupie dla każdego z panów oddzielnie. To zapytanie stanowi źródło dla podzapytania poziomu wyższego, nazwane t, w którym stosując okno logiczne, wyznacza się maksimum cech dla każdej z pań oraz wybiera się odpowiednie identyfikatory. Zapytanie nadrzędne przekształca identyfikatory na nazwiska pań i panów dzięki złączeniu z odpowiednimi tabelami oraz dekoduje maksymalną liczbę cech w ten sposób, że jeśli jest ona większa niż 0, wyświetla tę wartość, a jeśli wynosi 0, wyświetla komunikat 'b. kan.'. Do tego wyniku za pomocą operatora UNION dołączono rekordy reprezentujące panie, które nie przedstawiły żadnych wymagań. Oczywiście można rozważyć również rozwiązanie, że jeśli nie określono żadnych wymagań, wszyscy kandydaci spełniają je w jednakowym stopniu. SELECT k.Nazwisko AS NazwiskoKobiety, NazwiskoMezczyzny= CASE WHEN t.IloscWspolnychCech = 0 THEN 'b. kan.' ELSE m.Nazwisko END, t.IloscWspolnychCech FROM Kobiety k JOIN (SELECT MAX(x.IloscWspolnychCech) OVER (PARTITION BY x.IdKobiety) AS maksIlosc, x.IdKobiety, x.IdMezczyzny, x.IloscWspolnychCech
204
MS SQL Server. Zaawansowane metody programowania FROM (SELECT p.IdKobiety, m.IdMezczyzny, IloscWspolnychCech= CASE WHEN m.IdMezczyzny IS NULL THEN 0 ELSE COUNT(*) END FROM CechyPoszukiwane p LEFT JOIN CechyMezczyzny m ON p.Cecha = m.Cecha GROUP BY p.IdKobiety, m.IdMezczyzny ) AS x ) AS t ON t.IdKobiety = k.IdKobiety LEFT JOIN Mezczyzni m ON t.IdMezczyzny = m.IdMezczyzny WHERE t.maksIlosc = t.IloscWspolnychCech UNION SELECT k.Nazwisko, 'b. wym.', 0 FROM Kobiety k LEFT JOIN CechyPoszukiwane c ON c.IdKobiety = k.IdKobiety WHERE c.IdKobiety IS NULL
Podobnie jak to miało miejsce w przypadku urządzeń i dostawców części, możemy założyć, że cechy oczekiwane i posiadane mogą zostać skatalogowane w postaci zbiorczego słownika. Wtedy w tabelach CechyPoszukiwane oraz CechyMezczyzny zamiast ich nazw pojawią się identyfikatory, a struktura może zostać przedstawiona tak jak na rysunku 4.4. Przekształcenie dotychczas stosowanej struktury może zostać wykonane za pomocą skryptu — jeśli w tym skrypcie istnieje tabela Cechy, to jest ona kasowana, a następnie tworzona z uwzględnieniem właściwej struktury. Następnie jest ona zasilana nazwami cech z połączonych tabel CechyPoszukiwane oraz CechyMezczyzny. Zastosowanie operatora UNION powoduje usunięcie duplikatów. Następnie do tabel Cechy Poszukiwane oraz CechyMezczyzny dodawane są kolumny kluczy obcych odwołujące się do utworzonego słownika. W kolejnym kroku kolumny te w obu tabelach są zasilane identyfikatorem zgodnym z nazwą cechy, a na koniec usuwa się kolumny z nazwami. Całość skryptu przedstawiono poniżej. IF EXISTS (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Cechy') DROP TABLE Cechy; CREATE TABLE Cechy (IdCechy integer IDENTITY(1,1) PRIMARY KEY, Cecha varchar(25) NOT NULL); GO INSERT INTO Cechy SELECT Cecha FROM CechyPoszukiwane UNION SELECT Cecha FROM CechyMezczyzny GO ALTER TABLE CechyPoszukiwane ADD IdCechy int FOREIGN KEY REFERENCES Cechy(IdCechy) GO ALTER TABLE CechyMezczyzny ADD IdCechy int FOREIGN KEY REFERENCES Cechy(IdCechy) GO UPDATE CechyPoszukiwane SET IdCechy =
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
205
(SELECT IdCechy FROM Cechy WHERE Cechy.Cecha=CechyPoszukiwane.Cecha) GO UPDATE CechyMezczyzny SET IdCechy = (SELECT IdCechy FROM Cechy WHERE Cechy.Cecha=CechyMezczyzny.Cecha) GO ALTER TABLE CechyPoszukiwane DROP COLUMN Cecha GO ALTER TABLE CechyMezczyzny DROP COLUMN Cecha GO
Rysunek 4.4. Schemat relacyjny do zadania dzielenia relacyjnego z resztą z zastosowaniem słownika cech
Zmiana schematu relacyjnego nie pociąga za sobą znaczącej zmiany zapytania rozwiązującego postawiony problem. Praktycznie należy tylko zastąpić identyfikatorami pola realizujące złączenie między tabelami określającymi cechy. SELECT k.Nazwisko AS NazwiskoKobiety, NazwiskoMezczyzny= CASE WHEN t.IloscWspolnychCech = 0 THEN 'b. kan.' ELSE m.Nazwisko END, t.IloscWspolnychCech FROM Kobiety k JOIN (SELECT MAX(x.IloscWspolnychCech) OVER (PARTITION BY x.IdKobiety) AS maksIlosc, x.IdKobiety, x.IdMezczyzny, x.IloscWspolnychCech FROM (SELECT p.IdKobiety, m.IdMezczyzny, IloscWspolnychCech= CASE WHEN m.IdMezczyzny IS NULL THEN 0 ELSE COUNT(*) END FROM CechyPoszukiwane p LEFT JOIN CechyMezczyzny m ON p.IdCechy = m.IdCechy GROUP BY p.IdKobiety, m.IdMezczyzny ) AS x ) AS t ON t.IdKobiety = k.IdKobiety LEFT JOIN Mezczyzni m ON t.IdMezczyzny = m.IdMezczyzny WHERE t.maksIlosc = t.IloscWspolnychCech UNION SELECT k.Nazwisko, 'b. wym.', 0 FROM Kobiety k LEFT JOIN CechyPoszukiwane c ON c.IdKobiety = k.IdKobiety WHERE c.IdKobiety IS NULL
206
MS SQL Server. Zaawansowane metody programowania
Rozwiązanie nie jest symetryczne. Aby to zilustrować i nie zmieniać przy tym nazw tabel, ale sens zawartej w nich informacji, dla każdego z panów wyszukujemy panie, które posiadają najwięcej oczekiwanych cech. Możemy teraz zauważyć, że jeśli do pani X najbardziej pasował pan Y, to najwięcej cech oczekiwanych przez niego może spełniać pani Z. Mówiąc ściśle, operacja dzielenia relacyjnego z resztą nie jest przemienna, w przeciwieństwie do dzielenia relacyjnego bez reszty, które taką cechę posiada. SELECT m.Nazwisko AS NazwiskoMezczyzny, NazwiskoKobiety= CASE WHEN t.IloscWspolnychCech = 0 THEN 'b. kan.' ELSE k.Nazwisko END , t.IloscWspolnychCech FROM Mezczyzni m JOIN (SELECT MAX(x.IloscWspolnychCech) OVER (PARTITION BY x.IdMezczyzny) AS maksIlosc, x.IdMezczyzny, x.IdKobiety, x.IloscWspolnychCech FROM (SELECT m.IdMezczyzny, p.IdKobiety, IloscWspolnychCech= CASE WHEN p.IdKobiety IS NULL THEN 0 ELSE COUNT(*) END FROM CechyMezczyzny m LEFT JOIN CechyPoszukiwane p ON p.IdCechy = m.IdCechy GROUP BY m.IdMezczyzny, p.IdKobiety ) AS x ) AS t ON t.IdMezczyzny = m.IdMezczyzny LEFT JOIN Kobiety k ON t.IdKobiety = k.IdKobiety WHERE t.maksIlosc = t.IloscWspolnychCech UNION SELECT m.Nazwisko, 'b. wym.', 0 FROM Mezczyzni m LEFT JOIN CechyMezczyzny c ON c.IdMezczyzny = m.IdMezczyzny WHERE c.IdMezczyzny IS NULL
Wróćmy teraz do wariantu podstawowego wyszukiwania najbardziej dopasowanych panów. Spróbujemy zrobić to bez odwoływania się do konstrukcji OVER (). Prowadzi to do konieczności wyznaczenia liczby cech wspólnych z podzapytania nazwanego xx, a wyświetlającego identyfikatory kobiety i mężczyzny. Połączenie z zapytaniem wyznaczającym maksimum cech dla każdej kobiety za pomocą jej identyfikatora i warunku porównania maksimum z każdą z liczebności wspólnych cech dopełnia warunek: SELECT k.Nazwisko AS NazwiskoKobiety, NazwiskoMezczyzny= CASE WHEN t.IloscWspolnychCech = 0 THEN 'b. kan.' ELSE m.Nazwisko END, t.IloscWspolnychCech FROM Kobiety k JOIN (SELECT xx.IdKobiety,xx.IdMezczyzny, xx.IloscWspolnychCech, maksIlosc FROM (SELECT p.IdKobiety, m.IdMezczyzny, IloscWspolnychCech= CASE WHEN m.IdMezczyzny IS NULL THEN 0
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
207
ELSE COUNT(*) END FROM CechyPoszukiwane p LEFT JOIN CechyMezczyzny m ON p.IdCechy = m.IdCechy GROUP BY p.IdKobiety, m.IdMezczyzny )AS xx JOIN (SELECT x.IdKobiety, MAX(x.IloscWspolnychCech) AS maksIlosc FROM (SELECT p.IdKobiety, IloscWspolnychCech= CASE WHEN m.IdMezczyzny IS NULL THEN 0 ELSE COUNT(*) END FROM CechyPoszukiwane p LEFT JOIN CechyMezczyzny m ON p.IdCechy = m.IdCechy GROUP BY p.IdKobiety, m.IdMezczyzny ) AS x GROUP BY x.IdKobiety) AS y ON y.IdKobiety=xx.IdKobiety )AS t ON t.IdKobiety = k.IdKobiety LEFT JOIN Mezczyzni m ON t.IdMezczyzny = m.IdMezczyzny WHERE t.maksIlosc = t.IloscWspolnychCech UNION SELECT k.Nazwisko, 'b. wym.', 0 FROM Kobiety k LEFT JOIN CechyPoszukiwane c ON c.IdKobiety = k.IdKobiety WHERE c.IdKobiety IS NULL
Innym rozwiązaniem problemu jest zrezygnowanie z piętra nadrzędnego, co powoduje, że konieczne jest zastosowanie klauzuli HAVING do porównania liczebności cech wspólnych każdej pary z maksimum dla każdej z kobiet. SELECT Kobiety.Nazwisko AS Kobieta, CASE WHEN Mezczyzni.Nazwisko IS NULL THEN 'b. kan.' ELSE Mezczyzni.Nazwisko END AS Mezczyzna, COUNT(CechyMezczyzny.IdMezczyzny) AS ile FROM CechyPoszukiwane LEFT JOIN CechyMezczyzny ON CechyPoszukiwane.IdCechy=CechyMezczyzny.IdCechy JOIN Kobiety ON CechyPoszukiwane.IdKobiety=Kobiety.IdKobiety LEFT JOIN Mezczyzni ON CechyMezczyzny.IdMezczyzny=Mezczyzni.IdMezczyzny LEFT JOIN (SELECT IdKobiety, MAX(Ile) AS maksimum FROM (SELECT IdKobiety, IdMezczyzny, COUNT(IdMezczyzny) AS ile FROM CechyPoszukiwane LEFT JOIN CechyMezczyzny ON CechyPoszukiwane.IdCechy=CechyMezczyzny.IdCechy GROUP BY IdKobiety, IdMezczyzny) AS x GROUP BY IdKobiety) AS y ON CechyPoszukiwane.IdKobiety=y.IdKobiety GROUP BY Kobiety.Nazwisko, Mezczyzni.Nazwisko, maksimum HAVING COUNT(CechyMezczyzny.IdMezczyzny)=maksimum UNION
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL INSERT INSERT INSERT INSERT INSERT INSERT INSERT INSERT
INTO INTO INTO INTO INTO INTO INTO INTO
Klasy Klasy Klasy Klasy Klasy Klasy Klasy Klasy
VALUES(1.3, VALUES(1.1, VALUES(2.6, VALUES(4.3, VALUES(5.7, VALUES(4.6, VALUES(2.7, VALUES(0.2,
2.2, 3.1, 1.2, 5.2, 6.6, 4.2, 2.2, 4.1,
209
'A'); 'A'); 'B'); 'C'); 'C'); 'C'); 'B'); 'A');
Podstawowy problem polega na ustaleniu odległości między klasami [47] – [56]. Definicji takiej miary może być wiele. Przyjmijmy, że będzie to średnia odległość między każdą parą punktów pochodzących z tych dwóch kategorii. Odległość będzie obliczana za pomocą metryki euklidesowej. Zadanie należy rozwiązać tak, aby nie było zależne od liczby klas i dało się zrealizować za pomocą pojedynczego zapytania wybierającego. W tym celu należy zastosować samozłączenie między dynamicznymi kopiami tabeli Klasy, aliasowanymi jako k i c. Połączenie musi być realizowane za pomocą nierówności „różne” (<>), a ponieważ odległość jest symetryczna, d (A, B) = d (B, A), to możemy wyeliminować dolną część macierzy, ograniczając się do macierzy górnej za pomocą nierówności „większe niż” (>). Pozostaje wyznaczyć odległości, korzystając ze znanego wzoru
d A,B
xA xB yA yB 2
2
,
a następnie wyznaczyć wartość średnią, grupując wyniki względem obu klas. W przykładzie dodatkowo zastosowano porządkowanie wyników względem nazw. Wyniki przetworzenia tego zapytania dla przykładowych danych zawiera tabela 4.3. SELECT c.Klasa AS Klasa1, k.Klasa AS Klasa2, AVG(SQRT(POWER(c.X-k.X,2)+POWER(c.Y-k.Y,2))) AS SredniaOdleglosc FROM Klasy k JOIN Klasy c ON k.Klasa > c.Klasa GROUP BY c.Klasa, k.Klasa ORDER BY c.Klasa, k.Klasa
Tabela 4.3. Skutek wykonania zapytania wyznaczającego odległość między klasami Klasa1
Klasa2
SredniaOdleglosc
A
B
2,36683189128359
A
C
4,69867226788684
B
C
4,27706728256864
Kolejne zadanie polega na określeniu właściwości każdej z klas. Ograniczmy się do dwóch podstawowych: położenia środka ciężkości, będącego średnią arytmetyczną współrzędnych X i Y wszystkich punktów należących do grupy, oraz promienia klasy, który określa najmniejszy okrąg o środku w środku ciężkości i zawierający w swoim wnętrzu i na krawędzi wszystkie punkty grupy [47] [55]. O ile wyznaczenie pierwszych dwóch wartości wymaga jedynie wyznaczenia funkcji agregującej AVG dla grup wyznaczonych przez każdą z klas, to promień okręgu wymaga znajomości położenia środka ciężkości. Stąd dokonano połączenia podzapytania z zapytaniem dla tej samej klasy, w którym obliczono wszystkie odległości punktu od środka i wybrano maksymalną spośród nich. Przykładowe wyniki zawarto w tabeli 4.4.
210
MS SQL Server. Zaawansowane metody programowania SELECT t.Klasa, t.SrodekX, t.SrodekY, MAX(SQRT(POWER(t.SrodekX-X,2)+POWER(t.SrodekY-Y,2))) AS Promien FROM (SELECT Klasa, AVG(X) AS SrodekX, AVG(Y) AS SrodekY FROM Klasy GROUP BY Klasa )AS t JOIN Klasy k ON t.Klasa=k.Klasa GROUP BY t.Klasa, t.SrodekX, t.SrodekY
Tabela 4.4. Skutek wykonania zapytania wyznaczającego współrzędne środka oraz promień klasy Klasa
SrodekX
SrodekY
Promien
A
0,866666666666667
3,13333333333333
1,17426099692057
B
2,65
1,7
0,502493781056045
C
4,86666666666667
5,33333333333333
1,51620872207255
Rozwiązanie możemy przedstawić w postaci graficznej, wykorzystując typy złożone Spatial, reprezentujące grafikę wektorową. Szczegółowo ten rodzaj danych omówiono w rozdziale 7.3. Zapytanie składa się z części połączonych operatorem UNION ALL. W każdej z nich pierwsze pole opisuje przynależność do klasy, drugie graficzną reprezentację obiektów: punktów oraz kół lub okręgów definiujących granice grupy. W pierwszych dwóch przypadkach wykorzystano proste odwołanie do tabeli Klasy, w drugim zapytanie przedstawione poprzednio. Zdecydowano się na podwójną reprezentację punktów, ponieważ obiekt POINT jest mało widoczny. Każdy z punktów został zatem otoczony kołem o promieniu zdefiniowanym pomocniczą zmienną @pom. Definicja obiektów graficznych została poskładana ze składowych elementów konwertowanych do napisu. Rezultat przedstawiono na rysunku 4.6. DECLARE @pr real SET @pr=0.1 SELECT Klasa AS Label, geometry::STGeomFromText('POINT(' + CAST(X AS varchar) +' ' + CAST(Y AS varchar)+ ')', 0) AS Punkty FROM Klasy UNION ALL SELECT Klasa, geometry::STGeomFromText('CURVEPOLYGON(CIRCULARSTRING('+ CAST(X-@pr AS varchar(max))+ ' '+ CAST(Y AS varchar(max))+',' + CAST(X AS varchar(max))+ ' '+ CAST(Y-@pr AS varchar(max))+',' + CAST(X+@pr AS varchar(max))+ ' '+ CAST(Y AS varchar(max))+',' + CAST(X AS varchar(max))+ ' '+ CAST(Y+@pr AS varchar(max))+',' + CAST(X-@pr AS varchar(max))+ ' '+ CAST(Y AS varchar(max))+ '))',0) as grupa FROM Klasy UNION ALL SELECT Klasa, geometry::STGeomFromText('CIRCULARSTRING('+ CAST(SrodekX-Promien AS varchar(max))+ ' '+ CAST(SrodekY AS varchar(max))+',' + CAST(SrodekX AS varchar(max))+ ' '+ CAST(SrodekY-Promien AS varchar(max))+',' + CAST(SrodekX+Promien AS varchar(max))+ ' '+ CAST(SrodekY AS varchar(max))+',' + CAST(SrodekX AS varchar(max))+ ' '+ CAST(SrodekY+Promien AS varchar(max))+',' + CAST(SrodekX-Promien AS varchar(max))+ ' '+ CAST(SrodekY AS varchar(max))+ ')',0) as grupa FROM
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
211
(SELECT t.Klasa, t.SrodekX, t.SrodekY, MAX(SQRT(POWER(t.SrodekX-X,2)+POWER(t.SrodekY-Y,2))) AS Promien FROM (SELECT Klasa, AVG(X) AS SrodekX, AVG(Y) AS SrodekY FROM Klasy GROUP BY Klasa )AS t JOIN Klasy k ON t.Klasa=k.Klasa GROUP BY t.Klasa, t.SrodekX, t.SrodekY) AS xxx
Rysunek 4.6. Prezentacja dystrybucji punktów i granic klas z zastosowaniem typu geometry
Wyznaczone poprzednio odległości między grupami są trudne w interpretacji, ponieważ wyrażono je w jednostkach bezwzględnych. Czytelniejsze są odległości względne, które można wyrazić na wiele sposobów. W przykładzie wyznaczono je w dwóch wariantach: odległości środków ciężkości oraz średniej odległości między punktami grup odniesionych do sumy promieni tych grup. Wykorzystane zostały poprzednio utworzone zapytania połączone za pomocą pola klasy. Zapytanie wyznaczające promień wykorzystano dwukrotnie dla każdej z klas, między którymi wyznaczono odległość. Stąd wynika podwójne połączenie dla każdej klasy oddzielnie. Skutek działania zapytania zawiera tabela 4.5. SELECT pocz.Klasa AS Klasa1, kon.Klasa AS Klasa2, pocz.Promien+kon.Promien AS SumaProm, SredniaOdleglosc, SQRT(POWER(pocz.SrodekX-kon.SrodekX,2)+POWER(pocz.SrodekY-kon.SrodekY,2))AS OdlSrodkow, SQRT(POWER(pocz.SrodekX-kon.SrodekX,2)+POWER(pocz.SrodekY-kon.SrodekY,2))/ (pocz.Promien+kon.Promien) AS OdlWzgl1, SredniaOdleglosc/(pocz.Promien+kon.Promien) AS OdlWzgl2 FROM (SELECT t.Klasa, SrodekX, SrodekY, MAX(SQRT(POWER(SrodekX-X,2)+POWER(SrodekY-Y,2))) AS Promien FROM (SELECT Klasa, AVG(X) AS SrodekX, AVG(Y) AS SrodekY FROM Klasy GROUP BY Klasa )AS t JOIN Klasy k ON t.Klasa=k.Klasa GROUP BY t.Klasa, SrodekX, SrodekY) AS pocz
212
MS SQL Server. Zaawansowane metody programowania JOIN (SELECT t.Klasa, SrodekX, SrodekY, MAX(SQRT(POWER(SrodekX-X,2)+POWER(SrodekY-Y,2))) AS Promien FROM (SELECT Klasa, AVG(X) AS SrodekX, AVG(Y) AS SrodekY FROM Klasy GROUP BY Klasa )AS t JOIN Klasy k ON t.Klasa=k.Klasa GROUP BY t.Klasa, SrodekX, SrodekY) AS kon ON kon.Klasa>pocz.Klasa JOIN (SELECT c.Klasa AS Klasa1, k.Klasa AS Klasa2, AVG(SQRT(POWER(c.X-k.X,2)+POWER(c.Y-k.Y,2))) AS SredniaOdleglosc FROM Klasy k JOIN Klasy c ON k.Klasa > c.Klasa GROUP BY c.Klasa, k.Klasa ) AS Sr ON pocz.Klasa=Sr.Klasa1 AND kon.Klasa=Sr.Klasa2 ORDER BY pocz.Klasa, kon.Klasa
Tabela 4.5. Skutek wykonania zapytania wyznaczającego względne odległości klas Klasa1
Klasa2 SumaProm
SredniaOdleglosc
OdlSrodkow
OdlWzgl1
OdlWzgl2
A
B
1,67675477797661
2,36683189128359
2,28795153406322
1,36451171281238
1,41155517930876
A
C
2,69046971899312
4,69867226788684
4,56508488420533
1,69676129486927
1,7464133622159
B
C
2,0187025031286
4,27706728256864
4,25613935653219
2,10835393027749
2,11872094869849
Tym razem możemy powiedzieć, że ponieważ obie odległości względne są wyraźnie większe od 1, to grupy są dobrze od siebie oddzielone. Najniższa wartość dla pary grup A i B wskazuje na najsłabsze, ale jednak pozytywne właściwości dyskryminacyjne tak zdefiniowanych grup. Nieznaczne różnice między wartościami uzyskanymi za pomocą obu metod świadczą o tym, że można je stosować wymiennie. Rozważmy teraz przypadek podziału przestrzeni na klasy za pomocą prostych [47] [57] – [59]. Dla tego przypadku skonstruowanie rozwiązania znacznie ułatwi nam wstawienie pola automatycznie inkrementowanego. ALTER TABLE Klasy ADD IdPunktu int IDENTITY PRIMARY KEY
Podziału na klasy dokonamy w ten sposób, że najpierw określimy proste wyznaczone przez wszystkie pary punktów należące do jednej klasy. W tym celu dokonamy samozłączenia tabeli za pomocą pola identyfikatora. Do wyznaczenia współczynników równania wykorzystamy znane wzory definiujące prostą na podstawie współrzędnych dwóch punktów. Ponieważ równanie prostej nie zależy od tego, który z punktów potraktujemy jako pierwszy, a który jako drugi — jest ono względem tych danych symetryczne — wyeliminujmy wszystkie punkty powyżej diagonali, tworząc macierz trójkątną górną przy zastosowaniu warunku nierównościowego. Poza współczynnikami równania w przykładzie jako trzecią kolumnę wyprowadzimy w postaci napisu wzór opisujący prostą.
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
213
SELECT ((k.Y-c.Y)/(k.X-c.X)) AS a, ((c.Y*k.X-k.Y*c.X)/(k.X-c.X)) AS b, 'y = '+CONVERT(varchar,(k.Y-c.Y)/(k.X-c.X))+'*x + ('+ CONVERT(varchar,(c.Y*k.X-k.Y*c.X)/(k.X-c.X))+')' AS wzor, k.Klasa AS Klasa FROM Klasy k JOIN Klasy c ON k.Klasa = c.Klasa WHERE k.IdPunktu < c.IdPunktu
Aby przybliżyć wyniki, rozwiązanie zostało przekształcone do postaci wyprowadzającej klasy i wszystkie odcinki łączące je w ramach jednej klasy — rysunek 4.7. Jak w przypadku poprzedniej reprezentacji graficznej, zastosowano typ geometry oraz jej obiekty — punkt POINT i łamaną LINESTRING (rozdział 7.3). Oba elementy połączone zostały operatorem UNION ALL. SELECT Klasa AS Label, geometry::STGeomFromText('POINT(' + CAST(X AS varchar) +' ' + CAST(Y AS varchar)+ ')' , 0) AS Punkty FROM Klasy UNION ALL SELECT Klasa, geometry::STGeomFromText( 'LINESTRING(' + CONVERT(varchar,kX)+' ' + CONVERT(varchar,a*kX +b)+ ', ' + + CONVERT(varchar,cX)+' '+ CONVERT(varchar,cX*a+b)+ ')' ,0) FROM (SELECT ((k.Y-c.Y)/(k.X-c.X)) AS a, ((c.Y*k.X-k.Y*c.X)/(k.X-c.X)) AS b, 'y = '+CONVERT(varchar,(k.Y-c.Y)/(k.X-c.X))+'*x + ('+ CONVERT(varchar,(c.Y*k.X-k.Y*c.X)/(k.X-c.X))+')' AS wzor, k.Klasa AS Klasa, k.Y AS kY,c.Y AS cY,k.X AS kX, c.X AS cX FROM Klasy k JOIN Klasy c ON k.Klasa = c.Klasa WHERE k.IdPunktu < c.IdPunktu) AS linie
Rysunek 4.7. Prezentacja odcinków łączących wszystkie pary punktów każdej z klas
Kolejnym krokiem jest usunięcie spośród wyznaczonych prostych tych, które przecinają którąkolwiek z pozostałych klas. Innymi słowy, w jakiejś innej klasie istnieją punkty leżące po różnych stronach tej prostej. Dokonujemy tego, stosując operator różnicy zbiorów EXCEPT. W odejmowanej części wyznaczamy ponownie współczynniki dla każdej pary, a to podzapytanie łączymy z pozostałymi klasami operatorem
214
MS SQL Server. Zaawansowane metody programowania
„nierówny” (!=). Następnie podzapytanie to łączymy z kolejną kopią tabeli Klasy, nazwaną x, aby sprawdzić, czy istnieją dwa punkty leżące po dwóch stronach prostej. Formalnie sprawdzamy, czy zachodzi jeden z dwóch przypadków połączonych operatorem OR. Jeśli współrzędna Y pierwszego punktu jest mniejsza niż wartość Y wyliczona z równania prostej, to dla drugiego jest większa — lub odwrotnie. SELECT 'y = '+CONVERT(varchar,(k.Y-c.Y)/(k.X-c.X))+'*x + ('+ CONVERT(varchar,(c.Y*k.X-k.Y*c.X)/(k.X-c.X))+')' AS wzor FROM Klasy k JOIN Klasy c ON k.Klasa = c.Klasa WHERE k.IdPunktu < c.IdPunktu EXCEPT SELECT t.wzor FROM (SELECT ((k.Y-c.Y)/(k.X-c.X)) AS a, ((c.Y*k.X-k.Y*c.X)/(k.X-c.X)) AS b, 'y = '+CONVERT(varchar,(k.Y-c.Y)/(k.X-c.X))+'*x + ('+ CONVERT(varchar,(c.Y*k.X-k.Y*c.X)/(k.X-c.X))+')' AS wzor, k.Klasa AS Klasa FROM Klasy k JOIN Klasy c ON k.Klasa = c.Klasa WHERE k.IdPunktu < c.IdPunktu )AS t JOIN Klasy k ON k.Klasa != t.Klasa JOIN Klasy x ON x.Klasa = k.Klasa AND x.IdPunktu < k.IdPunktu WHERE (x.X*t.a+t.b > x.Y AND k.X*t.a+t.b < k.Y) OR (x.X*t.a+t.b < x.Y AND k.X*t.a+t.b > k.Y)
W zaprezentowanym rozwiązaniu odejmowanie odbywa się na podstawie porównania łańcuchów opisujących wzór prostej. Jest to operacja czasochłonna, ponieważ porównujemy długie napisy. Możliwe jest przepisanie tego zapytania do postaci, kiedy porównywane są współczynniki równania, a wzór wyprowadzany jest nadrzędnym zapytaniem. Skutek wykonania takiego zapytania jest przedstawiony w tabeli 4.6. W porównaniu z wynikiem poprzedniego zapytania będzie różnił się tylko kolejnością wyprowadzania wierszy ze względu na sposób porządkowania wynikający ze stosowania operatora EXCEPT, który ma zaszytą w sobie dyrektywę DISTINCT, a ta z kolei klauzulę ORDER BY, zgodną z kolejnością pól zapytania. SELECT 'y = '+CONVERT(varchar,a)+'*x + ('+ CONVERT(varchar,b)+')' AS wzor FROM (SELECT ((k.Y-c.Y)/(k.X-c.X)) AS a, ((c.Y*k.X-k.Y*c.X)/(k.X-c.X)) AS b FROM Klasy k JOIN Klasy c ON k.Klasa = c.Klasa WHERE k.IdPunktu < c.IdPunktu EXCEPT SELECT a,b FROM (SELECT ((k.Y-c.Y)/(k.X-c.X)) AS a, ((c.Y*k.X-k.Y*c.X)/(k.X-c.X)) AS b, k.Klasa AS Klasa FROM Klasy k JOIN Klasy c ON k.Klasa = c.Klasa WHERE k.IdPunktu < c.IdPunktu )AS t JOIN Klasy k ON k.Klasa != t.Klasa JOIN Klasy x ON x.Klasa = k.Klasa AND x.IdPunktu < k.IdPunktu WHERE (x.X*t.a+t.b > x.Y AND k.X*t.a+t.b < k.Y) OR (x.X*t.a+t.b < x.Y AND k.X*t.a+t.b > k.Y)) AS zap
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
215
Tabela 4.6. Skutek wykonania zapytania wyznaczającego proste dzielące przestrzeń na klasy wzor y = –4.5*x + (8.05) y = –3.33333*x + (19.5333) y = –1.72727*x + (4.44545) y = 1*x + (0.9) y = 2.18182*x + (–5.83636) y = 10*x + (–24.8)
Zajmijmy się odmiennym wariantem klasyfikatora liniowego. Działanie jego będzie polegało na znalezieniu dwóch najbliższych punktów pochodzących z dwóch różnych klas i połączeniu ich odcinkiem. W jego środku tworzymy prostą prostopadłą, która będzie granicą podziału między klasami. Rozwiązanie wykonamy dla takiej samej tabeli jak w poprzednim zadaniu. Pomimo tego, że obecny klasyfikator działa tylko dla dwóch klas, a w tabeli mamy przypisanie do trzech klas, możemy powiedzieć, że podział będzie teraz następował pomiędzy punktami należącymi do wybranej klasy, np. A, a tymi, które należą do wszystkich pozostałych klas, lub mówiąc inaczej, tymi, które stanowią dopełnienie do klasy A. Rozwiązanie rozpoczynamy od wyznaczenia wszystkich odległości między każdą parą punktów analizowanej klasy i dopełnienia do niej, wykorzystując dobrze znaną metrykę euklidesową w podzapytaniu o nazwie wsp. Następnie wyznaczane jest minimum tej odległości. SELECT MIN(wsp.odleglosc) FROM (SELECT SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))AS odleglosc FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='A') AS wsp
Wykorzystujemy następnie równanie opisujące prostą przechodzącą przez dwa punkty. Przekształcamy to równanie na postać prostej przechodzącej przez punkt, który jest środkiem odcinka, czyli ma współrzędne równe średniej wartości współrzędnych najbliżej leżących punktów, i prostopadłej do tego odcinka, wiedząc, że w takim przypadku współczynnik kierunkowy jest odwrotnością współczynnika wyznaczonego w podstawowym równaniu. Trzeba jednak zaznaczyć, że należy rozważyć dwa przypadki. Pierwszy, gdy wartości współrzędnych Y dwóch najbliżej położonych punktów są różne, bo wtedy równanie prostej ma postać y = ax + b. Drugi, kiedy te współrzędne są równe, bo wtedy prosta podziału jest pionowa i ma postać x = b. Oczywiście wyznaczenie tego równania następuje dla najbliższych punktów, co zapewnia warunek w klauzuli WHERE przyrównujący odległość do minimum wyznaczonego poprzednio opisanym podzapytaniem skalarnym. Również w tym warunku zapisano, do której klasy należą analizowane punkty. Skutek wykonania tego zapytania dla przykładowych danych pokazuje tabela 4.7. Jak widać, mamy do czynienia z drugim z przypadków, czyli prostą pionową. SELECT a.x AS x1, a.y AS y1, a.Klasa AS c1, b.x AS x2, b.y AS y2, b.Klasa AS c2, SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)) AS odleglosc, rownanie_prostej= CASE
216
MS SQL Server. Zaawansowane metody programowania WHEN a.y<>b.y THEN 'y='+CONVERT(varchar(8),(-b.x+a.x)/(b.y-a.y))+'x + ' +CONVERT(varchar(8),(b.y*b.y-a.y*a.y-a.x*a.x+b.x*b.x)/(2*(b.y-a.y))) ELSE 'x='+CONVERT(varchar(8),(a.x+b.x)/2) END FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='A' AND SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))= (SELECT MIN(wsp.odleglosc) FROM (SELECT SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))AS odleglosc FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='A') AS wsp) GROUP BY a.x,a.y,a.Klasa,b.x,b.y,b.Klasa ;
Tabela 4.7. Skutek wykonania zapytania wyznaczającego prostą dzielącą przestrzeń na klasy A i A x1
y1
c1
x2
y2
c2
odleglosc
rownanie_prostej
1,3
2,2
A
2,7
2,2
B
1,4
x = 2
Dodajemy teraz dwa rekordy o danych zawartych w tabeli Klasy (tabela 4.8). Można sprawdzić, czy nie spowoduje to zmiany poprzednio uzyskanego rezultatu. Tabela 4.8. Nowe rekordy dodane do tabeli Klasy X
Y
Klasa
6
0
C
3
8
A
Tak zbudowany klasyfikator nie jest doskonały i łatwo sobie wyobrazić, że istnieją pewne punkty klasy A leżące po tej stronie prostej klasyfikującej, po której leżą punkty pozostałych klas. Taką cechę ma drugi z dodanych punktów (rysunek 4.8). Naszym zadaniem jest teraz wychwycenie wszystkich takich punktów za pomocą zapytania. Podstawą jest poprzednio utworzone zapytanie definiujące prostą klasyfikującą, tym razem niezawierające wzoru, lecz jedynie współczynniki równania. Dodatkowo zostały wyznaczone współczynniki kierunekx i kieruneky, definiujące, w którą stronę od prostej względem odpowiednich współrzędnych leży punkt klasy A, który został wykorzystany do jej wyznaczenia. Zastosowana została funkcja SIGN określająca znak różnicy, która jest jej argumentem. W głównym zapytaniu wyznaczono współrzędne punktów źle zaklasyfikowanych, a przypisanych do klasy wskazanej w klauzuli WHERE. Niepoprawną klasyfikację określają dwa alternatywne warunki — kieruneky<>0 dla prostych skośnych oraz kieruneky=0 dla prostych pionowych. Sprawdzane są nierówności określające w pierwszym przypadku współrzędną y, a w drugim x dla badanego punktu w porównaniu z wartością tej współrzędnej wynikającej z równania prostej przemnożonej przez właściwe znaki określające kierunek. Wykonanie zapytania wyświetla wskazany punkt, co potwierdza tabela 4.9. SELECT x, y, glowna.Klasa FROM Klasy AS glowna JOIN (SELECT (-b.x+a.x)/(b.y-a.y) AS a, (b.y*b.y-a.y*a.y-a.x*a.x+b.x*b.x)/(2*(b.y-a.y)) AS b,
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
217
SIGN(b.y-a.y) AS kieruneky, SIGN(b.x-a.x) AS kierunekx, a.Klasa, a.x AS x1, b.x AS x2 FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='A' and SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))= (SELECT MIN(wsp.odleglosc) FROM (SELECT SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))AS odleglosc FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='A') AS wsp) ) AS prosta ON (glowna.Klasa = prosta.Klasa) WHERE (kieruneky<>0 and y*prosta.kieruneky>(prosta.a*x+prosta.b)*prosta.kieruneky) OR (kieruneky=0 and x*kierunekx>((prosta.x1+prosta.x2)/2)*kierunekx);
Rysunek 4.8. Punkty trzech klas oraz proste rozdzielające te klasy
Tabela 4.9. Źle sklasyfikowane punkty należące do klasy A x
y
Klasa
3
8
A
Takie same operacje mogą być wykonane dla każdej z pozostałych dwóch klas. Wymaga to jedynie zmiany nazwy klasy w dwóch warunkach filtrujących dla każdego z zapytań. W przykładzie zrealizowano taką modyfikację dla klasy C. Wyniki określające prostą klasyfikującą oraz źle zakwalifikowane punkty tej klasy zostały przedstawione w tabelach 4.9 i 4.10. Ilustrację graficzną wykonanych zapytań przedstawia rysunek 4.8. SELECT a.x AS x1, a.y AS y1, a.Klasa AS c1, b.x AS x2, b.y AS y2, b.Klasa AS c2, SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)) AS odleglosc, rownanie_prostej= CASE WHEN a.y<>b.y THEN 'y='+CONVERT(varchar(8),(-b.x+a.x)/(b.y-a.y))+'x + ' +CONVERT(varchar(8),(b.y*b.y-a.y*a.y-a.x*a.x+b.x*b.x)/(2*(b.y-a.y))) ELSE 'x='+CONVERT(varchar(8),(a.x+b.x)/2) END FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa)
218
MS SQL Server. Zaawansowane metody programowania WHERE a.Klasa='C' AND SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))= (SELECT MIN(wsp.odleglosc) FROM (SELECT SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))AS odleglosc FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='C') AS wsp) GROUP BY a.x,a.y,a.Klasa,b.x,b.y,b.Klasa ; SELECT x,y,glowna.Klasa FROM Klasy AS glowna JOIN (SELECT (-b.x+a.x)/(b.y-a.y) AS a, (b.y*b.y-a.y*a.y-a.x*a.x+b.x*b.x)/(2*(b.y-a.y)) AS b, SIGN(b.y-a.y) AS kieruneky, SIGN(b.x-a.x) AS kierunekx, a.Klasa, a.x AS x1, b.x AS x2 FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='C' and sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))= (SELECT MIN(wsp.odleglosc) FROM (SELECT SQRT((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))AS odleglosc FROM Klasy AS a LEFT JOIN Klasy AS b ON(a.Klasa!=b.Klasa) WHERE a.Klasa='C') AS wsp) ) AS prosta ON (glowna.Klasa = prosta.Klasa) WHERE (kieruneky<>0 and y*prosta.kieruneky>(prosta.a*x+prosta.b)*prosta.kieruneky) OR (kieruneky=0 and x*kierunekx>((prosta.x1+prosta.x2)/2)*kierunekx);
Tabela 4.9. Skutek wykonania zapytania wyznaczającego prostą dzielącą przestrzeń na klasy C i C x1
y1
c1
x2
y2
c2
odleglosc
rownanie_prostej
4,6
4,2
C
2,7
2,2
B
2,75862284482674
y = –0.95x + 6.6675
Tabela 4.10. Źle sklasyfikowane punkty należące do klasy C x
y
Klasa
6
0
C
Jak powiedziano, klasyfikator tego rodzaju może być zaliczony do grupy naiwnych, ponieważ łatwo sobie wyobrazić taką dystrybucję punktów, że wiele spośród punktów jakichś klas zostanie błędnie sklasyfikowanych. Jednak taki klasyfikator może być wykorzystywany rekurencyjnie czy też iteracyjnie. Załóżmy, że pierwsza prosta dzieli przestrzeń tak, że po jej jednej stronie znajdują się tylko punkty poprawnie przypisane do klasy, natomiast po drugiej są punkty różnych klas. Możemy wtedy odrzucić z analizy punkty leżące po właściwej stronie prostej i zająć się tylko określeniem prostej, klasyfikując pozostałe punkty. Postępując dalej według tego schematu, będziemy w każdym kroku zwiększali liczbę poprawnie sklasyfikowanych punktów. Oczywiście początkowe założenie, że jedna ze stron jest czysta, nie musi być spełnione. Jego niespełnienie prowadzi tylko do tego, że obie strony będą dzielone kolejnymi prostymi, oczywiście po odrzuceniu punktów z drugiej strony. Tak działający klasyfikator może być wystarczająco precyzyjny, jednak jego realizacja nie jest możliwa za pomocą pojedynczego zapytania i wymaga zastosowania rozszerzenia proceduralnego. Należałoby od-
Rozdział 4. Problemy rozwiązywane z wykorzystaniem SQL
219
powiedzieć na pytanie, czy powinniśmy dążyć do czystych klas, czy też godzić się z pewną niedokładnością klasyfikatora [47] [58]. Bez wątpienia prawdziwe jest drugie stwierdzenie, ponieważ klasyfikator powinien być dość ogólny, zawierać względnie mało prostych, tak aby dało się na podstawie niego sensownie wnioskować. Mam nadzieję, że tę kwestię uda mi się wyczerpująco wyjaśnić w kolejnej książce.
220
MS SQL Server. Zaawansowane metody programowania
Rozdział 5.
Rozszerzenia proceduralne Transact-SQL 5.1. Podstawowe instrukcje Zwykle mówimy, że Transact-SQL jest proceduralnym rozszerzeniem języka SQL. Ja wolę jednak myśleć o nim jako o języku programowania, w którym zawarte są polecenia SQL. Bez względu na sposób podejścia Transact-SQL oferuje programiście wiele cech dobrze znanych z języków programowania wyższego rzędu. Podstawowy zakres stanowią instrukcje sterujące, a wśród nich instrukcje warunkowe. Przedstawmy podstawową instrukcję IF w skrypcie, w którym zadeklarowane zostały dwie zmienne numeryczne. Istnieje obowiązek deklarowania wszystkich zmiennych. Dokonujemy tego po słowie kluczowym DECLARE w dowolnym miejscu skryptu, ale przed pierwszym odwołaniem do zmiennej. Dobrym zwyczajem jest umieszczanie deklaracji zmiennych na początku. Możliwe jest deklarowanie wielu zmiennych po słowie kluczowym DECLARE, w takim przypadku definicje zmiennych, na które składają się nazwa i typ, są rozdzielane przecinkiem. Należy pamiętać, że nazwy zmiennych rozpoczynają się od znaku at (@). Do zadeklarowanych zmiennych podstawiane są wartości, co wymaga zastosowania słowa kluczowego SET. Aby kolejne wykonania skryptu mogły się różnić, zastosowano funkcję generatora liczb pseudolosowych RAND(), która zwraca liczbę z przedziału <0, 1). Instrukcja IF wymaga podania warunku, którym jest porównanie wartości zmiennych. Polecenie występujące po niej, w przykładzie wyświetlające komunikat, wykona się wtedy, kiedy warunek będzie prawdziwy. Jeśli warunek będzie fałszywy, polecenie to nie zostanie wykonane i przetwarzanie przejdzie do kolejnej linii, co w tym przypadku jest jednoznaczne z zakończeniem skryptu. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real
222
MS SQL Server. Zaawansowane metody programowania SET @nr1=RAND() SET @nr2=RAND() IF @nr1> @nr2 PRINT 'Pierwsze większe'
Skutkiem wykonania będzie komunikat: Pierwsze większe
lub informacja o pozytywnym zakończeniu przetwarzania: Command(s) completed successfully.
Instrukcja warunkowa może zostać wzbogacona o opcjonalną sekcję ELSE, która zostanie wykonana, kiedy warunek będzie fałszywy. W stanie domyślnym obie sekcje są jednolinijkowe. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real SET @nr1=RAND() SET @nr2=RAND() IF @nr1> @nr2 PRINT 'Pierwsze większe' ELSE PRINT 'Drugie większe'
Skutkiem wykonania skryptu będzie albo pierwszy komunikat: Pierwsze większe
albo komunikat z drugiej sekcji instrukcji: Drugie większe
W większości przypadków w każdej z sekcji instrukcji warunkowej może się pojawić więcej niż jedna linia kodu. Linie muszą być wtedy ujęte w klamrę instrukcji grupujących BEGIN…END. Powoduje to, że wnętrze takiego bloku jest traktowane jak jedna linia. W przykładzie takie postępowanie zastosowano w celu wzbogacenia komunikatu o wartości zmiennych. Funkcja CAST powoduje konwersję atrybutu do typu wskazanego po słowie kluczowym AS i jest niezbędna, kiedy łączymy napis z wartością numeryczną. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real SET @nr1=RAND() SET @nr2=RAND() IF @nr1> @nr2 BEGIN PRINT 'Pierwsze większe' PRINT CAST(@nr1 AS varchar(10)) + '>' + CAST(@nr2 AS varchar(10)) END ELSE BEGIN PRINT 'Drugie większe' PRINT CAST(@nr1 AS varchar(10)) + '<=' + CAST(@nr2 AS varchar(10)) END
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
223
Przykładowy skutek wykonania skryptu może wyglądać tak: Pierwsze większe 0.863206>0.351206
lub tak: Drugie większe 0.413556<=0.961594
Kolejną instrukcją sterującą jest pętla, która jest realizowana za pomocą słowa kluczowego WHILE, po którym następuje warunek sprawdzany przy każdym wykonaniu cyklu. Ciało jest wykonywane dopóty, dopóki warunek jest prawdziwy. Ponieważ jego sprawdzenie następuje na początku, możliwy jest stan, gdy pętla nie zostanie wykonana ani razu. Analogicznie do instrukcji warunkowej ciało pętli jest jednolinijkowe, co w przykładzie wymusiło zastosowanie instrukcji grupującej BEGIN…END. W praktyce trudno sobie wyobrazić jednoliniowe ciało pętli, szczególnie jeśli uwzględnimy fakt, że musi w nim być zmieniany warunek, a przynajmniej jeden jego składnik, tak aby możliwe było zakończenie jej przetwarzania. Gdyby warunek był niezmienny, pętla albo nie wykonałaby się ani razu, albo spowodowałoby to nieskończone przetwarzanie jej ciała. W przykładzie sprawdzany jest warunek porównujący zmienne @nr1 i @nr2, które zarówno przed pętlą, jak i w pętli są zasilane z generatora liczb pseudolosowych. Zmienna @nr3 zainicjowana wartością 0 i inkrementowana w pętli o 1 wskazuje liczbę przebiegów pętli. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real SET @nr1=RAND() SET @nr2=RAND() SET @nr3 =0 WHILE @nr1> @nr2 BEGIN SET @nr1=RAND() SET @nr2=RAND() SET @nr3 = @nr3+1 END PRINT 'Wykonano ' + CAST(@nr3 AS varchar(5)) + ' pętli'
Skutkiem przetworzenia skryptu może być poniższy komunikat: Wykonano 5 pętli
Istnieje możliwość przerwania wykonywania pętli za pomocą instrukcji BREAK. Jej bezwarunkowe zastosowanie spowodowałoby, że pętla wykonałaby się najwyżej raz, i to nie cała. Dlatego w przykładzie umieszczono ją w ciele instrukcji warunkowej sprawdzającej zależność między zmiennymi @nr3 i @nr1, dodatkowo dodając stosowny komunikat. Teraz przerwanie wykonywania pętli wykona się, kiedy warunek będzie prawdziwy. Ponadto dodana została zmienna @licz, która przechowuje informacje o liczbie wykonanych przebiegów. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real DECLARE @licz int SET @nr1=RAND()
224
MS SQL Server. Zaawansowane metody programowania SET @nr2=RAND() SET @licz=1 WHILE @nr1> @nr2 BEGIN PRINT 'Wykonuję ' + CAST(@licz AS varchar(5)) + ' pętlę' SET @nr2=RAND() SET @nr3=RAND() SET @licz = @licz+1 IF @nr3 > @nr1 BEGIN PRINT CAST(@nr1 AS varchar(11)) + ' - ' + CAST(@nr2 AS varchar(11)) + ' - ' + CAST(@nr3 AS varchar(11)) BREAK END END PRINT 'normalne zakończenie po ' + CAST(@licz AS varchar(5))
Przykładowy skutek wykonania skryptu może mieć następującą postać: Wykonuję 1 pętlę 0.11343 - 0.183313 - 0.669944 normalne zakończenie po 2
Nieco inaczej działa instrukcja CONTINUE. Powoduje ona przerwanie wykonywania tylko aktualnego przebiegu pętli. Inaczej mówiąc, przenosi punkt wykonywania na początek pętli, co oznacza, że instrukcje, które występują po niej, w danym przebiegu nie zostaną wykonane. Podobnie jak poprzednio, warunek powodujący wykonanie instrukcji CONTINUE jest umieszczony w ciele instrukcji warunkowej sprawdzającej zależność między zmiennymi @nr3 i @nr1. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real DECLARE @licz int SET @nr1=RAND() SET @nr2=RAND() SET @licz=1 WHILE @nr1> @nr2 BEGIN PRINT 'Wykonuję ' + CAST(@licz AS varchar(5)) + ' pętlę' SET @nr2=RAND() SET @nr3=RAND() SET @licz = @licz+1 IF @nr3 > @nr1 BEGIN PRINT CAST(@nr1 AS varchar(11)) + ' - ' + CAST(@nr2 AS varchar(11))+ ' - ' + CAST(@nr3 AS varchar(11)) CONTINUE END PRINT 'Wykonuję sekcję po CONTINUE' END PRINT 'normalne zakończenie po ' + CAST(@licz AS varchar(5))
Przykładowy skutek wykonania skryptu może mieć następującą postać: Wykonuję 1 pętlę Wykonuję sekcję po CONTINUE Wykonuję 2 pętlę
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
225
0.555855 - 0.202167 - 0.763164 Wykonuję 3 pętlę Wykonuję sekcję po CONTINUE Wykonuję 4 pętlę Wykonuję sekcję po CONTINUE Wykonuję 5 pętlę Wykonuję sekcję po CONTINUE normalne zakończenie po 6
Poza operatorem CASE, który był prezentowany podczas omawiania zapytań wybierających, istnieje instrukcja o tej samej nazwie. Również sposób posługiwania się nią jest analogiczny. W przykładzie w kolejnych sekcjach WHEN porównano trzy zasilone z generatora pseudolosowego zmienne, wyświetlając po słowie kluczowym THEN odpowiedni komunikat. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real DECLARE @wynik varchar(22) SET @nr1=RAND() SET @nr2=RAND() SET @nr3=RAND() SET @wynik=CASE WHEN @nr1>@nr2 THEN 'Większe' WHEN @nr1<@nr3 THEN 'Mniejsze' WHEN @nr2>@nr3 THEN 'Większe 2' END PRINT @wynik
W tej instrukcji możliwe jest zastosowanie opcjonalnej sekcji ELSE, która jest wykonywana wtedy, kiedy żaden z wcześniej wymienionych warunków nie jest prawdziwy. DECLARE @nr1 real DECLARE @nr2 real DECLARE @nr3 real DECLARE @wynik varchar(22) SET @nr1=RAND() SET @nr2=RAND() SET @nr3=RAND() SET @wynik=CASE WHEN @nr1>@nr2 Then 'Większe' WHEN @nr1<@nr3 Then 'Mniejsze' WHEN @nr2>@nr3 Then 'Większe 2' ELSE 'Inne' END PRINT @wynik
Istnieje również prosta postać tej instrukcji stosowana wtedy, kiedy mamy do czynienia z porównaniem zmiennej lub wyrażenia z określoną wartością. W przykładzie zmienną @nr1 zasilono z generatora w ten sposób, że na skutek przeskalowania i przycięcia wyniku do liczby całkowitej może ona przyjąć wartości {0, 1, 2, 3}. DECLARE @nr1 real DECLARE @wynik varchar(22) SET @nr1=ROUND(3*RAND(),0) SET @wynik=CASE @nr1 WHEN 0 Then 'zero'
226
MS SQL Server. Zaawansowane metody programowania WHEN 1 Then 'jeden' WHEN 2 Then 'dwa' ELSE 'Inne' END PRINT @wynik
Należy zaznaczyć, że w każdej postaci tej instrukcji przetwarzana jest ona tylko do napotkania pierwszego prawdziwego wyrażenia. Czytelnik może to sprawdzić, powielając wartość albo wyrażenie i zmieniając komunikat. Drugi z przypadków nigdy nie zostanie wykonany. Kolejnym elementem składniowym jest instrukcja powodująca czasowe zatrzymanie przetwarzania. Pierwszym sposobem definiowania takiego interwału może być określenie chwili ponownego rozpoczęcia przetwarzania. W przykładzie zmienna @data została zasilona czasem pobranym z zegara systemowego, a następnie została z zastosowaniem funkcji DATEADD powiększona o 2 sekundy. Nowa wartość została użyta do określenia chwili rozpoczęcia przetwarzania w poleceniu WAITFOR, po słowie kluczowym TIME. Całość została wzbogacona o instrukcje PRINT wyświetlające czas w różnych chwilach przetwarzania skryptu. DECLARE @data time DECLARE @napis varchar(30) SET @data =getdate() PRINT @data SET @data =DATEADD(ss,2,@data) SET @napis=CONVERT(varchar(30),@data,8) PRINT @data PRINT @napis WAITFOR TIME @napis SET @data =getdate() PRINT @data
Przykładowy zestaw komunikatów otrzymanych podczas przetwarzania skryptu przedstawia poniższy listing: 12:34:23.4800000 12:34:25.4800000 12:34:25 12:34:25.0100000
Inną postacią polecenia WAITFOR jest zastosowanie słowa kluczowego DELAY, po którym w postaci napisu o poprawnym formacie czasu podajemy opóźnienie — w przykładzie wynosi ono 1.3 s. Przykładowy wynik zamieszczony został bezpośrednio po kodzie. DECLARE @data time DECLARE @napis varchar(30) SET @data =getdate() PRINT @data WAITFOR DELAY '00:00:01.3' SET @data =getdate() PRINT @data 11:48:31.1470000 11:48:32.4430000
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
227
Poza przedstawionymi postaciami możliwe jest zastosowanie instrukcji WAITFOR dla systemu wymiany komunikatów Service Broker. Ma ona wtedy postać przedstawioną za pomocą metakodu. WAITFOR [(otrzymana_informacja) | (pobrana_grupa_informacji)] [, TIMEOUT timeout]
Ostatnią instrukcją jest polecenie skoku bezwzględnego GOTO, po którym następuje nazwa etykiety, którą może być dowolny, różny od słów kluczowych napis zakończony dwukropkiem. W przykładzie zdefiniowano pętlę, której ciało jest wykonywane dla kolejnych liczb naturalnych nie większych niż 5. W instrukcji warunkowej sprawdzono resztę z jej dzielenia przez 2. Jeśli wynosi ona 0, akcja zostaje przeniesiona do etykiety Parzysty:, w przeciwnym wypadku do Nieparzysty:, gdzie wyświetlany jest odpowiedni komunikat. Na końcu fragmentu kodu rozpoczynającego się od Parzysty: zastosowano skok do etykiety Koniec:, aby nie był wykonywany kolejny fragment dotyczący sytuacji, kiedy liczba jest nieparzysta. DECLARE @licz int; SET @licz = 0; WHILE @licz < 5 BEGIN SET @licz = @licz + 1 PRINT @licz IF @licz%2 = 0 GOTO Parzysty ELSE GOTO Nieparzysty Parzysty: PRINT CAST(@licz AS varchar(2))+ ' to liczba parzysta' GOTO Koniec Nieparzysty: PRINT CAST(@licz AS varchar(2))+ ' to liczba nieparzysta' Koniec: PRINT 'Koniec' END
Należy pamiętać, że w dobrze zorganizowanym kodzie instrukcje skoku nie powinny się pojawiać. W praktyce nie istnieje kod, w którym nie dałoby się wyeliminować takich instrukcji. Jednak na skutek konieczności zachowania zgodności z wcześniejszymi realizacjami T-SQL instrukcja GOTO jest nadal obsługiwana.
5.2. Procedury składowane Wszystkie do tej pory wykorzystywane kody T-SQL były skryptami anonimowymi, które żyły do końca sesji. Możliwe jest jednak utworzenie obiektów trwale zapisanych na serwerze. Pierwszym ich rodzajem są procedury składowane. Tworzone są one na skutek wykonania polecenia CREATE PROCEDURE, po którym następuje nazwa, a po słowie kluczowym AS ciało procedury. W skład ciała mogą wchodzić dowolne instrukcje języka oraz wszystkie polecenia SQL, również zapytania wybierające SELECT. Jest to specyficzna cecha środowiska, ponieważ w innych serwerach w definicji procedury zapytań wybierających stosować nie wolno. W przykładzie użyto zapytania wyświetlającego z tabeli Osoby pola Nazwisko i Imie. Całość kodu zakończono poleceniem GO, które nie jest konieczne, jeśli skrypt zawiera pojedynczy kod tworzenia procedury.
228
MS SQL Server. Zaawansowane metody programowania CREATE PROCEDURE wyb AS SELECT Nazwisko, Imie FROM Osoby GO
Po utworzeniu procedura może zostać wywołana na jeden z trzech zaprezentowanych niżej sposobów. EXECUTE wyb EXEC wyb wyb
Pierwsze dwa wywołania dają gwarancję powodzenia, o ile procedura nie zawiera błędów. Natomiast ostatnie takiej gwarancji nie daje. Jeśli piszemy kod, nie używając średnika jako symbolu końca linii, w niektórych przypadkach parser może mieć kłopot z rozróżnieniem, czy ma do czynienia z nazwą procedury, którą należy wykonać, czy też z kontynuacją poprzedniej linii skryptu. Utworzona procedura jest bardzo uboga, ponieważ nie zawiera żadnego parametru. Jeśli chcemy dokonać modyfikacji istniejącej procedury, powinniśmy ją najpierw usunąć poleceniem DROP PROCEDURE. Ponieważ tworzenie procedury musi być pierwszym elementem skryptu, konieczne jest zastosowanie słowa kluczowego GO. Definicja parametrów rozpoczyna się bezpośrednio po nazwie procedury i wymaga podania jego nazwy rozpoczynającej się od znaku at oraz typu. Jeśli typ wymaga określenia długości, musi się ona również pojawić w tym miejscu. Tak zdefiniowany parametr może wystąpić w ciele procedury. W przykładzie został on wykorzystany do definicji wyrażenia filtrującego w klauzuli WHERE dla operatora podobieństwa LIKE. Ponieważ ideą jest to, aby wyświetlane były nazwiska zaczynające się od frazy danej parametrem, to aby operator nie musiał podawać znaku % przy wywołaniu, znak ten został dołączony do wyrażenia filtrującego w postaci statycznego napisu. DROP PROCEDURE wyb GO CREATE PROCEDURE wyb @nazw varchar(40) AS SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE @nazw + '%' GO
Teraz wywołanie procedury wymaga podania wartości parametru. W przykładzie zostaną wyświetlone nazwiska i imiona tych osób, których nazwisko zaczyna się od litery k. Pominięcie przy wywołaniu parametru spowoduje wyświetlenie komunikatu o błędzie. EXEC wyb 'k'
Zamiast usuwać i tworzyć procedurę od nowa, można zastosować polecenie ALTER PROCEDURE, które nadpisuje definicję istniejącego obiektu. ALTER PROCEDURE wyb @nazw varchar(40) AS SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE @nazw + '%' GO
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
229
Próba wykonania takiego skryptu dla nieistniejącego obiektu skończy się komunikatem o błędzie. Oczywiście procedura może mieć więcej niż jeden parametr, w takim przypadku separatorem ich listy jest przecinek. W przykładzie dodano drugi parametr @im, którego przez analogię do pierwszego użyto w wyrażeniu filtrującym, tym razem dotyczącym imienia. Dodatkowo pokazano, że w definicji parametru może zostać określona wartość domyślna ustalona po znaku =. W obu przypadkach jest to napis %. DROP PROCEDURE wyb GO CREATE PROCEDURE wyb @nazw varchar(40)='%', @im varchar(20)='%' AS SELECT Nazwisko, Imie FROM Osoby WHERE Nazwisko LIKE @nazw + '%' AND Imie LIKE @im + '%' GO
Teraz wywołanie procedury wymaga podania wartości dwóch parametrów. Parametry występujące w przykładzie oznaczają, że zostaną wyświetlone dane osób, których nazwiska rozpoczynają się od litery k, a imiona od j. Jak widać, podstawienie ma charakter pozycyjny — pierwsza wartość do pierwszego parametru, druga do drugiego (itp., gdyby było ich więcej). EXEC wyb 'k', 'j'
Ponieważ dla obu parametrów określono wartość domyślną, możemy po kolei, licząc od tyłu, pomijać wartości parametrów podczas wywołania. W przykładzie najpierw zostaną wyświetlone dane o nazwiskach rozpoczynających się od znaku k, a następnie dane wszystkich osób, co jest konsekwencją przyjęcia przez pomijane parametry wartości domyślnej %, która dla operatora LIKE zastępuje dowolny ciąg znaków. EXEC wyb 'k' EXEC wyb
Gdybyśmy jednak chcieli odwołać się do wartości domyślnej drugiego z parametrów, to pominięcie pierwszej wartości przy wywołaniu nie jest możliwe. W tym przypadku konieczne jest zamienienie sposobu wywołania na nazewniczy. Podawana jest nazwa parametru, do której przypisujemy wartość. W przykładzie oznacza to wyświetlenie danych osób o imieniu rozpoczynającym się od j. EXEC wyb @im = 'j'
O ile przy wywołaniu pozycyjnym możliwe jest tylko pomijanie wartości n końcowych parametrów o zdefiniowanej wartości domyślnej, to wywołanie nazewnicze pozwala na pominięcie dowolnego ich zestawu bez względu na pozycje na liście. Dodatkową cechą tego sposobu wykonywania procedury jest możliwość zmiany kolejności podawania wartości parametrów. O wiele częściej będziemy chcieli, żeby zamiast wyświetlać wynikowy zestaw rekordów, procedura obliczała jakąś wartość, a wynik przekazywała do miejsca wywołania.
230
MS SQL Server. Zaawansowane metody programowania
Utwórzmy tego typu procedurę, która ma obliczać liczbę osób wyższych niż podana wartość. Zdefiniujmy dla niej dwa parametry: pierwszy @mini, określający wartość progową wzrostu, który będzie typu rzeczywistego o wartości domyślnej 0, oraz całkowity @ile, przeznaczony na wynik obliczeń. Wyznaczenie wyniku wymagało użycia funkcji agregującej COUNT(), a podstawienie odbywa się przez przypisanie do zmiennej. Wykorzystane zostało w tym celu zapytanie skalarne, które zwraca dokładnie jedną kolumnę w jednym wierszu. Należy zauważyć, że sensowne przypisanie wymaga, aby zapytanie zwracało jeden rekord. Wtedy kolejne przypisania do zmiennej odbywają się po przecinku. Każde ze zwracanych pól musi być podstawione do parametru. Jeśli chociaż jeden z elementów nie zostanie przypisany, pojawi się błąd. Warto zauważyć, że jeśli przypisanie będzie dotyczyło zapytania zwracającego wiele wierszy, to błąd nie wystąpi, a w zmiennych pojawią się wartości pól z ostatniego zwracanego przez zapytanie rekordu. Takie działanie w większości przypadków jest niecelowe, błędne. CREATE PROCEDURE licz @mini real = 0, @ile int AS SELECT @ile=COUNT(IdOsoby) FROM Osoby WHERE Wzrost > @mini GO
Jeśli będziemy chcieli wywołać tak utworzoną procedurę, musimy zadeklarować pomocniczą zmienną @ile, a następnie użyć polecenia EXEC, podając nazwę procedury, wartość progową wzrostu oraz zmienną przeznaczoną na wynik. Skrypt uzupełnia polecenie PRINT wyprowadzające parametr na standardowe urządzenie wyjściowe. DECLARE @ile int EXEC licz 1.8, @ile PRINT @ile
Pomimo poprawnego wykonania na wyjściu nie pojawiają się żadne dane, ponieważ domyślnie zmienna jest przekazywana tylko do procedury. Możemy się o tym przekonać, wprowadzając do ciała procedury i wywołującego ją skryptu dodatkowe instrukcje PRINT oraz dodając w miejscu wywołania ustawienie wartości początkowej zmiennej @ile. DROP PROCEDURE licz GO CREATE PROCEDURE licz @mini real = 0, @ile int AS PRINT @ile SELECT @ile=COUNT(IdOsoby) FROM Osoby WHERE Wzrost > @mini PRINT @ile GO DECLARE @ile int SET @ile =1 PRINT @ile EXEC licz 1.8, @ile PRINT @ile
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
231
Dwie pierwsze wypisane wartości wynoszą 1, co świadczy o poprawnym przypisaniu wartości początkowej oraz o tym, że wartość ta przekazywana jest do ciała procedury. Kolejna wartość, różna od 1 i zmieniająca się w zależności od wartości progowej wzrostu podstawionej w wywołaniu, świadczy o poprawnie wykonanym obliczeniu funkcji COUNT. Ostatnia wartość 1 potwierdza fakt, że wyznaczony wynik nie jest przekazywany na zewnątrz, do miejsca wywołania. Ponieważ poprzednio nie została wyprowadzona żadna wartość, możemy wnioskować, że po zadeklarowaniu zmienne są inicjowane wartością NULL. Aby zapewnić przekazywanie zmiennej w obie strony, parametr musi być określony dyrektywą OUTPUT przy tworzeniu procedury. CREATE PROCEDURE licz @mini real = 0, @ile int OUTPUT AS SELECT @ile=COUNT(IdOsoby) FROM Osoby WHERE Wzrost > @mini GO
Tak samo w miejscu wywołania musi zostać dodana ta sama dyrektywa. DECLARE @ile int EXEC licz 1.8, @ile OUTPUT PRINT @ile
Od wersji 2005 synonimem dyrektywy OUTPUT jest OUT i mogą być one stosowane zamiennie. Przez chwilę odejdźmy od głównego nurtu, aby zająć się innym zastosowaniem polecenia EXEC. W tym celu zadeklarujmy dwie zmienne znakowe, a następnie do pierwszej z nich przypiszmy nazwę kolumny np. Imie. Druga zawiera zapytanie wybierające, odwołujące się do tabeli Osoby, w którym nazwę pola zastąpiono zawartością pierwszej zmiennej. Innymi słowy, zmienna @zap zawiera złożone z trzech fragmentów poprawne zapytanie wybierające. Do wykonania zapytania danego zmienną albo przez napis służy również polecenie EXEC, tym razem jednak atrybut jest zawarty w nawiasie. DECLARE @k varchar(20), @zap varchar(200) SET @k='Imie' SET @zap='SELECT ' + @k + ' FROM Osoby' EXEC (@zap)
Możemy powiedzieć, że jeśli po EXEC pojawia się nawias, atrybut lub napis będzie traktowany jako zapytanie do przetworzenia, w przeciwnym wypadku parser poszukuje procedury o danej nazwie. Oczywiście zastosowanie takiego skryptu w praktyce jest mało prawdopodobne, dlatego spróbujmy skonstruować bardziej użyteczną procedurę, wykorzystującą taki mechanizm w swoim ciele. Jej parametrami są dwie zmienne znakowe z przeznaczeniem na nazwę pola i tabeli. Zadeklarowana w ciele znakowa zmienna lokalna @zap zawiera złożone z fragmentów zapytanie wybierające pole z tabeli, gdzie obie nazwy przekazane zostały właściwymi zmiennymi. Na koniec za pomocą polecenia EXEC jest wykonywana zawartość zmiennej. Po utworzeniu procedura jest wywoływana z wartościami parametrów ustawionymi na wartości 'Nazwisko' oraz 'Osoby'.
232
MS SQL Server. Zaawansowane metody programowania CREATE PROCEDURE wykonaj @pole varchar(30), @tabela varchar(30) AS DECLARE @zap varchar(max) SET @zap='SELECT ' + @pole+ ' FROM ' + @tabela EXEC (@zap) GO EXEC wykonaj 'Nazwisko', 'Osoby'
Jak widać, uzyskaliśmy uniwersalne narzędzie do odpytywania dowolnej tabeli o jedno wskazane pole. Na podobnych zasadach możemy uzyskać procedurę powodującą przepisanie wskazanego pola tekstowego w wybranej tabeli do postaci określonej nazwą funkcji. CREATE PROCEDURE zmien @pole varchar(30), @tabela varchar(30), @fun varchar(30) AS DECLARE @zap varchar(max) SET @zap='UPDATE ' + @tabela + ' SET ' + @pole + '=' + @fun + '(' + @pole + ')' EXEC (@zap) GO EXEC zmien 'Nazwisko', 'Osoby', 'UPPER'
Jak widać, możliwe jest zawarcie w ciele procedury zapytań innych niż wybierające, również takich, które są wykonywane na podstawie napisu, który je zawiera. O takim wykonaniu zapytań SQL mówi się ogólnie „dynamiczny SQL”. Próby z innymi typami zapytań, np. tworzącymi lub modyfikującymi obiekty bazy danych, pozostawiam uważnemu Czytelnikowi. Warto zauważyć, że zmienna wykonywana za pomocą EXEC może zawierać nie tylko pojedyncze zapytanie, ale również złożony skrypt. Rozpatrzmy teraz przykład, w którym tworzone są dwie procedury, o nazwach innerproc i outerproc. W ciele każdej z nich wyświetlana jest wartość zwracana przez wbudowaną funkcję @@NESTLEVEL oraz wywoływana druga z procedur. Jeśli tak jak w skrypcie najpierw tworzona jest procedura innerproc, to w tym momencie pojawi się komunikat z ostrzeżeniem, ponieważ odwołujemy się do nieistniejącego obiektu. Procedura zostanie jednak utworzona. Dlatego utworzenie drugiej z nich outerproc zakończy się powodzeniem. DROP PROCEDURE innerproc DROP PROCEDURE outerproc GO CREATE PROCEDURE innerproc AS SELECT @@NESTLEVEL AS 'Inner Level' EXEC outerproc GO CREATE PROCEDURE outerproc AS SELECT @@NESTLEVEL AS 'Outer Level' EXEC innerproc GO The module 'innerproc' depends on the missing object 'outerproc'. The module will still be created; however, it cannot run successfully until the object exists.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
233
Jeśli uruchomimy jedną z procedur w naszym przykładzie outerproc, to nastąpi cykl rekurencyjnych wywołań, co w przypadku języków wyższego rzędu kończyło się nieskończoną pętlą. W przypadku MS SQL seria rekurencji kończy się po uzyskaniu wyświetlanego przez @@NESTLEVEL poziomu zagnieżdżenia 32. Jest to ograniczenie sztywno ustawione dla serwera, na co wskazuje umieszczony na końcu skryptu komunikat systemowy. EXECUTE outerproc GO Outer Level ----------1 (1 row(s) affected) Inner Level ----------2 (1 row(s) affected) Outer Level ----------3 (1 row(s) affected) ... Inner Level ----------32 (1 row(s) affected) Msg 217, Level 16, State 1, Procedure innerproc, Line 3 Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).
Doświadczenie to pokazuje, że nie można wykonać nieskończonej rekurencji, co jest cechą pozytywną. Jednak czasami obraca się przeciwko programiście, gdyż nie można w prosty sposób zrealizować wielu zadań. Na przykład dla ciągu Fibonacciego możliwe jest wyznaczenie najwyżej 34 elementu — dwa pierwsze dane są jawnie. Problemy takie dotyczą wielu użytecznych zadań praktycznych, dla których ograniczenie do 32 poziomu rekurencji może być bardzo kłopotliwe i powodujące konieczność stosowania innych rozwiązań [1] [5] – [7] [60] – [64], np. wykorzystujących klauzulę WITH lub obiektowe biblioteki użytkownika CLR. Możemy próbować przysłonić wykonanie procedury, stosując polecenie EXEC do napisu zawierającego skrypt uruchamiający procedurę. DROP PROCEDURE innerproc DROP PROCEDURE outerproc GO CREATE PROCEDURE innerproc AS SELECT @@NESTLEVEL AS 'Inner Level' EXEC('EXEC outerproc') GO CREATE PROCEDURE outerproc AS SELECT @@NESTLEVEL AS 'Outer Level' EXEC ('EXEC innerproc') GO EXECUTE outerproc GO
234
MS SQL Server. Zaawansowane metody programowania
Niestety, jak pokazuje seria komunikatów, w takim przypadku wykonanie procedury wykorzystuje dwa poziomy rekurencji. Pierwszy na wykonanie polecenia EXEC uruchamiającego skrypt, a drugi na właściwe wykonanie procedury. Powoduje to, że kolejne liczby zwracane przez polecenie @@NESTLEVEL są nieparzyste. Outer Level ----------1 (1 row(s) affected) Inner Level ----------3 (1 row(s) affected) Outer Level ----------5 (1 row(s) affected) ..... Inner Level ----------31 (1 row(s) affected) Msg 217, Level 16, State 1, Line 1 Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).
Pokazano mechanizmy tworzenia i wykonywania procedur, ale nie przedstawiono dotąd przyczyn, dla których warto z nich korzystać. Podstawowy argument, znany dobrze z języków wyższego rzędu, mówi, że jeśli jakiś fragment kodu jest wykonywany wielokrotnie, powinien być ujęty w procedurę, a w ciele nadrzędnego programu lub procedury tylko wywoływany. Taka argumentacja związana z jakością tworzonego kodu jest również aktualna w bazach danych, jednak nie jest najważniejsza. Innymi wskazaniami o charakterze organizacyjnym i porządkowym, dotyczącymi kodu podzielonego na procedury, są: łatwość czytania i analizy, skuteczniejsze możliwości testowania, możliwość stosowania „zaślepek”, pustych procedur dla funkcjonalności jeszcze niezrealizowanych lub wcześniej przetestowanych. Ogólnie mówiąc, tak zorganizowany kod ma dobrą jakość [1] [6] [7] [65]. Poza tym w bazach danych można wskazać inne, nie mniej ważne, a zdaniem wielu osób ważniejsze argumenty. Utworzona procedura jest przede wszystkim nie tylko składowana w postaci kodu, w którym ją utworzono, ale również dla wszystkich zapytań zawartych w jej ciele tworzone są plany wykonania, które są optymalizowane, następnie ciało jest kompilowane i w takiej postaci jest również przechowywane w bazie. Wywołanie procedury stanowi odwołanie do kodu skompilowanego. Istnieją przypadki, kiedy kompilacja odbywa się dopiero podczas pierwszego uruchomienia kodu. Mieliśmy z nim do czynienia, gdy budowaliśmy procedury wywoływane rekurencyjne. Ponieważ podczas tworzenia pierwszej z nich druga wywoływana z jej ciała nie istniała, kompilacja w momencie tworzenia nie była możliwa. Dopiero podczas uruchomienia, kiedy przyczyna błędu zniknęła, wykonywana była kompilacja. Ponieważ przyczyn, dla których kompilacja mogła się wykonać dopiero podczas pierwszego wywołania, może być wiele, ważne jest, aby w przypadku testowania wydajności pomijać wyniki z pierwszych prób. Dodatkowo należy pamiętać, aby testy były wykonywane wielokrotnie, a zmierzone czasy były poddawane obróbce statystycznej [1]
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
235
[3] [5] [31]. Na marginesie chciałbym zauważyć, że wielokrotnie, nawet w poważnych pracach podawane są takie wyniki, że np. pierwsza procedura rozwiązująca jakiś problem wykonywała się 10ms, a druga 15ms. Stąd wnioskuje się, że pierwsza jest wydajniejsza niż druga. Moim zdaniem jest to poważne nadużycie, gdyż jedynym wnioskiem jest to, że obie wykonywały się szybko. W trakcie przetwarzania kodu w systemie operacyjnym, ale również w serwerze bazy danych wykonywanych jest wiele działających asynchronicznie procesów (procesów tła). Może się tak nieszczęśliwie zdarzyć, że podczas wykonywania drugiej procedury system lub serwer bazy danych był nimi bardziej obciążony, co spowodowało zafałszowanie pomiaru czasów. Dopiero czasy przetwarzania kodu mierzone w sekundach są w miarę wiarygodne, czyli nasze testy powinny być wykonywane na dostatecznie obszernych zestawach danych [3] [31] [65]. Warto poświęcić chwilę na próby dla zmiennej liczby danych, tak aby można było zaobserwować faktyczny koszt obliczeń. Zmiany liniowe są oczywiście zmianami najbardziej oczekiwanymi. Należy podkreślić, że w każdym przypadku przeprowadzenie właściwej analizy statystycznej powoduje uwiarygodnienie otrzymanych wyników. Trzeba zauważyć, że nie wszystkie polecenia w ciele procedury czy skryptu mogą być zoptymalizowane. Procesowi temu nie podlegają polecenia dynamicznego SQL, czyli te, które są składane do postaci zmiennej znakowej, a następnie uruchamiane poleceniem EXEC. Jest to dość oczywiste, ponieważ faktyczna treść zapytania jest znana dopiero podczas jego uruchomienia, a w takim przypadku nie można mówić o planie wykonania czegoś, czego postaci nie znamy. Trzecim argumentem podkreślającym wagę stosowania procedur jest bezpieczeństwo [23] – [26] [66] [67]. Przede wszystkim wywoływanie procedur z końcówki klienckiej powoduje, że potencjalny intruz nie widzi rzeczywistej struktury bazy. Przypisanie praw do procedur powoduje ograniczenie liczby wykonywanych na bazie operacji. Jednym z najczęściej spotykanych sposobów przeprowadzania ataku na bazę jest wstrzyknięcie kodu (SQL injection). W takim przypadku intruz próbuje za pomocą kontrolki umieszczonej na końcówce klienta dopisać do kodu inne polecenie SQL. Jeżeli parametry procedury są liczbami, taka forma ataku jest z definicji niemożliwa do realizacji. Jeśli jednak parametrami są zmienne znakowe, mogą być przed podstawieniem do zapytań weryfikowane, walidowane, tak aby wyeliminować potencjalny atak. Możemy wyszukiwać słowa kluczowe obecne w SQL i eliminować je. Podsumowując, za tworzeniem procedur przemawiają trzy argumenty: porządek, wydajność i bezpieczeństwo. Na zakończenie rozdziału efemeryda, stan pośredni pomiędzy procedurą a funkcją. Poza zwracaniem wartości przez listę parametrów w Transact-SQL jest możliwe utworzenie procedury, która tak jak funkcja zwraca wartość przez nazwę. Taka postać składniowa jest specyficzna dla omawianego środowiska. Jej składnia różni się tylko tym, że na końcu pojawia się słowo kluczowe RETURN, po którym wskazywana jest zwracana wartość typu całkowitego. W prezentowanym przykładzie obliczana jest liczba osób wyższych, niż wynosi wartość pierwszego parametru. Wynik ten jest przekazywany do miejsca wywołania przez parametr typu OUTPUT. Natomiast przez nazwę jest przekazywana informacja w postaci wartości 0, gdy są jakiekolwiek osoby wyższe od progu, w przeciwnym wypadku przekazywane jest 1. Po słowie kluczowym RETURN zastosowano wartości stałe, ale równie dobrze można zastosować dowolne całkowitoliczbowe zmienne pomocnicze. W wywołaniu zaprezentowane zostały oba przypadki wykonania procedury.
236
MS SQL Server. Zaawansowane metody programowania DROP PROCEDURE wysocyp GO CREATE PROCEDURE wysocyp @mm real = 0, @ile int OUTPUT AS SELECT @ile=COUNT(wzrost) FROM Osoby WHERE Wzrost >= @mm IF @ile>0 RETURN 0 ELSE RETURN 1 GO DECLARE @a int, @ile int EXEC @a=wysocyp 3, @ile OUTPUT PRINT @ile PRINT @a EXEC @a=wysocyp 1.5, @ile OUTPUT PRINT @ile PRINT @a
Przykładowy skutek dwukrotnego wywołania procedury z różnymi zestawami parametrów może mieć poniższą postać. 0 1 12 0
Wartości zwracane przez nazwę mogą być wykorzystywane do przekazywania kodów błędów użytkownika, czyli takich kodów, które nie powodują błędu przetwarzania, ale wymagają zasygnalizowania operatorowi. Może to być np. spadek stanu magazynowego poniżej dopuszczalnego minimum. Zwykle stosuje się konwencje, że wykonanie poprawne zwraca 0, a każdy inny stan jest kodowany przez wartości różne od 0. Stosowanie takiego mechanizmu wymaga sporządzenia książki kodów tłumaczącej wartości na właściwe opisy.
5.3. Funkcje Podobnym składniowo do procedury elementem Transact-SQL jest funkcja. Pierwszą różnicą jest to, że lista parametrów jest ujęta w nawiasy, natomiast podstawowa definicja parametrów, na którą składają się nazwa i typ z opcjonalną wartością domyślną, pozostaje bez zmian. Następnie pojawia się słowo kluczowe RETURNS, po którym definiowany jest typ, jaki funkcja zwraca przez nazwę. Ciało funkcji musi być ujęte w blok BEGIN…END, którego elementami mogą być, tak samo jak w przypadku procedury, wszystkie instrukcje oraz polecenia SQL. Deklaracje zmiennych lokalnych są obowiązkowe i występują w ciele najpóźniej przed pierwszym ich użyciem. Ostatnim elementem ciała musi być instrukcja RETURN, po której występuje wartość zwracana przez funkcję daną statycznie albo przez zmienną i zgodna co do typu z deklaracją w nagłówku. W funkcjach jest zabronione zwracanie wartości przez parametr, co oznacza, że nie wolno używać dyrektywy OUTPUT lub OUT. W przykładzie pokazano tworzenie i uruchomienie funkcji wyznaczającej liczbę osób wyższych niż parametr @mm.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
237
Należy zauważyć, że wywołanie funkcji odbywa się przez jej podstawienie do zmiennej zadeklarowanej w skrypcie. CREATE FUNCTION wysocy (@mm decimal (3,2) = 0) RETURNS int AS BEGIN DECLARE @ile int SELECT @ile=COUNT(Wzrost) FROM Osoby WHERE Wzrost >= @mm RETURN @ile END GO DECLARE @a int SET @a=dbo.wysocy(1.5) SELECT @a
Należy zauważyć, że w przeciwieństwie do procedury funkcja może zostać wykorzystana w zapytaniu wybierającym. W przykładzie wyświetlane są nazwisko i wzrost pracowników, a funkcja, której parametrem jest wzrost, została obdarzona aliasem Ile i wyświetla liczbę osób wyższych niż ten pracownik. SELECT Nazwisko, Wzrost, dbo.wysocy(Wzrost) AS Ile FROM Osoby
W obu przypadkach wywołania użyta została nazwa kwalifikowana, na którą składa się właściciel dbo (database owner) oraz nazwa funkcji. Używanie pełnej nazwy jest konieczne, w przeciwnym wypadku funkcja nie jest wykrywana, co pokazuje komunikat. Msg 195, Level 15, State 10, Line 2 'wysocy' is not a recognized built-in function name.
W przypadku procedury odwołanie do wartości domyślnej odbywało się przez pominięcie parametrów końcowych lub użycie wywołania nazewniczego z niepełnym wykazem zmiennych. W przypadku funkcji konieczne jest zastosowanie słowa kluczowego default; obowiązkowe jest wywołanie pozycyjne. DECLARE @a int SET @a=dbo.wysocy(default) SELECT @a
Rozważmy przypadek funkcji, której wynik jest ustalany z wykorzystaniem instrukcji warunkowej. Przykład pokazuje funkcję zwracającą 1, gdy istnieje co najmniej jeden pracownik, którego nazwisko rozpoczyna się od podanej frazy, w przeciwnym wypadku zwracane jest 0. Zastosowano w tym celu funkcję EXIST, której argumentem jest zapytanie wybierające. Po słowie kluczowym RETURN ustawiono właściwe statyczne wartości. CREATE FUNCTION spr (@kto varchar(15))RETURNS int AS BEGIN IF EXISTS (SELECT * FROM Osoby WHERE Nazwisko LIKE @kto + '%') RETURN 1; ELSE RETURN 0; END;
238
MS SQL Server. Zaawansowane metody programowania
Konsekwencją takiego działania jest komunikat o błędzie, który informuje nas, że instrukcja RETURN nie jest ostatnią instrukcją ciała funkcji, co jest wymogiem składniowym. Msg 455, Level 16, State 2, Procedure spr, Line 8 The last statement included within a function must be a return statement.
Patrząc na tekst skryptu, widzimy, że słowo kluczowe RETURN bezpośrednio poprzedza END. Jednak faktycznie nie jest ono ostatnim poleceniem ciała. Będzie to wyraźnie widać, kiedy każde z poleceń RETURN ujmiemy w ramy instrukcji grupującej BEGIN…END, co jest konieczne, gdy w którejś z sekcji instrukcji warunkowej IF będziemy mieli co najmniej dwie instrukcje. W takim przypadku koniec ciała będzie stanowiło polecenie END. Najprostszym rozwiązaniem dylematu jest dodanie trzeciej instrukcji RETURN, która będzie zwracała dowolną wartość całkowitą. Wartość ta nie ma znaczenia, ponieważ nie istnieje możliwość, aby funkcja zakończyła się na skutek jej użycia. W każdym przypadku przetwarzania użyte będzie jedno z poprzednio określonych wyjść, ale jest to zabieg formalny, mający na celu zapewnienie wymagań składniowych. CREATE FUNCTION spr (@kto varchar(15))RETURNS int AS BEGIN IF EXISTS (SELECT * FROM Osoby WHERE Nazwisko LIKE @kto + '%') RETURN 1; ELSE RETURN 0; RETURN 7; END;
Poprzednie rozwiązanie pomimo formalnej poprawności nie jest specjalnie eleganckie. Trudno zrozumieć rolę trzeciego RETURN, dlatego lepszym rozwiązaniem będzie zastosowanie zmiennej pomocniczej @ok, której wartość będzie wyznaczona w instrukcji warunkowej, natomiast funkcja zwróci wartość za pomocą pojedynczego RETURN z tą zmienną jako argumentem. DROP FUNCTION spr GO CREATE FUNCTION spr (@kto varchar(15))RETURNS int AS BEGIN DECLARE @ok int IF EXISTS (SELECT * FROM Osoby WHERE Nazwisko LIKE @kto + '%') SET @ok=1 ELSE SET @ok =0 RETURN @ok END
Należy stosować się do zasady, że wartość funkcji nie powinna być zwracana w instrukcji warunkowej, szczególnie gdy jest ona złożona. Lepszym, czytelniejszym rozwiązaniem jest zastosowanie zmiennej pomocniczej. Prezentowane dotąd funkcje zwracały do miejsca wywołania pojedynczą wartość. Funkcje tego typu określamy jako skalarne. Drugim typem funkcji charakterystycznym dla MS SQL Server są funkcje zwracające tabelę (zmienną tabelaryczną) — nazywa-
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
239
ne funkcjami tabelarycznymi. W ich podstawowej składni po słowie kluczowym RETURNS następuje nazwa zmiennej oraz słowo kluczowe TABLE, po którym w nawiasie definiowane są pola według zasad obowiązujących dla tabeli. W ciele funkcji następuje zasilenie tabeli (zmiennej tabelarycznej) danymi. Na końcu pojawia się słowo kluczowe RETURN, po którym nie pojawia się żadna wartość ani zmienna, ponieważ precyzyjna definicja tego, co ma zostać zwrócone, jest zawarta w części nagłówkowej funkcji. W przykładzie utworzono funkcję, która zwraca tabelę zawierającą nazwisko i wzrost osób wyższych niż wartość dana pierwszym parametrem. DROP FUNCTION wysocy_table GO CREATE FUNCTION wysocy_table (@mm decimal (3,2) = 0) RETURNS @Wysocy TABLE (Nazwisko varchar(15), Wzrost decimal(3,2)) AS BEGIN INSERT INTO @Wysocy SELECT Nazwisko, Wzrost FROM Osoby WHERE Wzrost >= @mm ORDER BY Wzrost DESC RETURN END GO
Wywołanie funkcji jest analogiczne do odwołania się do tabeli. Możliwe jest jej zastosowanie jako źródła danych w zapytaniu wybierającym. Możliwe jest stosowanie wszystkich elementów składniowych dla takiego typu zapytania, w którym źródłem jest pojedynczy obiekt. Przykładowe wyniki zapytania zawiera tabela 5.1. SELECT * FROM dbo.wysocy_table(1.5)
Tabela 5.1. Przykładowy zestaw rekordów uzyskany na skutek odwołania się do funkcji tabelarycznej Nazwisko
Wzrost
Nowicki
1.93
Nowak
1.93
Majewski
1.87
…
…
Nowak
1.60
Analizując starą postać składniową, można zauważyć, że czas życia tabeli stosowanej do zwracania wynikowego zestawu rekordów jest równy czasowi przetwarzania funkcji. Ponadto nazwy i typy zwracanych kolumn mogą być odziedziczone po źródłowej tabeli, do której kierujemy zapytanie wybierające. Mając świadomość tych cech, twórcy MS SQL Server wprowadzili nową, uproszczoną postać, funkcjonującą od wersji 2005. W tym przypadku po słowie kluczowym RETURNS występuje tylko słowo kluczowe TABLE. Natomiast w ciele funkcji bezpośrednio po słowie AS występuje polecenie RETURN, którego argumentem ujętym w nawias jest zapytanie wybierające. Jedynym ograniczeniem jest brak możliwości użycia sortowania ORDER BY. Przykład prezentuje realizacje poprzedniej funkcjonalności z zastosowaniem nowej składni.
240
MS SQL Server. Zaawansowane metody programowania CREATE FUNCTION wysocyt (@minimum real = 0) RETURNS TABLE AS RETURN ( SELECT Nazwisko, Imie, Wzrost FROM Osoby WHERE Wzrost >=@minimum ) GO
Wywołanie funkcji tworzonej według starej lub nowej składni niczym się nie różni. Nie pokazano tego w poprzednim przykładzie, ale warto zauważyć, że w wywołaniu funkcji zwracającej tabelę nie jest potrzebne stosowanie nazw kwalifikowanych (dbo.wysocyt) — wystarczy podać nazwę krótką. Natomiast tak samo jak w funkcjach skalarnych konieczne jest jawne użycie słowa kluczowego default przy odwoływaniu się do wartości domyślnych. SELECT * FROM wysocyt (1.5) SELECT * FROM wysocyt (default)
Pozostaje odpowiedzieć na pytanie, która z postaci funkcji tabelarycznej jest ogólniejsza. Łatwo zauważyć, że będzie to stara postać, która nie ogranicza stosowania żadnej z klauzul — również ORDER BY — i ponadto pozwala na zasilanie tabeli z wynikowym zestawem rekordów wieloma zapytaniami wybierającymi. Ponadto możliwe jest stosowanie składni INSERT…VALUES, co pozwala na zasilanie pojedynczymi rekordami, a w połączeniu z kursorami (opisane w podrozdziale 5.6) umożliwia wstawianie złożonych danych. Nowa wersja ograniczona do pojedynczego zapytania jest jednak przejrzysta, prosta składniowo, a w większości przypadków absolutnie satysfakcjonująco funkcjonalna. Nieco odmiennie niż w przypadku dwóch tabel realizowane jest złączenie tabeli z funkcją zwracającą rekordy. Dla potrzeb analizy utwórzmy pomocniczą funkcję prac, która zwraca zestaw rekordów opisujący pracowników działu danego parametrem. DROP FUNCTION prac GO CREATE FUNCTION prac (@dzial int) RETURNS TABLE AS RETURN (SELECT Nazwisko, Imie FROM Osoby WHERE IdDzialu = @dzial) GO
Aby zrealizować złączenie, musimy użyć jednego z dwóch operatorów — CROSS APPLY, odpowiadającego złączeniu wewnętrznemu INNER JOIN, albo OUTER APPLY, który odpowiada LEFT JOIN w przypadku łączenia tabel. Możliwe jest jednorazowe zastosowanie obu tych operatorów w zapytaniu. SELECT Nazwa, Nazwisko FROM Dzialy CROSS APPLY dbo.prac(IdDzialu); SELECT Nazwa, Nazwisko FROM Dzialy OUTER APPLY dbo.prac(IdDzialu);
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
241
Argumenty przemawiające za stosowaniem funkcji są takie same jak omówione w przypadku procedur [1] [3] [5] – [7] [23] – [26] [31] [65] – [67]. Pozostaje odpowiedzieć na pytanie, czy któryś z tych rodzajów elementów proceduralnych powinniśmy preferować. Co wybierać — procedury czy funkcje? W zasadzie poza przypadkiem, kiedy stosujemy funkcje w zapytaniach wybierających, nie ma różnic funkcjonalnych ani wydajnościowych między tymi obiektami, dlatego programista ma pełną swobodę wyboru. Z reguły decyzja jest podyktowana przyzwyczajeniami bądź doświadczeniami zawodowymi.
5.4. Synonimy i błędy użytkownika Kolejną dygresją jest tworzenie synonimów dla obiektów schematu relacyjnego tabeli lub procedury bądź elementu proceduralnego — procedury składowanej lub wątku. Po słowach kluczowych CREATE SYNONIM następuje alternatywna nazwa obiektu, a po słowie kluczowym FOR nazwa oryginalna. Od momentu utworzenia synonimu dostępne są dla użytkownika obie nazwy — oryginalna i zastępcza. DROP SYNONYM syn GO CREATE SYNONYM syn FOR wysocy_table GO SELECT * FROM syn(1.8) SELECT * FROM dbo.wysocy_table(1.8)
Podczas tworzenia synonimu dla funkcji skalarnej możemy stosować jej nazwę krótką, natomiast w wywołaniu, bez względu na to, czy używamy nazwy oryginalnej, czy synonimu, konieczne jest stosowanie postaci kwalifikowanej. DROP SYNONYM syn GO CREATE SYNONYM syn FOR wysocy GO SELECT Nazwisko, Wzrost, dbo.syn(Wzrost)AS ile FROM Osoby SELECT Nazwisko, Wzrost, dbo.wysocy(Wzrost) AS ile FROM Osoby
Synonimy używane są do tworzenia nazw bardziej przyjaznych użytkownikowi i jeśli stosujemy nazwy opisujące zawartość obiektu lub jego funkcje, synonimy mają małe znaczenie praktyczne. W komercji czasami stosowane są nazwy robocze dla uniwersalnego schematu bazy, a synonimy mają przystosować tę nomenklaturę do konkretnej realizacji praktycznej. Druga dygresja jest związana z możliwością generowania błędu przez użytkownika [68]. Dokonujemy tego, używając polecenia RAISERROR, po którym następują w nawiasie trzy parametry: napis lub zmienna znakowa reprezentująca wyświetlany komunikat, liczba całkowita z zakresu 1 do 25, wskazująca przynależność do grupy, oraz liczba mogąca reprezentować informację dodatkową. Trzeci z parametrów jest rzadko wykorzystywany praktycznie i z reguły ma wartość 1. Generowanie błędów tego rodzaju jest wykorzystywane najczęściej w przypadku chęci zasygnalizowania przez programistę sytuacji wyjątkowej, która nie jest błędem przetwarzania, powodującym przerwanie wykonywania skryptu lub procedury, np. gdy na skutek operacji na bazie
242
MS SQL Server. Zaawansowane metody programowania
stan magazynu spadł poniżej ustalonego minimum (należy złożyć zamówienie na nową dostawę). W przykładzie pokazano sposób wygenerowania błędu, który wyświetli napis Komunikat i który należy do pierwszej grupy błędów. Po skrypcie przedstawiona jest informacja ukazująca się w oknie Messages. RAISERROR ('Komunikat', 1, -- grupa błędów 1); -- informacja dodatkowa 0255 Komunikat Msg 50000, Level 1, State 1
Taką samą postać formalną będą miały komunikaty przypisane do grup od 1 do 9. Sytuacja nieznacznie zmieni się, kiedy błąd będzie miał ustawioną grupę 10; wtedy komunikat ma uproszczoną formę ograniczoną do jego treści. RAISERROR ('Komunikat', 10, -- grupa błędów 1); -- informacja dodatkowa 0255 Komunikat
Sytuacja zmieni się, gdy komunikat będzie występował w grupie 11 do 18. Wtedy postać wyświetlanej informacji będzie najobszerniejsza, a kolor wyświetlania zmieni się na czerwony. Możemy powiedzieć, że błędy z tych grup są sygnalizowane jako ważne. RAISERROR ('Komunikat', 11, -- grupa błędów. 1); -- informacja dodatkowa 0255. Msg 50000, Level 11, State 1, Line 1 Komunikat
Ogólnie możemy powiedzieć, że numery grup określają: 0 – 18 — zakres dostępny dla każdego użytkownika; 19 – 25 — zakres dostępny tylko dla użytkownika sysadmin lub użytkownika z uprawnieniami ALTER TRACE; w przypadku użycia opcji WITH LOG (błędy od 20 do 25 traktowane są jako krytyczne).
Jeśli nieuprawniony użytkownik spróbuje przypisać komunikat do wyższego zakresu, pojawia się błąd sygnalizowany przez napis: Msg 2754, Level 16, State 1, Line 1 Error severity levels greater than 18 can only be specified by members of the sysadmin role, using the WITH LOG option.
Jeśli w poleceniu generującym błąd użytkownika ustawimy opcje WITH LOG bez względu na przypisanie do grupy, informacja o nim pojawi się również w systemowym dzienniku zdarzeń, w grupie Aplikacja (Application), co pokazuje rysunek 5.1. Przypisanie do grupy 15 sprawi, że zamiast ikony wskazującej na błąd pojawi się ikona ostrzeżenia, a dla grup o niższych numerach ikona informacji. RAISERROR ('Komunikat', 19, -- grupa błędów 1) WITH LOG Msg 50000, Level 19, State 1, Line 1 Komunikat
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
243
Rysunek 5.1. Komunikat o błędzie użytkownika w systemowym dzienniku zdarzeń
Dla grup błędów, począwszy od numeru 20, komunikat jest obszerniejszy z inicjalnym odwołaniem do błędu 2745 i wyświetleniem właściwego błędu użytkownika w drugiej kolejności. Ponadto wyświetlana jest informacja o konieczności odrzucenia zmian — wycofania transakcji. RAISERROR ('Komunikat', 20, -- grupa błędów 1) WITH LOG Msg 2745, Level 16, State 2, Line 1 Process ID 54 has raised user error 50000, severity 20. SQL Server is terminating this process. Msg 50000, Level 20, State 1, Line 1 Komunikat Msg 0, Level 20, State 0, Line 0 W bieżącym poleceniu wystąpił poważny błąd. Ewentualne wyniki powinny zostać odrzucone
Jeśli ustawimy poziom błędu większy niż 25, zostanie on potraktowany tak, jakby miał ustawioną najwyższą dopuszczalną wartość. Pomimo tego, że przypisanie błędu do grupy ma tylko znaczenie porządkowe, dokumentacja zaleca pewne zasady ich stosowania, co pokazuje tabela 5.2. Tabela 5.2. Grupy błędów i ich opis formalny Poziom zagrożenia
Opis
0–9
Komunikaty informacyjne; silnik bazy danych nie powoduje ustawienia żadnego z komunikatów tej grupy.
10
Komunikaty informacyjne; aby zachować zgodność wsteczną, silnik bazy danych konwertuje numer 10 do 0 przed zwróceniem do aplikacji, sam nie korzysta z tego numeru.
11 – 16
Wskazuje błędy, których przyczyny mogą być skorygowane przez użytkownika.
11
Wskazuje, że obiekt nie istnieje.
12
Specjalny poziom dla zapytań niewykorzystujących blokowania rekordów.
13
Wskazuje na błędy związane z zakleszczeniem transakcji.
244
MS SQL Server. Zaawansowane metody programowania
Tabela 5.2. Grupy błędów i ich opis formalny — ciąg dalszy Poziom zagrożenia
Opis
14
Wskazuje na błędy wynikające z zabezpieczeń, np. związane ze zbyt małymi uprawnieniami.
15
Wskazuje błędy składniowe Transact-SQL.
16
Wskazuje ogólne błędy, których przyczyny mogą być skorygowane przez użytkownika.
17 – 19
Wskazuje błędy programistyczne, których przyczyny mogą być skorygowane przez użytkownika, powiadamiając jednocześnie administratora.
17
Wskazuje na ten fragment SQL, który spowodował wyczerpanie się zasobów (pamięć, zakleszczenia, przestrzeń na dysku) lub przekroczenie ograniczeń ustanowionych przez administratora.
18
Wskazuje na problemy z silnikiem bazy danych, nawet wtedy, gdy zakończono wykonywanie poleceń i nie ma problemów z połączeniem i zarządzaniem nim, powiadamiając jednocześnie administratora.
19
Wskazuje na niekonfigurowalny błąd silnika bazy danych związany z przekroczeniem limitów; bieżące zadania są przerywane, naprawa wymaga interwencji administratora, jest zapisywane do dziennika systemu operacyjnego.
20 – 25
Wskazuje na błędy systemu oraz błędy krytyczne silnika bazy danych; bieżące zadania są przerywane, naprawa wymaga interwencji administratora, jest zapisywane do dziennika systemu operacyjnego.
20
Wskazuje zadanie, które napotkało problem; ponieważ problem dotyczy bieżącego zadania, baza danych nie ulega uszkodzeniu.
21
Wskazuje, że wszystkie zadania instancji napotkały problem, może to oznaczać, że uszkodzona została baza danych.
22
Wskazuje, że problem dotyczy uszkodzenia tabeli lub indeksu wymienionego w komunikacie; przyczyny mogą być zarówno typu programistycznego, jak i sprzętowego; pojawia się rzadko, najczęściej wymaga uruchomienia DBCC CHECKDB do określenia uszkodzonego elementu; jeśli uszkodzenie dotyczy bufora cache, to wystarczy restart instancji (ewentualnie skorzystanie z DBCC); w przypadku błędu dyskowego można usunąć uszkodzony obiekt lub jeśli wskazanie dotyczy indeksu, należy go przebudować.
23
Wskazuje na zagrożenie integralności danych spowodowane błędem programistycznym lub sprzętowym; pojawia się rzadko, najczęściej wymaga uruchomienia DBCC CHECKDB do określenia uszkodzonego elementu; jeśli uszkodzenie dotyczy bufora cache, to wystarczy restart instancji (ewentualnie skorzystanie z DBCC).
24
Wskazuje na błąd urządzenia, wymaga odzyskania bazy danych (RESTORE), czasami konieczna jest wymiana uszkodzonego sprzętu.
25
Inne błędy krytyczne.
Do zdefiniowania komunikatu możliwe jest używanie nie tylko prostego napisu, ale również wartości zmiennych lub funkcji. W tym celu w wybranym miejscu umieszczamy znak definiujący format, natomiast po określeniu grupy i informacji dodatkowej umieszczamy wartości, które mają być wstawione w miejsca wskazane przez znaki specjalne. Podstawienie ich odbywa się pozycyjnie: pierwsza wartość w miejsce
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
245
pierwszego symbolu, druga drugiego itd. W przykładzie do komunikatu wstawiono identyfikator bazy oraz jej nazwę. Obydwie informacje są uzyskiwane na skutek wywołania odpowiednich funkcji systemowych DB_ID() oraz DB_NAME() i podstawienia ich do zmiennych pomocniczych. Dopuszczalne jest pominięcie n końcowych wartości; w takim przypadku w ich miejsce podstawiane jest NULL. DECLARE @DBID INT; SET @DBID = DB_ID(); DECLARE @DBNAME NVARCHAR(128); SET @DBNAME = DB_NAME(); RAISERROR ('Identyfikator bazy ID =%d, Nazwa bazy = %s.', 10, -- grupa błędów 1, -- podgrupa @DBID, -- pierwszy argument @DBNAME); -- drugi argument GO
Symbole formatowania danych stosowane w definicji komunikatów prezentuje tabela 5.3. Tabela 5.3. Symbole formatowania zmiennych Format
Opis
d lub i
Rzeczywista lub całkowita ze znakiem
o
Ósemkowa bez znaku
s
Łańcuch
u
Całkowita bez znaku
x lub X
Szesnastkowa bez znaku
Generowane dotychczas komunikaty o błędach nie były składowane w bazie danych; były tylko tworzone w miejscu ich wywołania. Mówimy często o nich „błędy ad hoc”. Możliwe jest jednak utworzenie błędów, które będą utrwalone i które mogą zostać wykorzystane później. Możemy mówić, że w ten sposób przygotowujemy własny system powiadamiania o sytuacjach wyjątkowych. W starszych wersjach MS SQL Server do 2000 włącznie można było w tym celu zastosować narzędzie wizualne, jednak z przyczyn zupełnie dla mnie niezrozumiałych twórcy serwera zrezygnowali z niego. Pozostały jednak systemowe procedury składowane, które pozwalają na tworzenie takich obiektów. Składowane błędy użytkownika usuwamy za pomocą procedury sp_dropmessage, której argumentem jest numer błędu. Tworzymy je procedurą sp_addmessage, której atrybutami obowiązkowymi są: numer błędu, numer grupy oraz komunikat. Dla błędów użytkownika dostępne są numery, począwszy od 50001. Wywołanie błędu składowanego jest realizowane poleceniem RAISERROR, którego pierwszym argumentem jest numer ustawianego błędu. Jak widać w przykładzie, błąd podczas tworzenia został przypisany do grupy 13, a podczas wywołania do grupy 10. Takie działanie jest dopuszczalne i podkreśla drugorzędną, porządkową rolę grup. sp_dropmessage 50001 GO sp_addmessage 50001, 15, 'Komunikat';
246
MS SQL Server. Zaawansowane metody programowania GO RAISERROR (50001,10,2) GO
Zamiast usuwać i tworzyć ponownie błąd, możemy nadpisać jego definicję, ustawiając parametr @replace na właściwą wartość. Możliwe jest również stosowanie symboli formatujących, definiujących miejsce wstawiania wartości kolejnych parametrów podczas wywołania. W przykładzie w miejsca te kolejno wstawiono wartości 11 oraz 'napis', co potwierdza zaprezentowany wynik wykonania skryptu. sp_addmessage 50001, 15, 'Komunikat %d, %s', @replace='replace'; GO DECLARE @i int, @s varchar(15) SET @i=11 SET @s='napis' RAISERROR (50001,10,2, @i, @s) Komunikat Komunikat 11, napis
Informacje o wszystkich błędach składowanych — systemowych i utworzonych przez użytkownika — uzyskamy, odpytując perspektywę systemową sys.messages. SELECT * FROM sys.messages ORDER BY message_id DESC
Ponieważ produkty Microsoftu wspierają informacje wielojęzyczne Globalization, również dla komunikatów możemy ustalić język, co realizujemy, definiując właściwą wartość parametru @lang. Domyślnym językiem jest angielski 'us_english', a utworzenie wersji językowej różnej od podstawowej powoduje, że pojawia się jego druga kopia we wskazanym języku. Dlatego, co pokazuje przykład, aby skutecznie usunąć błąd, należy usunąć wszystkie jego wersje językowe. Dodatkowo, w przypadku komunikatu ze wskazaniem miejsc wstawienia zmiennych, w podstawowym języku stosujemy omówione poprzednio symbole formatowania, natomiast w pozostałych wersjach pokazujemy numer pozycji, na której ma być ona wstawiona. Wynika to z różnic składniowych, które mogą prowadzić do zmian kolejności wstawiania wartości parametrów, tak aby komunikat był zgodny z gramatyką wybranego języka. Zmiana języka wyświetlania komunikatów w trakcie sesji jest realizowana za pomocą polecenia SET LANGUAGE. Domyślne ustawienia są ustalane podczas instalacji i mogą być na trwałe zmienione przez zmianę parametrów konfiguracyjnych serwera lub bazy. sp_dropmessage 50001, @lang='polish' GO sp_dropmessage 50001, @lang='us_english' GO --Nadpisanie definicji błędu i uzupełnienie jej o parametry sp_addmessage 50001, 15, 'Komunikat %d, %s', @replace='replace'; GO sp_addmessage 50001, 15, 'Inny język %1!, %2!', @lang='polish'; GO DECLARE @i int, @s varchar(15) SET @i=11 SET @s='napis' RAISERROR (50001,10,2, @i, @s) SET LANGUAGE polish; RAISERROR (50001,10,2, @i, @s) SET LANGUAGE us_english GO
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
247
Komunikat 11, napis Changed language setting to polski. Inny język 11, napis Changed language setting to us_english.
Możliwe jest modyfikowanie zapisanego błędu użytkownika za pomocą procedury składowanej sp_altermessage. Jedynym parametrem, który można zmieniać, jest zapis informacji o błędzie do systemowego dziennika zdarzeń: sp_altermessage 50001, WITH_LOG, FALSE GO RAISERROR (50001,10, 2) Komunikat (null), (null)
Generowanie błędów użytkownika ma duże znaczenie praktyczne [68]. Pozwala na sygnalizowanie operatorowi stanów wyjątkowych, które nie wynikają z błędów przetwarzania. Nie są wtedy, tak jak pokazywano w tym podrozdziale, wywoływane bezwarunkowo, ale po spełnieniu pewnych kryteriów. Przykłady takiego postępowania zostaną pokazane podczas omawiania procedur wyzwalanych oraz przetwarzania transakcyjnego.
5.5. Procedury wyzwalane Aby dotychczas tworzone elementy rozszerzenia proceduralnego mogły być użyte, wymagały jawnego wywołania — albo w zewnętrznym skrypcie, albo w innym elemencie proceduralnym. Oprócz nich można utworzyć procedury wyzwalane, triggery, które nie są jawnie wywoływane, ale wykonywane na skutek pojawienia się pewnych zdarzeń [3] [69] – [73]. Podstawowym zakresem obsługiwanych zdarzeń są te, które dotyczą modyfikowania danych zawartych w tabelach czy też w perspektywach, które są ich dynamicznymi kopiami. W przykładzie utworzono procedurę wyzwalaną o nazwie selall, działającą podczas wstawiania oraz aktualizacji danych w tabeli Osoby. W przypadku wyzwalania triggera przez wiele zdarzeń ich lista jest separowana przecinkiem. W ciele wyzwalacza, które następuje po słowie kluczowym AS, mogą pojawić się, tak jak w innych elementach proceduralnych, wszystkie instrukcje oraz polecenia SQL, w tym również zapytania wybierające. Utworzony trigger w odpowiedzi na wstawianie wiersza lub aktualizacji wartości dowolnego pola wyświetli wszystkie rekordy z tabeli Osoby. CREATE TRIGGER selall ON Osoby FOR INSERT, UPDATE AS SELECT * FROM Osoby
Dla każdej tabeli i dla każdej z modyfikacji istnieje możliwość utworzenia dowolnej liczby triggerów. Drugi z utworzonych dla tabeli Osoby wyzwalacz działa tylko dla wstawiania wierszy i wyświetla wszystkie nazwiska z tej tabeli. CREATE TRIGGER selall1 ON Osoby FOR INSERT AS SELECT Nazwisko FROM Osoby
248
MS SQL Server. Zaawansowane metody programowania
Na skutek wykonania zapytania wstawiającego dane jest wykonanie obu triggerów. Ponieważ w zwracanych przez wyzwalacze zestawach rekordów widać nowo wpisywane nazwisko, możemy stwierdzić, że wykonują się one po zdarzeniu — rysunek 5.2. Rysunek 5.2. Skutek działania triggerów utworzonych dla tabeli Osoby podczas wykonywania wstawiania danych
Do wersji 2005 do określenia miejsca wykonywania się ciała wyzwalacza było używane słowo kluczowe FOR, od wersji 2008 można posługiwać się równoważnym AFTER, które jednoznacznie pokazuje chwilę, w której wykonuje się trigger. Skrypt tworzący wyzwalacze mógłby bez zmiany sposobu ich działania zostać przepisany do postaci: DROP TRIGGER selall GO CREATE TRIGGER selall ON Osoby AFTER INSERT, UPDATE AS SELECT * FROM Osoby GO DROP TRIGGER selall1 GO CREATE TRIGGER selall1 ON Osoby AFTER INSERT AS SELECT Nazwisko FROM Osoby
Utworzone procedury wyzwalane są składowane na serwerze danych, a w strukturze hierarchicznej obiektów są umieszczone pod definicją tabeli, na rzecz której działają — rysunek 5.3. Usuńmy poprzednio utworzone wyzwalacze, a w ich miejsce dla tej samej tabeli utwórzmy taki, który będzie uruchamiany przy każdej możliwej modyfikacji zawartości tabeli — przy wstawianiu i usuwaniu wierszy, aktualizacji danych. Ciałem procedury
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
249
Rysunek 5.3. Miejsce składowania triggerów w hierarchicznej strukturze obiektów bazy danych
wyzwalanej będzie wyświetlenie zawartości dwóch systemowych tabel tymczasowych INSERTED i DELETED, tworzonych na czas działania triggera i istniejących tylko w pamięci operacyjnej. CREATE TRIGGER selall ON Osoby AFTER INSERT, UPDATE, DELETE AS SELECT * FROM DELETED SELECT * FROM INSERTED
Prześledźmy zawartość obu tych tabel dla każdej z wykonywanych modyfikacji. INSERT INTO Osoby(Nazwisko) VALUES('Dopisany')
Przede wszystkim należy zauważyć, że obie tabele dziedziczą nazwy i typy pól po tabeli, dla której działa wyzwalacz. Wynika stąd, że nie można utworzyć triggera wyzwalanego zdarzeniem pochodzącym z więcej niż jednej tabeli. W przypadku wstawiania wierszy tabela DELETED jest pusta (tabela 5.4), natomiast INSERTED zawiera wstawiany wiersz lub przy operacji masowej wstawiane wiersze (tabela 5.5). Tabela 5.4. Zawartość tabeli DELETED podczas wykonywania polecenia INSERT na tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
DataZatr
IdSzefa
Tabela 5.5. Zawartość tabeli INSERTED podczas wykonywania polecenia INSERT na tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
DataZatr
IdSzefa
58
NULL
Dopisany
NULL
NULL
NULL
NULL
NULL
W przypadku aktualizacji danych poleceniem UPDATE możemy powiedzieć, że z punktu widzenia triggera jest ono podzielone na dwie operacje — usuwanie wierszy zawierających stare dane i wstawianie wierszy z nowymi danymi. Powoduje to, że tabela DELETED zawiera wiersze z danymi sprzed operacji (tabela 5.6), a tabela INSERTED wiersze po jej wykonaniu (tabela 5.7). Tabela 5.6. Zawartość tabeli DELETED podczas wykonywania polecenia UPDATE na tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
DataZatr
IdSzefa
58
NULL
Dopisany
NULL
NULL
NULL
NULL
NULL
250
MS SQL Server. Zaawansowane metody programowania
Tabela 5.7. Zawartość tabeli INSERTED podczas wykonywania polecenia UPDATE na tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
DataZatr
IdSzefa
58
NULL
Dodany
NULL
NULL
NULL
NULL
NULL
UPDATE Osoby SET Nazwisko='Dodany' WHERE Nazwisko='Dopisany‘
Dla porządku możemy zauważyć, że podczas usuwania danych tabela DELETED (tabela 5.8) zawiera usuwane wiersze, natomiast tabela INSERTED jest pusta (tabela 5.9). Tabela 5.8. Zawartość tabeli DELETED podczas wykonywania polecenia DELETE na tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
DataZatr
IdSzefa
58
NULL
Dodany
NULL
NULL
NULL
NULL
NULL
Tabela 5.9. Zawartość tabeli INSERTED podczas wykonywania polecenia DELETE na tabeli Osoby IdOsoby
IdDzialu
Nazwisko
Imie
RokUrodz
Wzrost
DataZatr
IdSzefa
DELETE FROM Osoby WHERE Nazwisko='Dopisany'
Przedstawiane do tej pory wyzwalacze miały niewielkie znaczenie praktyczne, a służyły jedynie prezentacji składni oraz ich cech charakterystycznych. Spróbujmy utworzyć bardziej użyteczny trigger, który będzie pilnował, aby nazwisko i imię pracownika były pisane dużymi literami, bez względu na sposób wprowadzenia oraz ewentualne późniejsze modyfikacje. W tym celu utworzono procedurę wyzwalaną up, działającą na zdarzeniach INSERT i UPDATE tabeli Osoby; procedura ta w ciele modyfikuje obydwa pola do właściwej postaci. Aby zweryfikować poprawność działania utworzonego obiektu, w przykładowym skrypcie dodano trzy instrukcje — przepisującą imiona wybranych osób na pisane małymi literami, modyfikującą zawartość pola RokUrodz oraz wstawiającą nowy wiersz. CREATE TRIGGER up ON Osoby FOR INSERT, UPDATE AS UPDATE Osoby SET Nazwisko=UPPER(Nazwisko), Imie=UPPER(Imie) PRINT 'Wykonano' GO UPDATE Osoby SET Nazwisko=LOWER(Nazwisko) WHERE IdOsoby >10 UPDATE Osoby Set RokUrodz=0 WHERE RokUrodz IS NULL INSERT INTO Osoby(Nazwisko) VALUES('Nowy')
Analizując zawartość tabeli Osoby, po wykonaniu skryptu możemy zauważyć, że formalnie działa on poprawnie. Wskazuje to, że polecenie UPDATE zawarte w ciele triggera nie wywołuje tego samego wyzwalacza, czyli nie następuje wygenerowanie nieskończonej rekurencji. Należy jednak pamiętać, że gdyby taka rekurencja miała miejsce — co następuje między wyzwalaczami dla różnych tabel — pojawi się lawina wzajemnych wywołań. Pomimo tej pozytywnej cechy możemy stwierdzić, że z punktu widzenia wydajności utworzony obiekt jest bardzo rozrzutny. Jeśli wstawiamy lub modyfikujemy jeden rekord, trigger i tak przepisze zawartość całej tabeli. Ponadto jeśli modyfikacja dotyczy pola innego niż te, które mają być pilnowane przez wyzwalacz,
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
251
również zostanie wykonane polecenie UPDATE, które w tym przypadku jest zupełnie bezużyteczne. Oczywiście problem nie jest newralgiczny, jeśli działamy na małych wolumenach danych, ale jeśli tabela będzie miała milion rekordów, co nie jest wartością dużą we współczesnych bazach, mała wydajność rozwiązania może prowadzić do poważnych kłopotów, łącznie z blokowaniem zasobów. Spróbujmy poprawić najpierw wadę polegającą na tym, że procedura wyzwalana wykonuje się dla wszystkich wierszy, bez względu na liczbę modyfikowanych lub wstawianych rekordów. W tym celu wykorzystajmy wiedzę o systemowych tabelach tymczasowych INSERTED i DELETED. Zmiana polega na dodaniu w zapytaniu aktualizującym, występującym w ciele triggera, klauzuli WHERE, w której sprawdzamy, czy identyfikator IdOsoby jest na liście modyfikowanych rekordów, które zapisane są w tabeli INSERTED. CREATE TRIGGER up ON Osoby FOR INSERT, UPDATE AS UPDATE Osoby SET Nazwisko=UPPER(Nazwisko), Imie=UPPER(Imie) WHERE IdOsoby IN (SELECT IdOsoby FROM INSERTED) PRINT 'Wykonano' GO UPDATE Osoby SET Nazwisko=LOWER(Nazwisko) WHERE IdOsoby >10 UPDATE Osoby Set RokUrodz=0 WHERE RokUrodz IS NULL INSERT INTO Osoby(Nazwisko) VALUES('Nowy')
Po takich modyfikacjach ciało triggera jest wykonywane tylko dla wstawianych lub modyfikowanych rekordów. Niestety, w tym drugim przypadku wyzwalacz działa również wtedy, kiedy zmieniane są wartości pól, które nie są istotne z punktu widzenia jego funkcjonalności. Aby to zmienić, przed wykonaniem polecenia aktualizującego sprawdzamy w instrukcji warunkowej IF za pomocą funkcji UPDATE(pole), który z atrybutów był zmieniany. Jeśli dotyczy to jednego z dwóch pól, Nazwisko lub Imie, wykonywane jest polecenie UPDATE i wyświetlany jest komunikat Wykonano. W przeciwnym wypadku aktualizacja nie jest wykonywana, jednak aby pokazać w testach sposób działania triggera, wyświetlany jest komunikat Ominięto, który w praktyce mógłby być pominięty. Z analogicznych przyczyn w obu sekcjach instrukcji warunkowej wyświetlono rezultat działania funkcji systemowej COLUMNS_UPDATED(). CREATE TRIGGER up ON Osoby FOR INSERT, UPDATE AS IF UPDATE(Nazwisko) OR UPDATE(Imie) BEGIN UPDATE Osoby SET Nazwisko=UPPER(Nazwisko), Imie=UPPER(Imie) WHERE IdOsoby IN (SELECT IdOsoby FROM INSERTED) PRINT 'Wykonano' PRINT COLUMNS_UPDATED() END ELSE PRINT 'Ominięto' PRINT COLUMNS_UPDATED() GO UPDATE Osoby SET Nazwisko=LOWER(Nazwisko) WHERE IdOsoby >10 UPDATE Osoby Set RokUrodz=0 WHERE RokUrodz IS NULL INSERT INTO Osoby(Nazwisko) VALUES('Nowy')
252
MS SQL Server. Zaawansowane metody programowania
Teraz zasadnicza funkcja ciała procedury wyzwalanej wykonuje się jedynie wtedy, gdy modyfikowana jest jedna z dwóch kolumn. Wykonuje się również dla INSERT, gdy wstawiane są niepuste wartości. Niestety, w przypadku konfiguracji, w której ustawiono nierozróżnianie wielkości liter, nie jest możliwe proste zablokowanie sytuacji, kiedy modyfikacja polega na wykonaniu funkcji UPPER(pole), czyli będziemy mieli do czynienia ze zduplikowaniem polecenia w skrypcie i ciele wyzwalacza. Użyta w ciele funkcja COLUMNS_UPDATED() wyświetla zakodowaną informacje o tym, które z kolumn zostały zmienione, co ilustruje tabela 5.10. Tabela 5.10. Komunikaty wyświetlane przez wyzwalacz oraz ich transkrypcja na postać binarną Rzeczywisty komunikat
Komunikat z wartościami skonwertowanymi do postaci binarnej
Wykonano
Wykonano
0x04
000100
0x04
000100
Ominięto
Ominięto
0x10
010000
Wykonano
Wykonano
0x3F
111111
0x3F
111111
Jak widać z transkrypcji oryginalnej notacji heksadecymalnej na postać binarną, w przypadku polecenia UPDATE funkcja COLUMNS_UPDATED() ustawia bit na 1 dla tego pola, którego wartość jest zmieniana, co można sprawdzić, analizując tabelę 5.11. W przypadku wstawiania nowego wiersza wszystkie bity są ustawione na 1. Tabela 5.11. Przykłady wartości generowanych przez funkcję COLUMNS_UPDATED() dla modyfikacji kolumn tabeli Osoby Kolumna
Kod heksadecymalny
Kod binarny
IdOsoby
0x01
000001
IdDzialu
0x02
000010
Nazwisko
0x04
000100
Imie
0x08
001000
RokUrodz
0x10
010000
Wzrost
0x20
100000
Kolejnym praktycznym zastosowaniem triggera może być weryfikacja wprowadzanych danych [74] [75]. Przeanalizujmy to na przykładzie, w którym blokowana jest możliwość wpisania lub zmodyfikowania pola RokUrodz w ten sposób, że pojawi się osoba młodsza od najmłodszej osoby istniejącej w tabeli Osoby. W tym celu zadeklarowano dwie zmienne lokalne, @max_v oraz @act_v. Deklaracje mogą pojawić się w dowolnym miejscu ciała wyzwalacza, przed pierwszym odwołaniem się do zmiennej. Pierwsza zmienna jest użyta do przechwycenia maksymalnej wartości pola RokUrodz. Ponieważ ciało triggera wykonywane jest po zdarzeniu, to aby nie uwzględniać wartości aktualnie wprowadzanej, zastosowano złączenie tabel Osoby oraz INSERTED z warunkiem nierówności identyfikatorów wierszy IdOsoby. Zmienna @act_v przechwytuje aktualnie
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
253
wprowadzaną wartość pola, pobraną z tabeli INSERTED. Po sprawdzeniu warunku generowany jest błąd użytkownika. W komunikacie dotyczącym tego błędu są zawarte informacje o wprowadzonej i najmniejszej dopuszczalnej wartości. Następnie, aby przywrócić stan sprzed modyfikacji, transakcja jest wycofywana — szczegółowe omówienie transakcyjności znajduje się w rozdziale 6. Skrypt został uzupełniony o instrukcję wstawiającą jeden wiersz do tabeli Osoby, która służy do przetestowania zaproponowanego rozwiązania. DROP TRIGGER upd GO CREATE TRIGGER upd ON Osoby FOR INSERT, UPDATE AS DECLARE @max_v int, @act_v int SELECT @max_v = MAX(o.RokUrodz) FROM Osoby o, INSERTED i WHERE o.IdOsoby <> i.IdOsoby SELECT @act_v = i.RokUrodz FROM INSERTED i PRINT COLUMNS_UPDATED() PRINT @max_v PRINT @act_v if @act_v > @max_v BEGIN RAISERROR ('Wartość powinna być mniejsza równa %d a jest równa %d.',17,127, @max_v, @act_v) ROLLBACK TRANSACTION END GO INSERT INTO Osoby(RokUrodz) VALUES(1999)
Pierwszy z komunikatów przedstawionych niżej jest konsekwencją zastosowania w ciele procedury wyzwalanej funkcji agregującej i informuje, że wartości NULL nie będą brane pod uwagę do jej wyznaczania, co jest naturalnym sposobem jej działania. Kolejno wyświetlane są kod wskazujący na modyfikowane kolumny (tabela 5.11) oraz maksymalna i wprowadzana do tabeli wartość roku urodzenia. Następnie podawana jest informacja o wycofaniu transakcji, a po niej zdefiniowany w ciele triggera komunikat dotyczący błędu użytkownika. Warning: Null value is eliminated by an aggregate or other SET operation. 0x0C 1982 1999 Msg 3609, Level 16, State 1, Procedure up, Line 7 The transaction ended in the trigger. The batch has been aborted. Msg 50000, Level 17, State 127, Procedure upd, Line 12 Wartość powinna być mniejsza równa 1982, a jest równa 1999.
Pokazany w przykładzie trigger będzie działał poprawnie tylko w przypadku pojedynczo wprowadzanych wierszy oraz tak samo realizowanych modyfikacji. Opracowanie ogólniejszego wyzwalacza, działającego selektywnie na całym zestawie nowych danych, wymaga zastosowania kursora, który będzie omówiony w kolejnym rozdziale. Mówiliśmy dotąd o procedurach, które były wyzwalane po wystąpieniu modyfikacji. Drugim rodzajem są wyzwalacze INSTEAD OF, działające zamiast zdarzenia, które je spowodowało. Dotyczą one tak jak poprzednio wszystkich poleceń modyfikujących zawartość tabeli oraz mogą być zdefiniowane dla tabeli oraz perspektywy. Przykład pokazuje tworzenie triggera działającego zamiast polecenia usuwającego rekordy z tabeli
254
MS SQL Server. Zaawansowane metody programowania Osoby, którego ciało zawiera wyświetlenie komunikatu. Oznacza to, że usuwanie wierszy z tej tabeli jest bezwarunkowo zablokowane, o czym informuje komunikat. CREATE TRIGGER zamiast ON Osoby INSTEAD OF DELETE AS PRINT 'Zakaz kasowania' GO DELETE FROM Osoby
Jednak po wykonaniu polecenia DELETE wyświetlane są dwie informacje. Pierwsza stwierdzająca, że nie możemy wykonać kasowania, a druga, że jednak niezerowa liczba wierszy została przetworzona. Ta druga informacja może przyprawić operatora o ból głowy, bo starał się przeciwdziałać usuwaniu wierszy, a komunikat pokazuje, że te wysiłki były bezowocne. Zakaz kasowania (23 row(s) affected)
Druga linia komunikatu wskazuje, że tyle wierszy było przeznaczonych do wykasowania, natomiast trigger nie pozwolił na realizację tej operacji. W praktyce rzadko kiedy będzie nam zależało na aż tak restrykcyjnym działaniu. Częściej blokada będzie występowała tylko wtedy, kiedy nie będzie spełniony jakiś warunek, a w przypadku przeciwnym akcja uruchamiająca wyzwalacz będzie ponawiana. Możemy to prześledzić na przykładzie procedury wyzwalanej, działającej zamiast DELETE, która blokuję tę operację tylko wtedy, kiedy usuwany pracownik miał jakąkolwiek wypłatę. W tym celu zadeklarowano lokalną zmienną @ile, do której podstawiana jest liczba wypłat pracowników, których identyfikatory znajdują się na liście usuwanych osób — w tabeli DELETED. Jeśli wartość ta jest większa od zera, wyświetlany jest komunikat, w przeciwnym wypadku wykonywane jest polecenie DELETE, a informacja o tym, które rekordy próbowano usunąć, jest pobierana z tej samej tabeli tymczasowej. CREATE TRIGGER zamiast ON Osoby INSTEAD OF DELETE AS DECLARE @ile int SELECT @ile=COUNT(IdZarobku) FROM Zarobki WHERE IdOsoby IN (SELECT IdOsoby FROM DELETED) IF (@ile >0) PRINT 'Zakaz kasowania' ELSE DELETE FROM Osoby WHERE Idosoby IN (SELECT IdOsoby FROM DELETED) GO
Podobnie jak w przypadku triggera blokującego wprowadzanie do bazy zbyt młodego pracownika, również i ten wyzwalacz działa mało selektywnie. Jeśli będziemy chcieli usunąć co najmniej dwa rekordy jednym poleceniem DELETE, to wystarczy, aby jedna z osób miała chociaż jedną wypłatę, by cała operacja została zablokowana. Z punktu
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
255
widzenia bezpieczeństwa jest to być może działanie pożądane, jednak aby utworzyć bardziej selektywny trigger, konieczne jest zastosowanie kursora (zob. następny rozdział). Jeśli Czytelnik chce spróbować sił z własnymi rozwiązaniami triggerów typu INSTEAD OF, może przekształcić na ten rodzaj wymieniony w tym akapicie wyzwalacz blokujący wstawianie pracowników młodszych niż najmłodszy na liście. Procedury wyzwalane mogą być również uruchamiane w odpowiedzi na wystąpienie zdarzeń związanych ze zmianami struktury bazy danych, uprawnień itp. [1] [77]. Mogą one dotyczyć dwóch zakresów całej instancji ALL SERVER oraz bazy danych, w której jest on stworzony, DATABASE. Pełny wykaz zdarzeń tego typu zawiera perspektywa systemowa sys.trigger_event_types. Są one zorganizowane w strukturę hierarchiczną, dlatego aby pokazać jej postać, zamiast zwykłego zapytania wybierającego zastosowano postać bardziej złożoną, wykorzystującą klauzulę WITH. Klauzula zawiera pokazywany w rozdziale 3. mechanizm rekurencji z zastosowaniem operatora UNION ALL i odwołaniem się w drugim zapytań do tworzonej struktury. Pierwsze z zapytań zawiera odwołanie do metody GetRoot typu opisującego hierarchię hierarchyid. Kolejne pola, type i type_name, są bezpośrednio pobierane ze słownika. Czwarte pole zawiera napis NULL jawnie skonwertowany do typu znakowego, aby ominąć znany problem ze zgodnością typów między polami występującymi na tej samej pozycji w dwóch składnikach łączonych operatorem mnogościowym UNION. Wyrażeniu temu został nadany alias parent_type, zgodny z oryginalną nazwą pola słownika. Zastosowanie napisu zamiast oryginalnej wartości NULL, co gwarantuje wyrażenie zawarte w klauzuli WHERE, wynika z późniejszej konkatenacji napisów, w której otrzymalibyśmy wartość pustą zamiast oczekiwanej informacji o poziomie najwyższym. Następne pole zawiera wartość 0 z nadanym aliasem Level i posłuży nam do określenia poziomu zagnieżdżenia w strukturze drzewa. Ostatnie z pól zawiera spację, która będzie znakiem rozpoczynającym opis struktury drzewa [7] [76]. Drugi składnik sumy mnogościowej dodaje w pierwszym polu do węzła wyższego poziomu numer kolejnego dziecka uzyskany za pomocą funkcji rangowej ROW_NUMBER(), obliczonej nad oknem logicznym, którego zakres wyznacza ta sama wartość identyfikatora elementu nadrzędnego — rodzica. Całość jest domykana znakiem / oraz jawnie konwertowana do typu Hierarchyid, co jest wymuszone przez typ zastosowany w pierwszym składniku sumy. Dwa kolejne pola są pobrane bezpośrednio ze słownika, a użycie nazwy kwalifikowanej wynika z zastosowanego w zapytaniu złączenia. Czwarte pole jest skonwertowanym do typu znakowego polem słownika parent_type, co jak poprzednio, wynika z konieczności zachowania zgodności typów odpowiednich pól sumy mnogościowej. Następne z pól zawiera inkrementację wartości wyznaczającej poziom, a ostatnie powoduje dodanie do ścieżki znaku |, co reprezentuje gałąź drzewa. Ponieważ mamy do czynienia ze strukturą hierarchiczną, zawarte w tym zapytaniu złączenie jest realizowane przez przyrównanie identyfikatora elementu poziomu nadrzędnego z identyfikatorem rodzica dla bieżącego rekordu. Finalne zapytanie wybierające wyświetla napis reprezentujący węzeł w strukturze przez opis analogiczny do wskazania drogi do bieżącego folderu od początku ścieżki, napis próbujący odtworzyć strukturę drzewa — na który składa się odpowiednia do poziomu liczba symboli | uzyskanych w klauzuli WITH, do których dopisano znaki _ o liczbie będącej dwukrotnością poziomu — numer i nazwę zdarzenia oraz numer rodzica
256
MS SQL Server. Zaawansowane metody programowania
separowane spacjami, a także numer poziomu separowany tabulatorem CHR(9). Całość została posortowana na podstawie opisu węzła. WITH Hierarchia AS ( SELECT hierarchyid::GetRoot() AS Wezel, type, type_name, CAST('NULL' AS varchar(max)) AS parent_type, 0 AS Level, CAST(' ' AS varchar(max)) AS Sciezka FROM sys.trigger_event_types WHERE parent_type IS NULL UNION ALL SELECT CAST(Wezel.ToString() + CAST(ROW_NUMBER() OVER (PARTITION BY sys.trigger_event_types.parent_type ORDER BY sys.trigger_event_types.parent_type) AS varchar(30)) + '/' AS hierarchyid), sys.trigger_event_types.type, sys.trigger_event_types.type_name, CAST(sys.trigger_event_types.parent_type AS varchar(max)), Level+1, CAST(Sciezka +'|'AS varchar(max)) FROM sys.trigger_event_types JOIN Hierarchia ON sys.trigger_event_types.parent_type = Hierarchia.type ) SELECT CAST(Wezel AS varchar(12)), Sciezka + REPLICATE('_',2*LEVEL) + CAST (type as varchar(6)) +' '+ type_name+ ' ' +CAST(parent_type AS varchar(6))+ CHAR(9)+ CAST(Level AS varchar(1)) FROM Hierarchia ORDER BY Wezel
Skutkiem wykonania przedstawionego zapytania jest bardzo rozległy zestaw rekordów, co świadczy o dużej liczbie zdarzeń obsługiwanych przez tego typu wyzwalacze. W celach prezentacyjnych ograniczono się do wybranego, dość wąskiego, silnie ze sobą połączonego zakresu. Całość rozpoczynają dwa zdarzenia, które nie mają elementów nadrzędnych. Pierwsze z nich, ALTER_SERVER_CONFIGURATION, nie tworzy drzewa potomków, natomiast kolejne, DDL_EVENTS, jest rodzicem wszystkich pozostałych. Reszta przedstawionych zdarzeń, począwszy od DDL_DATABASE_LEVEL_EVENTS, jest związana z modyfikacją schematu bazy danych. / 296 ALTER_SERVER_CONFIGURATION NULL / 10001 DDL_EVENTS NULL 0 /1/ |__10002 DDL_SERVER_LEVEL_EVENTS 10001 /1/1/ ||____214 ALTER_INSTANCE 10002 … /2/ |__10016 DDL_DATABASE_LEVEL_EVENTS 10001 /2/1/ ||____241 RENAME 10016 2 /2/2/ ||____10017 DDL_TABLE_VIEW_EVENTS 10016 /2/2/1/ |||______10018 DDL_TABLE_EVENTS 10017 /2/2/1/1/ ||||________21 CREATE_TABLE 10018 /2/2/1/2/ ||||________22 ALTER_TABLE 10018 /2/2/1/3/ ||||________23 DROP_TABLE 10018 /2/2/2/ |||______10019 DDL_VIEW_EVENTS 10017 /2/2/2/1/ ||||________41 CREATE_VIEW 10019 /2/2/2/2/ ||||________42 ALTER_VIEW 10019 /2/2/2/3/ ||||________43 DROP_VIEW 10019
0 1 2 1 2 3 4 4 4 3 4 4 4
Jeśli z przedstawionych zdarzeń wybierzemy dwa związane z tworzeniem i usuwaniem tabeli, to dla zakresu bieżącej bazy danych DATABASE możliwe jest utworzenie procedury wyzwalanej, która po ich wystąpieniu wyświetli komunikat. Całość skryptu wzbogacono o polecenia: tworzące, modyfikujące przez dodanie i usunięcie kolumny, usuwające tabelę oraz tworzące i usuwające perspektywę. Bezpośrednio po poleceniach, które uruchamiają trigger, pokazano właściwy dla niego komunikat.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
257
DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON DATABASE FOR CREATE_TABLE, DROP_TABLE AS SELECT 'Zmiana bazy danych ' + user GO CREATE Table xxx (id int) GO Zmiana bazy danych dbo
ALTER Table xxx ADD pole varchar(3) GO ALTER Table xxx DROP COLUMN pole GO DROP Table xxx GO Zmiana bazy danych dbo
CREATE VIEW xxx AS SELECT Nazwisko FROM Osoby GO DROP VIEW xxx
Aby obsłużyć wszystkie zdarzenia związane z tabelami, możemy do poprzednio zastosowanych dołączyć zdarzenie ALTER_TABLE albo analizując ich hierarchię, zauważyć, że cała trójka ma jednego rodzica o identyfikatorze 10018 i nazwie DDL_TABLE_EVENTS. Zdarzenie rodzic zastępuje wszystkie zdarzenia potomne. Według takich zasad zmodyfikowano poprzednio utworzony trigger. Należy zauważyć, że do usunięcia triggera poleceniem DROP nie wystarczy podać jego nazwę, tak jak to było przy wyzwalaczach działających podczas modyfikacji zawartości tabel, ale należy podać zakres, dla którego został utworzony. Brak takiego określenia wygeneruje komunikat, że wskazany trigger nie został znaleziony. Wynika to z odrębnego miejsca składowania wyzwalaczy o zakresie DATABASE (rysunek 5.4) w porównaniu z triggerami dla tabel (rysunek 5.3). Analizując komunikaty występujące po odpowiednich poleceniach SQL, możemy potwierdzić, że wyzwalacz wykonany został zarówno podczas tworzenia, jak i usuwania oraz modyfikowania tabeli. DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON DATABASE FOR DDL_TABLE_EVENTS AS SELECT 'Zmiana bazy danych ' + user GO CREATE Table xxx (id int) GO Zmiana bazy danych dbo
ALTER Table xxx ADD pole varchar(3) GO Zmiana bazy danych dbo
258
MS SQL Server. Zaawansowane metody programowania ALTER Table xxx DROP COLUMN pole GO Zmiana bazy danych dbo
DROP Table xxx GO Zmiana bazy danych dbo
CREATE VIEW xxx AS SELECT Nazwisko FROM Osoby GO DROP VIEW xxx
Rysunek 5.4. Miejsce składowania triggerów o zakresie DATABASE w hierarchicznej strukturze obiektów bazy danych
Jeśli chcemy, aby procedura wyzwalana była wykonywana zarówno dla operacji dotyczących tabel, jak i perspektyw, możemy użyć albo dwóch trójek nazw odpowiadających właściwym operacjom, albo pary DDL_TABLE_EVENTS i DDL_VIEW_EVENTS będącej ich rodzicami. Możemy jednak zauważyć, że para ta posiada wspólnego przodka o numerze 10017 DDL_TABLE_VIEW_EVENTS. Takie rozwiązanie zastosowano w przedstawionym triggerze. Teraz komunikaty pojawiają się po każdym poleceniu skryptu testującego działanie wyzwalacza. DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON DATABASE FOR DDL_TABLE_VIEW_EVENTS AS SELECT 'Zmiana bazy danych ' + user GO CREATE Table xxx (id int) GO Zmiana bazy danych dbo
ALTER Table xxx ADD pole varchar(3)
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
259
GO Zmiana bazy danych dbo
ALTER Table xxx DROP COLUMN pole GO Zmiana bazy danych dbo
DROP Table xxx GO Zmiana bazy danych dbo
CREATE VIEW xxx AS SELECT Nazwisko FROM Osoby GO Zmiana bazy danych dbo
DROP VIEW xxx Zmiana bazy danych dbo
Idąc w górę hierarchii, możemy znaleźć kolejnego rodzica DDL_DATABASE_LEVEL_EVENTS. Przy takiej definicji zdarzeń wyzwalacz będzie wykonywany dla wszystkich poleceń dotyczących obiektów bazy danych, między innymi procedur, funkcji, triggerów, ale również i zmian uprawnień. Utworzenie skryptu sprawdzającego ten fakt pozostawiam Czytelnikowi do samodzielnego wykonania, gdyż nie będzie on bardzo skomplikowany, natomiast będzie bardzo długi. DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS SELECT 'Zmiana bazy danych ' + user GO
Ostatnim rodzicem na tej drodze jest DDL_EVENTS, który zawiera w sobie wszystkie zdarzenia na serwerze związane z jego obiektami (dochodzą do tego między innymi operacje na bazie, loginach, związane z systemem komunikatów, dołączanymi serwerami, etc.). Grupa ta nie obejmuje tylko ALTER_SERVER_CONFIGURATION (również najwyższy poziom w hierarchii) oraz LOGON, które nie jest wymienione w perspektywie słownikowej (co jest dziwne), a które zostanie omówione później. Skorzystanie z tej grupy zdarzeń wymaga zmiany zakresu na ALL SERVER, podobnie jak w przypadku potomków, począwszy od numeru 10002 DDL_SERVER_LEVEL_EVENTS. DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON ALL SERVER FOR DDL_EVENTS AS SELECT 'Zmiana bazy danych ' + user GO
Jeśli nie dokonamy zmiany zakresu z DATABASE na ALL SERVER, pojawi się komunikat o błędzie. Msg 1098, Level 15, State 1, Procedure Db_schemat, Line 5 The specified event type(s) is/are not valid on the specified target object.
260
MS SQL Server. Zaawansowane metody programowania
Skupmy się teraz na informacji, jaką możemy odczytać z ciała bazy i przedstawić np. w postaci komunikatu. Możemy zastosować odwołanie do struktury XML o poziomie root EVENT_INSTANCE, często tak właśnie określanej. Do obsługi tej struktury możemy zastosować obiekt EVENTDATA() z metodą value(). Ponieważ odwołujemy się do obiektowości, która jest realizowana za pomocą języków wyższego rzędu (platforma .NET), wielkość liter użytych w nazwach metod, ale również parametrów, ma znaczenie (rozdział 7.). Zastosowana metoda ma dwa parametry znakowe. Pierwszy z nich wskazuje ujętą w nawias ścieżkę do interesującego nas poziomu struktury XML, począwszy od elementu najwyższego. Do tego wskazania dodany jest wskaźnik do elementu macierzy [1]. Nie oznacza to, że pod innymi wskaźnikami znajdują się inne wartości, ale jest to najprostsza metoda wymuszenia posługiwania się przekazywaniem parametru przez referencję, a nie, jak ma to miejsce domyślnie, przez wartość po stronie języka wyższego rzędu. Wynika to z konieczności zachowania zgodności z miejscem wywołania metody po stronie SQL. Drugim parametrem jest łańcuch reprezentujący typ zwracanej zmiennej — można przyjąć, że zawsze będzie on tekstem. W przykładzie zaprezentowany został trigger działający dla wszystkich zdarzeń modyfikujących strukturę bazy danych, a wyświetlający treść wykonywanego polecenia, co ilustrują komunikaty przedstawione po skrypcie testującym. DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS SELECT 'Polecenie '+ EVENTDATA().value( '(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]','nvarchar(max)') + ' wykonane przez - '+ user GO CREATE Table xxx (id int) GO ALTER Table xxx ADD pole varchar(3) GO ALTER Table xxx DROP COLUMN pole GO DROP Table xxx GO CREATE VIEW xxx AS SELECT Nazwisko FROM Osoby GO DROP VIEW xxx Polecenie CREATE Table xxx (id int) wykonane przez - dbo Polecenie ALTER Table xxx ADD pole varchar(3) wykonane przez - dbo Polecenie ALTER Table xxx DROP COLUMN pole wykonane przez - dbo Polecenie DROP Table xxx wykonane przez - dbo
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
261
Polecenie CREATE VIEW xxx AS SELECT Nazwisko FROM Osoby wykonane przez - dbo Polecenie DROP VIEW xxx wykonane przez - dbo
Jeśli jako informacja zwrotna wystarczy nam typ polecenia zamiast pełnej jego treści, która może być dość długa, wystarczy w przykładzie zmienić wskazanie znacznika w strukturze EVENT_INSTANCE. DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS SELECT 'Polecenie '+ EVENTDATA().value( '(/EVENT_INSTANCE/EventType)[1]','nvarchar(max)') + ' wykonane przez - '+ user Polecenie CREATE_TABLE wykonane przez - dbo Polecenie ALTER_TABLE wykonane przez - dbo Polecenie ALTER_TABLE wykonane przez - dbo Polecenie DROP_TABLE wykonane przez - dbo Polecenie CREATE_VIEW wykonane przez - dbo Polecenie DROP_VIEW wykonane przez - dbo
Należałoby teraz przedstawić strukturę XML EVENT_INSTANCE wykorzystywaną przez EVENTDATA().value(), jednak jej pełna postać jest bardzo złożona. Dlatego w pierwszym podejściu możemy przyjąć, że ma ona dla wszystkich poleceń znaczniki takie jak w górnej części pliku — rodzaj zdarzenia, moment wykonania, identyfikator, nazwa serwera, nazwa loginu (określenie użytkownika podczas logowania). Natomiast w większości przypadków możemy stosować te, które wymieniono na końcu — nazwa użytkownika, nazwa serwera, nazwa schematu, nazwa oraz typ obiektu polecenie SQL. Jeżeli dla zdarzenia dany znacznik nie istnieje lub nie ma sensu, EVENTDATA(). value() zwróci wartość pustą. Inaczej mówiąc, nie musimy bać się nadmiarowości pobieranej informacji. Taki stan nie spowoduje wygenerowania błędu, a w najgorszym przypadku informacja będzie pusta — NULL.
type date-time spid name name
name name name name type command
Jeśli chcemy świadomie wykorzystywać znaczniki EVENT_INSTANCE, to pełną ich strukturę zawiera plik, który jest dostępny w lokalizacji sieciowej: http://schemas.microsoft.com/ sqlserver/2006/11/eventdata/events.xsd. Możemy również posłużyć się plikiem, który jest zapisywany na dysku, a który przy domyślnej strukturze folderów znajduje się w lokalizacji:
262
MS SQL Server. Zaawansowane metody programowania
C:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\schemas\sqlserver\200 6\11\events. Pełna prezentacja pliku mija się z celem, przede wszystkim ze względu na jego objętość oraz dlatego że wiele fragmentów się powtarza. Prezentuję zatem tylko fragment dotyczący jednego zdarzenia — tworzenia tabeli. Aby znaleźć interesujący nas fragment, wystarczy wyszukać znacznik complexType, którego atrybut name zawiera nazwę szukanego stanu — w naszym przypadku CREATE_TABLE. Pełna nazwa jest poprzedzona stałym prefiksem i ma postać EVENT_INSTANCE_CREATE_TABLE.
Zawarte w pliku komentarze określają rolę lub zakres znacznika. Poza nazwą znacznik wskazuje typ atrybutu. Precyzyjne definicje typów są określone w początkowej części pliku, na przykład dla elementu TSQLCommand jego typ EventTag_TSQLCommand jest określony jako:
Należy zauważyć, że na najniższym poziomie zawartość znacznika jest zwykle napisem, a różnice wynikają tylko z jego maksymalnej długości. Przejdźmy teraz do praktycznego zastosowania triggerów oraz metody EVENTDATA(). value() operującej na opisanej strukturze. W tym celu utwórzmy pomocniczą tabelę o nazwie Audyt_ddl, która poza polem automatycznie inkrementowanego klucza podstawowego zawiera pola, których nazwy odpowiadają wybranym znacznikom pliku XML. Jak widać, poza polem automatycznie inkrementowanego klucza głównego zdecydowano się na zastosowanie typów znakowych. W przypadku ostatniego z nich, TSQLCommand, określono rozmiar pola jako max, co wynika z dużej zmienności długości poleceń SQL. CREATE TABLE Audyt_ddl ( IdAudyt_ddl int IDENTITY(1,1) PRIMARY KEY, EventType varchar(30), PostTime varchar(30), SPID varchar(30),
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
263
ServerName varchar(30), LoginName varchar(30), UserName varchar(30), DatabaseName varchar(30), SchemaName varchar(30), ObjectName varchar(30), ObjectType varchar(30), TSQLCommand varchar(max) )
Utworzony dla bazy danych wyzwalacz Db_schemat jest uruchamiany po wystąpieniu każdego ze zdarzeń modyfikujących jej strukturę DDL_DATABASE_LEVEL_EVENTS. W jego ciele zasilana jest tabela pomocnicza, w ten sposób, że poza kluczem głównym, którego wartość jest generowana automatycznie, pozostałe pola są zasilane zawartością właściwego znacznika EVENT_INSTANCE. DROP TRIGGER Db_schemat ON DATABASE GO CREATE TRIGGER Db_schemat ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS INSERT INTO Audyt_ddl VALUES( EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]','varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/PostTime)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SPID)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ServerName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/UserName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/DatabaseName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SchemaName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ObjectName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ObjectType)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/TSQLCommand)[1]',' varchar(max)')
Po wykonaniu skryptu, w którym tworzono, modyfikowano i usuwano tabelę, a także tworzono i usuwano perspektywę, zawartość tabeli pomocniczej będzie miała postać pokazaną w tabeli 5.12. Jak widać, wyzwalacz pozwala na śledzenie wszystkich operacji modyfikujących bazę, co może być bardzo użyteczne w praktyce. Należy jednak pamiętać, że przy dużej liczbie zmian przyrost rozmiaru tabeli będzie znaczny. Oznacza to konieczność śledzenia jej wielkości i cyklicznego usuwania najstarszych wpisów, co może być również zautomatyzowane przez odpowiedni wyzwalacz. Innym przykładem zastosowania triggera dla bazy danych jest dynamiczne blokowanie wykonywania pewnych operacji. Możliwa jest statyczna realizacja takiej blokady przez odebranie lub nienadanie właściwych uprawnień, stosując polecenia GRANT lub REVOKE. Jednak takie postępowanie powoduje, że blokada dotyczy wszystkich baz instancji, do których użytkownik ma prawo się zalogować. W przypadku wyzwalacza blokada dotyczy tylko tej bazy, w której został on utworzony. W prezentowanym przykładzie dla użytkowników nienależących do grupy db_owner zablokowane są wszystkie operacje dotyczące tabel — tworzenie, modyfikowanie, usuwanie. W konsekwencji próby wykonania jednej z tych operacji wyświetlany jest komunikat, a transakcja jest wycofywana. Dla użytkowników posiadających właściwą rolę wyświetlany komunikat
264
MS SQL Server. Zaawansowane metody programowania
Tabela 5.12. Przykładowa zawartość tabeli Audyt_ddl IdAudyt_ ddl
Event Type
Post Time
SPID
Server Name
Login Name
User Name
Database Name
Schema Name
Object Name
Object Type
TSQL Command
1
CREATE _TABLE
20120807T20: 45:08. 230
53
AP
sa
dbo
Baza Relacyjna
dbo
xxx
TABLE
CREATE TABLE xxx (id int)
2
ALTER _TABLE
20120807T20: 45:08. 390
53
AP
sa
dbo
Baza Relacyjna
dbo
xxx
TABLE
ALTER TABLE xxx ADD pole varchar(3)
3
ALTER _TABLE
20120807T20: 45:08. 397
53
AP
sa
dbo
Baza Relacyjna
dbo
xxx
TABLE
ALTER TABLE xxx DROP COLUMN pole
4
DROP _TABLE
20120807T20: 45:08. 400
53
AP
sa
dbo
Baza Relacyjna
dbo
xxx
TABLE
DROP TABLE xxx
5
CREATE _VIEW
20120807T20: 45:08. 403
53
AP
sa
dbo
Baza Relacyjna
dbo
xxx
VIEW
CREATE VIEW xxx AS SELECT Nazwisko FROM Osoby
6
DROP _VIEW
20120807T20: 45:08. 417
53
AP
sa
dbo
Baza Relacyjna
dbo
xxx
VIEW
DROP VIEW xxx
zawiera informację, które z poleceń zostało wykonane, oraz użytkownika, który uruchomił polecenie. Uzupełnieniem pokazywanego skryptu są polecenia testujące poprawność jego działania. DROP TRIGGER Db_Table ON DATABASE GO CREATE TRIGGER Db_Table ON DATABASE FOR DROP_TABLE, ALTER_TABLE, CREATE_TABLE AS IF IS_MEMBER('db_owner') = 0 BEGIN SELECT 'Musisz mieć uprawnienia DBA dla usuwania i modyfikowania tabel!' ROLLBACK TRANSACTION END ELSE SELECT 'Polecenie '+ EVENTDATA().value( '(/EVENT_INSTANCE/EventType)[1]','nvarchar(max)') + ' wykonane przez - '+ user GO CREATE TABLE xxx (id int) GO ALTER TABLE xxx ADD pole varchar(3) GO
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
265
ALTER TABLE xxx DROP COLUMN pole GO DROP TABLE xxx GO
Jeśli w czasie wykonywania skryptu jesteśmy zalogowani jako użytkownik z uprawnieniami db_owner, pojawi się seria komunikatów. Polecenie CREATE_TABLE wykonane przez - dbo Polecenie ALTER_TABLE wykonane przez - dbo Polecenie ALTER_TABLE wykonane przez - dbo Polecenie DROP_TABLE wykonane przez - dbo
Gdy zalogujemy się jako inny użytkownik, dla każdego z poleceń komunikat będzie miał postać: Musisz mieć uprawnienia DBA dla usuwania i modyf kowania tabel! Msg 3609, Level 16, State 2, Line 1 The transaction ended in the trigger. The batch has been aborted.
Dodatkowym atutem dynamicznego blokowania w porównaniu z blokadą za pomocą uprawnień, jest to, że informacje o próbach wykonywania zabronionych poleceń mogą być składowane, oraz to, że możemy stosować kary, np. w postaci obniżenia uprawnień lub czasowej bądź całkowitej blokady konta. Kolejnym zdarzeniem, nieujętym w perspektywie systemowej sys.trigger_event_ types, jest zdarzenie LOGON, które ma sens tylko dla zakresu ALL SERVER. Pozwala ono na reagowanie na poprawnie zrealizowane logowanie do serwera bez względu na to, jaka została zastosowana autoryzacja — dla bazy danych czy odziedziczona po systemie. Przykładowa realizacja tej funkcjonalności może wyglądać tak, jak pokazuje przykład, w którym alternatywnie zastosowano wyświetlenie komunikatu za pomocą poleceń PRINT lub SELECT. DROP TRIGGER dla_logowania ON ALL SERVER GO CREATE TRIGGER dla_logowania ON ALL SERVER FOR LOGON AS PRINT 'Nastąpiło logowanie' --SELECT EVENTDATA().value( --'(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]','varchar(max)') GO
Niestety, takie rozwiązanie nie jest skuteczne, ponieważ próba wykonania SELECT powoduje wygenerowanie komunikatu o błędzie, natomiast PRINT nie pozostawia żadnego śladu na konsoli. Wynika to z faktu, że w chwili zakończenia logowania standardowe wyjście nie jest jeszcze aktywne. Dlatego jedynym sensownym rozwiązaniem jest zapisywanie informacji o logowaniu w tabeli. W przykładzie tabelę Logowanie utworzono w bazie systemowej master. Tabela poza polem automatycznie generowanego klucza ma kolumny znakowe odpowiadające podstawowym elementom struktury EVENT_INSTANCE. Dla tabeli tej zapewniono poleceniem GRANT uprawnienia do wstawiania danych dla roli public, która jest przypisana do każdego tworzonego użytkownika.
266
MS SQL Server. Zaawansowane metody programowania
Gdy tabela została utworzona w innej bazie niż master, należy dodać dodatkowe uprawnienia do tej bazy dla roli public: USE master GO DROP TABLE Logowanie GO CREATE TABLE Logowanie (IdLogowanie int IDENTITY(1,1) PRIMARY KEY, EventType varchar(30), PostTime varchar(30), SPID varchar(30), ServerName varchar(30), LoginName varchar(30)) GO GRANT INSERT ON Logowanie TO public GO
Tym razem wyzwalacz dla_logowania zapisuje dane, korzystając z informacji pobieranych z EVENT_INSTANCE za pomocą metody EVENTDATA().value(). DROP TRIGGER dla_logowania ON ALL SERVER GO CREATE TRIGGER dla_logowania ON ALL SERVER FOR LOGON AS DECLARE @kto AS varchar(30) SET @kto= EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]',' varchar(30)') INSERT INTO master.dbo.Logowanie VALUES( EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]','varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/PostTime)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SPID)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ServerName)[1]',' varchar(30)'), @kto ) GO
Jeśli utworzymy dodatkowego użytkownika o nazwie inny i zalogujemy się na jego konto, a następnie zalogujemy się jako użytkownik sa, to po krótkim czasie bezczynności zawartość tabeli Logowanie może mieć postać pokazaną w tabeli 5.13. Jak widać, w czasie krótkiego czasu pracy pojawiły się trzy wpisy dla autoryzacji ZARZĄDZANIE NT\SYSTEM, chociaż jawnie nie dokonano takiego połączenia. Ich źródłem są procesy tła wykonywane cyklicznie na rzecz serwera, a odpowiedzialne za zapis asynchroniczny są kontrole zakleszczeń itp., które do wykonania swoich zadań potrzebują uwierzytelnienia i stosują uwierzytelnienie takie, jakie było ustanowione podczas instalacji w zakładce wskazującej na to, kto ma uruchamiać serwisy (rysunki 2.6 i 2.7 w rozdziale 2., „Instalacja i konfiguracja środowiska”). Ponieważ połączenia takie wykonywane są nawet wtedy, kiedy serwer nie przetwarza żadnych poleceń użytkownika, może to prowadzić do szybkiego przyrostu tabeli audytu. Aby wyeliminować wpisy dotyczące cyklicznego logowania procesów drugoplanowych, należy zmodyfikować trigger do następującej postaci:
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
267
Tabela 5.13. Przykładowa zawartość tabeli Logowanie IdLogowanie
EventType
PostTime
SPID
ServerName
LoginName
1
LOGON
2012-08-07T21:10:29.967
54
AP
ZARZĄDZANIE NT\SYSTEM
2
LOGON
2012-08-07T21:10:34.770
51
AP
inny
3
LOGON
2012-08-07T21:10:34.913
60
AP
inny
4
LOGON
2012-08-07T21:10:34.947
60
AP
inny
5
LOGON
2012-08-07T21:10:34.960
60
AP
inny
6
LOGON
2012-08-07T21:10:40.120
54
AP
ZARZĄDZANIE NT\SYSTEM
7
LOGON
2012-08-07T21:10:44.037
51
AP
sa
8
LOGON
2012-08-07T21:10:44.057
60
AP
sa
9
LOGON
2012-08-07T21:10:44.087
61
AP
sa
10
LOGON
2012-08-07T21:10:44.130
61
AP
sa
11
LOGON
2012-08-07T21:10:50.387
54
AP
ZARZĄDZANIE NT\ SYSTEM
12
LOGON
2012-08-07T21:10:52.023
51
AP
inny
DROP TRIGGER dla_logowania ON ALL SERVER GO CREATE TRIGGER dla_logowania ON ALL SERVER FOR LOGON AS DECLARE @kto AS varchar(30) SET @kto= EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]',' varchar(30)') IF( @kto <>'ZARZĄDZANIE NT\SYSTEM') INSERT INTO master.dbo.Logowanie VALUES( EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]','varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/PostTime)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SPID)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ServerName)[1]',' varchar(30)'), @kto ) GO
Jeśli sprawdzimy w pliku events.xsd znaczniki struktury EVENT_INSTANCE, to dla logowania można ją przedstawić w postaci: event_type post_time spid server_name login_name login_type sid client_host is_pooled
268
MS SQL Server. Zaawansowane metody programowania
Aby uwzględnić wszystkie informacje dostępne podczas logowania, możemy zmienić pomocniczą tabelę oraz wyzwalacz do postaci pokazanej w skrypcie. USE master GO DROP TABLE Logowanie GO CREATE TABLE Logowanie (IdLogowanie int IDENTITY(1,1) PRIMARY KEY, EventType varchar(30), PostTime varchar(30), SPID varchar(30), ServerName varchar(30), LoginName varchar(30), LoginType varchar(30),SID varchar(30), ClientHost varchar(30), IsPooled varchar(30)) GO GRANT INSERT ON Logowanie TO public GO DROP TRIGGER dla_logowania ON ALL SERVER GO CREATE TRIGGER dla_logowania ON ALL SERVER FOR LOGON AS DECLARE @kto AS varchar(30) SET @kto= EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]',' varchar(30)') IF( @kto <>'ZARZĄDZANIE NT\SYSTEM') INSERT INTO master.dbo.Logowanie VALUES( EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]','varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/PostTime)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SPID)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ServerName)[1]',' varchar(30)'), @kto, EVENTDATA().value('(/EVENT_INSTANCE/LoginType)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SID)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ClientHost)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/IsPooled)[1]',' varchar(30)'))
Przykład zawartości tabeli audytu zawiera tabela 5.14, w której widać zarówno logowania użytkowników sa, jak i inny z autoryzacją na poziomie serwera bazy danych oraz uwierzytelnienie dziedziczone po systemie operacyjnym. Niestety, tworzenie triggerów dla logowania obarczone jest niebagatelnym niebezpieczeństwem. Jeżeli w ciele wyzwalacza pojawi się błąd podczas przetwarzania, np. spowodowany brakiem możliwości konwersji odczytanej ze struktury EVENT_INSTANCE wartości znacznika na typ pola tabeli, proces logowania zakończy się niepowodzeniem. Jeżeli takie wpisy są wykonywane dla wszystkich użytkowników, to formalnie nie ma możliwości wyłączenia, usunięcia wyzwalacza lub korekty błędu. Deską ratunkową może być w takim przypadku skorzystanie z końcówki klienckiej uruchamianej z linii poleceń sqlcmd. Najistotniejszym parametrem jest –A, który powoduje, że dla administratora pomijane są wszystkie elementy proceduralne związane z logowaniem, również błędny trigger. W przykładzie zastosowano polecenie DISABLE TRIGGER powodujące czasową blokadę wskazanego wyzwalacza. Zastosowano nadmiarowy parametr ALL, który blokuje wszystkie triggery dla zakresu serwera bazy danych, zamiast której można podać jawnie nazwę blokowanej procedury wyzwalanej. C:\WINDOWS> sqlcmd -S NazwaHosta -U sa -d master -A Password: 1> DISABLE TRIGGER ALL ON ALL SERVER 2> GO
Event Type
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
LOGON
IdLogowanie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SPID
52
53
55
55
52
53
53
53
52
53
53
53
53
55
56
PostTime
2012-08-07T21: 18:58.543
2012-08-07T21: 18:58.613
2012-08-07T21: 18:58.640
2012-08-07T21: 18:58.657
2012-08-07T21: 19:07.240
2012-08-07T21: 19:07.263
2012-08-07T21: 19:07.307
2012-08-07T21: 19:07.353
2012-08-07T21: 19:15.970
2012-08-07T21: 19:16.017
2012-08-07T21: 19:16.073
2012-08-07T21: 19:24.200
2012-08-07T21: 20:39.367
2012-08-07T21: 20:39.693
2012-08-07T21: 20:39.703 AP
AP
AP
AP
AP
AP
AP
AP
AP
AP
AP
AP
AP
AP
AP
Server Name
Tabela 5.14. Przykładowa zawartość tabeli Logowanie
AP\Adam
AP\Adam
AP\Adam
AP\Adam
AP\Adam
AP\Adam
AP\Adam
sa
sa
sa
sa
inny
inny
inny
inny
Login Name
Windows (NT) Login
Windows (NT) Login
Windows (NT) Login
Windows (NT) Login
Windows (NT) Login
Windows (NT) Login
Windows (NT) Login
SQL Login
SQL Login
SQL Login
SQL Login
SQL Login
SQL Login
SQL Login
SQL Login
LoginType
AQUAAAAAAAUVAAAA cwe2O+3X1u+N5E
AQUAAAAAAAUVAAAA cwe2O+3X1u+N5E
AQUAAAAAAAUVAAAA cwe2O+3X1u+N5E
AQUAAAAAAAUVAAAA cwe2O+3X1u+N5E
AQUAAAAAAAUVAAAA cwe2O+3X1u+N5E
AQUAAAAAAAUVAAAA cwe2O+3X1u+N5E
AQUAAAAAAAUVAAAA cwe2O+3X1u+N5E
AQ==
AQ==
AQ==
AQ==
tNTCDgvSc0ifA0dqIcs PEA==
tNTCDgvSc0ifA0dqIcs PEA==
tNTCDgvSc0ifA0dqIcs PEA==
tNTCDgvSc0ifA0dqIcs PEA==
SID
ClientHost
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
IsPooled
Rozdział 5. Rozszerzenia proceduralne Transact-SQL 269
270
MS SQL Server. Zaawansowane metody programowania
Stosując tego rodzaju triggery, należy zawsze pamiętać, że uruchamiane są one po poprawnym logowaniu, w związku z tym nie nadają się do wykrywania ataków [25] [26], podczas których podano złe dane do autoryzacji i proces ten nie zakończył się sukcesem. Mogą tylko śledzić aktywność poprawnie uwierzytelnianych operatorów. Niepoprawne uwierzytelnienia mogą być odczytane tylko z plików dziennika SQL Server Logs pod warunkiem właściwej ich konfiguracji albo po utworzeni odpowiedniego audytu Audits na poziomie Security w hierarchicznej strukturze obiektów serwera. Równie ważne z punktu widzenia bezpieczeństwa jest śledzenie zmian uprawnień. W celu rozwiązania tego problemu utwórzmy pomocniczą tabelę o polach, których nazwy odpowiadają znacznikom właściwego fragmentu struktury EVENT_INSTANCE. USE master GO DROP TABLE Autoryzacja GO CREATE TABLE Autoryzacja ( IdAutoryzacja int IDENTITY(1,1) PRIMARY KEY, EventType varchar(30), PostTime varchar(30), SQLInstance varchar(30), SPID varchar(30), ServerName varchar(30), LoginName varchar(30), LoginType varchar(30), SID varchar(30), ObjectName varchar(30), ObjectType varchar(30), DefaultLanguage varchar(30), DefaultDatabase varchar(30) )
Teraz pozostaje utworzenie właściwej procedury wyzwalanej dla zdarzenia DDL_LOGIN_ EVENTS, obejmującego tworzenie i usuwanie loginów, czyli obiektów, po których użytkownik dziedziczy właściwości związane z autoryzacją (nie dotyczy uprawnień do obiektów, które przypisywane są do użytkownika). DROP TRIGGER dla_loginow ON ALL SERVER GO CREATE TRIGGER dla_loginow ON ALL SERVER FOR DDL_LOGIN_EVENTS AS INSERT INTO master.dbo.Autoryzacja VALUES( EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]','varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/PostTime)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SQLInstance)[1]','varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SPID)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ServerName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/LoginType)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/SID)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ObjectName)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/ObjectType)[1]',' varchar(30)'),
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
271
EVENTDATA().value('(/EVENT_INSTANCE/DefaultLanguage)[1]',' varchar(30)'), EVENTDATA().value('(/EVENT_INSTANCE/DefaultDatabase)[1]',' varchar(30)') ) GO
Aby przetestować działanie wyzwalacza, możemy wykonać procedurę systemową sp_addlogin, tworzącą login o nazwie dodany i haśle kajak, którego bazą domyślną będzie BazaRelacyjna. Następnie za pomocą procedury sp_droplogin obiekt ten usuwamy, aby na zakończenie wyświetlić zawartość tabeli pomocniczej, co powinno powodować skutek pokazany w tabeli 5.15. sp_addlogin 'dodany', 'kajak', 'BazaRelacyjna' GO sp_droplogin 'dodany‘ GO SELECT * FROM master.dbo.Autoryzacja
ServerName
ObjectName
ObjectType
DefaultLanguage
DefaultDatabase
NULL
53
AP
AP\ Adam
SQL Login
2j3DBAkD6Uu FLZlImld3 og==
dodany
LOGIN
us_english
Baza Rela -cyjna
2
DROP _LOGIN
2012-0807T21:28 :00.323
NULL
53
AP
AP\ Adam
SQL Login
2j3DBAkD6Uu FLZlImld3 og==
dodany
LOGIN
us_english
Baza Rela -cyjna
SID
SPID
2012-0807T21:28 :00.130
LoginType
SQLInstance
CREATE _LOGIN
LoginName
PostTime
1
IdAutoryzacja
EventType
Tabela 5.15. Przykładowa zawartość tabeli Autoryzacja
Aby rozszerzyć zakres śledzonych zdarzeń związanych z bezpieczeństwem, zmianą uprawnień, możemy zamienić zakres z DDL_LOGIN_EVENTS na jego rodzica DDL_SERVER_ SECURITY_EVENTS. Wówczas dla niektórych przypadków uruchomienia wyzwalacza część pól tabeli nie będzie wypełniana. Kolejny przykład pokazuje trigger śledzący operacje na bazie danych i zasilający tabele pomocniczą ddl_log. W jego ciele skorzystano z funkcji systemowych GETDATE() do ustalenia chwili wykonywania polecenia oraz CURRENT_USER do określenia nazwy użytkownika, który je uruchomił. Poza tym zamiast odwoływać się bezpośrednio do obiektu EVENTDATA(), utworzona została zmienna pomocnicza @data typu XML, która stała się instancją tego obiektu po podstawieniu poleceniem SET. Odwołanie do wartości struktury EVENT_INSTANCE wykonano, stosując metodę value(…) dla zmiennej @data. CREATE TABLE ddl_log (PostTime datetime, DB_User nvarchar(100), Event nvarchar(100), TSQL nvarchar(2000)); GO CREATE TRIGGER log ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS DECLARE @data XML SET @data = EVENTDATA()
272
MS SQL Server. Zaawansowane metody programowania INSERT ddl_log (PostTime, DB_User, Event, TSQL) VALUES (GETDATE(), CONVERT(nvarchar(100), CURRENT_USER), @data.value('(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(100)'), @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(2000)') ) ; GO
Kończąc rozważania dla triggerów operujących na bazie danych oraz serwerze, warto zwrócić uwagę na różne miejsca ich składowania, co pokazuje rysunek 5.5. Rysunek 5.5. Różne miejsca składowania triggerów o zakresie DATABASE i ALL SERVER w hierarchicznej strukturze obiektów bazy danych
Wyzwalacz nie tylko może zostać usunięty i utworzony na nowo. Możliwe jest czasowe jego zablokowanie poleceniem DISABLE lub odblokowanie poleceniem ENABLE zgodnie z przedstawioną składnią. Użycie opcji ALL powoduje, że akcja zostanie wykonana dla wszystkich triggerów we wskazanym zakresie.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
273
DISABLE TRIGGER {[schemat.]nazwa_triggera [,...n]|ALL} ON {nazwa_tabeli | DATABASE | ALL SERVER}[;] ENABLE TRIGGER {[schemat.]nazwa_triggera [,...n]|ALL} ON {nazwa_tabeli | DATABASE | ALL SERVER}[;]
Dla stworzonych procedur wyzwalanych na poziomie bazy danych przykładowe operacje blokowania i odblokowywania mogą mieć postać: DISABLE TRIGGER Db_schemat ON DATABASE GO DISABLE TRIGGER ALL ON DATABASE GO ENABLE TRIGGER Db_schemat ON DATABASE GO ENABLE Trigger ALL ON DATABASE GO
Czasowa blokada triggera najczęściej jest wykonywana wtedy, kiedy jego funkcjonowanie znacznie opóźnia proces przetwarzania, a mamy pewność, że brak jego aktywności nie jest groźny ani dla danych, ani dla serwera. Przykładem takiego działania jest masowe kopiowanie danych, kiedy mamy pewność, że dane źródłowe są poprawne. Ma to miejsce podczas migracji albo integracji danych pochodzących z różnych serwerów [4] [5] [19] – [21]. Należy zaznaczyć, że nie ma różnicy w graficznej prezentacji zablokowanego i aktywnego triggera. Powtórzenie polecenia ENABLE lub DISABLE dla tego samego wyzwalacza nie powoduje komunikatu o błędzie. Powróćmy do problemu dynamicznej walidacji danych z zastosowaniem wyzwalaczy, który był zasygnalizowany podczas omawiania zadania dotyczącego rozgrywek piłkarskich. W tym celu zmodyfikujmy zawartość tabeli Sedziowie, przyjmując, że nie każdy z sędziów ma uprawnienia do wykonywania każdej z funkcji. Zasygnalizujmy to, wprowadzając trzy kolumny całkowitoliczbowe. Każda z nich przyjmie wartość 1, kiedy sędzia ma właściwe uprawnienia, a 0 w przypadku przeciwnym. Kolumna G określa uprawnienia do funkcji arbitra głównego, L liniowego, a T technicznego. W skrypcie uzupełniono dane w wierszach o odpowiednie wpisy, a na koniec sprawdzono wynikową zawartość zmodyfikowanej tabeli, co pokazuje tabela 5.16. ALTER TABLE Sedziowie ADD GO ALTER TABLE Sedziowie ADD GO ALTER TABLE Sedziowie ADD GO UPDATE Sedziowie SET G=1, UPDATE Sedziowie SET G=1, UPDATE Sedziowie SET G=0, UPDATE Sedziowie SET G=1, GO SELECT * FROM Sedziowie;
G int; L int; T int; L=1, L=1, L=1, L=0,
T=1 T=0 T=1 T=1
WHERE WHERE WHERE WHERE
IdSedziego=1; IdSedziego=2; IdSedziego=3; IdSedziego=4;
Aby wyeliminować możliwość wpisywania do pól G, L oraz T wartości różnych od dopuszczalnych, wprowadźmy trzy ograniczenia CHECK sprawdzające ich zgodność z listą zawierającą dwie wartości — 0 oraz 1. Tę funkcjonalność sprawdza pierwsze z zapytań
274
MS SQL Server. Zaawansowane metody programowania
Tabela 5.16. Zawartość tabeli Sedziowie po modyfikacji IdSedziego
Imie
Nazwisko
G
L
T
1
Jan
Kowal
1
1
1
2
Piotr
Niewidomy
1
1
0
3
Janusz
Demon
0
1
1
4
Kazimierz
Tama
1
0
1
modyfikujących, a potwierdza ją komunikat o błędzie. Jednak, jak to pokazuje druga aktualizacja, nie jest blokowana możliwość wpisywania wartości pustych NULL. Stan po wykonaniu skryptu pokazuje tabela 5.17. ALTER TABLE Sedziowie ADD CONSTRAINT sprG CHECK(G IN(0,1)); GO ALTER TABLE Sedziowie ADD CONSTRAINT sprL CHECK(L IN(0,1)); GO ALTER TABLE Sedziowie ADD CONSTRAINT sprT CHECK(T IN(0,1)); GO UPDATE Sedziowie SET G=2 WHERE IdSedziego=1; UPDATE Sedziowie SET G=NULL WHERE IdSedziego=1; SELECT * FROM Sedziowie; Msg 547, Level 16, State 0, Line 1 The UPDATE statement conflicted with the CHECK constraint "sprG". The conflict occurred in database "test", table "dbo.SEDZIOWIE", column 'G'. The statement has been terminated.
Tabela 5.17. Zawartość tabeli Sedziowie po modyfikacji IdSedziego
Imie
Nazwisko
G
L
T
1
Jan
Kowal
NULL
1
1
2
Piotr
Niewidomy
1
1
0
3
Janusz
Demon
0
1
1
4
Kazimierz
Tama
1
0
1
Gdy zostanie przywrócona wartość pola G w pierwszym wierszu, wprowadzamy ograniczenie zabraniające wpisywania wartości NULL do trzech pól. Tym razem próba aktualizacji pola kończy się niepowodzeniem, co potwierdza przedstawiony komunikat. Jednocześnie zawartość tabeli Sedziowie jest taka sama jak w tabeli 5.16. UPDATE Sedziowie SET G=1 WHERE IdSedziego=1; GO ALTER TABLE Sedziowie ALTER COLUMN G int NOT NULL; GO ALTER TABLE Sedziowie ALTER COLUMN L int NOT NULL; GO ALTER TABLE Sedziowie ALTER COLUMN T int NOT NULL; GO UPDATE Sedziowie SET G=NULL WHERE IdSedziego=1; SELECT * FROM Sedziowie; Msg 515, Level 16, State 2, Line 1 Cannot insert the value NULL into column 'G', table 'test.dbo.SEDZIOWIE'; column does not allow nulls. UPDATE fails. The statement has been terminated.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
275
Na tym kończą się możliwości statycznej weryfikacji danych. Pozostaje otwarty problem zapobiegania wpisywania w meczach sędziów z nieodpowiednimi uprawnieniami. Pierwszym krokiem jest skonstruowanie zapytania, w którym dla każdego meczu obok identyfikatora sędziego o określonej funkcji pojawi się informacja o uprawnieniach do tej roli. W tym celu tabela Mecze została złączona z czterema dynamicznymi kopiami tabeli Sedziowie, o aliasach S1, S2, S3 oraz S4. Do połączenia używamy pola IdSedziego z tabeli Sedziowie oraz odpowiadającego mu pola w tabeli Mecze, reprezentującego właściwą funkcję arbitra. Dla pary Mecze, S1 złączenie odbywa się za pomocą pola IdSedziegoG i z tej dynamicznej kopii tabeli Sedziowie pobierane jest pole G, potwierdzające uprawnienia do pełnienia funkcji głównego sędziego spotkania. Analogicznie połączenie i sprawdzenie jest wykonywane dla pozostałych par tabel. Aby prezentowane zapytanie było łatwiejsze do zrozumienia, jego graficzną formę prezentuje rysunek 5.6, a zestaw rekordów przez nie zwracanych zawiera tabela 5.18. SELECT IdSedziegoG, S1.G, IdSedziegoL1, S2.L AS L1, IdSedziegoL2, S3.L AS L2, IdSedziegoT, S4.T FROM MECZE INNER JOIN Sedziowie AS S1 ON IdSedziegoG = S1.IdSedziego INNER JOIN Sedziowie AS S2 ON IdSedziegoL1 = S2.IdSedziego INNER JOIN Sedziowie AS S3 ON IdSedziegoL2 =S3.IdSedziego INNER JOIN Sedziowie AS S4 ON IdSedziegoT = S4.IdSedziego
Rysunek 5.6. Ilustracja złączenia zrealizowanego w zapytaniu Tabela 5.18. Zestaw rekordów zwracany przez zapytanie sprawdzające uprawnienia sędziów IdSedziegoG
G
IdSedziegoL1
L1
IdSedziegoL2
L2
IdSedziegoT
T
1
1
2
1
3
1
4
1
1
1
3
1
2
1
4
1
2
1
4
1
3
1
1
1
276
MS SQL Server. Zaawansowane metody programowania
Analizując zawartość tabeli 5.18, możemy zauważyć, że przy identyfikatorach sędziów odgrywających w meczu różne role występuje w kolumnach odpowiednich uprawnień wartość 1. Potwierdza to fakt poprawnego przypisania funkcji. Czyli jeśli do tabeli wpisaliśmy właściwych arbitrów, suma pól z ich uprawnieniami powinna wynosić 4. Sprawdzenia uprawnień nie można zweryfikować statycznie, ponieważ wymagałoby to umieszczenia w ograniczeniu CHECK skorelowanego zapytania odwołującego się do innej tabeli, na co nie zezwala składnia. Dlatego konieczna jest weryfikacja dynamiczna za pomocą wyzwalacza. Skrypt pokazuje trigger wykonywany po poleceniach modyfikujących INSERT oraz UPDATE. W jego ciele zadeklarowano dwie zmienne pomocnicze @wstawiano i @zgodne, a następnie rozpoczynana jest transakcja BEGIN TRAN. Do pierwszej ze zmiennych wstawiono liczbę modyfikowanych wierszy, czyli tych, które zawarte są w tabeli INSERTED, która jest zasilana podczas obu modyfikacji danych. W przypadku drugiej zmiennej skorzystano z konstrukcji takiej samej jak w przypadku zapytania sprawdzającego uprawnienia, zastępując tabelę Mecze tabelą INSERTED, która ze względu na obiekt, dla którego tworzymy wyzwalacz, ma taką samą strukturę. Do zmiennej podstawiono zaś liczbę wierszy, w których suma pól uprawnień jest równa 4, czyli wszystkie są poprawne. Jeśli liczba wstawianych wierszy jest różna od liczby wierszy, w których wszystkie role sędziów są zgodne, transakcja jest wycofywana ROLLBACK TRAN — przywracany jest stan z chwili przed modyfikacjami. W przeciwnym wypadku, jeśli obie zmienne są równe, transakcja jest zatwierdzana COMMIT TRAN. Ponowne zatwierdzenie transakcji, występujące poza instrukcją warunkową IF, na końcu ciała triggera stanowi zabezpieczenie przed „wiszącymi” transakcjami, czyli takimi, które nie są ani zatwierdzone, ani wycofane. Szersze omówienie przetwarzania transakcyjnego zawiera rozdział 6. DROP TRIGGER Spr_sedziego GO CREATE TRIGGER Spr_sedziego ON Mecze FOR INSERT, UPDATE AS DECLARE @wstawiano int, @zgodne int BEGIN TRAN SELECT @wstawiano= COUNT(*) FROM INSERTED SELECT @zgodne=COUNT(*) FROM INSERTED INNER JOIN Sedziowie AS S1 ON IdSedziegoG = S1.IdSedziego INNER JOIN Sedziowie AS S2 ON IdSedziegoL1 = S2.IdSedziego INNER JOIN Sedziowie AS S3 ON IdSedziegoL2 =S3.IdSedziego INNER JOIN Sedziowie AS S4 ON IdSedziegoT = S4.IdSedziego WHERE S1.G+S2.L+S3.L+S4.T=4 PRINT CAST(@wstawiano AS varchar(5)) + ' ' + CAST(@zgodne AS varchar(5)) IF (@wstawiano<>@zgodne) ROLLBACK TRAN ELSE COMMIT TRAN COMMIT TRAN
Jeśli wstawimy poprawne dane, pojawi się tylko informacja o liczbie wstawionych oraz zgodnych rekordów i polecenie zakończy się sukcesem: INSERT INTO Mecze VALUES (1, 2, 1, 1, 2, 3, 4, GETDATE())
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
277
Jeśli jednak spróbujemy wstawić niepoprawne dane, pojawi się komunikat o błędzie: INSERT INTO Mecze VALUES (1, 2, 3, 2, 4, 3, 1,GETDATE()) Msg 3609, Level 16, State 1, Line 1 The transaction ended in the trigger. The batch has been aborted.
Po takich modyfikacjach zawartość tabeli Mecze będzie taka jak w tabeli 5.19, co potwierdza, że tylko jeden ze wstawianych wierszy został zatwierdzony. Tabela 5.19. Zestaw rekordów zwracany przez zapytanie IdMeczu
IdSezonu
IdGosc
IdGosp IdSedziegoG
IdSedziegoL1
IdSedziegoL2
IdSedziegoT
DataM
1
1
1
2
1
2
3
4
2011-11-23
2
2
2
1
1
3
2
4
2011-11-23'
3
1
3
2
2
4
3
1
2011-12-12
4
1
2
1
1
2
3
4
2012-04-12
Podobnie jest podczas modyfikacji istniejących już danych — jeśli modyfikacja nie narusza reguł uprawnień sędziów, zostanie zatwierdzona. W przykładzie zamieniono sędziów głównego i pierwszego liniowego w czwartym rekordzie. UPDATE Mecze SET IdSedziegoG=2, IdsedziegoL1=1 WHERE IdMeczu =4
Podczas modyfikacji, która jest niezgodna z regułami, w przykładzie dla IdMeczu równego 4 pojawi się komunikat o błędzie, a rekord nie zostanie zapisany. UPDATE Mecze SET IdSedziegoL2=4, IdsedziegoT=3 WHERE IdMeczu =4 Msg 3609, Level 16, State 1, Line 1 The transaction ended in the trigger. The batch has been aborted.
Konsekwencje wykonania przykładowych modyfikacji zawiera tabela 5.20. Tabela 5.20. Zestaw rekordów zwracany przez zapytanie IdMeczu
IdSezonu
IdGosc
IdGosp
IdSedziegoG
IdSedziegoL1
IdSedziegoL2
IdSedziegoT
DataM
1
1
1
2
1
2
3
4
2011-11-23
2
2
2
1
1
3
2
4
2011-11-23'
3
1
3
2
2
4
3
1
2011-12-12
4
1
2
1
2
1
3
4
2012-04-12
Niestety, takie rozwiązanie powoduje, że jeśli rekordy są wprowadzane lub aktualizowane masowo, akcja dotyczy wielu wierszy. Wówczas wszystkie wiersze są zatwierdzane, a jeśli choć jeden z nich jest niepoprawny, wszystkie są wycofywane. Warto rozważyć sytuacje działania wybiórczego, kiedy wycofanie operacji dotyczy tylko niepoprawnych danych. Warto również zastanowić się nad możliwością uniknięcia poleceń zatwierdzających i wycofujących transakcje. Musimy jednak sięgnąć do mechanizmu kursora. Mechanizm ten jeszcze nie był omawiany — pełne wyjaśnienie zawiera kolejny podrozdział. Do dalszych rozważań wystarczy informacja, że pozwala on na nawigowanie między rekordami, których zestaw określony został w deklaracji kursora. Zmodyfikowany wyzwalacz wykonywany
278
MS SQL Server. Zaawansowane metody programowania
jest zamiast INSTEAD OF wstawiania wierszy i zawiera deklaracje dwóch zmiennych pomocniczych, @suma oraz @ktory. Ponadto zadeklarowano kursor o nazwie cur, który zawiera sumę uprawnień sędziów na wszystkich pozycjach, oraz identyfikator meczu, którego to wyrażenie dotyczy. Podobnie jak poprzednio skorzystano z tabeli INSERTED, która zawiera tylko modyfikowane wiersze. Po otwarciu kursora następuje przejście do pierwszego rekordu zestawu FETCH NEXT i przypisanie pól rekordu do zmiennych. Jeśli suma uprawnień jest równa 4, wykonywana jest operacja wstawiania z wartościami pobieranymi z tabeli INSERTED. We wnętrzu pętli poza opisywanym sprawdzeniem wykonywana jest nawigacja po wszystkich rekordach zestawu. Natomiast na zakończenie triggera kursor jest zamykany, a następnie są zwalniane zasoby do niego przypisane. CREATE TRIGGER Spr_sedziego ON Mecze INSTEAD OF INSERT AS DECLARE @suma int, @ktory int DECLARE cur CURSOR FOR SELECT S1.G+S2.L+S3.L+S4.T, IdMeczu FROM INSERTED INNER JOIN Sedziowie AS S1 ON IdSedziegoG = S1.IdSedziego INNER JOIN Sedziowie AS S2 ON IdSedziegoL1 = S2.IdSedziego INNER JOIN Sedziowie AS S3 ON IdSedziegoL2 =S3.IdSedziego INNER JOIN Sedziowie AS S4 ON IdSedziegoT = S4.IdSedziego OPEN cur FETCH NEXT FROM cur INTO @suma, @ktory WHILE @@FETCH_STATUS=0 BEGIN IF @suma=4 INSERT INTO Mecze (Sezon, IdGosc, IdGosp, IdSedziegoG, IdSedziegoL1, IdSedziegoL2, IdSedziegoT, DataM) SELECT Sezon, IdGosc, IdGosp, IdSedziegoG, IdSedziegoL1, IdSedziegoL2, IdSedziegoT, DataM FROM INSERTED WHERE IdMeczu=@ktory FETCH NEXT FROM cur INTO @suma, @ktory END CLOSE cur DEALLOCATE cur
Jeśli wstawimy właściwe dane: INSERT INTO Mecze Values (1,2,3,1,2,3,4,GETDATE())
to w odpowiedzi uzyskamy podwojony komunikat o przetworzeniu tylu wierszy, ile zostało wpisanych do tabeli: (1 row(s) affected) (1 row(s) affected)
Pierwszy komunikat jest wygenerowany przez polecenie INSERT i świadczy o liczbie wierszy przygotowanych do wstawienia, drugi, z wnętrza procedury wyzwalanej, potwierdza wstawienie poprawnych wierszy. Jeśli dane są błędne, to liczba wierszy w dwóch komunikatach jest różna, a jeśli wszystkie są niepoprawne, to pojawi się tylko jeden komunikat. Zawartość tabeli Mecze po wykonanych modyfikacjach zawiera tabela 5.21. INSERT INTO Mecze Values (1,3,2,2,4,3,1,GETDATE()) (1 row(s) affected)
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
279
Tabela 5.21. Zestaw rekordów w tabeli Mecze IdMeczu
IdSezonu IdGosc IdGosp IdSedziegoG
IdSedziegoL1 IdSedziegoL2
IdSedziegoT
DataM
1
1
1
2
1
2
3
4
2011-11-23
2
2
2
1
1
3
2
4
2011-11-23'
3
1
3
2
2
4
3
1
2011-12-12
4
1
2
1
2
1
3
4
2012-04-12
5
1
2
3
1
2
3
4
2012-04-12
Ponieważ poprzedni wyzwalacz obsługiwał tylko zdarzenie wstawiania, opracujmy analogiczny dla aktualizacji danych. Działa on zamiast zdarzenia UPDATE i ma strukturę bardzo podobną do poprzednika. Aby ciało triggera wykonywało się tylko w przypadku modyfikacji danych dotyczących sędziów, na początku za pomocą instrukcji warunkowej IF sprawdzono, czy aktualizacja dotyczy któregokolwiek z tych pól. Zasadniczą różnicą funkcjonowania wyzwalacza jest to, że po sprawdzeniu sumy uprawnień wykonywane jest tym razem polecenie UPDATE, które jak poprzednie, INSERT, korzysta z danych zawartych w tabeli INSERTED. CREATE TRIGGER Spr_sedziegoU ON Mecze INSTEAD OF UPDATE AS IF UPDATE(IdSedziegoG) OR UPDATE(IdSedziegoL1) OR UPDATE(IdSedziegoL2) OR UPDATE(IdSedziegoT) BEGIN DECLARE @suma int, @ktory int DECLARE cur CURSOR FOR SELECT S1.G+S2.L+S3.L+S4.T, IdMeczu FROM INSERTED INNER JOIN Sedziowie AS S1 ON IdSedziegoG = S1.IdSedziego INNER JOIN Sedziowie AS S2 ON IdSedziegoL1 = S2.IdSedziego INNER JOIN Sedziowie AS S3 ON IdSedziegoL2 =S3.IdSedziego INNER JOIN Sedziowie AS S4 ON IdSedziegoT = S4.IdSedziego OPEN cur FETCH NEXT FROM cur INTO @suma, @ktory WHILE @@FETCH_STATUS=0 BEGIN IF @suma=4 SELECT * FROM INSERTED AS i WHERE i.IdMeczu=@ktory UPDATE Mecze SET IdSedziegoG=i.IdSedziegoG, IdSedziegoL1=i.IdSedziegoL1, IdSedziegoL2=i.IdSedziegoL2, IdSedziegoT=i.IdSedziegoT FROM INSERTED AS i WHERE i.IdMeczu=@ktory FETCH NEXT FROM cur INTO @suma, @ktory END CLOSE cur DEALLOCATE cur END
Sprawdzenie poprawności działania triggera uzyskujemy, wykonując dwie modyfikacje, z których pierwsza prowadzi do wprowadzenia poprawnych, a druga błędnych danych. UPDATE Mecze SET IdSedziegoG=2, IdsedziegoL1=1 WHERE IdMeczu =4 GO UPDATE Mecze SET IdSedziegoL2=4, IdsedziegoT=3 WHERE IdMeczu =4
Zawartość tabeli Mecze po wykonaniu skryptu przedstawia tabela 5.22.
280
MS SQL Server. Zaawansowane metody programowania
Tabela 5.22. Zestaw rekordów w tabeli Mecze IdMeczu IdSezonu
IdGosc IdGosp IdSedziegoG
IdSedziegoL1 IdSedziegoL2
IdSedziegoT
DataM
1
1
1
2
1
2
3
4
2011-11-23
2
2
2
1
1
3
2
4
2011-11-23'
3
1
3
2
2
4
3
1
2011-12-12
4
1
2
1
2
1
3
4
2012-04-12
Jak można wnioskować na podstawie zakresu funkcjonalności oraz rozmiaru podrozdziału poświęconego wyzwalaczom, narzędzie to jest bardzo istotne. Pozwala na dynamiczne sprawdzanie poprawności danych, automatyczne uzupełnianie wpisów oraz na szeroki zakres działań audytorskich, zarówno na poziomie tabel i widoków, jak i schematu relacyjnego oraz uprawnień i autoryzacji.
5.6. Kursory Jak już zasygnalizowano w poprzednim podrozdziale, kursor jest obiektem zawierającym jeden rekord z zestawu rekordów. Kursor przed użyciem musi zostać zadeklarowany, a jego nazwa w przeciwieństwie do zmiennych nie rozpoczyna się od znaku @. Jeśli zatem w deklaracji zmiennej pominęliśmy ten znak, komunikat o błędzie mówi o złej definicji kursora. Po nazwie kursora pojawiają się słowa kluczowe CURSOR FOR, a po nich dowolne poprawne składniowo zapytanie wybierające, określające zestaw rekordów. Zadeklarowany kursor wymaga otwarcia poleceniem OPEN, po którym znajduje się przed pierwszym rekordem. Aby przenieść się do pierwszego rekordu, wykonujemy polecenie FETCH NEXT FROM. Kolejne wykonanie tego polecenia spowodowałoby przejście do drugiego itd. Jeśli wykorzystaliśmy już funkcje kursora, powinniśmy go zamknąć CLOSE, a następnie zwolnić przyznane mu podczas deklaracji zasoby DEALLOCATE. DECLARE osoby_c CURSOR FOR SELECT Nazwisko FROM Osoby OPEN osoby_c FETCH NEXT FROM osoby_c CLOSE osoby_c DEALLOCATE osoby_c
Po wykonaniu skryptu otrzymujemy pola zawarte w deklaracji kursora, które dotyczą pierwszego rekordu zestawu. Poniżej zaprezentowany został sposób wyświetlania w postaci tekstowej: Nazwisko --------------KOWALSKI
Z reguły chcemy przejść przez wszystkie rekordy zestawu. W tym celu po pierwszym poleceniu FETCH NEXT otwieramy pętlę WHILE, w której sprawdzamy wartość funkcji @@FETCH_STATUS. Ma ona wartość 0, dopóki kursor znajduje się w zestawie rekordów. Wartości, jakie może przyjmować ta funkcja, pokazuje tabela 5.23.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
281
DECLARE osoby_c CURSOR FOR SELECT Nazwisko FROM Osoby OPEN osoby_c FETCH NEXT FROM osoby_c WHILE @@FETCH_STATUS = 0 BEGIN FETCH NEXT FROM osoby_c END CLOSE osoby_c DEALLOCATE osoby_c
Tabela 5.23. Wartości przyjmowane przez funkcję @@FETCH_STATUS Wartość
Opis
0
Przejście do rekordu zakończyło się sukcesem.
–1
Przejście do rekordu zakończyło się błędem lub kursor wyszedł poza zestaw rekordów.
–2
Próba przejścia do nieistniejącego wiersza.
Uzyskany za pomocą skryptu wynik przedstawiono poniżej. Należy zwrócić uwagę, że ze względu na miejsce i sposób umieszczenia warunku ostatni rekord jest pusty, co świadczy o wyjściu poza ostatni element zestawu. Nazwisko --------------KOWALSKI (1 row(s) affected) Nazwisko --------------NOWAK (1 row(s) affected) ... Nazwisko --------------NOWAK (1 row(s) affected) Nazwisko --------------(0 row(s) affected)
W obszarze jednej deklaracji kursor może być wielokrotnie otwierany i zamykany. W przykładzie po otwarciu kursor został przestawiony o dwie pozycje do przodu za pomocą dwóch poleceń FETCH NEXT, a po zamknięciu ponownie został otwarty i przesunięty, tak samo jak w pierwszej części skryptu. DECLARE osoby_c CURSOR FOR SELECT Nazwisko FROM Osoby OPEN osoby_c FETCH NEXT FROM osoby_c FETCH NEXT FROM osoby_c CLOSE osoby_c OPEN osoby_c FETCH NEXT FROM osoby_c FETCH NEXT FROM osoby_c CLOSE osoby_c DEALLOCATE osoby_c
282
MS SQL Server. Zaawansowane metody programowania
Ponieważ otrzymano dwie takie same pary rekordów, można stwierdzić, że po każdym otwarciu kursor jest przenoszony przed pierwszy zestaw rekordów, bez względu na to, w jakim położeniu został on zamknięty. Nazwisko --------------KOWALSKI (1 row(s) affected) Nazwisko --------------NOWAK (1 row(s) affected) Nazwisko --------------KOWALSKI (1 row(s) affected) Nazwisko --------------NOWAK (1 row(s) affected)
Należy pamiętać, że nie można otwierać już otwartego kursora ani zamykać nieotwartego. W obu przypadkach otrzymamy komunikaty o błędzie przetwarzania. Innymi słowy, ciała skryptów zawarte między poleceniami OPEN i CLOSE muszą być rozłączne. Kursorem niekoniecznie musimy nawigować do przodu. Jeśli jednak w którymś z poprzednich skryptów spróbujemy zmienić sposób nawigowania, np. z FETCH NEXT na FETCH LAST, otrzymamy komunikat o błędzie. DECLARE osoby_c CURSOR FOR SELECT Nazwisko FROM Osoby OPEN osoby_c FETCH LAST FROM osoby_c … CLOSE osoby_c DEALLOCATE osoby_c Msg 16911, Level 16, State 1, Line 3 fetch: The fetch type last cannot be used with forward only cursors.
Informacja zawarta w komunikacie świadczy o tym, że domyślnie zadeklarowany kursor pozwala na nawigację tylko do przodu FORWARD ONLY. Ponieważ kursor nie ma domyślnie żadnej informacji o poprzednim położeniu, dostępne jest tylko polecenie FETCH NEXT. Aby to zmienić, należy zadeklarować kursor jako przewijalny SCROLL. W przykładzie zrealizowano nawigację wstecz, ponieważ po otwarciu przesunięto kursor do ostatniego rekordu zestawu FETCH LAST, a w pętli nawigowano o jeden rekord do przodu FETCH PRIOR, co można zaobserwować, analizując zamieszczony po skrypcie fragment wyniku. DECLARE osoby_c SCROLL CURSOR FOR SELECT Nazwisko FROM Osoby OPEN osoby_c FETCH LAST FROM osoby_c WHILE @@FETCH_STATUS = 0 BEGIN FETCH PRIOR FROM osoby_c END CLOSE osoby_c
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
283
DEALLOCATE osoby_c Nazwisko --------------NOWAK (1 row(s) affected) Nazwisko --------------NOWY (1 row(s) affected) ... Nazwisko --------------KOWALSKI (1 row(s) affected) Nazwisko --------------(0 row(s) affected)
Opcje nawigowania kursorami w środowisku MS SQL Server są dużo szersze, a ich wykaz wraz z opisem zawiera tabela 5.24. Tabela 5.24. Możliwe opcje nawigowania za pomocą polecenia FETCH Opcja
Opis
NEXT
Przejście do następnego rekordu.
PRIOR
Przejście do poprzedniego rekordu.
FIRST
Przejście do pierwszego rekordu.
LAST
Przejście do ostatniego rekordu.
ABSOLUTE @n
Przejście do rekordu, którego numer bezwzględny, liczony od 1, jest dany przez stałą lub zmienną będącą liczbą naturalną.
RELATIVE @c
Przejście do rekordu, którego pozycja względem rekordu bieżącego różni się o wartość daną przez stałą lub zmienną będącą liczbą całkowitą; możliwe jest użycie wartości 0 wskazującej ponownie na bieżący rekord.
Do tej pory każdy krok kursora był związany z wyprowadzeniem rekordu na standardowe urządzenie wejścia-wyjścia. Cały proces nawigacji wyglądał tak, jakby składał się z szeregu zapytań wybierających, odnoszących się do jednego rekordu. Poza takim działaniem możliwe jest przypisanie danych zwracanych przez kursor do zmiennych. W tym celu w przykładzie zdefiniowano dwie znakowe zmienne pomocnicze, @nazw i @im. Natomiast w poleceniu FETCH, odpowiadającym za nawigację, po nazwie kursora użyto słowa kluczowego INTO, po którym wymieniono nazwy zmiennych, za które podstawiane są pozycyjnie wartości pól z bieżącego rekordu, zgodnie z zapytaniem wybierającym, definiującym zestaw rekordów kursora. Pierwsze polecenie ciała pętli powoduje wyświetlenie informacji składającej się z przechwyconych wartości. Polecenie to musi poprzedzać FETCH, ponieważ wejście do pierwszego rekordu odbywa się przed pętlą. W praktycznym zastosowaniu polecenie PRINT wskazuje tylko miejsce, w którym będzie odbywało się przetwarzanie danych: sprawdzanie warunków, operacje algebraiczne, etc. Fragment skutku wykonania skryptu jest przedstawiony poniżej skryptu. DECLARE @nazw varchar(40), @im varchar(20) DECLARE osoby_c CURSOR FOR SELECT Nazwisko, Imie FROM Osoby
284
MS SQL Server. Zaawansowane metody programowania OPEN osoby_c FETCH NEXT FROM osoby_c INTO @nazw, @im WHILE @@FETCH_STATUS = 0 BEGIN PRINT 'Kto to? To ' + @im + ' ' + @nazw FETCH NEXT FROM osoby_c INTO @nazw, @im END CLOSE osoby_c DEALLOCATE osoby_c Kto to? To Jan KOWALSKI Kto to? To Karol NOWAK ..... Kto to? To PIOTR KOWAL Kto to? To JAN NOWAK
Oprócz funkcji @@FETCH_STATUS do badania stanu kursora może posłużyć funkcja CURSOR_ STATUS, która wymaga podania dwóch parametrów. Pierwszym jest zasięg, dla którego kursor jest widoczny, i może przyjmować dwie wartości — 'global' lub 'local'. Drugim jest podana jako napis nazwa kursora. Ponieważ funkcja zwraca wartość numeryczną, w celu wyprowadzenia jej w połączeniu z napisem konieczne jest użycie jawnej konwersji, np. przez zastosowanie funkcji CAST. W prezentowanym przykładzie badano i wyświetlano w ten sposób stan kursora, przy założeniu, że jest on globalny — bezpośrednio po zadeklarowaniu, po otwarciu, po wykonaniu poleceniem FETCH każdego z dwóch kroków nawigacji, po zamknięciu i po zwolnieniu zasobów. DECLARE @nazw varchar(40) DECLARE osoby_c CURSOR FOR SELECT Nazwisko FROM Osoby PRINT 'stan ' +CAST(CURSOR_STATUS('global', OPEN osoby_c PRINT 'stan ' +CAST(CURSOR_STATUS('global', FETCH NEXT FROM osoby_c INTO @nazw PRINT 'stan ' +CAST(CURSOR_STATUS('global', FETCH NEXT FROM osoby_c INTO @nazw CLOSE osoby_c PRINT 'stan ' +CAST(CURSOR_STATUS('global', DEALLOCATE osoby_c PRINT 'stan ' +CAST(CURSOR_STATUS('global',
'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3))
Analizując wynik przetwarzania skryptu, widzimy, że wartości są zgodne z bieżącym stanem kursora, tak jak to opisuje tabela 5.25. Oznacza to, że kursor w stanie domyślnym jest globalny. stan -1 stan 1 stan 1 stan -1 stan –3
Jeżeli w skrypcie zamiast parametru 'global' zastosujemy 'local' we wszystkich wywołaniach funkcji CURSOR_STATUS, to skrypt dla każdego stanu kursora zwróci wartość -3, oznaczającą, że nie ma lokalnego kursora o wskazanej nazwie. To potwierdza wyciągnięty poprzednio wniosek o jego globalnym zasięgu.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
285
Tabela 5.25. Stan kursora zwracany przez funkcje CURSOR_STATUS('local', 'nazwa_kursora') lub CURSOR_STATUS('global', 'nazwa_kursora') Wartość
Opis dla kursora Kursor zawiera przynajmniej jeden wiersz.
Opis dla zmiennej typu kursor Kursor przypisany do zmiennej jest otwarty oraz: zawiera przynajmniej jeden wiersz; dla kursorów dynamicznych zawiera zero wierszy, jeden wiersz lub więcej wierszy.
1
Dla kursorów dynamicznych zawiera zero wierszy, jeden wiersz lub więcej wierszy.
0
Zestaw rekordów jest pusty.
Kursor przypisany do zmiennej jest otwarty, ale zestaw rekordów jest pusty.
–1
Kursor jest zamknięty.
Kursor przypisany do zmiennej jest zamknięty.
–2
Nie występuje dla kursora.
Może określać stany, gdy: kursor nie został przypisany do zmiennej OUTPUT; kursor został przypisany do zmiennej OUTPUT, ale jest w stanie zamkniętym; przypisany kursor może być również w stanie DEALLOCATED. Nie istnieje kursor przypisany do zmiennej.
–3
Nie ma kursora o podanej nazwie.
Nie istnieje podana zmienna kursorowa albo kursor nie został jeszcze do niej przypisany.
DECLARE @nazw varchar(40) DECLARE osoby_c CURSOR FOR SELECT Nazwisko FROM Osoby PRINT 'stan ' +CAST(CURSOR_STATUS('local', OPEN osoby_c PRINT 'stan ' +CAST(CURSOR_STATUS('local', FETCH NEXT FROM osoby_c INTO @nazw PRINT 'stan ' +CAST(CURSOR_STATUS('local', FETCH NEXT FROM osoby_c INTO @nazw CLOSE osoby_c PRINT 'stan ' +CAST(CURSOR_STATUS('local', DEALLOCATE osoby_c PRINT 'stan ' +CAST(CURSOR_STATUS('local',
'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3)) 'osoby_c') AS varchar(3))
stan -3 stan -3 stan -3 stan -3 stan -3
Lokalność i globalność kursora ma znaczenie w przypadku używania go w ciele procedury, co zostanie pokazane w dalszej części tego podrozdziału. Podobnie możemy testować stan kursora, tym razem bez względu na jego zasięg, używając funkcji @@CURSOR_ROWS, co przedstawia skrypt ze zmienioną nazwą funkcji określającej jego status. Poniżej niego zamieszczony został wynik przetwarzania, a wszystkie wartości, które może zwrócić zastosowana funkcja, zostały zawarte w tabeli 5.26. DECLARE @nazw varchar(40) DECLARE osoby_c CURSOR FOR SELECT Nazwisko FROM Osoby PRINT 'wiersz ' +CAST(@@CURSOR_ROWS AS varchar(3))
286
MS SQL Server. Zaawansowane metody programowania OPEN osoby_c PRINT 'wiersz ' +CAST(@@CURSOR_ROWS FETCH NEXT FROM osoby_c INTO @nazw PRINT 'wiersz ' +CAST(@@CURSOR_ROWS FETCH NEXT FROM osoby_c INTO @nazw PRINT 'wiersz ' +CAST(@@CURSOR_ROWS CLOSE osoby_c PRINT 'wiersz ' +CAST(@@CURSOR_ROWS DEALLOCATE osoby_c PRINT 'wiersz ' +CAST(@@CURSOR_ROWS wiersz 0 wiersz -1 wiersz -1 wiersz -1 wiersz 0 wiersz 0
AS varchar(3)) AS varchar(3)) AS varchar(3)) AS varchar(3)) AS varchar(3))
Tabela 5.26. Stan kursora zwracany przez funkcje @@CURSOR_ROWS Wartość
Opis
–m
Kursor jest wypełniany (zasilany) dynamicznie — m określa liczbę rekordów w zestawie.
–1
Kursor jest dynamiczny, a ponieważ taki kursor pamięta wszystkie zmiany, liczba rekordów do niego przypisana cały czas się zmienia, w związku z tym nie jest w stanie precyzyjnie określić liczby odczytanych wierszy.
0
Kursor nie jest otwarty lub po otwarciu nie zawiera wierszy albo został zamknięty lub dealokowany.
n
Kursor w pełni zasilony, n jest całkowitą liczbą wierszy w kursorze.
Pełna deklaracja kursora w postaci zgodnej z ISO ma postać: DECLARE cursor_name [INSENSITIVE] [SCROLL] CURSOR FOR zapytanie [FOR {READ ONLY | UPDATE [OF kolumna [ ,...n ]]}]
Wymienione w niej dyrektywy mają następujące znaczenie: INSENSITIVE — (stan domyślny) określa, że kursor tworzy swoją lokalną kopię
rekordów, z której korzysta, a wszystkie odpowiedzi na żądania kursora pochodzą z bazy tempdb, żadne modyfikacje nie mogą być wykonywane; SCROLL — powoduje, że dostępne są opcje nawigacji różne od NEXT (FIRST, LAST, PRIOR, NEXT, RELATIVE, ABSOLUTE), nie może być użyty z opcją FAST_FORWARD; READ ONLY — nie pozwala na wykonywanie modyfikacji za pomocą kursora z użyciem wskazania WHERE CURRENT OF; UPDATE [OF kolumna [,...n]] — wskazuje na kolumny, które mogą być modyfikowane z zastosowaniem wskazania WHERE CURRENT OF; jeśli nie
wyspecyfikowano listy, można modyfikować wszystkie kolumny. Nie wszystkie z wymienionych dyrektyw mogą być ze sobą łączone, co pokazuje tabela 5.27. Ze względu na sposób prezentacji tabela ta musi być symetryczna, natomiast główna diagonala nie zawiera wartości, ponieważ powielenie tej samej dyrektywy jest traktowane jako błąd składniowy.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
287
Tabela 5.27. Możliwość łączenia dyrektywy kursora według składni ISO INSENSITIVE INSENSITIVE
SCROLL
READ ONLY
UPDATE
TAK
TAK
NIE
SCROLL
TAK
READ ONLY
TAK
TAK
TAK
UPDATE
NIE
TAK
TAK NIE
NIE
Dla składni kursora wprowadzonej przez Microsoft i rozszerzającej możliwości oferowane przez składnię zgodną z ISO dostępne są kolejne dyrektywy, których użycie przedstawiono w postaci metaskładni. DECLARE kursor CURSOR [LOCAL | GLOBAL] [FORWARD_ONLY | SCROLL] [STATIC | KEYSET | DYNAMIC | FAST_FORWARD] [READ_ONLY | SCROLL_LOCKS | OPTIMISTIC] [TYPE_WARNING] FOR zapytanie [FOR UPDATE [OF kolumna [ ,...n ]]]
Mają one następujące znaczenie: LOCAL — wskazuje, że zakres kursora jest lokalny, czyli nie będzie widoczny
poza miejscem deklaracji (na zewnątrz procedury, funkcji, wyzwalacza); można przekazać kursor do miejsca wywołania za pomocą dyrektywy OUTPUT, zasoby są zwalniane automatycznie (niejawne użycie DEALLOCATE) z chwilą zakończenia przetwarzania elementu proceduralnego, w którym go zadeklarowano; GLOBAL — (domyślny) zakres kursora obejmuje miejsce, z którego został
wywołany element proceduralny, i miejsce w ciele innych procedur tej samej sesji; zasoby są zwalniane (niejawne użycie DEALLOCATE) z chwilą zakończenia sesji, w której zadeklarowano kursor; FORWARD_ONLY — (stan domyślny) określa, że kursor może być przewijany tylko do przodu, za pomocą polecenia FETCH NEXT; nie może występować razem z dyrektywami STATIC, KEYSET i DYNAMIC; podobnie odwołując się do kursora za pomocą API, nie można mieszać tych opcji; STATIC — określa, że kursor tworzy tymczasową, lokalną kopię rekordów,
z której korzysta; wszystkie odpowiedzi na żądania kursora pochodzą z bazy tempdb; żadne modyfikacje nie mogą być wykonywane; KEYSET — wskazuje, że przynależność do zestawu rekordów i ich porządek są
określone w momencie jego otwarcia; zestaw kluczy identyfikujących wiersze jest tworzony w tempdb i określany jako keyset; kiedy definiujemy taki kursor dla tabel, z których przynajmniej jedna nie ma indeksu unikalnego, jest on konwertowany do typu STATIC; zmiany wprowadzone za pomocą tego kursora są widoczne po zakończeniu zestawu rekordów; zmiany wprowadzane przez innych użytkowników nie są w kursorze widoczne; kiedy próbujemy przejść do usuniętego rekordu, funkcja @@FETCH_STATUS jest ustawiana na –2;
288
MS SQL Server. Zaawansowane metody programowania DYNAMIC — określa, że gdy kursor jest przewijany, odwzorowuje wszystkie zmiany
w zestawie rekordów; dotyczy to wartości, porządku rekordów i usuniętych wierszy; nie może występować razem z ABSOLUTE; FAST_FORWARD — określa, że kursor jest typu FORWARD_ONLY i READ_ONLY
z optymalizacją wydajności przetwarzania; nie może występować razem z SCROLL i FOR_UPDATE; READ_ONLY — zapobiega możliwości wykonywania zmian w zestawie rekordów za pomocą WHERE CURRENT OF; SCROLL_LOCKS — określa, że wskazane modyfikacje i usunięcia rekordów
wykonają się z powodzeniem na skutek blokad założonych na wiersze odczytywane przez kursor; nie może występować z FAST_FORWARD i STATIC; OPTIMISTIC — określa, że modyfikacje wykonywane za pomocą kursora mogą
nie zakończyć się sukcesem, jeśli zmiany zostały wykonane od otwarcia do czasu odczytania wiersza przez kursor; kursor nie stosuje blokad na wiersze, w zamian za to stosuje znaczniki czasowe TIMESTAMP lub sumy kontrolne do określenia, czy i kiedy wiersz został zmodyfikowany; nie może występować z FAST_FORWARD; TYPE_WARNING — określa, że będą generowane i przesyłane do klienta ostrzeżenia,
kiedy następują jawne konwersje typów w zestawie rekordów. Możliwość łączenia dyrektyw kursora dla składni rozszerzającej ISO, które zostały wprowadzone przez Microsoft, pokazuje tabela 5.28.
LOCAL
NIE
UPDATE
TYPE WARNING
OPTIMISTIC
SCROLL LOCKS
READ ONLY
FAST FORWARD
DYNAMIC
KEYSET
STATIC
FORWARD ONLY
GLOBAL
LOCAL
Tabela 5.28. Możliwość łączenia dyrektywy kursora według składni Transact-SQL
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
NIE
TAK
TAK
TAK
TAK
TAK
NIE
NIE
NIE
TAK
NIE
TAK
TAK
NIE
NIE
NIE
TAK
TAK
TAK
TAK
TAK
NIE
TAK
TAK
TAK
TAK
TAK
GLOBAL
NIE
FORWARD ONLY
TAK
TAK
STATIC
TAK
TAK
TAK
KEYSET
TAK
TAK
TAK
NIE
DYNAMIC
TAK
TAK
TAK
NIE
FAST FORWARD
TAK
TAK
NIE
NIE
NIE
NIE
READ ONLY
TAK
TAK
TAK
TAK
TAK
TAK
SCROLL LOCKS
TAK
TAK
TAK
NIE
TAK
TAK
NIE
NIE
OPTIMISTIC
TAK
TAK
TAK
TAK
TAK
TAK
NIE
NIE
NIE
TYPE WARNING
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
TAK
UPDATE
TAK
TAK
TAK
NIE
TAK
TAK
NIE
NIE
TAK
TAK
NIE
TAK TAK
NIE
NIE
TAK
NIE
NIE
NIE
TAK
NIE
NIE
TAK
TAK
TAK
TAK TAK
TAK
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
289
Powróćmy do rozważań nad zasięgiem kursorów, analizując go dla elementów proceduralnych. W tym celu utwórzmy procedurę PracDzial, która wyświetla nazwiska pracowników w dziale danym parametrem. Ponadto dla każdego ze stanów kursora jest wyświetlana informacja zwracana przez funkcję CURSOR_STATUS. Zarówno w deklaracji kursora, jak i jako pierwszy parametr funkcji testującej zastosowano wartość LOCAL. W ciele procedury nie zamknięto i nie zwolniono zasobów przydzielonych kursorowi. W bloku anonimowym, pokazanym po ciele procedury, najpierw wykonano jej wywołanie, a następnie zamknięto kursor CLOSE i zwolniono zasoby DEALLOCATE. Po każdej z tych czynności wyświetlono informację o stanie kursora. DROP PROCEDURE PracDzial GO CREATE PROCEDURE PracDzial @dzial int AS DECLARE @nazw varchar(20) DECLARE cur CURSOR LOCAL FOR SELECT Nazwisko FROM Osoby WHERE IdDzialu=@dzial PRINT 'stan ' +CAST(CURSOR_STATUS('local', 'cur') OPEN cur PRINT 'stan ' +CAST(CURSOR_STATUS('local', 'cur') FETCH NEXT FROM cur INTO @nazw WHILE @@FETCH_STATUS = 0 BEGIN PRINT 'Pracownik - ' + @nazw FETCH NEXT FROM cur INTO @nazw END PRINT 'stan ' +CAST(CURSOR_STATUS('local', 'cur') GO EXEC PracDzial 2 PRINT 'stan ' +CAST(CURSOR_STATUS('local', 'cur') CLOSE cur PRINT 'stan ' +CAST(CURSOR_STATUS('local', 'cur') DEALLOCATE cur PRINT 'stan ' +CAST(CURSOR_STATUS('local', 'cur')
AS varchar(3)) AS varchar(3))
AS varchar(3)) AS varchar(3)) AS varchar(3)) AS varchar(3))
Analizując skutek wykonania skryptu, zamieszczony poniżej, możemy zaobserwować, że w ciele procedury wskazywane są stany –1 oraz 1 zgodnie z opisem danym w tabeli 5.25. Natomiast w zewnętrznym bloku anonimowym wszystkie komunikaty pokazują stan –3, wskazujące na brak kursora o określonej nazwie, co potwierdza pojawiający się komunikat o błędzie. stan -1 stan 1 Pracown k - NOWAK Pracown k - JANIK Pracown k - KOWALSKI stan 1 stan -3 Msg 16916, Level 16, State 1, Line 3 A cursor with the name 'cur' does not exist. stan -3 Msg 16916, Level 16, State 1, Line 5 A cursor with the name 'cur' does not exist. stan -3
290
MS SQL Server. Zaawansowane metody programowania
Przedstawione wyniki potwierdzają lokalny zasięg kursora. Oznacza to, że nie jest on widoczny poza procedurą, czyli miejscem zadeklarowania, a zasoby, jeśli nie użyto jawnie DEALLOCATE, są zwalniane w chwili zakończenia przetwarzania elementu proceduralnego, w którym ta deklaracja się znajduje. Powtórzmy nasz eksperyment, zamieniając w deklaracji zasięg kursora na globalny oraz dokonując takiej samej zmiany w pierwszym parametrze wywołania funkcji CURSOR_ STATUS. DROP PROCEDURE PracDzial GO CREATE PROCEDURE PracDzial @dzial int AS DECLARE @nazw varchar(20) DECLARE cur CURSOR GLOBAL FOR SELECT Nazwisko FROM Osoby WHERE IdDzialu=@dzial PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') OPEN cur PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') FETCH NEXT FROM cur INTO @nazw WHILE @@FETCH_STATUS = 0 BEGIN PRINT 'Pracownik - ' + @nazw FETCH NEXT FROM cur INTO @nazw END PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') GO EXEC PracDzial 2 PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') CLOSE cur PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') DEALLOCATE cur PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') stan -3 stan -1 stan 1 Pracown k - NOWAK Pracown k - JANIK Pracown k - KOWALSKI stan 1 stan -1 stan -3
AS varchar(3)) AS varchar(3))
AS varchar(3)) AS varchar(3)) AS varchar(3)) AS varchar(3))
Jak widzimy, w wynikowym zestawie komunikatów stan kursora jest odczytywany zarówno w ciele procedury, jak i w bloku anonimowym. Poza tym nie pojawiają się komunikaty o błędzie, jakie obserwowaliśmy poprzednio. Świadczy to o globalnym zasięgu kursora, czyli o tym, że jest on widoczny poza elementem proceduralnym, w którym został zadeklarowany, i że zasoby, jeśli nie użyto jawnie DEALLOCATE, są zwalniane z chwilą zakończenia sesji. Należy pamiętać, że domyślnie zadeklarowany kursor ma zasięg globalny, dlatego powinniśmy pamiętać o jawnym zwolnieniu zasobów, nie zdając się na mechanizm automatyczny, który ze względu na czas trwania sesji może blokować zasoby przez długi czas. Zamiast używać kursora, możemy zastosować zmienną typu kursor CURSOR, która jest instancją obiektu tego typu. Określenie zestawu rekordów odbywa się przez podstawienie po słowie kluczowym SET. Należy zwrócić uwagę na powtórzenie słowa klu-
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
291
czowego CURSOR, ponieważ za pomocą dyrektyw nie można określić sposobu nawigacji czy też zasięgu lub dostępu do danych. Pozostałe operacje związane z posługiwaniem się zmienną typu kursor są takie same jak w przypadku zwykłego kursora. Ze względu na brak możliwości użycia dodatkowych dyrektyw nawigacja może odbywać się tylko do przodu, również pozostałe cechy są zgodne z wartościami domyślnymi. Pokazany przykład ilustruje sposób nawigowania za pomocą zmiennej typu kursor po wszystkich działach. Poniżej skryptu przedstawiono przykładowy wynik działania. DECLARE @pcursor CURSOR DECLARE @nazw varchar(15) SET @pcursor = CURSOR FOR SELECT Nazwa FROM Dzialy OPEN @pcursor FETCH NEXT FROM @pcursor INTO @nazw WHILE (@@FETCH_STATUS = 0) BEGIN PRINT 'Ten dział to ' + @nazw FETCH NEXT FROM @pcursor INTO @nazw END CLOSE @pcursor DEALLOCATE @pcursor Ten dział to Dyrekcja Ten dział to Administracja Ten dział to Techniczny Ten dział to Handlowy Ten dział to Pomocniczy Ten dział to Nowy
W kolejnym przykładzie skrypt został uzupełniony o informacje o stanie kursora otrzymaną za pomocą funkcji CURSOR_STATUS, w której wywołaniu zastosowano parametr 'global'. Otrzymane wyniki przedstawione poniżej skryptu potwierdzają, że tak samo jak w domyślnej deklaracji kursora, zmienna tego typu ma zasięg globalny. DECLARE @pcursor CURSOR DECLARE cur CURSOR FOR SELECT Nazwa FROM Dzialy DECLARE @nazw varchar(15) SET @pcursor=cur PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') AS varchar(3)) OPEN @pcursor FETCH NEXT FROM @pcursor INTO @nazw WHILE (@@FETCH_STATUS = 0) BEGIN PRINT 'Ten dział to ' + @nazw + ' stan ' + CAST(CURSOR_STATUS('global', 'cur') AS varchar(3)) FETCH NEXT FROM @pcursor INTO @nazw END CLOSE @pcursor PRINT 'stan ' +CAST(CURSOR_STATUS('global', 'cur') AS varchar(3)) DEALLOCATE @pcursor DEALLOCATE cur stan -1 Ten dział to Dyrekcja stan 1 Ten dział to Administracja stan 1 Ten dział to Techniczny stan 1 Ten dział to Handlowy stan 1 Ten dział to Pomocniczy stan 1 Ten dział to Nowy stan 1 stan -1
292
MS SQL Server. Zaawansowane metody programowania
Zmienna typu kursor może być również parametrem procedury. W przykładzie w jej ciele zdefiniowano zestaw rekordów dla parametru tego typu i dokonano jego otwarcia. Należy zwrócić uwagę, że obowiązuje użycie słowa kluczowego VARYING oraz określenie kierunku przekazywania do miejsca wywołania OUTPUT. Z powodu zastosowania drugiej z dyrektyw nie jest możliwe użycie kursora jako parametru funkcji. DROP PROCEDURE kursor_proc GO CREATE PROCEDURE kursor_proc @pcursor CURSOR VARYING OUTPUT AS SET @pcursor = CURSOR FOR SELECT Nazwa FROM Dzialy OPEN @pcursor GO
Jeśli w bloku anonimowym zadeklarujemy pomocniczą zmienną typu kursor, to po wywołaniu procedury kursor_proc z tą zmienną jako parametrem mamy określony i otwarty zestaw rekordów. Ponieważ zmienna typu kursor ma zasięg globalny, możemy dokonać nawigowania za pomocą polecenia FETCH, tak jak w przypadku zwykłego kursora, otrzymując wynik pokazany poniżej ciała skryptu. Zmienna z wywołania może zostać zamknięta, a związane z nią zasoby powinny zostać zwolnione. DECLARE @vcursor CURSOR DECLARE @nazw varchar(15) EXEC kursor_proc @vcursor OUTPUT FETCH NEXT FROM @vcursor INTO @nazw WHILE (@@FETCH_STATUS = 0) BEGIN PRINT 'Ten dział to ' + @nazw FETCH NEXT FROM @vcursor INTO @nazw END CLOSE @vcursor Ten dział to Dyrekcja Ten dział to Administracja Ten dział to Techniczny Ten dział to Handlowy Ten dział to Pomocniczy Ten dział to Nowy
Do tej pory zarówno w bloku anonimowym, jak i ciele procedury zmienna typu kursor była deklarowana statycznie, to znaczy jawnie podawano definicję zestawu rekordów. Możliwe jest jednak zastosowanie definicji dynamicznej, określającej dane za pomocą napisu lub zmiennej tekstowej bądź też ich konkatenacji. Po zadeklarowaniu zmiennej znakowej @zap podstawiono do niej napis zawierający zapytanie wybierające. Następnie podstawiono początek deklaracji kursora, uzupełniając ją dzięki konkatenacji o definicję zestawu rekordów opisaną poprzednią zawartością zmiennej. Aby sprawdzić zawartość zmiennej, zmienna ta jest wyświetlana na standardowym wyjściu. Gdy skrypt zawarty w zmiennej zostanie uruchomiony za pomocą polecenia EXEC(@zap), zagnieżdżonego w zewnętrznym bloku anonimowym, kursor jest zadeklarowany. Ponieważ domyślnie kursor jest globalny, jest widoczny na zewnątrz skryptu, w którym go zadeklarowano, czyli w miejscu wykonania polecenia EXEC. Stąd dozwolone są operacje otwarcia, nawigowania nim, zamknięcia oraz zwolnienia zasobów. Przykładowy skutek wykonania bloku anonimowego pokazano poniżej jego ciała.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
293
DECLARE @nazw varchar(1000) DECLARE @zap varchar(1000) SET @zap='SELECT Nazwisko FROM Osoby WHERE IdOsoby<6' SET @zap='DECLARE cur CURSOR FOR '+ @zap PRINT @zap EXEC (@zap) OPEN cur FETCH NEXT FROM cur INTO @nazw WHILE @@FETCH_STATUS=0 BEGIN PRINT @nazw FETCH NEXT FROM cur INTO @nazw END CLOSE cur DEALLOCATE cur DECLARE cur CURSOR FOR SELECT Nazwisko FROM Osoby WHERE IdOsoby<6 KOWALSKI NOWAK KOW …
Zaprezentowany przykład dynamicznej deklaracji może być z łatwością zastąpiony wersją statyczną. Dopiero zastosowanie kursora we wnętrzu procedury pokazuje w pełni funkcjonalność takiego podejścia. Parametrem wejściowym procedury jest teraz zmienna znakowa, przez którą przekazywane będzie zapytanie wybierające. W jej ciele następuje połączenie początkowej deklaracji kursora z przekazaną z zewnątrz definicją zestawu rekordów. Polecenie zawierające pełną deklaracje kursora jest realizowane na skutek wykonania polecenia EXEC(@zap), po którym następuje sekwencja poleceń odpowiedzialnych za otwarcie, nawigację, zamknięcie i zwolnienie zasobów kursora. DROP PROC DynCur GO CREATE PROC DynCur @zap varchar (1000) AS DECLARE @nazw varchar(1000) SET @zap='DECLARE cur CURSOR FOR '+ @zap PRINT @zap EXEC (@zap) OPEN cur FETCH NEXT FROM cur INTO @nazw WHILE @@FETCH_STATUS=0 BEGIN PRINT @nazw FETCH NEXT FROM cur INTO @nazw END CLOSE cur DEALLOCATE cur GO
Przykład zastosowania procedury zawiera jej wywołania dla dwóch różnych definicji zestawu rekordów. W pierwszym przypadku obsługiwane będą osoby o identyfikatorze mniejszym niż 6, a w drugim takie, których nazwisko rozpoczyna się od litery n. Zastosowany w drugim przypadku ciąg składający się z czterech apostrofów '''' za
294
MS SQL Server. Zaawansowane metody programowania
stępuje napis złożony z pojedynczego apostrofu. Poniżej skryptu pokazano fragmenty wyników zwracanych przez oba użyte wywołania. Należy zwrócić uwagę na zaprezentowane definicje rekordów, które otwierają oba zestawy wynikowe. DECLARE @zap varchar(1000) SET @zap='SELECT Nazwisko FROM Osoby WHERE IdOsoby<6' EXEC DynCur @zap SET @zap='SELECT Nazwisko FROM Osoby WHERE Nazwisko LIKE '+ ''''+'N%'+'''' EXEC DynCur @zap DECLARE cur CURSOR FOR SELECT Nazwisko FROM Osoby WHERE IdOsoby<6 KOWALSKI NOWAK KOW … DECLARE cur CURSOR FOR SELECT Nazwisko FROM Osoby WHERE Nazwisko LIKE 'N%' NOWAK NOWICKI NOWAK …
Podobne efekty możemy uzyskać, stosując dynamiczną definicję zmiennej typu kursor. Dla wielu programistów, którzy mają doświadczenie w pisaniu oprogramowania w językach wyższego rzędu, a którzy poznają później bazy danych, SQL i rozszerzenia proceduralne — kursory stają się objawieniem. Wreszcie przestaje się mówić o złączeniach i relacjach, a operuje się dobrze znanymi pojęciami pętli i nawigowania po zestawie danych. Mechanizm ten zaćmiewa wszystkie dotychczasowe informacje, bo jest prostą analogią tego, co jest dobrze poznane, i pojawia się pokusa stosowania go wszędzie, gdzie to tylko możliwe. Jednak kursory mają wiele niekorzystnych cech. Podstawową jest ich mała wydajność. Można powiedzieć, że pełna nawigacja przez zestaw rekordów wymaga wykonania tylu zwracających jeden wiersz zapytań wybierających, ile liczy ich ten zestaw. Taki schemat przetwarzania nie może oferować dużej szybkości. Kolejna wada to konieczność przydzielenia kursorowi zasobów pamięci, które jeśli nie są sensownie zwalniane, mogą powodować znaczne ich zablokowanie. Dzieje się tak najczęściej podczas rekurencyjnego wywoływania procedur używających w swoim ciele kursorów lub zmiennych tego typu. Kolejna wada to możliwość zablokowania dostępu do rekordów i doprowadzenie do zakleszczenia. Przykład skryptów prowadzących do takiego stanu i skutki ich wykonania zawiera tabela 5.29. Przedstawia ona dwie sesje z możliwie równolegle przetwarzanymi blokami anonimowymi. W sesji A kursor jest zdefiniowany jako zestaw nazwisk uporządkowanych rosnąco, a w sesji B malejąco. Obydwa obiekty mają dyrektywę SCROLL_LOCKS, powodującą blokowanie aktualnie przetwarzanego rekordu przed dostępem z innych sesji. W obu skryptach realizowana jest nawigacja od pierwszego rekordu do ostatniego co jeden rekord. Sama nawigacja dla tak krótkiego zestawu danych wykonuje się szybko, więc aby dokonać przełączenia między sesjami i umożliwić względnie równoległe przetwarzanie, w obu skryptach wprowadzono w pętli opóźnienie o 30ms. Poniżej skryptów umieszczono rezultaty. Ponieważ blok w sesji B był uruchomiony później, w momencie dojścia do tego samego rekordu pojawia się w jej rezultatach komunikat o błędzie wynikającym z zakleszczenia oraz informacja o konieczności ponownego uruchomienia tego bloku.
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
295
Tabela 5.29. Blokowanie rekordów przez kursory Sesja A
Sesja B
DECLARE @nazw varchar(40)
DECLARE @nazw varchar(40)
DECLARE cur CURSOR SCROLL_LOCKS FOR
DECLARE cur CURSOR SCROLL_LOCKS FOR
SELECT Nazwisko FROM Osoby
SELECT Nazwisko FROM Osoby
ORDER BY RokUrodz ASC
ORDER BY RokUrodz DESC
OPEN cur
OPEN cur
FETCH NEXT FROM cur INTO @nazw
FETCH NEXT FROM cur INTO @nazw
WHILE @@FETCH_STATUS = 0
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN
PRINT 'Pracownik ' + @nazw
PRINT 'Pracownik ' + @nazw
WAITFOR DELAY '00:00:00.30'
WAITFOR DELAY '00:00:00.30'
FETCH NEXT FROM cur INTO @nazw
FETCH NEXT FROM cur INTO @nazw
END
END
CLOSE cur
CLOSE cur
DEALLOCATE cur
DEALLOCATE cur
Skutek — sesja A
Skutek — sesja B
Pracownik NOWY
Pracown k KOWALCZYK
Pracownik NOWAK
Pracownik SZEWCZYK
Pracownik KOWALSKI
...
...
Pracown k JANIK
Pracownik KOWALCZYK
Pracownik KOWALSKI
Pracownik SZEWCZYK
Msg 1205, Level 13, State 21, Line 10 Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Podobny eksperyment został wykonany dla kursorów stosujących opcje SCROLL_LOCKS oraz FOR UPDATE (tabela 5.30). Druga z nich pokazuje, że można dokonać wskazania bieżącego rekordu, np. w celu modyfikacji, za pomocą dyrektywy CURRENT OF. Poza wymienionymi dyrektywami oraz modyfikacją UPDATE skrypty są takie same jak w poprzednim przykładzie. Taki sam jest również skutek przetwarzania. Przedstawione fakty świadczą o tym, że wykorzystywanie kursorów nie jest najlepszym rozwiązaniem. Jeśli jakieś zadanie daje się rozwiązać za pomocą innych środków niż kursory, należy wybierać tę alternatywną drogę. Szczególnie jeżeli rozwiązanie możemy uzyskać, stosując zapytanie wybierające SQL — zyski w wydajności oraz zmniejszeniu obciążenia będą znaczące. Jednak wielu zadań nie można rozwiązać inaczej. Kursory muszą być wykorzystywane, kiedy operacje mają być wykonywane na danych pochodzących z różnych wierszy i różnych kolumn. Przykładem może być zadanie magazynowe polegające na zdejmowaniu ze stanu magazynu towarów zgodnie z wystawioną fakturą, jeżeli stan magazynu jest opisywany przez liczbę towarów otrzymanych z różnych dostaw. Należy wtedy
296
MS SQL Server. Zaawansowane metody programowania
Tabela 5.30. Blokowanie rekordów przez kursory Sesja A
Sesja B
DECLARE @nazw varchar(40)
DECLARE @nazw varchar(40)
DECLARE cur CURSOR SCROLL_LOCKS FOR
DECLARE cur CURSOR SCROLL_LOCKS FOR
SELECT Nazwisko FROM Osoby ORDER BY
SELECT Nazwisko FROM Osoby ORDER BY
RokUrodz ASC FOR UPDATE
RokUrodz DESC FOR UPDATE
OPEN cur
OPEN cur
FETCH NEXT FROM cur INTO @nazw
FETCH NEXT FROM cur INTO @nazw
WHILE @@FETCH_STATUS = 0
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN
PRINT 'Pracownik ' + @nazw
PRINT 'Pracownik ' + @nazw
UPDATE Osoby SET
UPDATE Osoby SET
Nazwisko=UPPER(Nazwisko)
Nazwisko=LOWER (Nazwisko)
WHERE CURRENT OF cur
WHERE CURRENT OF cur
WAITFOR DELAY '00:00:00.30'
WAITFOR DELAY '00:00:00.30'
FETCH NEXT FROM cur INTO @nazw
FETCH NEXT FROM cur INTO @nazw
END
END
CLOSE cur
CLOSE cur
DEALLOCATE cur
DEALLOCATE cur
Skutek — sesja A
Skutek — sesja B
Pracownik nowy
Pracown k KOWALCZYK
(1 row(s) affected)
(1 row(s) affected)
Pracownik nowak
....
(1 row(s) affected)
Pracown k KOWALSKI
Pracownik kowalski
(1 row(s) affected)
...
Msg 1205, Level 13, State 21, Line 13
(1 row(s) affected)
Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Pracownik szewczyk (1 row(s) affected)
zidentyfikować wszystkie dostawy danego towaru i zmniejszać stan, począwszy od najstarszej dostawy, tak aby dostawy sumarycznie pokryły liczbę widniejącą na fakturze. Także kiedy jedynym rozwiązaniem problemu jest rekurencja, np. znajdowanie drogi w grafie. Jednak i w prostszych przypadkach kursory są niezbędne. Zdarzyło mi się, że podczas projektowania schematu relacyjnego, konkretnie struktury tabeli Towar, wprowadziłem pole Cena. Jednak w późniejszej realizacji przykładów okazało się, że aby wyznaczyć zysk, przydatne byłoby dodanie pola CenaZakupu. Dodanie pola nie stanowi problemu, ale wygenerowanie zawartości już nie jest trywialne. Chciałem, aby wartości pól różniły się w zmiennej proporcji, jednak tak, aby ich iloraz mieścił się w założonym przedziale. Konieczne było użycie funkcji generatora liczb pseudolosowych RAND(), która generuje wartości z przedziału <0, 1). Wydaje się, że wystarczy
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
297
w zapytaniu modyfikującym zastosować odpowiednie współczynniki przesunięcia 0.9 oraz skali 0.3, aby zakres generowanych ilorazów mieścił się w przedziale (0.6, 0.9>: UPDATE Towar SET CenaZakupu=(0.9 - 0.3* RAND()) * Cena
Jednakże takie rozwiązanie nie jest właściwe. Rzeczywiście dla każdego uruchomienia ilorazy ceny i ceny zakupu są różne, ale są stałe dla wszystkich rekordów. Wynika to z tego, że funkcja RAND() jest wykonywana jednokrotnie przy uruchomieniu zapytania, a nie dla każdego wiersza. Dlatego właściwe jest zastosowanie kursora, który wybiera identyfikator towaru. Gdy nawigujemy w pętli, modyfikacja UPDATE jest wykonywana dla każdego z towarów niezależnie — wskazuje go zmienna @ktory odczytana z kolejnych rekordów zestawu. Poniżej skryptu modyfikującego zawartość pola CenaTowaru w tabeli Towar pokazano zapytanie wybierające, pokazujące zmienne wartości ilorazu tych pól. Wartości współczynnika Wsp pokazane w tabeli 5.31 pokazują jego zmienność dla kolejnych rekordów. Należy zauważyć, że jeśli Czytelnik będzie wykonywał wielokrotnie skrypt, wyniki będą zmienne, co wynika z cech generatora liczb pseudolosowych. DECLARE cur CURSOR FOR SELECT IdTowaru FROM Towar DECLARE @ktory INTEGER OPEN cur FETCH NEXT FROM CUR INTO @ktory WHILE @@FETCH_STATUS = 0 BEGIN UPDATE Towar SET CenaZakupu=(0.9 - 0.3* RAND()) * Cena WHERE IdTowaru = @ktory FETCH NEXT FROM cur INTO @ktory END CLOSE cur DEALLOCATE cur GO SELECT IdTowaru, Cena, CenaZakupu, CenaZakupu/Cena AS Wsp FROM TOWAR
Tabela 5.31. Przykład zawartości tabeli Towar po wykonaniu skryptu modyfikującego pole CenaZakupu IdTowaru
Cena
CenaZakupu
Wsp
1
108,44
65,4606
0,6036
2
108,90
67,2284
0,6173
3
68,55
46,7231
0,6815
4
30,14
25,073
0,8318
5
68,67
56,0668
0,8164
6
77,66
62,5994
0,806
7
26,09
17,9889
0,6894
8
22,00
16,8669
0,7666
…
…
…
…
Inne przykłady zastosowania kursorów Czytelnik znajdzie w dalszych rozdziałach książki.
298
MS SQL Server. Zaawansowane metody programowania
5.7. Zmienna tabelaryczna i typ tabelaryczny Przy okazji przetwarzania z zastosowaniem rozszerzenia proceduralnego przedstawmy kolejny po XML typ złożony, o równie szerokim zastosowaniu [62] [78] [79]. Tym typem jest zmienna tabelaryczna. Można by ją przedstawić wcześniej, przy omawianiu tworzenia i modyfikacji tabel, jednak ze względu na zastosowane w przykładach elementy Transact-SQL postanowiłem omówić ją dopiero w tym miejscu. Zanim jednak przybliżymy jej właściwości, przypomnijmy sobie fragment zawartości tabeli Zarobki, a dokładnie chodzi o wypłaty dla osoby o identyfikatorze IdOsoby=1, co realizuje poniższe zapytanie, a rezultat przedstawia tabela 5.32. SELECT * FROM Zarobki WHERE IdOsoby =1;
Tabela 5.32. Zestaw rekordów dotyczący wypłat pracownika o IdOsoby=1 IdZarobku
IdOsoby
Brutto
1
1
111,00
3
1
333,00
6
1
666,00
9
1
999,00
Sprawdźmy teraz skutek wykonywania modyfikacji na wskazanym zestawie rekordów. W tym celu zadeklarujmy zmienną tabelaryczną @tabela. Operacja ta jest analogią definiowania klasycznej tabeli. Po słowie kluczowym TABLE następuje definicja pól zmiennej tego typu. W skład definicji pola wchodzi jego nazwa i typ, dodatkowo można określić wszystkie ograniczenia, które tworzyliśmy w przypadku tabel. W przykładzie zmienna ma pięć kolumn — pierwsza jest automatycznie inkrementowanym identyfikatorem, trzy kolejne są całkowitoliczbowe, a ostatnia, typu „data i czas”, ma określoną wartość domyślną, którą jest czas systemowy. Podczas modyfikacji wypłat dla pierwszego pracownika przekierowano za pomocą dyrektywy OUTPUT wartości pól z tabeli INSERTED zawierające dane po zmianie i DELETED przed zmianą. W kolejności są to identyfikator pracownika, nowa i stara wartość wypłaty oraz data systemowa uzyskana za pomocą funkcji getdate(). Ponieważ zasileniu podlegają wszystkie pola zmiennej tabelarycznej oprócz pola automatycznie inkrementowanego, po słowie kluczowym INTO wystarczy wymienić nazwę zasilanej zmiennej. Na koniec pokazano zawartość zmiennej, co ilustruje tabela 5.33. DECLARE @tabela TABLE( ID int Identity(1,1), kto int, StareBrutto int, NoweBrutto int, data datetime DEFAULT getdate()); UPDATE Zarobki SET Brutto = Brutto * 1.25 OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto, getdate() INTO @tabela WHERE IdOsoby =1; SELECT * FROM @tabela; GO
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
299
Tabela 5.33. Zawartość zmiennej tabelarycznej po wykonaniu modyfikacji wypłat dla pierwszego pracownika ID
kto
StareBrutto
NoweBrutto
data
1
1
111
139
2012-09-29 21:47:13.763
2
1
333
416
2012-09-29 21:47:13.763
3
1
666
833
2012-09-29 21:47:13.763
4
1
999
1249
2012-09-29 21:47:13.763
Sprawdzenie rezultatu za pomocą zapytania wybierającego z tabeli Zarobki prowadzi do potwierdzenia wykonania modyfikacji. Widoczne w tabeli 5.33 i 5.34 różnice wynikają z zastosowania typów całkowitoliczbowych w zmiennej tabelarycznej, a w tabeli oryginalnej rzeczywistego typu money, co jest dowodem działania konwersji „w locie”. SELECT * FROM Zarobki WHERE IdOsoby =1;
Tabela 5.34. Zestaw rekordów dotyczący wypłat pracownika o IdOsoby=1 po wykonaniu modyfikacji jego wypłat IdZarobku
IdOsoby
Brutto
1
1
138,75
3
1
416,25
6
1
832,50
9
1
1248,75
Podobnie możemy przetestować modyfikacje, wykorzystując wartość domyślną określoną dla pola data zmiennej tabelarycznej. Ponieważ tym razem zasileniu podlegają wybrane pola po wskazaniu docelowej zmiennej poleceniem INTO i podaniu jej nazwy, w nawiasie musimy wymienić pola, do których wstawiamy dane. SELECT * FROM Zarobki WHERE IdOsoby =1; GO DECLARE @tabela table( ID int Identity(1,1), kto int, StareBrutto int, NoweBrutto int, data datetime DEFAULT getdate()); UPDATE Zarobki SET Brutto = Brutto * 1.25 OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto INTO @tabela (kto, StareBrutto, NoweBrutto) WHERE IdOsoby =1; SELECT * FROM @tabela;
Zamiast definiować strukturę zmiennej tabelarycznej, możemy najpierw zdefiniować typ tego rodzaju. Na definicję typu składa się nazwa, a po słowach kluczowych AS TABLE podajemy w nawiasie definicje pól. Tak jak w przypadku zmiennej, obowiązują takie same zasady jak podczas tworzenia tabel. CREATE TYPE Sprawdzenie AS TABLE (ID int Identity(1,1) PRIMARY KEY, Identyfikator int,
300
MS SQL Server. Zaawansowane metody programowania Stare int, Nowe int, data datetime DEFAULT getdate())
Typ jest obiektem składowanym na serwerze i można się do niego odwołać w dowolnym skrypcie. Dlatego tym razem do sprawdzenia wykonania modyfikacji wykorzystujemy zmienną, która została zdefiniowana jako poprzednio utworzony typ. Skutek odpytania zmiennej potwierdza modyfikację, tak jak to miało miejsce w poprzednich przykładach. DECLARE @tabela Sprawdzenie UPDATE Zarobki SET Brutto = Brutto * 1.25 OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto INTO @tabela (Identyfikator, Stare, Nowe ) WHERE IdOsoby =1 SELECT * FROM @tabela
Należy pamiętać, że zmienna typu tabelarycznego, tak jak wszystkie inne zmienne, jest bytem lokalnym i przestaje być widoczna poza skryptem, w którym została zadeklarowana. Zmienna tabelaryczna może pojawić się również jako parametr procedury. W takim przypadku konieczne jest uprzednie zdefiniowanie typu, ponieważ definicja zmiennej tabelarycznej podczas tworzenia procedury jest niedopuszczalna. W przykładzie takim typem jest Zestaw, zawierający dwa pola — Identyfikator oraz Wartosc. Ponieważ nie można usunąć typów, które są wykorzystywane w innych obiektach TransactSQL, kolejność wykonywania poleceń DROP jest obowiązkowa. Najpierw usuwamy procedurę, a dopiero później użyty w niej typ. Tworzona procedura ma dwa parametry. Pierwszy, typu tabelarycznego, jest wejściowy, tylko do odczytu (READONLY), co jest obowiązkowe dla takiego rodzaju parametrów. Drugi parametr jest rzeczywisty i przekazuje wyniki do miejsca wywołania procedury. W ciele policzono sumę wypłat pracowników, których identyfikatory znajdują się w pierwszym parametrze, a ponieważ jest on tabelą, zastosowano w stosunku do niego operator listy IN. Drugie z pól zmiennej tabelarycznej nie zostało wykorzystane w ciele, ale mogłoby zostać użyte do wygenerowania kolejnego warunku. DROP Procedure SumaPrac GO DROP TYPE Zestaw GO CREATE TYPE Zestaw AS TABLE (Identyfikator int PRIMARY KEY, Wartosc real) GO CREATE Procedure SumaPrac @kto Zestaw READONLY, @suma real OUTPUT AS SELECT @suma = SUM(Brutto) FROM Zarobki WHERE IdOsoby IN (SELECT Identyfikator FROM @kto) GO
Rozdział 5. Rozszerzenia proceduralne Transact-SQL
301
W skrypcie, który testuje działanie procedury, zadeklarowano zmienną typu Zestaw, a następnie zasilono ją poleceniem INSERT danymi z tych rekordów z tabeli Osoby, które należą do pracowników działu o numerze 1. W celu sprawdzenia wyświetlono zawartość zmiennej @kto — tabela 5.35. Następnie poleceniem EXEC wywołano procedurę, przekazując parametr oraz wskazując na parametr wyjściowy, którego wartość wyświetlono — tabela 5.36. DECLARE @kto Zestaw DECLARE @Suma real INSERT INTO @kto SELECT IdOsoby, Wzrost FROM Osoby WHERE IdDzialu=1 SELECT * FROM @kto EXEC SumaPrac @kto, @suma OUTPUT SELECT @Suma
Tabela 5.35. Zestaw rekordów zawarty w zmiennej @kto Identyfikator
Wartosc
1
1,67
9
1,69
Tabela 5.36. Wartość zmiennej @Suma (No column name) 2969,25
W podanych przykładach widać znaczne podobieństwo zmiennej tabelarycznej do tabeli tymczasowej. Pamiętać jednak należy, że tabela tymczasowa żyje do końca sesji, w której ją utworzono. W trakcie trwania sesji tabela tymczasowa może się pojawić w wielu skryptach, podczas gdy zmienna jest obiektem żyjącym tylko tak długo, jak długo przetwarzany jest jeden skrypt. Dodatkowo tabela może być globalna, czyli może być widoczna z wielu sesji twórcy, natomiast zmienna nie może być dostępna z innych sesji. Dodatkowa różnica pojawia się podczas przetwarzania transakcyjnego. Aby to sprawdzić, utwórzmy tabelę tymczasową #T oraz zmienną tabelaryczną @T. Obydwa obiekty zawierają jedno pole znakowe. Do obu z nich wstawmy napisy Stara z dodatkiem # dla tabeli oraz @ dla zmiennej. Po rozpoczęciu transakcji dokonajmy modyfikacji polegającej na zmianie napisu na Nowa z zachowaniem symbolu dodatkowego. Jeśli transakcja zostanie wywołana, to w tabeli tymczasowej przywrócona zostanie stara wartość, natomiast zmienna będzie zawierać zmodyfikowany wpis. Zostało to sprawdzone zapytaniami wybierającymi, których skutek pokazuje rysunek 5.7. CREATE TABLE #T (s varchar(128)) DECLARE @T TABLE (s varchar(128)) INSERT INTO #T SELECT 'Stara #' INSERT INTO @T SELECT 'Stara @' BEGIN TRANSACTION UPDATE #T SET s='Nowa #' UPDATE @T SET s='Nowa @' ROLLBACK TRANSACTION SELECT * FROM #T SELECT * FROM @T
302
MS SQL Server. Zaawansowane metody programowania
Rysunek 5.7. Zawartość tabeli tymczasowej i zmiennej tabelarycznej po wycofaniu transakcji
Poza tym należy zauważyć, że typ tabelaryczny nie może być stosowany do definiowania pola tabeli. Jeśli spróbujemy zrealizować to zgodnie ze skryptem, wykorzystując utworzony uprzednio typ Sprawdzenie: CREATE TABLE spr (Id int IDENTITY PRIMARY KEY, dane Sprawdzenie)
to pojawi się komunikat o błędzie: Msg 350, Level 16, State 1, Line 1 The column "dane" does not have a valid data type. A column cannot be of a user-defined table type.
Innymi słowy, nie możemy zdefiniować tabel zagnieżdżonych nested table, co jest możliwe w środowisku ORACLE. Złożone typy użytkownika mogą być utworzone jako struktury języka wyższego rzędu, skompilowane do postaci bibliotek *.ddl, a następnie inkapsulowane do typów Transact-SQL, co przedstawiono w podrozdziale 7.4.
Rozdział 6.
Przetwarzanie transakcyjne 6.1. Transakcje. Podstawy teoretyczne Transakcję możemy zdefiniować jako przejściowy stan niestabilny między dwoma stanami stabilnymi. Takie określenie staje się czytelniejsze, jeśli za stan stabilny uznajemy taki, w którym wszystkie dane są trwale zapisane na nośniku fizycznym. Natomiast niestabilność wynika z faktu wprowadzenia modyfikacji, które jeszcze nie zostały utrwalone. Transakcja może zostać zatwierdzona — wtedy stan docelowy jest różny od wyjściowego i zawiera wszystkie wprowadzone zmiany — albo wycofana — w takim przypadku stan docelowy jest równoważny stanowi wyjściowemu, tak jakby nie wykonano żadnych zmian. Transakcyjność stanowi jeden z najważniejszych paradygmatów przetwarzania w bazach danych [1] [3] [7] [80] – [83]. Od transakcji żądamy, aby spełniała zasady: atomowości, spójności, izolacji i trwałości (ACID — Atomicity, Consistency, Isolation, Durability). Można je scharakteryzować następująco: atomowość oznacza, że zmiany dokonane w transakcji muszą wykonać się
w całości albo nie wykona się żadna z nich; jeżeli w trakcie transakcji dokonujemy sprzedaży kilku sztuk towaru i jednocześnie zmniejszamy stan magazynu o tę liczbę, to nie może dojść do sytuacji, w której wykonałaby się tylko jedna z tych operacji, co prowadziłoby do niezgodności stanu magazynu ze stanem rzeczywistym; spójność wymusza stan, w którym po wykonaniu transakcji system będzie spójny
i nie zostaną naruszone zasady integralności; izolacja powoduje, że dwie wykonujące się w tym samym czasie transakcje
nie mają dostępu (lub mają ograniczony dostęp — w zależności od poziomu izolacji) do zmienionych w konkurencyjnym procesie danych; poziom izolacji może być zmieniany programistycznie;
304
MS SQL Server. Zaawansowane metody programowania trwałość wymusza, że w przypadku awarii są dostępne i utrwalone wszystkie
zmiany danych z zatwierdzonych transakcji; jeśli transakcja do chwili pojawienia się awarii nie była zatwierdzona, dostępne są dane zapisane i te, które były dostępne przed rozpoczęciem tej transakcji. Poziom dostępu do danych z dwóch konkurujących, wykonywanych w tym samym czasie transakcji określa się mianem poziomu izolacji transakcji i ustawia się go za pomocą polecenia: SET TRANSACTION ISOLATION LEVEL poziom;
W SQL Server określono następujące poziomy, uszeregowane, począwszy od najmniej restrykcyjnego: READ UNCOMMITTED — najniższy poziom izolacji, transakcja może odczytywać
zmiany wprowadzone przez transakcje współbieżną przed ich zatwierdzeniem; mówimy wtedy o „brudnych” odczytach, ponieważ nie mamy pewności, czy zmiany nie zostaną wycofane; READ COMMITTED — (stan domyślny) mogą być odczytywane tylko zatwierdzone
dane, czyli tylko te, które pochodzą z zatwierdzonych transakcji; jeśli transakcja nie została zatwierdzona, dostępne są dane ze stanu poprzedzającego modyfikacje; ten poziom zabezpiecza przed „brudnymi” odczytami; REPEATABLE READ — transakcja może czytać tylko zatwierdzone w transakcji
współbieżnej dane i nie jest możliwe modyfikowanie odczytywanych danych; taki stan powoduje, że wykonywane w transakcji odczyty zawsze zwracają powtarzalny zestaw rekordów, dotyczy to jednak tylko modyfikacji UPDATE i kasowania DELETE, nie działa przy wstawianiu danych INSERT; SERIALIZABLE — pełna izolacja powodująca zakładanie blokad na wszystkie
modyfikowane i odczytywane wiersze; SNAPSHOT — migawka powoduje, że dla transakcji odczytywane dane są kopią
z chwili jej rozpoczęcia i kopia ta jest utrzymywana do jej końca; ten poziom izolacji jest wykorzystywany głównie przy przetwarzaniu analitycznym, kiedy wiele raportów musi być tworzonych z dokładnie takich samych danych, „zamrożonych” w ściśle określonej chwili, np. bilanse okresowe: miesięczne, roczne. Na sposób dostępu do danych mają również wpływ wskazówki HINTS dla silnika, dotyczące przetwarzania danych, na przykład opcja NOLOCK, którą możemy dodać do definicji tabeli w zapytaniu wybierającym, która powoduje, że w ramach tego zapytania ze wskazanej tabeli mogą być odczytywane niezatwierdzone dane, bez względu na inne ustawienia dotyczące bazy danych czy też transakcji. Definicja tej wskazówki pojawia się w zapytaniu w nawiasach, po nazwie tabeli, i może dotyczyć zapytań SELECT, UPDATE, DELETE, INSERT, MERGE. W przykładach ograniczono się do wybierania danych. SELECT Nazwa FROM Dzialy (NOLOCK)
Rozdział 6. Przetwarzanie transakcyjne
305
W przypadku opcji wymagających dodatkowych parametrów konieczne jest używanie słowa kluczowego WITH, które dla pozostałych nie jest obowiązkowe, ale jest dopuszczalne. SELECT Nazwa FROM Dzialy WITH (NOLOCK)
Jeśli źródło zawiera więcej tabel, np. połączonych operatorem złączenia JOIN, opcje mogą być użyte tylko do wybranych tabel. W przykładzie zastosowano opcje do jednej spośród dwóch tabel. SELECT Nazwa, Nazwisko FROM Dzialy (NOLOCK) JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu
Jest oczywiste, że możemy podać opcje dla wszystkich tabel źródłowych. SELECT Nazwa, Nazwisko FROM Dzialy (NOLOCK) JOIN Osoby WITH (NOLOCK) ON Dzialy.IdDzialu=Osoby.IdDzialu
Nie jest konieczne, aby były one takie same dla każdej tabeli. W przykładzie tabela Dzialy ma zdjęte blokady, a z tabeli Osoby będą odczytywane tylko zatwierdzone dane. SELECT Nazwa, Nazwisko FROM Dzialy (NOLOCK) JOIN Osoby WITH (READCOMMITTED) ON Dzialy.IdDzialu=Osoby.IdDzialu
Lista dostępnych opcji dotyczy nie tylko blokad i definiuje następujące właściwości: NOEXPAND — powoduje, że perspektywa będąca źródłem danych jest traktowana jak tabela z indeksem grupującym CLUSTERED, optymalizator nie odwołuje się
do tabel ją definiujących; INDEX (identyfikator [,... n ]) lub INDEX = (identyfikator) — specyfikuje,
które indeksy mają być brane pod uwagę przez optymalizator zapytań; KEEPIDENTITY — jest stosowana tylko podczas masowego wstawiania wierszy BULK INSERT z opcją OPENROWSET i określa, że dane z opcją IDENTITY z importowanych
danych mają taką samą cechę w danych docelowych; jeśli ta opcja nie jest ustawiona, dane te są wykorzystywane tylko do identyfikacji wierszy źródła, ale nie są importowane. KEEPDEFAULTS — jest stosowana tylko podczas masowego wstawiania wierszy BULK INSERT z opcją OPENROWSET i określa, że dla pól docelowych z ustaloną wartością domyślną w przypadku występowania wartości NULL w danych
źródłowych ta wartość domyślna zostanie użyta; FORCESEEK [(indeks(kolumna_indeksu[,... n ]))] — określa, że optymalizator
używa tylko operacji pomijanych wartości indeksu jako sposobu dostępu do danych; FORCESCAN — określa, że optymalizator używa tylko skanowania indeksów jako
sposobu dostępu do danych;
306
MS SQL Server. Zaawansowane metody programowania HOLDLOCK — jest równoważny SERIALIZABLE; IGNORE_CONSTRAINTS — jest stosowana tylko podczas masowego wstawiania wierszy BULK INSERT z opcją OPENROWSET i określa, że podczas tej operacji są ignorowane ograniczenia; nie dotyczy to UNIQUE, PRIMARY KEY oraz NOT NULL; IGNORE_TRIGGERS — jest stosowana tylko podczas masowego wstawiania wierszy BULK INSERT z opcją OPENROWSET i określa, że podczas tej operacji są ignorowane procedury wyzwalane, utworzone dla polecenia INSERT; NOLOCK — jest równoważna READUNCOMMITTED; NOWAIT — wskazuje, że silnik bazy danych przesyła informacje na standardowe
wyjście, gdy tylko napotka blokadę na tabeli; jest równoważna ustawieniu właściwości SET LOCK_TIMEOUT 0 dla tej tabeli; PAGLOCK — utrzymuje blokadę na stronę danych, podczas gdy zwykle blokady
dotyczą tylko wierszy albo kluczy; READCOMMITTED — wskazuje, że odczyty danych są zgodne z regułami poziomu izolacji transakcji READ COMMITTED; READCOMMITTEDLOCK — wskazuje, że odczyty danych są zgodne z regułami poziomu izolacji transakcji READ COMMITTED przez założenie właściwych blokad; READPAST — wskazuje, że silnik bazy danych nie odczytuje wierszy zablokowanych
przez inne transakcje; może być stosowana tylko dla transakcji pracujących w trybie READ COMMITTED lub REPEATABLE READ; nie może być stosowana, kiedy baza danych ma ustawioną właściwość READ_COMMITTED_SNAPSHOT (rysunek 6.1); READUNCOMMITTED — pozwala na odczyt niezatwierdzonych danych; nie są zakładane żadne blokady, nie ma zastosowania w przypadku UPDATE i DELETE; REPEATABLEREAD — określa, że przetwarzanie jest wykonywane tak jak przy zastosowaniu poziomu izolacji REPEATABLE READ; ROWLOCK — specyfikuje, że blokada dotyczy wiersza, gdy zwykle jest stosowana
do strony lub całej tabeli; SPATIAL_WINDOW_MAX_CELLS = liczba_całkowita — określa maksymalną liczbę
elementów, na które może być podzielony obiekt opisujący geometrię wektorową Spatial; może przyjąć wartość od 1 do 8192; SERIALIZABLE — równoważna HOLDLOCK, ustawia blokady do czasu zakończenia
transakcji, nie zwalniając ich jak w innych przypadkach po zakończeniu dostępu do tabeli czy strony; TABLOCK — wskazuje, że blokady są zakładane dla całej tabeli; dla wstawiania danych INSERT INTO … SELECT optymalizowany jest proces blokowania rekordów; przy zastosowaniu OPENROWSET dopuszczalny jest równoczesny dostęp do danych
dla różnych klientów; TABLOCKX — określa ustawienie wyłącznej blokady dla tabeli;
Rozdział 6. Przetwarzanie transakcyjne
307
Rysunek 6.1. Ustawienie opcji READ_COMMITTED_SNAPSHOT za pomocą narzędzi wizualnych UPDLOCK — określa, że ustawiane są blokady dla modyfikacji na poziomie wierszy lub stron danych; dla tej opcji poziomy izolacji transakcji READCOMMITTED i READCOMMITTEDLOCK są ignorowane; XLOCK — określa, że wyłączne blokady są utrzymywane do końca trwania
transakcji; można sprecyzować zakres ustawianych blokad, stosując opcje ROWLOCK, PAGLOCK lub TABLOCK.
Ustawienia opcji READ_COMMITTED_SNAPSHOT można dokonać za pomocą narzędzi wizualnych, wybierając dla bazy danych pozycję FACETS i zmieniając wartość dla odpowiedniej pozycji — rysunek 6.1.
6.2. Transakcje. Przykłady realizacji W MS SQL Server transakcję definiuje się w sposób jawny, wskazując na jej rozpoczęcie za pomocą polecenia BEGIN TRANSACTION, po którym następuje seria poleceń. Transakcja jest zatwierdzana za pomocą polecenia COMMIT TRAN, a wycofywana przez ROLLBACK TRAN. W przedstawionym przykładzie po zadeklarowaniu zmiennej pomocniczej @nr odpytywana jest tabela Osoby. Do zmiennej @nr przypisywana jest wartość funkcji @@ROWCOUNT, która zwraca liczbę wierszy przetworzonych za pomocą bezpośrednio poprzedzającego ją zapytania. Następnie wykonywana jest modyfikacja zawartości tabeli polegająca na przepisaniu pola Nazwisko na postać zawierającą tylko duże litery. Występująca w instrukcji warunkowej funkcja @@ROWCOUNT tym razem zwróci liczbę modyfikowanych wierszy. Jeśli porównanie wartości zmiennej i funkcji jest pozytywne, transakcja jest zatwierdzana i zmodyfikowane wartości są trwale zapisywane w tabeli. Jeśli wyrażenie jest nieprawdziwe, to liczba otwartych transakcji okre-
308
MS SQL Server. Zaawansowane metody programowania
ślona przez wartość funkcji @@TRANCOUNT jest większa od zera i transakcja jest wycofywana, co potwierdza wyświetlany komunikat. Innymi słowy, modyfikacje zostaną zapisane tylko wtedy, gdy zostaną wykonane na wszystkich wierszach tabeli. BEGIN TRANSACTION DECLARE @nr int SELECT Nazwisko FROM Osoby SET @nr=@@ROWCOUNT PRINT @nr UPDATE Osoby SET Nazwisko = UPPER(Nazwisko) IF @@ROWCOUNT = @nr COMMIT TRAN IF @@TRANCOUNT > 0 BEGIN PRINT 'Transakcja musi być cofnięta' ROLLBACK TRAN END
Prawdopodobieństwo, że przy tak małym wolumenie cokolwiek wydarzy się pomiędzy odpytaniem tabeli a jej modyfikacją, jest znikome. Ponadto warto rozważyć zastąpienie zapytania wybierającego wszystkie wiersze i sprawdzania ich liczby przez zapytanie zliczające wykorzystujące funkcję COUNT. Omówmy teraz sytuację, w której w transakcji sprawdzono wypłaty osób o identyfikatorach 2 i 3 (tabela 6.1). Następnie zmodyfikowano wiersze tak, aby wszystkie wypłaty pierwszego z pracowników zostały powiększone o 10 procent, a później zmniejszono wypłaty drugiego z nich o taki sam procent. Stan wypłat pracownika o identyfikatorze 3 w obrębie transakcji zawiera tabela 6.2. BEGIN TRANSACTION podwyzka SELECT IdOsoby, Brutto FROM Zarobki WHERE IdOsoby IN (2, 3) UPDATE Zarobki SET Brutto = Brutto*1.1 WHERE IdOsoby = 2 SAVE TRANSACTION podwyzka UPDATE Zarobki SET brutto = brutto*0.9 WHERE idosoby = 3 SELECT IdOsoby, Brutto FROM Zarobki WHERE IdOsoby= 3 ROLLBACK TRANSACTION podwyzka COMMIT TRANSACTION SELECT IdOsoby, Brutto FROM Zarobki WHERE IdOsoby IN (2, 3)
Tabela 6.1. Początkowe wynagrodzenia pracowników o identyfikatorach 2 i 3 IdOsoby
Brutto
3
222.00
2
444.00
2
888.00
3
444.00
Tabela 6.2. Wynagrodzenia pracownika o identyfikatorze 3 w niezatwierdzonej transakcji IdOsoby
Brutto
3
199.80
3
399.60
Rozdział 6. Przetwarzanie transakcyjne
309
Jeśli teraz wykonamy proste polecenie wycofujące transakcje, wszystkie modyfikacje nie zostaną utrwalone i zostanie przywrócony stan wyjściowy. Jednakże istnieje możliwość wyznaczenia punktu zachowania transakcji SAVE TRANSACTION nazwa i wskazanie go podczas wycofywania transakcji ROLLBACK TRANSACTION nazwa. Odwołaniu podlegają wtedy tylko zmiany pomiędzy tymi poleceniami. Jednak modyfikacje poprzedzające punkt zachowania nie są ani zatwierdzone, ani wycofane. Dlatego w zaprezentowanym skrypcie pojawia się polecenie COMMIT TRANSACTION zatwierdzające pierwszą modyfikację. Na koniec wykonywane jest zapytanie wybierające, pokazujące zatwierdzone zmiany (tabela 6.3). Transakcję z punktem zachowania możemy traktować jako dwie zagnieżdżone transakcje, z których zewnętrzna jest zatwierdzana, a wewnętrzna wycofywana. Tabela 6.3. Końcowe wynagrodzenia pracowników o identyfikatorach 2 i 3 IdOsoby
Brutto
3
222.00
2
448.40
2
976.80
3
444.00
Oczywiście możemy jawnie definiować transakcje zagnieżdżone. W tym celu utwórzmy pomocniczą tabelę TestTran o dwóch polach, numerycznym oraz tekstowym. Po rozpoczęciu transakcji o nazwie OuterTran wyświetlana jest wartość funkcji @@TRANCOUNT, która zwraca liczbę transakcji otwartych w bieżącej sesji, a następnie wstawiany jest jeden wiersz do tabeli, która została utworzona na początku. Otwierana jest kolejna transakcja, Inner1, i akcja się powtarza. Jako trzecia otwierana jest transakcja Inner2, w której wykonywane są takie same operacje. Na zakończenie transakcje są zatwierdzane w kolejności odwrotnej do otwierania. Po każdym zatwierdzeniu wyświetlana jest informacja o liczbie otwartych transakcji. Całość uzupełnia sprawdzenie końcowej zawartości tabeli, a następnie jej usunięcie. CREATE TABLE TestTran (Cola INT PRIMARY KEY, Colb CHAR(3)) PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO BEGIN TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (1, 'aaa') GO BEGIN TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (2, 'bbb') GO BEGIN TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (3, 'ccc') GO COMMIT TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION OuterTran
310
MS SQL Server. Zaawansowane metody programowania PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO SELECT * FROM TestTran DROP TABLE TestTran
Wykonanie skryptu powoduje wygenerowanie serii komunikatów, w których wyświetlana jest liczba otwartych transakcji od 0 do 3 i z powrotem do 0. Po powiększeniu liczby transakcji pokazywane są komunikaty potwierdzające wstawienie wiersza. Końcowy komunikat wskazuje na wybranie trzech wierszy, co potwierdza zawartość tabeli 6.4. Transakcja 0 Transakcja 1 (1 row(s) affected) Transakcja 2 (1 row(s) affected) Transakcja 3 (1 row(s) affected) Transakcja 2 Transakcja 1 Transakcja 0 (3 row(s) affected)
Tabela 6.4. Zawartość tabeli TestTran po zatwierdzeniu transakcji Cola
Colb
1
aaa
2
bbb
3
ccc
Dokonajmy teraz drobnej modyfikacji skryptu, która polega na zastąpieniu zatwierdzenia COMMIT ostatniej transakcji jej wycofaniem ROLLBACK. CREATE TABLE TestTran (Cola INT PRIMARY KEY, Colb CHAR(3)) PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO BEGIN TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (1, 'aaa') GO BEGIN TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (2, 'bbb') GO BEGIN TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (3, 'ccc') GO COMMIT TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) ROLBACK TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO SELECT * FROM TestTran DROP TABLE TestTran
Rozdział 6. Przetwarzanie transakcyjne
311
Różnica w komunikatach sprowadza się do potwierdzenia, że wyświetlonych zostanie 0 wierszy. Tabela jest pusta, co widać w tabeli 6.5. Oznacza to, że wycofanie najbardziej zewnętrznej transakcji powoduje wycofanie wszystkich transakcji w niej zagnieżdżonych (wewnętrznych). Żadna zmiana nie została zatwierdzona. Transakcja 0 Transakcja 1 (1 row(s) affected) Transakcja 2 (1 row(s) affected) Transakcja 3 (1 row(s) affected) Transakcja 2 Transakcja 1 Transakcja 0 (0 row(s) affected)
Tabela 6.5. Zawartość tabeli TestTran po wycofaniu najbardziej zewnętrznej transakcji Cola
Colb
Dokonajmy drobnej modyfikacji kodu w ten sposób, że tym razem najbardziej wewnętrzna transakcja będzie wycofywana, natomiast pozostałe będą zatwierdzane. CREATE TABLE TestTran (Cola INT PRIMARY KEY, Colb CHAR(3)) PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO BEGIN TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (1, 'aaa') GO BEGIN TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (2, 'bbb') GO BEGIN TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (3, 'ccc') GO ROLBACK TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO SELECT * FROM TestTran DROP TABLE TestTran
Pozornie zgodnie z zawartością tabeli 6.6 wszystkie wiersze zostały zapisane. Jednak jeśli przeanalizujemy wyświetlane komunikaty, znajdziemy między nimi informację o błędzie przetwarzania. Polega on na tym, że dla wycofywanej transakcji nie został zdefiniowany punkt zachowania i silnik bazy danych nie wie, jaki zakres modyfikacji ma zostać cofnięty. Oznacza to, że jeśli wycofywana jest transakcja zewnętrzna, domyślnie wycofywane są wszystkie modyfikacje; niejawnie punkt wycofania jest miejscem rozpoczęcia transakcji. Jeśli wycofujemy transakcję zagnieżdżoną, punkt wycofy-
312
MS SQL Server. Zaawansowane metody programowania
wania musi być zdefiniowany jawnie. Czytelnik może sprawdzić ten fakt, wycofując środkową transakcję. Skutek będzie analogiczny. Dodatkowo widzimy, iż na koniec skryptu liczba otwartych transakcji nie wraca do zera. Oznacza to, że modyfikacje są w stanie zawieszenia — nie są ani zatwierdzone, ani wycofane. Czyli wiersze zwrócone przez zapytanie wybierające nie są utrwalone fizycznie i mają sens tylko do końca transakcji lub sesji. Możemy się o tym przekonać, usuwając ze skryptu polecenie usuwające tabelę, a następnie zamykając sesję i odpytując ją o zawartość tabeli. Transakcja 0 Transakcja 1 (1 row(s) affected) Transakcja 2 (1 row(s) affected) Transakcja 3 (1 row(s) affected) Msg 6401, Level 16, State 1, Line 1 Cannot roll back Inner2. No transaction or savepoint of that name was found. Transakcja 3 Transakcja 2 Transakcja 1 (3 row(s) affected)
Tabela 6.6. Zawartość tabeli TestTran po wycofaniu najbardziej wewnętrznej transakcji Cola
Colb
1
aaa
2
bbb
3
ccc
Wielokrotne wykonanie poprzedniego skryptu spowoduje, że liczba „wiszących” transakcji po każdym wykonaniu będzie wzrastała o jeden. Dodajmy do skryptu definicję punktu zachowania, którą umieścimy zgodnie z logiką tuż po rozpoczęciu wycofywanej, najbardziej wewnętrznej transakcji. CREATE TABLE TestTran (Cola INT PRIMARY KEY, Colb CHAR(3)) PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO BEGIN TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (1, 'aaa') GO BEGIN TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (2, 'bbb') GO BEGIN TRANSACTION Inner2 SAVE TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (3, 'ccc') GO ROLBACK TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2))
Rozdział 6. Przetwarzanie transakcyjne
313
GO SELECT * FROM TestTran DROP TABLE TestTran
Tym razem nie otrzymamy komunikatu o błędzie, co oznacza, że wykonanie skryptu zakończyło się powodzeniem. Jednak liczba otwartych transakcji ponownie wzrosła o jeden. Oznacza to, że jakiś fragment transakcji nie jest jeszcze zatwierdzony ani wycofany. W analizowanym przypadku jest to fragment skryptu pomiędzy BEGIN TRANSACTION Inner2 a SAVE TRANSACTION Inner2. Pomimo że nie zawiera on żadnych modyfikacji, a ściśle mówiąc, jest pusty, to i tak został potraktowany jako oddzielna transakcja. Dlatego, podobnie jak poprzednio, musimy sceptycznie odnosić się do zawartości tabeli 6.7. Chociaż w tym przypadku wyświetlane rekordy są faktycznie zapisane, co można sprawdzić, usuwając polecenia kasujące tabelę, zamykając sesję, a po jej ponownym otwarciu sprawdzając zawartość tabeli. Transakcja 1 Transakcja 2 (1 row(s) affected) Transakcja 3 (1 row(s) affected) Transakcja 4 (1 row(s) affected) Transakcja 4 Transakcja 3 Transakcja 2 (2 row(s) affected)
Tabela 6.7. Zawartość tabeli TestTran po wycofaniu najbardziej wewnętrznej transakcji i ustaleniu punktu wycofywania na jej początku Cola
Colb
1
aaa
2
bbb
Określenie miejsca wycofywania transakcji nie jest ściśle zdeterminowane miejscem jej rozpoczęcia. Oczywiście może być ono umieszczone w środku po wykonaniu modyfikacji, ale również, co nie jest już takie oczywiste, przed jej rozpoczęciem. Możemy to sprawdzić, przenosząc polecenie SAVE TRANSACTION Inner2 w miejsce bezpośrednio po otwarciu najbardziej zewnętrznej transakcji. CREATE TABLE TestTran (Cola INT PRIMARY KEY, Colb CHAR(3)) PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO BEGIN TRANSACTION OuterTran SAVE TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (1, 'aaa') GO BEGIN TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (2, 'bbb') GO BEGIN TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (3, 'ccc')
314
MS SQL Server. Zaawansowane metody programowania GO ROLBACK TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO SELECT * FROM TestTran DROP TABLE TestTran
Jak widać, w serii komunikatów nie występuje informacja o błędzie, a zawartość tabeli 6.8 jest pusta. W dalszym ciągu liczba niezatwierdzonych transakcji rośnie. Transakcja 2 Transakcja 3 (1 row(s) affected) Transakcja 4 (1 row(s) affected) Transakcja 5 (1 row(s) affected) Transakcja 5 Transakcja 4 Transakcja 3 (0 row(s) affected)
Tabela 6.8. Zawartość tabeli TestTran po wycofaniu najbardziej wewnętrznej transakcji i ustaleniu punktu wycofywania na początku skryptu, bezpośrednio po otwarciu zewnętrznej transakcji Cola
Colb
Pozostaje rozwiązanie problemu „wiszących” transakcji. Możemy przypuszczać, że powinny być one zatwierdzone, a nie wycofane, ponieważ są to np. puste fragmenty transakcji, modyfikacje, które wykonano przed punktem wycofania. Dlatego na końcu skryptu została dodana pętla, która do czasu istnienia chociaż jednej niezamkniętej transakcji wykonuje polecenie COMMIT. Ponieważ po tym poleceniu nie wymieniono nazwy, transakcje będą zatwierdzane, począwszy od tej, która została najpóźniej otwarta — transakcje są odkładane na stos, co wymusza takie działanie. CREATE TABLE TestTran (Cola INT PRIMARY KEY, Colb CHAR(3)) PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO BEGIN TRANSACTION OuterTran SAVE TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (1, 'aaa') GO BEGIN TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (2, 'bbb') GO BEGIN TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (3, 'ccc') GO ROLBACK TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION Inner1
Rozdział 6. Przetwarzanie transakcyjne
315
PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO SELECT * FROM TestTran DROP TABLE TestTran WHILE @@TRANCOUNT >0 COMMIT TRAN
Rozważmy teraz, co się stanie, jeśli w obrębie transakcji umieścimy również tworzenie tabeli. Polecenie CREATE TABLE zostało przeniesione i stanowi teraz pierwsze polecenie najbardziej zewnętrznej transakcji. Transakcje wewnętrzne są zatwierdzane, natomiast transakcja zewnętrzna jest wycofywana, co powinno spowodować wycofanie wszystkich zmian. PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) BEGIN TRANSACTION OuterTran CREATE TABLE TestTran (Cola INT PRIMARY KEY, Colb CHAR(3)) GO PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (1, 'aaa') GO BEGIN TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (2, 'bbb') GO BEGIN TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) INSERT INTO TestTran VALUES (3, 'ccc') GO COMMIT TRANSACTION Inner2 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) COMMIT TRANSACTION Inner1 PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) ROLLBACK TRANSACTION OuterTran PRINT 'Transakcja '+ CAST(@@TRANCOUNT AS varchar(2)) GO SELECT * FROM TestTran DROP TABLE TestTran WHILE @@TRANCOUNT >0 COMMIT TRAN
Analizując komunikaty, widzimy, że ostatni wyświetla informację o błędzie. Odwołujemy się do nieistniejącej tabeli w poleceniu DROP TABLE. Oznacza to, że polecenie ROLLBACK wycofało również tworzenie tabeli. Transakcja 0 Transakcja 1 (1 row(s) affected) Transakcja 2 (1 row(s) affected) Transakcja 3 (1 row(s) affected) Transakcja 2 Transakcja 1 Transakcja 0 Msg 208, Level 16, State 1, Line 1 Invalid object name 'TestTran'.
316
MS SQL Server. Zaawansowane metody programowania
Takie zjawisko jest charakterystyczne dla MS SQL Server. W większości serwerów baz danych wycofaniu podlegają tylko modyfikacje danych, proces ten nie dotyczy zmian schematu relacyjnego. W serwerze Microsoftu wycofaniu podlegają wszystkie rodzaje modyfikacji. Uważny Czytelnik może to sprawdzić również dla poleceń typu ALTER i DROP, także dotyczących obiektów innych niż tabele, np. perspektyw.
6.3. Obsługa wyjątków Temat tego podrozdziału nie wynika bezpośrednio z przetwarzania transakcyjnego, ale ma o wiele szersze zastosowanie. Zastosowanie to jednak wiąże się najczęściej z koniecznością wycofania zmian, stąd jego lokalizacja w tym miejscu. Wyjątkiem będziemy nazywali stan, w którym pojawia się błąd w przetwarzaniu, który bez żadnych dodatkowych działań pociągałby za sobą przerwanie wykonywania skryptu lub polecenia oraz wygenerowanie komunikatu o błędzie [1] [22] [84] – [87]. Wyjątek wystąpi również w sytuacji, która z dowolnych przyczyn zostanie zdefiniowana przez programistę jako błędna, wyjątkowa. Najczęściej wyjątek jest utożsamiany z błędem, ale być nim nie musi. Pojawienie się wyjątku zazwyczaj spowoduje wycofanie transakcji, w której on wystąpił. Do wersji MS SQL Server 2005 nie było metod przechwytywania błędów; od wersji 2008 dokonujemy tego w sekcji BEGIN TRY, której istnienie wymusza zdefiniowanie sekcji BEGIN CATCH, w której następuje akcja będąca skutkiem pojawienia się sytuacji wyjątkowej w dowolnej linii bloku TRY. Jeśli zdefiniowano sekcję TRY, to brak odpowiedniej sekcji CATCH powoduje wystąpienie błędu składniowego o postaci: Msg 102, Level 15, State 1, Line 3 Incorrect syntax near 'TRY'.
Bardzo często występującym błędem przetwarzania jest dzielenie przez zero. W przykładowym skrypcie zostało ono zdefiniowane jawnie i umieszczone w sekcji przechwytywania wyjątków. W sekcji obsługi wyjątków wyświetlone zostały informacje dotyczące tego błędu. BEGIN TRY SELECT 1/0; END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS NumerBledu, ERROR_SEVERITY() AS SeverityBledu, ERROR_STATE() AS StateBledu, ERROR_PROCEDURE() AS ProceduraBledu, ERROR_LINE() AS LiniaBledu, ERROR_MESSAGE() AS KomunikatBledu; END CATCH;
Rezultat wykonania skryptu jest zawarty w tabeli 6.9. Należy podkreślić, że ze względu na obsłużenie wyjątku przetwarzanie nie zostało przerwane, a tylko pojawił się skutek akcji zdefiniowanej w sekcji obsługi. Ma to istotne znaczenie w przypadku końcówek klienta wykonujących kod T-SQL, ponieważ w razie błędu przetwarzania zostanie też przerwane działanie aplikacji, o ile ona również nie posiada własnego mechanizmu przechwytywania wyjątków.
Rozdział 6. Przetwarzanie transakcyjne
317
Tabela 6.9. Informacje o błędzie wyświetlane w sekcji CATCH NumerBłędu
SeverityBłędu
StateBłędu
ProceduraBłędu
LiniaBłędu
KomunikatBłędu
8134
16
1
NULL
2
Divide by zero error encountered.
Kolejną przyczyną błędu może być próba odwołania do nieistniejącego obiektu, np. tabeli. Taki przypadek zawiera kolejny skrypt, gdzie w sekcji TRY próbujemy wyciągnąć dane z tabeli NieIstniejaca. W sekcji obsługi ograniczono się tym razem tylko do wyświetlenia numeru błędu i komunikatu o nim. BEGIN TRY SELECT * FROM NieIstniejaca; END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS ErrorMessage; END CATCH
Niestety, w tym przypadku błąd nie został przechwycony, przetwarzanie zostało przerwane i pojawił się komunikat: Msg 208, Level 16, State 1, Line 4 Invalid object name 'NieIstniejaca'.
Spróbujmy tym razem odwołać się do nieistniejącej tabeli w ciele procedury BladPrac. DROP PROCEDURE BladProc; GO CREATE PROCEDURE BladProc AS SELECT * FROM NieIstniejaca; GO
Jeśli wywołamy ją bezpośrednio, bez obsługi wyjątków: EXEC BladProc;
zakończy się to przerwaniem przetwarzania i takim samym jak w poprzednim przypadku komunikatem o postaci: Msg 208, Level 16, State 1, Procedure BladProc, Line 3 Invalid object name 'NieIstniejaca'.
Zrealizujmy teraz wywołanie tej procedury w sekcji TRY. BEGIN TRY EXECUTE BladProc END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS ErrorMessage; END CATCH;
Tym razem wyjątek będzie przechwycony i zostanie wykonane ciało bloku CATCH, czego skutek pokazuje tabela 6.10.
318
MS SQL Server. Zaawansowane metody programowania
Tabela 6.10. Informacje o błędzie wyświetlane w sekcji CATCH ErrorNumber
ErrorMessage
208
Invalid object name 'NieIstniejaca'.
Jak widać z doświadczenia, przechwytywanie wyjątków w bloku anonimowym nie zawsze jest skuteczne. Taką skuteczność gwarantuje nam umieszczenie przetwarzanego kodu w ciele procedury lub funkcji. Spróbujmy przeanalizować inne często spotykane przypadki błędów, dodatkowo uwzględniając rolę transakcji podczas takiego przetwarzania. W kolejnym przykładzie rozpoczniemy od otwarcia transakcji, natomiast w sekcji TRY próbujemy usunąć z tabeli Osoby rekord opisujący pracownika o identyfikatorze 2. Należy przypomnieć, że w tabeli Zarobki został utworzony na kolumnie IdOsoby klucz obcy o domyślnym działaniu NO ACTION w przypadku usuwania lub modyfikacji danych w tabeli nadrzędnej. Ponieważ usuwany pracownik ma przynajmniej jedną wypłatę, prowadzi to do błędu przetwarzania. W sekcji CATCH zastosowano wyświetlenie informacji o błędzie. BEGIN TRANSACTION BEGIN TRY -- Osoba o IdOsoby =2 ma zarobki DELETE FROM Osoby WHERE IdOsoby=2 END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_SEVERITY() AS ErrorSeverity, ERROR_STATE() as ErrorState, ERROR_PROCEDURE() as ErrorProcedure, ERROR_LINE() as ErrorLine, ERROR_MESSAGE() as ErrorMessage IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION END CATCH IF @@TRANCOUNT > 0 COMMIT TRANSACTION
Jeśli przeszliśmy do sekcji przechwytywania wyjątków, to transakcja nie została zatwierdzona. Ponieważ chcemy, aby całość kodu wykonała się z powodzeniem albo wcale, to po sprawdzeniu, czy rzeczywiście transakcja jest jeszcze otwarta, zostaje ona wycofana. Jeśli cały blok TRY wykonał się z powodzeniem, to transakcja jest również otwarta, ale tym razem wymaga zatwierdzenia. Skutek działania skryptu zawarty jest w tabeli 6.11. Uważny Czytelnik może sprawdzić, jak zachowa się kod w przypadku usuwania osoby, która nie miała żadnych wypłat. Tabela 6.11. Informacje o błędzie wyświetlane w sekcji CATCH Error Number
547
Error Severity
16
Error State
0
Error Procedure
NULL
Error Line
ErrorMessage
5
The DELETE statement conflicted with the REFERENCE constraint "FK_Zarobki_Osoby". The conflict occurred in database "BazaRelacyjna", table "dbo.Zarobki", column 'IdOsoby'.
Rozdział 6. Przetwarzanie transakcyjne
319
Kolejną typową sytuacją jest niepoprawna modyfikacja obiektu. W przykładzie jest to próba usunięcia nieistniejącej kolumny kod z tabeli Dzialy. Pozostała część kodu nie uległa zmianie. BEGIN TRANSACTION BEGIN TRY -- Generowany jest błąd z powodu nieistnienia usuwanej kolumny. ALTER TABLE dzialy DROP COLUMN kod; -- Jeśli polecenie powiedzie się, zatwierdź transakcję. COMMIT TRANSACTION; END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_SEVERITY() AS ErrorSeverity, ERROR_STATE() as ErrorState, ERROR_PROCEDURE() as ErrorProcedure, ERROR_LINE() as ErrorLine, ERROR_MESSAGE() as ErrorMessage IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION END CATCH IF @@TRANCOUNT > 0 COMMIT TRANSACTION
Podobnie jak poprzednio, przechwytywany jest wyjątek, a sekcja obsługi zwraca informacje o błędzie. Czytelnik może przeprowadzić eksperymenty, zamieniając usuwanie kolumny na inne akcje, np. dodanie kolumny o nieunikalnej nazwie, usunięcie kolumny z ograniczeniem integralnościowym, etc. W każdym przypadku otrzymamy podobny rezultat, różniący się tylko danymi opisującymi błąd. Zamiast funkcji @@TRANCOUNT, do sprawdzenia istnienia otwartych transakcji możemy zastosować funkcję XACT_STATE(), która niesie nieco większą liczbę informacji, co pokazuje tabela 6.12. Tabela 6.12. Informacja zwracana przez funkcję XACT_STATE() Wartość
Opis
1
W danej chwili jest aktywna transakcja, możliwe jest wykonanie dowolnej akcji, w tym dowolnej modyfikacji oraz zatwierdzenia transakcji.
0
W danej chwili nie jest aktywna żadna transakcja.
-1
W danej chwili jest aktywna transakcja, lecz pojawił się błąd przetwarzania, co spowodowało, że transakcja jest niezatwierdzalna (UNCOMMITTABLE), nie można jej zatwierdzić COMMIT TRANSACTION ani wycofać do punktu wycofywania SAVE TRANSACTION; możliwe jest tylko pełne wycofanie ROLLBACK, możliwe jest wykonywanie poleceń związanych z odczytem danych, dopiero po pełnym wycofaniu możliwe jest wykonywanie modyfikacji. Przy przetwarzaniu wsadowym po jego zakończeniu wszystkie transakcje UNCOMMITTABLE są automatycznie wycofywane, a do klienta jest przesyłany komunikat.
Przeanalizujmy zastosowanie funkcji XACT_STATE() na przykładzie usuwania nieistniejącej kolumny, jak poprzednio. Tym razem w sekcji obsługi wyjątków zamiast sprawdzenia liczby otwartych transakcji sprawdzimy jej stan. W pierwszej instrukcji warunkowej sprawdzamy, czy wartość XACT_STATE() jest równa –1. W takim stanie transakcja nie może być zatwierdzona i musi zostać wycofana za pomocą polecenia
320
MS SQL Server. Zaawansowane metody programowania ROLBACK TRANSACTION. Dla wartości 1, co sprawdza druga z instrukcji IF, transakcja zostaje zatwierdzona. Oczywiście, gdy funkcja zwróci 0, nie będzie możliwe wykonanie
ani zatwierdzenia, ani wycofania.
BEGIN TRANSACTION BEGIN TRY -- Generowany jest błąd z powodu nieistnienia usuwanej kolumny. ALTER TABLE dzialy DROP COLUMN kod -- Jeśli polecenie powiedzie się, zatwierdź transakcję. COMMIT TRANSACTION END TRY BEGIN CATCH SELECT ERROR_NUMBER() as ErrorNumber, ERROR_MESSAGE() as ErrorMessage -- XACT_STATE = 0 nie jest otwarta żadna transakcja; COMMIT lub ROLLBACK będą generowały błąd. -- sprawdź XACT_STATE 1 lub -1. -- sprawdź, czy transakcja jest niezatwierdzalna (uncommittable). IF (XACT_STATE()) = -1 BEGIN PRINT N'Transakcja jest w stanie uncommittable. Wycofywanie transakcji.' ROLLBACK TRANSACTION END BEGIN -- sprawdź, czy transakcja jest aktywna i w stanie valid (dostępna). IF (XACT_STATE()) = 1 BEGIN PRINT 'Transakcja jest w stanie committable. Zatwierdzenie transakcji.' COMMIT TRANSACTION END END CATCH
Skutkiem wykonania skryptu jest komunikat: Transakcja jest w stanie uncommittable. Wycofywanie transakcji.
oraz informacja o błędzie zawarta w tabeli 6.13. Tabela 6.13. Informacje o błędzie wyświetlane w sekcji CATCH ErrorNumber
ErrorMessage
4924
ALTER TABLE DROP COLUMN failed because column 'kod' does not exist in table 'Dzialy'.
Pozostaje jeszcze do sprawdzenia sytuacja, w której użytkownik powoduje ustanowienie błędu. W praktyce dzieje się tak po spełnieniu pewnego warunku np. stan magazynu jest mniejszy niż limit, próba zmiany stanu konta poniżej sumy debetowej, etc. Takie warunki zależą od rozwiązywanych zadań biznesowych i może ich być wiele. W prezentowanym skrypcie błąd wywołano bezwarunkowo w sekcji TRY przez zastosowanie polecenia RAISERROR. W bloku CATCH powtórzono tę funkcję, której parametrami są informacje o błędzie pozyskane z odpowiednich funkcji systemowych. BEGIN TRY -- RAISERROR z grup 11 – 19 przeniesie wykonanie do bloku CATCH. -- Niższe grupy nie powodują przeniesienia. RAISERROR ('Błąd w bloku TRY.', 16, 1 ) END TRY
Rozdział 6. Przetwarzanie transakcyjne
321
BEGIN CATCH DECLARE @ErrorMessage NVARCHAR(4000) DECLARE @ErrorSeverity INT DECLARE @ErrorState INT SELECT @ErrorMessage = 'Z CATCH. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE() -- Zastosowano RAISERROR w bloku CATCH, aby zwrócić -- informacje o błędzie, który spowodował przeniesienie -- wykonywania skryptu do bloku CATCH. RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState) END CATCH
W przypadku wymuszenia grupy błędów ErrorSeverity >10 otrzymujemy komunikat: Msg 50000, Level 16, State 1, Line 16 Z CATCH. Błąd w bloku TRY.
Co potwierdza, że nastąpiło przechwycenie błędu i jest wykonywana jego obsługa. Dla ErrorSeverity <=10 błąd nie jest przechwytywany, co potwierdza komunikat: Błąd w bloku TRY.
Wskazuje to na uznanie przez twórców MS SQL Server błędów z grup o numerze nie większym niż 10 za mniej groźne, niewymagające obsługi. Niestety, przerywają one wykonywanie skryptu. Dodatkowo widać, że błąd pojawiający się w sekcji CATCH nie jest obsługiwany i jest propagowany do miejsca wywołania lub — jeśli ta obsługa pojawia się w nadrzędnym bloku TRY — do nadrzędnego bloku CATCH. Pozostaje analiza ważnego z praktycznego punktu widzenia problemu masowego wstawiania, migracji danych. Wyobraźmy sobie dużą tabelę, do której z innego źródła dopisujemy dużą porcję rekordów. W takim przypadku interesuje nas sytuacja, w której albo wszystkie dane są utrwalone, albo stan tabeli docelowej pozostaje bez zmian. Gdyby utrwaleniu uległa tylko część nowych danych, znalezienie w dużym wolumenie brakujących wpisów byłoby dużym wyzwaniem. Przetestujemy ten problem na mniejszym zadaniu, polegającym na wstawieniu czterech rekordów do tabeli Test, która ma dwa pola numeryczne. Pierwsze jest automatycznie inkrementowanym kluczem podstawowym, drugie pozwala na wpisanie dowolnej wartości całkowitej. DROP TABLE Test GO CREATE TABLE Test (IdTest int IDENTITY(1,1) PRIMARY KEY, wart int) GO
W skrypcie rozpoczynamy transakcję i otwieramy blok przechwytywania wyjątków. We wnętrzu bloku TRY tabela zostaje zasilona danymi. Pierwsze dwa wstawiane wiersze zawierają poprawne dane, chociaż drugi wiersz wymaga niejawnej konwersji z napisu do liczby. Trzeci wiersz zawiera błędną daną znakową, czwarty jest poprawny. Po każdym wstawieniu wykonywane jest polecenie SELECT, wyświetlające bieżącą zawartość tabeli. Na koniec bloku TRY otwarta transakcja jest zatwierdzana. W bloku CATCH wyświetlana jest informacja o błędzie, a następnie transakcja jest wycofywana. Wreszcie zatwierdzamy transakcję, jeśli z jakichkolwiek przyczyn jeszcze jest otwarta, a na koniec sprawdzamy, jakie dane zostały utrwalone.
322
MS SQL Server. Zaawansowane metody programowania BEGIN TRANSACTION BEGIN TRY INSERT INTO Test VALUES(11) SELECT * FROM Test INSERT INTO Test VALUES('22') SELECT * FROM Test INSERT INTO Test VALUES('aa') SELECT * FROM Test INSERT INTO Test VALUES(44) SELECT * FROM Test COMMIT TRANSACTION; END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_SEVERITY() AS ErrorSeverity, ERROR_STATE() as ErrorState, ERROR_PROCEDURE() as ErrorProcedure, ERROR_LINE() as ErrorLine, ERROR_MESSAGE() as ErrorMessage IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION END CATCH IF @@TRANCOUNT > 0 COMMIT TRANSACTION SELECT * FROM TEST
Ponieważ dane wstawiane w dwóch pierwszych wierszach są poprawne, zawartość tabeli Test po każdym z nich powiększa się — tabele 6.14 i 6.15. Tabela 6.14. Zawartość tabeli Test po wstawieniu pierwszego wiersza IdTest
wart
1
11
Tabela 6.15. Zawartość tabeli Test po wstawieniu pierwszego wiersza IdTest
wart
1
11
2
22
Ponieważ kolejny wiersz jest niepoprawny, nie zostaną wykonane kolejne zapytania i zostaniemy przeniesieni do sekcji CATCH, gdzie wyświetlona została informacja o błędzie (tabela 6.16), wskazująca na brak możliwości konwersji napisu do liczby. Tabela 6.16. Informacje o błędzie wyświetlane w sekcji CATCH ErrorNumber
ErrorSeverity
ErrorState
ErrorProcedure
ErrorLine
ErrorMessage
245
16
1
NULL
7
Conversion failed when converting the varchar value 'aa' to data type int.
Ponieważ w sekcji nastąpiło wycofanie transakcji, końcowe zapytanie wybierające pokaże pustą tabelę. Można sprawdzić, zmieniając dane wejściowe na poprawne, jak wtedy zachowa się skrypt.
Rozdział 7.
Typy złożone 7.1. Typ tabelaryczny W MS SQL Server istnieje możliwość korzystania z wielu wbudowanych typów złożonych. Pierwszym z nich, omówionym przy okazji zapytań wybierających, był XML. Kolejnym jest typ tabelaryczny [1] [3] [78] [79] [88] – [91]. Aby zadeklarować tego typu zmienną, postępujemy podobnie jak podczas tworzenia tabel. Po słowie kluczowym TABLE następuje ujęta w nawiasy definicja pól, które separowane są przecinkiem. W definicji możliwe jest zastosowanie wszystkich ograniczeń z wyjątkiem klucza obcego FOREIGN KEY. DECLARE @tabela TABLE( ID int Identity(1,1), Opis varchar(22))
W celu sprawdzenia działania zmiennej tego rodzaju odpytajmy tabelę zarobki o wypłaty pracownika o identyfikatorze 1 (tabela 7.1). SELECT * FROM Zarobki WHERE IdOsoby =1
Tabela 7.1. Zawartość tabeli Zarobki dla pracownika o identyfikatorze 1 przed wykonaniem zmian IdZarobku
IdOsoby
Brutto
1
1
111,00
3
1
333,00
6
1
666,00
9
1
999,00
Żeby przechwycić zmiany wykonywane w ciele skryptu, zadeklarujmy pomocniczą zmienną tabelaryczną @tabela, która ma pięć pól. Pierwsze cztery są całkowitoliczbowe, z których pierwsze, ID, jest automatycznie inkrementowane. Ostatnie jest typu „data i czas”, z wartością domyślną, którą jest aktualny czas systemowy. Następuje modyfikacja danych, w ten sposób, że wypłaty pierwszej osoby są powiększane o 25%. Występujące po wyrażeniu aktualizującym słowo kluczowe OUTPUT wskazuje na
324
MS SQL Server. Zaawansowane metody programowania
przekierowanie wyników do innego obiektu. W tym przypadku jest to zmienna tabelaryczna, zasilana tak, że do pola kto wędruje nowa wartość pola IdOsoby. Ponieważ nie podlega ono modyfikacji, stara wartość tego pola będzie taka sama. Kolejno wstawiane są stara i nowa wartość pola Brutto oraz bieżący czas uzyskiwany za pomocą funkcji getdate(). Stare i nowe wartości pól uzyskano za pomocą tymczasowych tabel systemowych DELETED i INSERTED, które dla każdego z rodzaju modyfikacji funkcjonują tak samo jak w procedurach wyzwalanych. Na koniec sprawdzana jest zawartość zmiennej tabelarycznej — tabela 7.2. DECLARE @tabela TABLE( ID int Identity(1,1), kto int, StareBrutto int, NoweBrutto int, data datetime DEFAULT getdate()); UPDATE Zarobki SET Brutto = Brutto * 1.25 OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto, getdate() INTO @tabela WHERE IdOsoby =1; SELECT * FROM @tabela; GO
Tabela 7.2. Zawartość zmiennej tabelarycznej w ciele skryptu po wykonaniu modyfikacji tabeli Zarobki dla pracownika o identyfikatorze 1 ID
kto
StareBrutto
NoweBrutto
data
1
1
111
139
2013-01-12 16:55:32.490
2
1
333
416
2013-01-12 16:55:32.490
3
1
666
833
2013-01-12 16:55:32.490
4
1
999
1249
2013-01-12 16:55:32.490
Jeśli teraz odpytamy tabelę Zarobki, jej zawartość potwierdzi nam to, co wcześniej zaobserwowaliśmy, sprawdzając zmienną tabelaryczną — tabela 7.3. Różnica w wartościach Brutto wynika jedynie z faktu, że w zmiennej tabelarycznej zastosowano typy całkowite, co wymusiło konwersję i przycięcie dokładności wyświetlanych danych. SELECT * FROM Zarobki WHERE IdOsoby =1
Tabela 7.3. Zawartość tabeli Zarobki dla pracownika o identyfikatorze 1 po wykonaniu zmian IdZarobku
IdOsoby
Brutto
1
1
138,75
3
1
416,25
6
1
832,50
9
1
1248,75
Ponieważ zmienna tabelaryczna ma określoną wartość domyślną dla pola data, możliwe jest wykorzystanie tej cechy w skrypcie. Różnica polega na określeniu listy zasilanych pól zmiennej oraz pominięciu funkcji zwracającej czas systemowy. Funkcjonalność kodu pozostaje bez zmian.
Rozdział 7. Typy złożone
325
DECLARE @tabela TABLE( ID int Identity(1,1), kto int, StareBrutto int, NoweBrutto int, data datetime default getdate()); UPDATE Zarobki SET Brutto = Brutto * 1.25 OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto INTO @tabela (kto, StareBrutto, NoweBrutto) WHERE IdOsoby =1; SELECT * FROM @tabela; GO
Zmienna tabelaryczna jest bytem nietrwałym i nie istnieje poza kodem, w którym została zadeklarowana. Nie będzie widoczna w innej sesji ani nawet w tym samym kodzie, jeśli został on rozdzielony słowem kluczowym GO. Zamiast definiować ad hoc zmienną takiego rodzaju, możemy zdefiniować typ. Metoda jest analogiczna do tworzenia tabeli. Podobnie jak w przypadku zmiennej, możliwe jest zdefiniowanie wszystkich ograniczeń z wyjątkiem klucza obcego. W przykładzie utworzono typ Sprawdzenie, który jest analogiczny do poprzednio używanej zmiennej. CREATE TYPE Sprawdzenie AS TABLE (ID int Identity(1,1) PRIMARY KEY, Identyfikator int, Stare int, Nowe int, data datetime default getdate())
Po utworzeniu typ użytkownika jest przechowywany w bazie danych, a jego miejsce w strukturze hierarchicznej i elementy składowe przedstawia rysunek 7.1. Rysunek 7.1. Miejsce składowania typów użytkownika
326
MS SQL Server. Zaawansowane metody programowania
Teraz możemy zdecydować się na zadeklarowanie zmiennej utworzonego wcześniej typu. Ponieważ w deklaracji typu zastosowano strukturę analogiczną do wykorzystanej w deklaracji zmiennej, używany do testowania skrypt będzie się różnił tylko w obrębie deklaracji. DECLARE @tabela Sprawdzenie UPDATE Zarobki SET Brutto = Brutto * 1.25 OUTPUT INSERTED.IdOsoby, DELETED.Brutto, INSERTED.Brutto INTO @tabela (Identyfikator, Stare, Nowe ) WHERE IdOsoby =1 SELECT * FROM @tabela
Należy pamiętać, że pomimo tego, iż typ jest trwałym obiektem, to zmienne zadeklarowane przy jego użyciu są w dalszym ciągu nietrwałe. Zmienna tabelaryczna może wystąpić również jako parametr procedury. W prezentowanym skrypcie na początku są usuwane najpierw procedura, a później typ przez nią stosowany. Kolejność jest obowiązkowa, ponieważ wykorzystywane w innych obiektach typy są chronione przed usunięciem. Następnie tworzymy typ tabelaryczny Zestaw, który ma dwie kolumny numeryczne: całkowitą, ze zdefiniowanym ograniczeniem klucza podstawowego, oraz zmiennoprzecinkową. Później tworzymy procedurę, której pierwszym parametrem jest zmienna @kto utworzonego poprzednio typu. Na liście parametrów takie zmienne muszą być jawnie określone jako tylko do odczytu (READONLY), co uniemożliwia, aby były parametrami wyjściowymi, przekazującymi dane do miejsca wywołania. Drugi parametr @suma przekazuje dane na zewnątrz. Procedura zlicza sumę wypłat pracowników, których identyfikatory są zapisane w pierwszym parametrze. Drugie pole tabeli nie jest wykorzystywane w ciele procedury. DROP Procedure SumaPrac GO DROP TYPE Zestaw GO CREATE TYPE Zestaw AS TABLE (Identyfikator int PRIMARY KEY, Wartosc real) GO CREATE Procedure SumaPrac @kto Zestaw READONLY, @suma real OUTPUT AS SELECT @suma = SUM(Brutto) FROM Zarobki WHERE IdOsoby IN (SELECT Identyfikator FROM @kto) GO
Aby wywołać procedurę, zadeklarowane zostają zmienne: typu tabelarycznego oraz całkowita. Pierwsza z nich jest zasilana wartościami IdOsoby i Wzrost pracowników z działu o identyfikatorze 1. Po sprawdzeniu jej zawartości (tabela 7.4) wywoływana jest procedura. Na koniec wyświetlana jest wartość zmiennej wyjściowej tabela 7.5. DECLARE @kto Zestaw DECLARE @Suma real INSERT INTO @kto SELECT IdOsoby, Wzrost FROM Osoby WHERE IdDzialu=1 SELECT * FROM @kto EXEC SumaPrac @kto, @suma OUTPUT SELECT @Suma
Rozdział 7. Typy złożone
327
Tabela 7.4. Zawartość zmiennej tabelarycznej w ciele skryptu po jej zasileniu danymi pracowników z działu o identyfikatorze 1 Identyfikator
Wartosc
1
1,67
9
1,69
Tabela 7.5. Wartość zmiennej wyjściowej procedury wyznaczającej sumę wypłat pracowników o danych zawartych w zmiennej tabelarycznej (No column name) 2969,25
Jak wynika z przytoczonych przykładów, zmienna tabelaryczna wykazuje bardzo duże podobieństwo do lokalnej tabeli tymczasowej. Istnieje jednak jedna zasadnicza różnica. Spróbujmy to sprawdzić z wykorzystaniem skryptu, w którym tworzony jest taki typ tabeli, a następnie tworzona jest deklarowana zmienna typu tabelarycznego. Dla ułatwienia oba obiekty mają jedno pole znakowe. Do każdego z tych obiektów wstawiany jest napis Stara z dodanym sufiksem # lub @, takim samym jak znak rozpoczynający nazwę obiektu. Otwieramy transakcje i dokonujemy zmiany zawartości obu obiektów, tak że zasadnicza część napisu zmienia się na Nowa. Następnie transakcja jest wycofywana. Na zakończenie sprawdzana jest zawartość obu obiektów lokalnej tabeli tymczasowej (tabela 7.6) oraz zmiennej tabelarycznej (tabela 7.7). CREATE TABLE #T (s varchar(128)) DECLARE @T TABLE (s varchar(128)) INSERT INTO #T SELECT 'Stara #' INSERT INTO @T SELECT 'Stara @' BEGIN TRANSACTION UPDATE #T SET s='Nowa #' UPDATE @T SET s='Nowa @' ROLLBACK TRANSACTION SELECT * FROM #T SELECT * FROM @T
Tabela 7.6. Zawartość lokalnej tabeli tymczasowej po wycofaniu zmian s Stara #
Tabela 7.7. Zawartość zmiennej tabelarycznej po wycofaniu zmian s Nowa @
Analizując wyniki, widzimy, że zmiany wprowadzone w lokalnej tabeli tymczasowej zostały wycofane, natomiast nie zostały wycofane zmiany wprowadzone w zmiennej tabelarycznej. Można powiedzieć, że tabele tymczasowe podlegają zasadom przetwarzania transakcyjnego, a zmienne tabelaryczne nie. Można to również sprawdzić, wykonując inne zapytania zmieniające ich rodzaj na INSERT, DELETE, co pozostawiam uważnemu Czytelnikowi.
328
MS SQL Server. Zaawansowane metody programowania
7.2. Typ hierarchiczny Od wersji MS SQL Server 2008 R2 dostępny jest typ danych opisujących hierarchię o nazwie hierarchyid [92] [97]. Ten rodzaj danych jest typem obiektowym. Do tej pory taką strukturę trzeba było opisywać w tabeli, która zawierała węzeł początkowy i końcowy [7] [76] [92] – [95], lub w tzw. notacji odwiedzinowej, która pokazywała rekord początkowy i końcowy, zawierający poziom potomny [7] [92]. Nowy typ sam wskazuje miejsce węzła w strukturze drzewiastej. Utwórzmy tabelę, która będzie przechowywała informację o podporządkowaniu pracowników. W tym celu zdefiniowane zostały pola: klucza podstawowego, węzła w grafie oraz nazwisko pracownika. Liczba pól niebędących kluczami może być oczywiście dużo większa. Ostatnią kolumną jest pole wyliczane, korzystające z metody GetLevel dla pola typu hierarchicznego, a wskazujące na poziom w hierarchii. CREATE TABLE Hierarchia (IdPracownika Int IDENTITY(1,1), IdWezla hierarchyid, Nazwisko varchar(22), Poziom AS Idwezla.GetLevel());
Typ hierarchyid oferuje dużo większy zestaw metod, których syntetyczny opis zawiera tabela 7.8. Należy pamiętać, że podobnie jak w większości środowisk obiektowych, również i w przypadku omawianego typu rozróżniana jest w nazwach metod wielkość liter. Gdybyśmy ostatnią linię definicji tabeli zastąpili: Poziom AS IdWezla.GETLEVEL());
to otrzymalibyśmy komunikat o błędzie: Msg 6506, Level 16, State 10, Line 5 Could not find method 'GETLEVEL' for type 'Microsoft.SqlServer.Types.SqlHierarchyId' in assembly 'Microsoft.SqlServer.Types'
Wskazuje on, że nie można znaleźć metody o wymienionej nazwie, ale również informuje, że błąd wystąpił w definicji klasy obiektowej Microsoft.SqlServer.Types. SqlHierarchyId, inkapsulowanej za pomocą pośredniczącego obiektu typu assembly. O metodach tworzenia typów użytkownika stosujących takie mapowanie napiszę w podrozdziale 7.4. Spróbujmy wstawić do naszej tabeli pierwszy rekord. Ponieważ klucz podstawowy jest automatycznie inkrementowany, a poziom jest obliczany na podstawie danych o węźle, ograniczymy się do dwóch kolumn: IdWezla i Nazwisko. Załóżmy, że pan Kowalski jest naczelnym szefem naszej firmy i nie ma żadnych przełożonych, dlatego stanowi najwyższy poziom w hierarchii — korzeń (root). Aby zasilić pole IdWezla, zastosowano statyczną metodę typu hierarchyid o nazwie GetRoot. INSERT Hierarchia (IdWezla, Nazwisko) VALUES (hierarchyid::GetRoot(), 'Kowalski') ; GO SELECT IdWezla.ToString() AS Wezel,* FROM Hierarchia
Rozdział 7. Typy złożone
329
Tabela 7.8. Wykaz metod typu obiektowego hierarchyid Metoda
Opis Zwraca wartość typu hierarchyid reprezentującą n-tego rodzica.
GetAncestor(n)
SELECT IdWezla.ToString(), IdWezla.GetAncestor(1).ToString() FROM Hierarchia
Zwraca pierwsze dziecko określone przez wartości definiujące przedział. GetDescendant (dziecko1, dziecko2)
SELECT IdWezla.ToString(), IdWezla.GetDescendant(NULL, NULL).ToString() FROM Hierarchia SELECT IdWezla.ToString(), IdWezla.GetDescendant('/1/', NULL).ToString() FROM Hierarchia WHERE IdPracownika=1 SELECT IdWezla.ToString(), IdWezla.GetDescendant( NULL, '/2/').ToString() FROM Hierarchia WHERE IdPracownika=1
Zwraca numer poziomu, na którym znajduje się węzeł, numeracja rozpoczyna się od 0.
GetLevel ()
SELECT IdWezla.ToString(), IdWezla.GetLevel() FROM Hierarchia
Zwraca korzeń hierarchii, jest metodą statyczną typu hierarchyid. GetRoot ()
SELECT IdWezla.ToString(), hierarchyid::GetRoot() FROM Hierarchia
Zwraca true, jeśli węzeł jest dzieckiem wskazanego rodzica.
IsDescendantOf (rodzic)
SELECT IdWezla.ToString(), IdWezla.IsDescendantOf('/1/') FROM Hierarchia
Dokonuje konwersji napisu do typu hierarchyid.
Parse (Database Engine)
SELECT hierarchyid::Parse ('/1/2/')
Wskazuje na ścieżkę, jaką opisywany byłby węzeł, gdyby korzeń został zamieniony na nową lokalizację. SELECT IdWezla.ToString(), IdWezla.GetReparentedValue(hierarchyid::GetRoot(), IdWezla.GetAncestor(1)).ToString(), IdWezla.GetReparentedValue(IdWezla.GetAncestor(1), IdWezla.GetAncestor(2)).ToString() FROM Hierarchia
GetReparentedValue (korzen1, korzen2)
Dokonuje konwersji typu hierarchyid do napisu.
ToString ()
Na koniec odpytujemy o wszystkie pola tabeli, dodając informacje opisową o węźle za pomocą metody ToString(), która tłumaczy wewnętrzną reprezentację binarną na czytelną dla użytkownika. Rezultaty zapytania zawiera tabela 7.9. Tabela 7.9. Zawartość tabeli po wpisaniu pierwszego pracownika Wezel
IdPracownika
IdWezla
Nazwisko
Poziom
/
1
0x
Kowalski
0
Aby dodać kolejny węzeł sieci, musimy wskazać jego miejsce w hierarchii. Możemy tego dokonać, wskazując na węzeł nadrzędny. Ponieważ wstawiamy drugi rekord, naturalne jest, że będzie nim korzeń drzewa. Informacje tę uzyskujemy, stosując statyczną metodę GetRoot() typu hierarchyid zawartego w tabeli Hierarchia. Wstawiając
330
MS SQL Server. Zaawansowane metody programowania
rekord, stosujemy metodę GetDescendant działającą na zmienną @Szef, która zawiera dane korzenia grafu. Skrypt kończy się sprawdzeniem poprawności wstawienia danych, co przedstawia tabela 7.10. DECLARE @Szef hierarchyid SELECT @Szef = hierarchyid::GetRoot() FROM Hierarchia ; INSERT Hierarchia(IdWezla, Nazwisko) VALUES (@Szef.GetDescendant(NULL, NULL), 'Nowak') ; GO SELECT IdWezla.ToString() AS Wezel,* FROM Hierarchia
Tabela 7.10. Zawartość tabeli po wpisaniu drugiego pracownika będącego podwładnym pierwszego Wezel
IdPracownika
IdWezla
Nazwisko
Poziom
/
1
0x
Kowalski
0
/1/
2
0x58
Nowak
1
Aby poprawić jakość wstawiania kolejnych węzłów w grafie, utwórzmy procedurę składowaną, której parametrami będą identyfikator węzła nadrzędnego — szefa oraz nazwisko wstawianego pracownika. W ciele procedury zadeklarujmy dwie zmienne lokalne @Wezel, @pom, obie typu hierarchyid. Pierwsza z nich jest zasilana identyfikatorem węzła pracownika wskazanego jako przełożony. Druga jest równa maksymalnej wartości identyfikatora dziecka, dla którego rodzicem jest ten, którego wskazuje zmienna @Wezel. Innymi słowy, najpierw tłumaczymy identyfikator wiersza reprezentujący szefa na obiekt stanowiący identyfikator hierarchii, a następnie wśród jego dzieci znajdujemy maksymalny identyfikator hierarchii. Do wstawiania wykorzystujemy metodę GetDescendant dla zmiennej reprezentującej szefa, która wskazuje pierwsze dziecko, którego identyfikator jest większy niż wskazany parametrem. CREATE PROC DodajPrac(@Szef int, @nazw varchar(20)) AS BEGIN DECLARE @Wezel hierarchyid, @pom hierarchyid SELECT @Wezel = IdWezla FROM Hierarchia WHERE IdPracownika = @Szef SELECT @pom = max(IdWezla) FROM Hierarchia WHERE IdWezla.GetAncestor(1)=@Wezel INSERT Hierarchia (IdWezla, Nazwisko) VALUES(@Wezel.GetDescendant(@pom, NULL), @nazw) END GO
Utworzoną procedurę możemy wykorzystać do wstawienia kilku kolejnych wierszy, przyporządkowanych do różnych przełożonych. Należy pamiętać, że pierwszy parametr musi wskazywać na już istniejący w tabeli rekord. Następnie odpytujemy zasilaną tabelę, czego efekt jest zawarty w tabeli 7.11. EXEC DodajPrac 2, 'Janik' EXEC DodajPrac 1, 'Wilk' EXEC DodajPrac 4, 'Lis' EXEC DodajPrac 3, 'Kowal' GO SELECT IdWezla.ToString() AS Wezel, * FROM Hierarchia ORDER BY IdPracownika;
Rozdział 7. Typy złożone
331
Tabela 7.11. Zawartość tabeli po wpisaniu kolejnych pracowników Wezel
IdPracownika
IdWezla
Nazwisko
Poziom
/
1
0x
Kowalski
0
/1/
2
0x58
Nowak
1
/1/1/
3
0x5AC0
Janik
2
/2/
4
0x68
Wilk
1
/2/1/
5
0x6AC0
Lis
2
/1/1/1/
6
0x5AD6
Kowal
3
Operacją, którą możemy wykonać na już zasilonym drzewie, jest przeniesienie liścia [76]. Innymi słowy, chcemy zmienić przełożonego pracownikowi, który nie ma własnych podwładnych. W tym celu do trzech zmiennych pomocniczych przypisujemy identyfikatory w hierarchii należące do: przenoszonego pracownika, starego i nowego przełożonego. Modyfikacja rekordu UPDATE wykorzystuje metodę GetReparentedValue obiektu reprezentującego pracownika z parametrami wskazującymi starego i nowego szefa. Obiekt wskazujący przenoszonego pracownika został również użyty w klauzuli WHERE do wskazania właściwego wiersza. Do tego celu można by zastosować zwykły identyfikator wiersza. Skutek opisanej operacji zawiera tabela 7.12. DECLARE @biezacy hierarchyid , @StaryRodzic hierarchyid, @NowyRodzic hierarchyid SELECT @biezacy = IdWezla FROM Hierarchia WHERE Idpracownika = 6; SELECT @StaryRodzic = IdWezla FROM Hierarchia WHERE Idpracownika = 3; SELECT @NowyRodzic = IdWezla FROM Hierarchia WHERE Idpracownika = 5; UPDATE Hierarchia SET IdWezla = @biezacy.GetReparentedValue(@StaryRodzic, @NowyRodzic) WHERE IdWezla = @biezacy GO SELECT IdWezla.ToString() AS Wezel, * FROM Hierarchia ORDER BY IdPracownika;
Tabela 7.12. Zawartość tabeli po przeniesieniu podwładnego o identyfikatorze 6 od szefa o identyfikatorze 3 do szefa o identyfikatorze 5 Wezel
IdPracownika
IdWezla
Nazwisko
Poziom
/
1
0x
Kowalski
0
/1/
2
0x58
Nowak
1
/1/1/
3
0x5AC0
Janik
2
/2/
4
0x68
Wilk
1
/2/1/
5
0x6AC0
Lis
2
/2/1/1/
6
0x6AD6
Kowal
3
O ile przeniesienie węzła będącego liściem drzewa nie sprawia żadnych większych problemów, to taka sama operacja dotycząca węzła występującego w środku drzewa jest już złożona. Wynika to z faktu, że nie jest zaimplementowany automat do renumeracji węzłów dzieci i taki proces musi przeprowadzić programista. Skrypt zaczyna się tak samo jak poprzedni, od zadeklarowania trzech zmiennych typu hierarchyid i zasilenia ich danymi: przenoszonego węzła, starego i nowego rodzica. Następnie deklarowany
332
MS SQL Server. Zaawansowane metody programowania
jest kursor dzieci zawierający zestaw rekordów, dla których rodzicem był węzeł starego przełożonego. Po jego otwarciu i przejściu do pierwszego rekordu identyfikator węzła pierwszego dziecka jest przypisywany do wcześniej zadeklarowanej zmiennej @IdDziecka. W pętli, która jest przetwarzana do końca zestawu rekordów, wykrywany jest maksymalny identyfikator dziecka nowego przełożonego @NowyRodzic.GetDescendant, a następnie wykonywana jest modyfikacja rekordu dziecka polegająca na zamianie rodzica z zastosowaniem metody IdWezla.GetReparentedValue. Zastosowane w niej parametry @IdDziecka, @NowyId wskazują zakres identyfikatorów dzieci, w którym ma być wstawiony nowy potomek. W zapytaniu modyfikującym UPDATE wskazywane są wiersze, dla których IdWezla.IsDescendantOf(@IdDziecka) = 1, czyli takie, które są dziećmi wskazanego rodzica. Ostatnim elementem pętli jest nawigacja kursorem do kolejnego rekordu. Na koniec kursor jest zamykany i zwalniane są zasoby przez niego zajęte. Skutek przeniesienia wybranego węzła jest pokazany w tabeli 7.13 DECLARE @biezacy hierarchyid , @StaryRodzic hierarchyid, @NowyRodzic hierarchyid SELECT @biezacy = IdWezla FROM Hierarchia WHERE Idpracownika = 5; SELECT @StaryRodzic = IdWezla FROM Hierarchia WHERE Idpracownika = 4; SELECT @NowyRodzic = IdWezla FROM Hierarchia WHERE Idpracownika = 2; DECLARE dzieci CURSOR FOR SELECT IdWezla FROM Hierarchia WHERE IdWezla.GetAncestor(1) = @StaryRodzic; DECLARE @IdDziecka hierarchyid; OPEN dzieci FETCH NEXT FROM dzieci INTO @IdDziecka; WHILE @@FETCH_STATUS = 0 BEGIN DECLARE @NowyId hierarchyid; SELECT @NowyId = @NowyRodzic.GetDescendant(MAX(IdWezla), NULL) FROM Hierarchia WHERE IdWezla.GetAncestor(1) = @NowyRodzic; UPDATE Hierarchia SET IdWezla = IdWezla.GetReparentedValue(@IdDziecka, @NowyId) WHERE IdWezla.IsDescendantOf(@IdDziecka) = 1; FETCH NEXT FROM dzieci INTO @IdDziecka; END CLOSE dzieci; DEALLOCATE dzieci; GO SELECT IdWezla.ToString() AS Wezel, * FROM Hierarchia ORDER BY IdPracownika;
Tabela 7.13. Zawartość tabeli po przeniesieniu podwładnego o identyfikatorze 5 od szefa o identyfikatorze 4 do szefa o identyfikatorze 2 Wezel
IdPracownika
IdWezla
Nazwisko
Poziom
/
1
0x
Kowalski
0
/1/
2
0x58
Nowak
1
/1/1/
3
0x5AC0
Janik
2
/2/
4
0x68
Wilk
1
/1/2/
5
0x5B40
Lis
2
/1/2/1/
6
0x5B56
Kowal
3
Rozdział 7. Typy złożone
333
W przykładzie przeniesiono węzeł między rodzicami tego samego poziomu. Możliwe jest przeniesienie go do dowolnego innego położenia w grafie, co pozostawiam do sprawdzenia uważnemu Czytelnikowi. Z powyższych przykładów może płynąć wniosek, że możliwe jest tylko wstawianie dzieci jako elementów o numerze wyższym niż najwyższy do tej pory istniejący na danym poziomie. Można powiedzieć, że mamy do czynienia ze wstawianiem w kolejności „narodzin”. Jest jednak możliwe wstawianie dzieci w luki numeracji danego poziomu. Możemy również przypuszczać, że jeśli na danym poziomie numeracja dzieci jest ciągła, to aby wstawić dziecko pomiędzy już istniejące, konieczne jest przesunięcie części węzłów, tak aby uzyskać „dziurę” w numeracji. Oczywiście takie podejście jest możliwe, ale typ hierarchyid radzi sobie z takim problemem również w inny sposób. Zadeklarujmy kilka zmiennych pomocniczych typu hierarchyid, jedną na dane węzła rodzica, pozostałe na dane węzłów dzieci. Pierwsza jest zasilona danymi węzła rodzica dziecka o identyfikatorze 3, kolejne dwie są danymi węzłów dzieci o identyfikatorach 3 i 5. Należy zauważyć, że mają one tego samego przodka, w związku z tym w pierwszym z zapytań można by bez zmiany rezultatu użyć identyfikatora 5. Następnie, stosując metodę GetDescendant węzła rodziców, wygenerujmy identyfikatory kilku węzłów dzieci, wskazując przedział, w którym ma się on zmieścić, za pomocą podanych parametrów. W pierwszym przypadku dziecko znajdzie się między węzłami o numerach 1 i 2 na danym poziomie. Ponieważ nie istnieje między nimi wolna liczba całkowita, hierarchyid zastosuje, przez analogię do liczb rzeczywistych, notację z kropką, generując wartość 1.1. Dwoje kolejnych dzieci zostanie wstawionych przed tym węzłem i po nim; otrzymają one numery 1.0 i 1.2. Pierwsza wartość świadczy o tym, że możliwe jest na tym poziomie zmniejszanie wartości poniżej 1, możliwe jest również otrzymanie liczby ujemnej. Gdyby okazało się, że na tym poziomie nie znajdzie się wolna liczba całkowita, to przez znak kropki zostanie wprowadzony kolejny poziom numeracji, np. dla węzłów 1.1 i 1.2 węzłem pośrednim będzie 1.1.1. Kolejne wygenerowane węzły pokazują wspomnianą poprzednio numerację poziomu mniejszą niż 1. Pozostawiając pierwszy parametr nieokreślony, szukamy węzłów poniżej węzła wskazanego drugim parametrem. Daje to na wybranym poziomie numery 0 oraz -1. DECLARE @IdRodzica hierarchyid; DECLARE @IdDziecka1 hierarchyid; DECLARE @IdDziecka2 hierarchyid; DECLARE @IdDziecka3 hierarchyid; DECLARE @IdDziecka4 hierarchyid; SELECT @IdRodzica= @IdDziecka1.GetAncestor(1) FROM Hierarchia WHERE IdPracownika=3 PRINT @IdRodzica.ToString() SELECT @IdDziecka1=IdWezla FROM Hierarchia WHERE IdPracownika=3 PRINT @IdDziecka1.ToString() SELECT @IdDziecka2=IdWezla FROM Hierarchia WHERE IdPracownika=5 PRINT @IdDziecka2.ToString() SET @[email protected](@IdDziecka1, @IdDziecka2) PRINT @IdDziecka3.ToString() SET @[email protected](@IdDziecka1, @IdDziecka3) PRINT @IdDziecka4.ToString() SET @[email protected](@IdDziecka3, @IdDziecka2) PRINT @IdDziecka4.ToString() SET @[email protected](null, @IdDziecka1) PRINT @IdDziecka3.ToString() SET @[email protected](null, @IdDziecka3) PRINT @IdDziecka4.ToString()
334
MS SQL Server. Zaawansowane metody programowania
Wykonanie przedstawionego skryptu powoduje wygenerowanie poniższej serii identyfikatorów węzłów. /1/ /1/1/ /1/2/ /1/1.1/ /1/1.0/ /1/1.2/ /1/0/ /1/-1/
Oczywiście zamiast wyprowadzać wartość na ekran, możliwe byłoby użycie polecenia INSERT wstawiającego właściwy element grafu. Nie wszystkie wymienione warianty numeracji zostały pokazane w skrypcie, ale łatwo mogą zostać uzyskane przez wskazanie innych granic dla metody GetDescendant.
7.3. Typy geometry i geography Kolejnym typem złożonym jest typ opisujący geometrię wektorową. Formalnie dzieli się on na dwa rodzaje, geometry i geography, ale ich zasadnicza funkcjonalność pozostaje taka sama. Ogólnie o danych tego rodzaju mówi się Spatial [97] – [102]. W środowisku MS SQL Server zdefiniowano kilka rodzajów geometrii obsługiwanej za pomocą tych typów. W tabeli 7.14 zawarto listę takich geometrii wraz z przykładami ich definicji oraz uproszczoną ilustrację graficzną. Należy zaznaczyć, że obiekty krzywoliniowe, takie jak CIRCULARSTRING, COMPOUNDCURVE oraz CURVEPOLYGON, zostały wprowadzone w wersji 2012 serwera. Współrzędne x i y każdego punktu są separowane spacją, natomiast kolejne punkty kształtu są rozdzielane przecinkami. Zastosujmy zmienne spatial do utworzenia kilku podstawowych kształtów. W tym celu zadeklarowano 9 zmiennych typu geometry. Oczywiście moglibyśmy zastosować w deklaracji domyślne przypisanie wartości, podając po znaku równości ujęty w apostrofy jeden z wpisów zawarty w kolumnie „Przykład” tabeli 7.14. Zastosujemy jednak konstruktor STGeomFromText dla spatial, pozwalający na zdefiniowanie dowolnego kształtu dostępnego w MS SQL Server. Drugim parametrem tej metody, który w skrypcie dla każdego obiektu ma wartość zero, jest numer warstwy. Nie oznacza to przesunięcia w kierunku trzeciej współrzędnej, lecz jedynie sposób logicznego powiązania pewnej grupy obiektów grafiki wektorowej. W końcowej części skryptu odpytano wszystkie utworzone zmienne. DECLARE @a geometry; DECLARE @b geometry; DECLARE @c geometry; DECLARE @d geometry; DECLARE @e geometry; DECLARE @f geometry; DECLARE @g geometry; DECLARE @h geometry; DECLARE @i geometry; SET @a = geometry::STGeomFromText('POINT(5 7)', 0); SET @b = geometry::STGeomFromText('MULTIPOINT(4 7, 11 5, 9 12, 8 8)', 0); SET @c = geometry::STGeomFromText('LINESTRING(5 6, 7 8, 11 3)', 0);
Rozdział 7. Typy złożone
335
Tabela 7.14. Rodzaje geometrii obsługiwane przez typy spatial MS SQL Server TYP
Ilustracja
Przykład
POINT
POINT(5 7)
MULTIPOINT
MULTIPOINT(4 7, 11 5, 9 12, 8 8)
LINESTRING
LINESTRING(5 6, 7 8, 11 3)
MULTILINESTRING
MULTILINESTRING((11 5, 7 4, 3 9), (5 3, 2 7, 6 5))
CIRCULARSTRING
CIRCULARSTRING(1 1, 2 0, 1 3, 1 1, 0 1);
COMPOUNDCURVE POLYGON
COMPOUNDCURVE( CIRCULARSTRING(1 0, 0 1, –1 0), (–1 0, 2 0)) POLYGON((8 3, 8 8, 12 3, 8 3))
MULTIPOLYGON
MULTIPOLYGON(((0 0, 5 5, 10 0, 0 0)), ((3 2, 5 1, 7 2, 3 2))) CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 6, 7 3, 1 3))
CURVEPOLYGON
CURVEPOLYGON((3 3, 5 3, 5 5, 3 3)) CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 6, 7 3, 1 3), (3 3, 5 3, 5 5, 3 3)) GEOMETRYCOLLECTION(POINT (5 7), MULTIPOINT(4 7, 11 5, 9 12, 8 8),
GEOMETRY COLLECTION
LINESTRING(5 6, 7 8, 11 3), MULTILINESTRING((11 5, 7 4, 3 9), (5 3, 2 7, 6 5)), POLYGON((8 3, 8 8, 12 3, 8 3)), MULTIPOLYGON(((0 0, 5 5, 10 0, 0 0)), ((3 2, 5 1, 7 2, 3 2))))
SET @d = geometry::STGeomFromText('MULTILINESTRING((11 5, 7 4, 3 9), (5 3, 2 7, 6 5))', 0); SET @e = geometry::STGeomFromText('POLYGON((8 3, 8 8, 12 3, 8 3))', 0); SET @f = geometry::STGeomFromText('MULTIPOLYGON(((0 0, 5 5, 10 0, 0 0)), ((3 2, 5 1, 7 2, 3 2)))', 0); SET @g = geometry::STGeomFromText('CIRCULARSTRING(1 1, 2 0, 1 3, 1 1, 0 1)', 0); SET @h = geometry::STGeomFromText('COMPOUNDCURVE(CIRCULARSTRING(1 0, 0 1, -1 0), (-1 0, 2 0))', 0); SET @i = geometry::STGeomFromText('CURVEPOLYGON(CIRCULARSTRING(-1 6, 1 8, 2 9, 5 6, -1 6),(1 6, 3 6, 3 8, 1 6))', 0); SELECT @a UNION ALL SELECT @b UNION ALL SELECT @c UNION ALL SELECT @d UNION ALL SELECT @e UNION ALL SELECT @f UNION ALL
336
MS SQL Server. Zaawansowane metody programowania SELECT @g UNION ALL SELECT @h UNION ALL SELECT @i
W celu łącznej prezentacji wszystkich kształtów zastosowano zapytania wybierające SELECT dla każdego obiektu, połączone operatorem UNION ALL. Jest to konieczne, ponieważ jeśli geometria zostanie wyświetlona w wielu wierszach, spowoduje to narysowanie kształtów na oddzielnych rysunkach. Próba użycia UNION zakończy się błędem, gdyż operator ten niejawnie używa dyrektywy DISTINCT, co jest niedopuszczalne dla tego typu danych. Podobnie ma się sprawa z pozostałymi operatorami mnogościowymi, o czym świadczy komunikat o błędzie. Msg 5335, Level 16, State 1, Line 21 The data type geometry cannot be used as an operand to the UNION, INTERSECT or EXCEPT operators because it is not comparable.
Jest to czytelniejsze w przypadku starszych wersji serwera, gdzie dla skryptu zawierającego połączenie operatorem UNION: DECLARE @a geometry; DECLARE @b geometry; SET @a = geometry::STGeomFromText('POINT(5 7)', 0); SET @b = geometry::STGeomFromText('MULTIPOINT(4 7, 11 5, 9 12, 8 8)', 0); SELECT @a UNION SELECT @b
otrzymujemy komunikat: Msg 421, Level 16, State 1, Line 7 The geometry data type cannot be selected as DISTINCT because it is not comparable.
To samo otrzymalibyśmy, stosując INTERSECT lub EXCEPT. Jeśli jednak wykonamy poprawny skrypt, to poza znanymi zakładkami Results i Messages pojawi się kolejna, Spatial results, na której przedstawiona jest graficzna reprezentacja zapytania. Warunkiem pojawienia się tego elementu jest to, aby w zapytaniu była chociaż jedna kolumna typu spatial. Na rysunku 7.2, po prawej stronie, widoczny jest obszar sterujący, w którym możemy wybrać: kolumnę zawierającą geometrię (jeśli są co najmniej dwie), kolumnę zawierającą etykiety opisujące kształty, stopień powiększenia oraz widoczność linii siatki. Jedną z wad tego typu prezentacji jest brak możliwości sterowania kolorami przypisywanymi do kolejnych obiektów; nadane z automatu kolejne elementy palety nie zawsze są dostatecznie kontrastowe. Zamiast stosować ogólną metodę STGeomFromText, możemy wykorzystać jej specjalizowane odpowiedniki, np.: STPointFromText, STLineFromText lub podobne. Należy przy tym pamiętać, że nazwa funkcji musi być zgodna z rodzajem tworzonego elementu. W przeciwnym wypadku otrzymamy komunikat o błędzie. Poniższy skrypt jest przekładem poprzedniej wersji z zastosowaniem specjalizowanych metod do tworzenia kształtów. Analizując skrypt, można jednak zauważyć, że wprowadzone w najnowszej wersji rodzaje geometrii nie posiadają jeszcze specjalizowanych metod statycznych i konieczne jest zastosowanie metody ogólnej.
Rozdział 7. Typy złożone
337
Rysunek 7.2. Graficzna prezentacja typów geometry
DECLARE @a geometry; DECLARE @b geometry; DECLARE @c geometry; DECLARE @d geometry; DECLARE @e geometry; DECLARE @f geometry; DECLARE @g geometry; DECLARE @h geometry; DECLARE @i geometry; SET @a = geometry::STPointFromText('POINT(5 7)', 0); SET @b = geometry::STMPointFromText('MULTIPOINT(4 7, 11 5, 9 12, 8 8)', 0); SET @c = geometry::STLineFromText('LINESTRING(5 6, 7 8, 11 3)', 0); SET @d = geometry::STMLineFromText('MULTILINESTRING((11 5, 7 4, 3 9), (5 3, 2 7, 6 5))', 0); SET @e = geometry::STPolyFromText('POLYGON((8 3, 8 8, 12 3, 8 3))', 0); SET @f = geometry::STMPolyFromText('MULTIPOLYGON(((0 0, 5 5, 10 0, 0 0)), ((3 2, 5 1, 7 2, 3 2)))', 0); SET @g = geometry::STGeomFromText('CIRCULARSTRING(1 1, 2 0, 1 3, 1 1, 0 1)', 0); SET @h = geometry::STGeomFromText('COMPOUNDCURVE(CIRCULARSTRING(1 0, 0 1, -1 0), (-1 0, 2 0))', 0); SET @i = geometry::STGeomFromText('CURVEPOLYGON(CIRCULARSTRING(-1 6, 1 8, 2 9, 5 6, -1 6),(1 6, 3 6, 3 8, 1 6))', 0); SELECT @a UNION ALL SELECT @b UNION ALL SELECT @c UNION ALL SELECT @d UNION ALL SELECT @e UNION ALL SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h UNION ALL SELECT @i
338
MS SQL Server. Zaawansowane metody programowania
Dotychczas do opisywania kształtu stosowano tylko dwie współrzędne: x i y (kształty płaskie). Formalnie każdy punkt może mieć trzy współrzędne: x, y, z, czyli obiekty mogą być trójwymiarowe. Jeśli w definicji punktu użyjemy czterech wartości, ostatnia z nich będzie oznaczała wagę, atrybut lub etykietę do niego przypisaną. Można by uważać ją za definicję współrzędnej w przestrzeni czterowymiarowej, jednak metody operujące na definicjach geometrii w większości przypadków wykorzystują tylko trzy pierwsze współrzędne. Aby więc obliczyć długość odcinka w przestrzeni czterowymiarowej, zamiast korzystać z dostarczonej metody, musielibyśmy opracować własną funkcję, która by ją wyznaczała. Jest to możliwe, zarówno po stronie SQL, jak i klas CLR, co zostanie pokazane później. Jeśli zdefiniujemy więcej niż cztery wartości określające punkt, spowoduje to wygenerowanie komunikatu o błędzie. Mimo że możliwe jest definiowanie obiektów w przestrzeni, w oknie Spatial results zawsze wyświetlane są tylko współrzędne x i y, bez względu na wartość współrzędnej z. Innymi słowy, jest to rzut obiektu na powierzchnię zdefiniowaną wyrażeniem z = 0. Aby zilustrować te właściwości, pokazany został skrypt, w którym każdy punkt obiektów geometrycznych ma zdefiniowane cztery wartości, a jego skutek graficzny pokazuje rysunek 7.3. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0 0 1, 2 0 0 2, 2 2 0 3, 0 2 0 4, 0 0 0 5))', 0); SET @h = geometry::STGeomFromText('POINT(1 1 0 6 )', 0); SET @f = geometry::STGeomFromText('POINT(3 3 0 7 )', 0); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h
Rysunek 7.3. Graficzna prezentacja typów geometry dla skryptu generującego obiekty trójwymiarowe z wagą
Dane opisujące geometrię są przechowywane po stronie serwera w postaci binarnej. Pozwolę sobie pominąć szczegółowy sposób kodowania, ponieważ jest on moim zdaniem mało użyteczny w zastosowaniach praktycznych. Chciałbym jednak pokazać na prostym przykładzie, że istnieje możliwość utworzenia kształtu bezpośrednio z takiej notacji, która nazywana jest WKB (Well-Known Binary), a wprowadzona została przez Open Geospatial Consortium (OGC). W przykładzie zastosowana została metoda STGeomFromWKB, której parametrem jest kształt zapisany w tej notacji. Aby pokazać
Rozdział 7. Typy złożone
339
tekstową interpretację krzywej, zastosowano dwie metody: ToString(), która jest obowiązkową metodą podczas tworzenia obiektów, oraz STAsText(), która jest tożsama z pierwszą, a wprowadzona została, aby zapewnić zgodność przedrostków nazw z innymi metodami obiektów typu spatial. Rezultat wykonania obu jest taki sam i wskazuje, że utworzono łamaną. Potwierdza to rysunek 7.4, będący skutkiem wykonania ostatniej linii kodu. DECLARE @g geometry; SET @g = geometry::STGeomFromWKB (0x01020000000300000000000000000059400000000000005940000000000000344000000000008066 4000000000008066400000000000806640, 0); SELECT @g.ToString(), @g.STAsText(); SELECT @g LINESTRING (100 100, 20 180, 180 180) LINESTRING (100 100, 20 180, 180 180)
Rysunek 7.4. Graficzna prezentacja typów geometry dla skryptu generującego obiekty na podstawie notacji WKB
Kolejne przykłady będą prezentowały metody działające na obiektach geometrycznych, których wynikiem jest również obiekt geometryczny. Pierwszą z nich jest STBoundray(), która wyznacza w postaci obiektu spatial granice kształtu. W pierwszym przykładzie zdefiniowano trzy obiekty: łamaną, punkt i odcinek. Składając metody STBoundary() oraz ToString(), otrzymano opis krzywej wynikowej. Pokazuje to możliwość wielokrotnego składania metod, pod warunkiem że poprzednik zwraca typ właściwy dla działania następnika. Ostatnie zapytanie składa się z połączonych operatorem UNION ALL podzapytań wyświetlających graficzny rezultat metody STBoundary() oraz podzapytań łączących kształty wyjściowe, co pokazuje rysunek 7.5. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POINT(1 1)', 0); SET @f = geometry::STGeomFromText('LINESTRING(3 3, 5 3, 4 5)', 0);
340
MS SQL Server. Zaawansowane metody programowania SELECT @f.STBoundary().ToString(), @g.STBoundary().ToString(), @h.STBoundary().ToString(); SELECT @f.STBoundary() UNION ALL SELECT @g.STBoundary() UNION ALL SELECT @h.STBoundary(); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h
Rysunek 7.5. Graficzna prezentacja granic zwracanych przez metodę STBoundary() obiektów typu geometry pokazanych na rysunku po stronie lewej
Analizując rysunek 7.5, można zauważyć, że granicą wielokąta jest łamana (w tym konkretnym przypadku kwadrat). Trudno jednak zauważyć granice łamanej. Dopiero prezentacja w postaci napisu pokazuje, że jest to zbiór punktów MULTIPOINT. Natomiast granicą punktu jest pusta kolekcja obiektów geometrycznych, czego oczywiście nie da się zauważyć przy prezentowaniu wyniku w postaci graficznej. MULTIPOINT ((4 5), (3 3)) LINESTRING (0 0, 2 0, 2 2, 0 2, 0 0) GEOMETRYCOLLECTION EMPTY
Kolejny przykład jest podobny. Różni się tylko tym, że zdefiniowanymi obiektami są trzy różne wielokąty: kwadrat i dwa trójkąty. Skutek wykonania skryptu jest przedstawiony w postaci opisowej poniżej kodu, a w postaci graficznej na rysunku 7.6. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 1 1, 2 3, 3 3))', 0); SELECT @f.STBoundary().ToString(), @g.STBoundary().ToString(), @h.STBoundary().ToString(); SELECT @f.STBoundary() UNION ALL
Rozdział 7. Typy złożone
341
SELECT @g.STBoundary() UNION ALL SELECT @h.STBoundary(); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h LINESTRING (1 1, 3 3, 2 3, 1 1) LINESTRING (0 0, 2 0, 2 2, 0 2, 0 0) LINESTRING (0 0, 1 1, 0 2, 0 0)
Rysunek 7.6. Graficzna prezentacja granic zwracanych przez metodę STBoundary() obiektów typu geometry pokazanych na rysunku po stronie lewej
Kolejną metodą jest STBuffer(x), która wyznacza otoczenie obiektu odległe od oryginału o wartość daną parametrem x. Wartość ta musi być nieujemna, w przeciwnym razie zwracana jest pusta kolekcja obiektów geometrycznych. Jeśli wynosi ona 0, zwracany jest obiekt oryginalny. W przykładzie posłużono się kształtami takimi samymi jak poprzednio, a odległość otoczenia dla wszystkich przypadków ustalono na 0.5. Graficzny rezultat jest przedstawiony na rysunku 7.7. Zrezygnowano z pokazywania opisowej postaci wyniku, ponieważ generowany jest wielokąt o bardzo dużej liczbie wierzchołków, co wynika z konieczności przybliżenia łuków. Pomimo że w wersji 2012 istnieją obiekty krzywoliniowe, funkcja zwraca definicję kształtu bez korzystania z tej możliwości, zachowując mechanizmy ze starszych wersji. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 1 1, 2 3, 3 3))', 0); SELECT @f.STBuffer(0.5).ToString(), @g.STBuffer(.5).ToString(), @h.STBuffer(.5).ToString(); SELECT @f.STBuffer(0.5) UNION ALL SELECT @g.STBuffer(0.5) UNION ALL SELECT @h.STBuffer(0.5); SELECT @f
342
MS SQL Server. Zaawansowane metody programowania UNION ALL SELECT @g UNION ALL SELECT @h
Rysunek 7.7. Graficzna prezentacja granic zwracanych przez metodę STBuffer(x) obiektów typu geometry pokazanych na rysunku po stronie lewej
W przypadku części metod rodzaj grafiki, która jest skutkiem ich działania, jest określony w sposób ścisły. Dla metody STCentroid(), która wyznacza środek obiektu, jest nią zawsze punkt. Obliczenia wykonane zostały dla obiektów pokazanych w lewej części rysunku 7.7. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 1 1, 2 3, 3 3))', 0); SELECT @f.STCentroid().ToString(), @g.STCentroid().ToString(), @h.STCentroid().ToString(); SELECT @f.STCentroid() UNION ALL SELECT @g.STCentroid() UNION ALL SELECT @h.STCentroid(); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h POINT (2 2.3333333333333335) POINT (1 1) POINT (0.33333333333333337 1)
W przypadku innej grupy metod wynikiem ich działania jest pojedyncza wartość, skalar. Przykładem może być metoda STArea(), która wyznacza pole powierzchni obiektu. Tutaj ponownie posłużono się obiektami pokazanymi w lewej części rysunku 7.7.
Rozdział 7. Typy złożone
343
DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 1 1, 2 3, 3 3))', 0); SELECT @f.STArea(), @g.STArea(), @h.STArea(); 1 4 1
Według podobnych zasad działa metoda STLength(), która wyznacza długość lub obwód obiektu. Tym razem użyte zostały: wielokąt (kwadrat), punkt oraz łamana, które pokazano na rysunku 7.8. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POINT(1 1)', 0); SET @f = geometry::STGeomFromText('LINESTRING(0 0, 2 2, 0 2, 2 0)', 0); SELECT @f.STLength (), @g.STLength (), @h.STLength (); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h 7.65685424949238
8 0
Rysunek 7.8. Obiekty użyte w przykładzie wyznaczającym długość lub obwód obiektów z zastosowaniem metody STLength()
Inny przykład to metoda STNumPoints(), która zwraca liczbę punktów (wierzchołków) wchodzących w skład obiektu. Przykład zrealizowany został na podstawie kształtów przedstawionych na rysunku 7.8. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POINT(1 1)', 0); SET @f = geometry::STGeomFromText('LINESTRING(0 0, 2 2, 0 2, 2 0)', 0); SELECT @f.STNumPoints(), @g.STNumPoints(), @h.STNumPoints(); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h 4 5 1
344
MS SQL Server. Zaawansowane metody programowania
Parametrami metod dla danych geometrycznych mogą być również zmienne ściśle określonego typu. Przykładem może być STDistance(), która zwraca najmniejszą odległość między punktem geometrii, dla której jest wykonywana, a punktem w drugiej geometrii, która stanowi jego parametr. Metoda ta jest symetryczna, czyli zamiana miejsc obiektu i parametru nie zmienia wyniku. Przykład został zrealizowany dla kształtów przedstawionych na rysunku 7.7. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 2 3, 1 1, 3 3))', 0); SELECT @g.STDistance(@h), @g.STDistance(@f), @h.STDistance(@f); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h 0 0 0
Poza wartościami rzeczywistymi metody takiego rodzaju mogą zwracać liczby całkowite ze zbioru {0, 1}. Taki wynik może być traktowany jako wartość logiczna, przy czym należy podkreślić, że formalnie zmienne tego typu w T-SQL nie występują. Przykładem może być metoda STOverlaps(), która zwraca 1, jeżeli figura pokrywa w całości drugi kształt, w przeciwnym razie zwraca 0. Należy zwrócić uwagę, że działanie tej metody nie jest zwrotne. Analogicznie przykład zrealizowano dla kształtów przedstawionych na rysunku 7.7. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 2 3, 1 1, 3 3))', 0); SELECT @g.STOverlaps(@h), @g.STOverlaps(@f), @h.STOverlaps(@f); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h 0 1 0
Według takich samych zasad działa metoda STTouches(), która zwraca 1, jeżeli figura ma wspólne wierzchołki lub krawędzie z drugą figurą (kształty się dotykają), w przeciwnym razie zwraca 0. Jeśli poza wierzchołkami lub krawędziami kształty mają wspólny fragment wnętrza, zwracane jest również 0. Użyte w przykładowym skrypcie geometrie przedstawia rysunek 7.9. Ta metoda w przeciwieństwie do poprzedniej jest zwrotna, czyli nie zależy od miejsca występowania atrybutów. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; DECLARE @e geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0);
Rozdział 7. Typy złożone
345
SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 2 3, 1 1, 3 3))', 0); SET @e = geometry::STGeomFromText('POLYGON((2 2, 3 1, 2 0, 2 2))', 0); SELECT @g.STTouches(@h), @g.STTouches(@f), @h.STTouches(@f), @g.STTouches(@e); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h UNION ALL SELECT @e 0 0 1 1
Rysunek 7.9. Graficzna prezentacja figur użytych do zilustrowania działania metody STTouches()
Innym rodzajem metod są takie, których parametrem jest kształt i które zwracają wartość w postaci typu graficznego. Realizują one najczęściej operacje na zbiorach, a przykładem może być STIntersection(), która wyznacza część wspólną obiektów. Ilustrację graficzną kształtów wykorzystanych w przykładowym skrypcie oraz wyników operacji zawiera rysunek 7.10. Należy zaznaczyć, że metoda ta jest zwrotna. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 2 3, 1 1, 3 3))', 0); SELECT @f.STIntersection(@g).ToString(), @g.STIntersection(@h).ToString(), @h.STIntersection(@f).ToString(); SELECT @f.STIntersection(@g) UNION ALL SELECT @g.STIntersection(@h) UNION ALL SELECT @h.STIntersection(@f) SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h POLYGON ((1 1, 2 2, 1.5 2, 1 1))
POLYGON ((0 0, 1 1, 0 2, 0 0)) POINT (1 1)
346
MS SQL Server. Zaawansowane metody programowania
Rysunek 7.10. Graficzna prezentacja części wspólnych kształtów zwracanych przez metodę STIntersection() obiektów typu geometry pokazanych na rysunku po stronie lewej
Podobnie działa metoda STDifference(), która wyznacza różnicę dwóch obiektów. Nie jest ona zwrotna. Skutek działania skryptu prezentuje rysunek 7.11. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 2 3, 1 1, 3 3))', 0); SELECT @f.STDifference(@g).ToString(), @f.STDifference(@h).ToString(), @g.STDifference(@h).ToString(), @g.STDifference(@f).ToString(); SELECT @f.STDifference(@g) UNION ALL SELECT @f.STDifference(@h) UNION ALL SELECT @g.STDifference(@h) UNION ALL SELECT @g.STDifference(@f); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h POLYGON ((1.5 2, 2 2, 3 3, 2 3, 1.5 2)) POLYGON ((1 1, 3 3, 2 3, 1 1)) POLYGON ((0 0, 2 0, 2 2, 0 2, 1 1, 0 0)) POLYGON ((0 0, 2 0, 2 2, 1 1, 1.5 2, 0 2, 0 0))
Możemy wskazać na inne metody, które trudno przypisać do określonej grupy. Przykładem jest AsGml(), która zwraca opis obiektu w znacznikowym języku GML (Geography Markup Language). W przykładzie zastosowano ją dla obiektów, które pokazano na rysunku 7.9. DECLARE @g geometry; DECLARE @f geometry; DECLARE @h geometry; DECLARE @e geometry; SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0);
Rozdział 7. Typy złożone
347
Rysunek 7.11. Graficzna prezentacja części wspólnych kształtów zwracanych przez metodę STDifference() obiektów typu geometry pokazanych na rysunku po stronie lewej SET @h = geometry::STGeomFromText('POLYGON((0 0, 1 1, 0 2 , 0 0))', 0); SET @f = geometry::STGeomFromText('POLYGON((3 3, 2 3, 1 1, 3 3))', 0); SET @e = geometry::STGeomFromText('POLYGON((2 2, 3 1, 2 0, 2 2))', 0); SELECT @e.AsGml(), @f.AsGml(), @g.AsGml(), @h.AsGml(); SELECT @f UNION ALL SELECT @g UNION ALL SELECT @h UNION ALL SELECT @e 2 2 3 1 2 0 2 2 3 3 2 3 1 1 3 3 0 0 2 0 2 2 0 2 0 0 0 0 1 1 0 2 0 0
Ważną metodą jest MakeValid(), która zmienia niewłaściwą instancję figury na instancję zgodną z wymaganiami OGC (Open Geospatial Consortium). W przykładzie zastosowano ją do łamanej, którą zdefiniowano w taki sposób, że wspólny wierzchołek został wymieniony dwa razy. Daje to skutek taki, jak gdyby dwa odcinki ją definiujące były rysowane w przeciwnych kierunkach. Taka definicja nie jest zgodna ze standardem. W wyniku działania metody MakeValid() otrzymujemy łamaną rysowaną w jednym kierunku. Stan wyjściowy i rezultat przedstawiono na rysunku 7.12. DECLARE @g geometry SET @g= geometry::STGeomFromText('LINESTRING(1 1, 1 4, 5 4, 1 4)', 0); SELECT @g.ToString(), @g.MakeValid().ToString() SELECT @g LINESTRING (1 1, 1 4, 5 4, 1 4)
LINESTRING (5 4, 1 4, 1 1)
348
MS SQL Server. Zaawansowane metody programowania
Rysunek 7.12. Graficzna prezentacja działania metody MakeValid() dla wyjściowej łamanej pokazanej po lewej stronie
Nie przedstawiłem wszystkich dostępnych dla typu spatial metod, koncentrując się na przedstawieniu ich grup i charakterystycznych funkcjonalności. Postaram się, aby pełniejszy ich obraz był omówiony w jednej z kolejnych planowanych publikacji. Muszę przypomnieć, że drugim z typów grafiki wektorowej jest geography, który posiada takie same metody jak dotąd omawiany typ geometry. Różnica polega na tym, że geography przedstawia obiekty umieszczone na powierzchni Ziemi, a współrzędne reprezentują szerokość i długość geograficzną wyrażoną w stopniach. Drugą istotną różnicą jest drugi z parametrów używany podczas definiowania zmiennej. W geometrii mógł być utożsamiany z numerem warstwy, dla geografii jest on określony jednoznacznie jako SRID (Spatial Reference System Identifier). System identyfikacji przestrzennej został zdefiniowany przez European Petroleum Survey Group (EPSG — http://www.epsg.org/) i jest powszechnie stosowany w kartografii i geodezji. Podstawowe wymagania dotyczące identyfikatora są następujące: każdy obiekt geograficzny musi mieć określony SRID; SRID określa system współrzędnych oraz przesunięcie czasu; obiekty na jednym rysunku mogą mieć różne wartości SRID; aby wykonać operacje na dwóch kształtach, muszą mieć one ten sam SRID.
Najczęściej wykorzystywaną wartością identyfikatora SRID jest 4326. Aby sprawdzić dane dotyczące identyfikatorów dostępnych w MS SQL Server, należy odpytać tabelę systemową SELECT * FROM SYS.SPATIAL_REFERENCE_SYSTEMS
Fragment danych zwracanych przez zapytanie zawiera tabela 7.15. Ponieważ ręczne wprowadzenie rzeczywistych obiektów geograficznych, np. granic administracyjnych państw, województw, miast, jest ze względu na ich złożony kształt kłopotliwe i bardzo pracochłonne, można skorzystać z gotowych, darmowych danych dostępnych w sieci WWW. Najczęściej są one prezentowane w postaci plików *.shp i mogą być pobrane z wielu lokalizacji, np. http://www.general-files.com/ download/source/gs5ccc83f7hdi0. Aby dokonać ich konwersji do typów spatial MS SQL Server, można zastosować darmowy program o nazwie Shape2SQL.exe, dostępny
Rozdział 7. Typy złożone
349
Tabela 7.15. Informacje o identyfikatorach przestrzennych SRID spatial_ reference_id
authority_ name
authorized_ spatial_ reference_id
well_known_text
unit_of_ measure
unit_ conversion_ factor
metre
1
4120
EPSG
4120
GEOGCS["Greek", DATUM["Greek", ELLIPSOID["Bessel 1841", 6377397.155, 299.1528128]], PRIMEM["Greenwich", 0], UNIT["Degree", 0.0174532925199433]]
…
…
…
…
…
…
4326
GEOGCS["WGS 84", DATUM["World Geodetic System 1984", ELLIPSOID["WGS 84", 6378137, 298.257223563]], PRIMEM["Greenwich", 0], UNIT["Degree", 0.0174532925199433]]
metre
1
4326
EPSG
pod adresem http://www.sharpgis.net/page/sql-server-2008-spatial-tools.aspx, a stanowiący część pakietu oprogramowania o nazwie SqlSpatialTools. Narzędzie to jest intuicyjne w obsłudze i nie wymaga specjalnego instruktażu. Wystarczy zdefiniować połączenie z serwerem bazy danych oraz wskazać źródłowy plik. Według takiego schematu zostały pozyskane i przetworzone dane opisujące podział administracyjny Polski na poziomie granic kraju, województw, powiatów. Podział na województwa został zapisany w tabeli POL_adm1, zgodnej z nazwą pliku, z wykorzystaniem domyślnych nazw kolumn, a następnie odpytano ją o zawartość kolumn zawierających nazwę oraz granice. SELECT VARNAME_1 ,geom FROM POL_adm1
Wykonanie tego zapytania przynosi, w zależności od wyboru opcji, jeden z przedstawionych na rysunku 7.13 sposobów prezentacji. Różnice spowodowane są tym, iż rzeczywiste dane rzutowane są na kulę, natomiast na rysunku muszą zostać przedstawione na płaszczyźnie. Musimy więc wybrać jedno z 4 dostępnych w środowisku MS SQL Serwer odwzorowań kartograficznych. Ponieważ operacje na danych geograficznych stanowią prawie wierne odwzorowanie operacji na danych geometrycznych, nie będą one szerzej omawiane.
7.4. Typy użytkownika CLR Do tej pory omówione zostały obiektowe typy danych wprowadzone do relacyjnego serwera bazy danych, jakim jest MS SQL Server, a opracowane przez jego twórców. Od wersji 2005 istnieje możliwość zdefiniowania obiektów przez programistę serwera [104] – [106]. Jest to sposób na wprowadzenie obiektowości do relacyjnej bazy danych. Aby tego dokonać, należy zaprogramować strukturę po stronie dowolnego języka wysokiego rzędu zaimplementowanego na platformie .NET, a następnie skompilować ją do postaci biblioteki *.dll. W przykładzie, ze względu na jego popularność,
350
MS SQL Server. Zaawansowane metody programowania
Equirectangular (równokątne, wiernokątne lub konforemne)
Mercatora (walcowe równokątne)
Robinsona
Bonne’a
Rysunek 7.13. Sposoby przedstawienia danych geograficznych w zależności od wyboru trybu odwzorowania kartograficznego
zastosowano język C# [110]. Pracę rozpoczynamy od uruchomienia Microsoft Visual Studio, utworzenia nowego projektu dla języka C# typu Class Library. W oknie dialogowym warto zmienić (skrócić) domyślną ścieżkę oraz nadać nazwę, np. udt. Jeśli korzystamy z nowszych wersji platformy, należy również zmienić środowisko uruchomieniowe na nie wyższe niż .NET Framework 3.5, gdyż jest to najwyższa realizacja obsługiwana z poziomu serwera bazy danych. Następnie piszemy kod struktury, która będzie reprezentowała punkt w dwuwymiarowej przestrzeni euklidesowej [107] [108]. Ponieważ jest on dość rozbudowany, zostanie pokazany i opisany w częściach. Pierwszym elementem jest import przestrzeni nazw, który pozwala na posługiwanie się krótkimi nazwami metod zamiast ich kwalifikowanymi odpowiednikami. Wpływa to na poprawę przejrzystości i tak dość złożonego kodu. Poza podstawową przestrzenią System zaimportowane zostały przestrzeń System.Data oraz jej dziecko SqlTypes, które odpowiadają za używanie typów danych: złożonych po stronie C# oraz typów zgodnych z implementacją po stronie SQL. Przestrzeń Microsoft.SqlServer.Server pozwala na używanie dyrektyw kompilatora powodujących utworzenie kodu zgodnego ze specyfikacją odpowiednich obiektów po stronie bazy danych. using using using using
System; System.Data; System.Data.SqlTypes; Microsoft.SqlServer.Server;
Rozdział 7. Typy złożone
351
using System.Text;
Kolejny fragment stanowią dyrektywy dla kompilatora, z których pierwsza wskazuje na serializację, czyli zdolność do zapisu obiektu w strumieniu danych i do odczytu obiektu ze strumienia, kolejna pokazuje, że docelowo struktura ma być interpretowana jako typ danych użytkownika. Druga z nich wymaga podania przynajmniej dwóch parametrów. Wskazanie natywnego sposobu formatowania oznacza, że będą używane tylko typy proste oraz metoda sprawdzania poprawności wprowadzanych danych; w przykładzie będzie miała ona nazwę SprawdzPunkt. Pozostałe parametry są opcjonalne. W przykładzie wskazano, że typ jest uporządkowany binarnie. [Serializable] [SqlUserDefinedType(Format.Native, IsByteOrdered = true, ValidationMethodName = "SprawdzPunkt")]
Tworzona struktura, która docelowo ma reprezentować typ obiektowy po stronie bazy danych, musi dziedziczyć po interfejsie INullable, aby dopuszczalne było stosowanie metody IsNull. Jest to uzasadnione tym, że każda zmienna powinna mieć możliwość przyjmowania wartości NULL, a w związku z tym powinna istnieć możliwość zweryfikowania takiego stanu. Następnie deklarujemy zmienne prywatne: pierwsza ma zwracać prawdę w przypadku pozytywnej weryfikacji wartości NULL obiektu, dwie pozostałe mają reprezentować współrzędne punktu w dwuwymiarowej przestrzeni euklidesowej. public struct Punkt : INullable { private bool is_Null; private Int32 _x; private Int32 _y;
W kolejnym kroku definiujemy metodę IsNull, która będzie zwracała poprzednio zadeklarowaną zmienną logiczną. Następnie w metodzie Null inicjujemy nową instancję obiektu Punkt oraz ustawiamy właściwość obiektu reprezentowaną przez zmienną prywatną is_Null na wartość true. public bool IsNull { get {return (is_Null); } } public static Punkt Null { get { Punkt pt = new Punkt(); pt.is_Null = true; return pt; } }
Teraz następuje przeciążenie metody ToString, w której w przypadku wartości null obiektu zwracany jest napis NULL. W przeciwnym razie następuje złożenie napisu reprezentującego współrzędne punktu. W tym celu tworzona jest nowa instancja
352
MS SQL Server. Zaawansowane metody programowania
obiektu StringBuilder, a następnie za pomocą metody Append dodawane są napisy reprezentujące kolejno: współrzędną X, separator, którym jest przecinek, oraz współrzędną Y. public override string ToString() { if(this.IsNull) return "NULL"; else { StringBuilder builder = new StringBuilder(); builder.Append(_x); builder.Append(","); builder.Append(_y); return builder.ToString(); } }
Metoda Parse ma za zadanie przekształcić wprowadzane jako napis dane reprezentujące punkt na postać obiektu. Zastosowana została dyrektywa SqlMethod z atrybutem OnNullCall ustawianym na false, co powoduje, że nie będzie ona wywoływana dla wartości null obiektu. Pomimo to w metodzie sprawdzono fakt przyjęcia takiej wartości i zwrócono właściwą wartość. W przypadku przeciwnym ustanowiono nową instancję obiektu Punkt. Następnie przekazywany przez parametr łańcuch opisujący punkt jest dzielony za pomocą metody Split w miejscu pojawienia się separatora i jest zapisywany do dwóch komórek macierzy xy. Zawartości kolejnych komórek są sprawdzane i zapisywane do zmiennych reprezentujących właściwe współrzędne. Na koniec, jeśli metoda walidująca nie zwróci wartości true, ustawiany jest wyjątek z właściwym komunikatem. [SqlMethod(OnNullCall = false)] public static Punkt Parse(SqlString s) { if (s.IsNull) return Null; Punkt pt = new Punkt(); string[] xy = s.Value.Split(",".ToCharArray()); pt.X = Int32.Parse(xy[0]); pt.Y = Int32.Parse(xy[1]); if (!pt.SprawdzPunkt()) throw new ArgumentException("Invalid XY coordinate values."); return pt; }
Teraz musimy ustawić wartości współrzędnych jako właściwości typu obiektowego z wykorzystaniem akcesorów get i set. Pierwszy z nich jest odpowiedzialny za zwrócenie właściwej wartości poleceniem return, drugi za pomocą prywatnego parametru value. Dla obu współrzędnych metody są analogiczne i poza wymienionym przekazaniem wartości do i od zmiennej dokonujemy sprawdzenia poprawności metodą SprawdzPunkt, a w przypadku wystąpienia błędu ustawiamy wyjątek z właściwym komunikatem. public Int32 X { get
Rozdział 7. Typy złożone
353
{ return this._x; } set { Int32 temp = _x; _x = value; if (!SprawdzPunkt()) { _x = temp;throw new ArgumentException("Zła współrzędna X."); } } } public Int32 Y { get { return this._y; } set { Int32 temp = _y; _y = value; if (!SprawdzPunkt()) { _y = temp; throw new ArgumentException("Zła współrzędna X."); } } }
Ostatnim obowiązkowym elementem jest implementacja metody sprawdzającej poprawność danych, której nazwa została podana w dyrektywie SqlUserDefinedType jako wartość atrybutu ValidationMethodName. Minimalną zawartością tej metody mogłaby być instrukcja return true. Tak też można by postąpić w naszym przypadku, ale aby pokazać niepustą walidację, sprawdzono, czy punkt leży w pierwszej ćwiartce układu współrzędnych. Jeśli warunek jest prawdziwy, zwracana jest wartość true, w przeciwnym razie zwracana jest wartość false. private bool SprawdzPunkt() { if ((_x >= 0) && (_y >= 0)) { return true; } else { return false; } }
Na tym moglibyśmy zakończyć definiowanie struktury, która musi zawierać: właściwości opisujące elementarne cechy obiektu (w naszym przypadku współrzędne punktu), metody odpowiedzialne za sprawdzenie, czy punkt ma wartość null, i przetworzenie napisu na postać obiektową i odwrotnie, oraz metody sprawdzające poprawność
354
MS SQL Server. Zaawansowane metody programowania
wprowadzonych danych. Jednak możliwe i celowe jest opracowanie metod pozwalających na wykonywanie operacji na obiekcie [103]. Podstawową operacją jest wyznaczenie odległości pomiędzy punktem reprezentowanym przez strukturę a drugim, danym jako parametr, zgodnie z zależnością: d
x x y y 2
i
j
i
j
2
.
Należy zaznaczyć, że punkt jest przekazywany do metody w postaci dwóch jego współrzędnych. Ciało metody zawiera tylko implementację wzoru pokazanego powyżej. [SqlMethod(OnNullCall = false)] public Double OdlegloscOdXY(Int32 iX, Int32 iY) { return Math.Sqrt(Math.Pow(iX - _x, 2.0) + Math.Pow(iY - _y, 2.0)); }
Jako uzupełnienie zrealizowano dwie metody obliczające odległość od początku układu współrzędnych oraz odległość od innego punktu, tym razem reprezentowanego przez parametr typu zgodnego ze zbudowaną strukturą. W obu przypadkach w ciele odwołano się do poprzednio utworzonej metody OdlegloscOdXY z odpowiednimi wartościami parametrów. W pierwszym przypadku oba mają wartość 0, w drugim są uzyskiwane przy zastosowaniu właściwości pozwalających na otrzymanie właściwych współrzędnych z obiektu reprezentującego punkt. [SqlMethod(OnNullCall = false)] public Double Odleglosc() { return OdlegloscOdXY(0, 0); } [SqlMethod(OnNullCall = false)] public Double OdlegloscOd(Punkt pFrom) { return OdlegloscOdXY(pFrom.X, pFrom.Y); } }
Należy zauważyć, że zdecydowano się na reprezentację w klasie współrzędnych w postaci liczb całkowitych, podczas gdy naturalne wydaje się stosowanie liczb rzeczywistych. Postępowanie takie należy tłumaczyć chęcią zapewnienia niezależności kodu od sposobu reprezentacji liczb tego drugiego rodzaju. W zależności od ustawień narodowych możliwe jest stosowanie jako separatora dziesiętnego zarówno kropki, jak i przecinka. W tym drugim przypadku konieczna byłaby zamiana separatora między współrzędnymi na inny niż przecinek, np. średnik. Natomiast bez względu na ten element należałoby odwołać się do przestrzeni nazw Globalization, aby właściwie odczytać dane rzeczywiste, co komplikuje kod. Jeśli Czytelnik ma ustawiony separator dziesiętny na kropkę, łatwo przekształci powyższy kod na dane rzeczywiste; w przeciwnym razie będzie musiał się sporo nagłowić. W tym miejscu kończymy budowanie obiektu. Pozostaje tylko skompilowanie kodu przez wybranie opcji Build Solution lub Rebuild Solution z pozycji menu Build. Aby skorzystać ze struktury po stronie MS SQL Server, musimy teraz powrócić do Management Studio. Proces tworzenia typu obiektowego jest dwuetapowy. W pierwszym
Rozdział 7. Typy złożone
355
etapie musimy utworzyć obiekt pośredniczący ASSEMBLY, reprezentujący bibliotekę po stronie serwera bazy danych. W poleceniu CREATE ASSEMBLY najważniejszym elementem jest klauzula FROM, w której wskazujemy nazwę używanej biblioteki. Należy podkreślić, że musimy posługiwać się pełną nazwą, zawierającą bezwzględną ścieżkę. Ponadto w klauzuli AUTHORIZATION wskazujemy, do jakiej grupy musi należeć użytkownik, który będzie mógł posługiwać się tym obiektem. W klauzuli WITH PERMISSION_SET wskazujemy zestaw uprawnień, jakie otrzymuje obiekt. Następnie za pomocą polecenia CREATE TYPE możemy utworzyć typ użytkownika, podając jego nazwę, a w klauzuli EXTERNAL NAME musimy podać nazwę kwalifikowaną obiektu, która składa się z rozdzielonych kropką: nazwy ASSEMBLY oraz nazwy struktury w bibliotece *.dll. Należy zauważyć, że pojedyncza biblioteka może zawierać wiele struktur, co oznacza, że jeden obiekt ASSEMBLY może posłużyć do utworzenia wielu typów użytkownika. Prezentowany skrypt rozpoczyna się od poleceń usuwających wcześniej utworzone obiekty TYP i ASSEMBLY, co przy pierwszym jego wykonywaniu jest zbędne. Trzeba jednak zauważyć, że aby usunąć ASSEMBLY, należy najpierw usunąć wszystkie obiekty, które się do niego odwołują, dlatego kolejność kasowania jest obowiązkowa. DROP Type Punkt GO DROP ASSEMBLY [Punkt_a] GO CREATE ASSEMBLY [Punkt_a] AUTHORIZATION [dbo] FROM 'C:\hurtownia_ap\udt\udt\bin\Debug\udt.dll' WITH PERMISSION_SET = SAFE GO CREATE TYPE dbo.Punkt EXTERNAL NAME Punkt_a.Punkt;
Możemy teraz spróbować wykorzystać utworzony typ użytkownika np. jako pole w tabeli. W przykładzie utworzona została tabela Punkty, która zawiera dwa pola. Pierwsze jest automatycznie inkrementowanym kluczem głównym, drugie jest typu Punkt. Zasilenie pola odbywa się z zastosowaniem jednej z dwóch funkcji konwertujących dane — CONVERT lub CAST — ponieważ dane wprowadzane są za pomocą napisu zawierającego współrzędne separowane przecinkiem. DROP TABLE Punkty GO CREATE TABLE dbo. Punkty (ID int IDENTITY(1,1) PRIMARY KEY, PunktyXY Punkt) GO INSERT INTO Punkty VALUES (CONVERT(Punkt, '3,4')); INSERT INTO Punkty VALUES (CONVERT(Punkt, '1,5')); INSERT INTO Punkty VALUES (CAST ('1,99' AS Punkt));
Możliwe jest, że podczas próby wykorzystania typu użytkownika, a w przypadku powyższego skryptu podczas wstawiania wiersza, pojawi się komunikat o błędzie: Msg 6263, Level 16, State 1, Line 1 Execution of user code in the .NET Framework is disabled. Enable "clr enabled" configuration option.
356
MS SQL Server. Zaawansowane metody programowania
Wynika to z faktu, że w stanie domyślnym używanie typów użytkownika jest zablokowane. Uzasadnieniem takiego działania jest to, że ponieważ po stronie języka wyższego rzędu dostępne są wszystkie operacje (np. na systemie plików i folderów), które mogą być groźne, dopuszczenie do ich użycia wymaga jawnej ingerencji operatora o wystarczająco wysokich uprawnieniach [23] – [26]. Programistycznie przestawienie parametru konfiguracyjnego może być zmienione za pomocą wbudowanej procedury systemowej sp_configure, a utrwalenie zmiany wymaga wykonania polecenia RECONFIGURE. sp_configure 'clr enabled', 1; GO RECONFIGURE; GO
Taką samą akcję możemy zrealizować, wykorzystując narzędzia wizualne. Klikając prawym przyciskiem myszy pozycję Server hierarchicznej struktury (pokazanej po prawej stronie na rysunku 2.6), wyświetlamy menu podręczne. Przy pozycji Facet należy wybrać ustawienie Surface Area Configuration; na liście Facet properties trzeba wybrać pozycję ClrIntegrationEnable i ustawić dla niej wartość True (rysunek 7.14).
Rysunek 7.14. Ustawienie parametru odpowiedzialnego za przetwarzanie obiektów CLR
Możemy teraz sprawdzić zawartość tabeli Punkty. Jak widać, podstawową postacią przechowywania danych reprezentujących typ użytkownika jest postać binarna (tabela 7.16), która niestety nie jest czytelna. SELECT ID, PunktyXY FROM Punkty
Rozdział 7. Typy złożone
357
Tabela 7.16. Zawartość tabeli Punkty ID
PunktyXY
1
0x008000000380000004
2
0x008000000180000005
3
0x008000000180000063
Aby poprawić czytelność uzyskiwanych wyników, możemy dokonać konwersji danych binarnych do postaci napisu. Możemy w tym celu użyć przeciążonej na poziomie definicji struktury metody ToString albo jednej z dwóch wbudowanych funkcji konwersji — CONVERT lub CAST — które na niskim poziomie odwołują się i tak do opracowanej przez nas metody. Skutek takiego działania zawiera tabela 7.17. SELECT ID, PunktyXY.ToString() AS PunktyXY FROM Punkty; GO SELECT ID, CAST(PunktyXY AS varchar) FROM Punkty; GO SELECT ID, CONVERT(varchar, PunktyXY) FROM Punkty;
Tabela 7.17. Zawartość tabeli Punkty po konwersji typu użytkownika do postaci napisu ID
PunktyXY
1
3,4
2
1,5
3
1,99
Metody i właściwości mogą być stosowane nie tylko do zawartości pól, ale również do zmiennych typu użytkownika. Deklaracja zmiennych tego rodzaju nie różni się od deklaracji zmiennych wbudowanych. Przypisanie może się odbyć zarówno przez zastosowanie konwersji wartości z napisu, jak i przez wykorzystanie zapytania skalarnego, co pokazano w skrypcie. Na koniec użyta została metoda ToString do wyprowadzenia wartości zmiennej w czytelnej postaci (tabela 7.18). DECLARE @PunktXY Punkt; SET @PunktXY = (SELECT PunktyXY FROM Punkty WHERE ID = 2); SELECT @PunktXY.ToString() AS PunktXY;
Tabela 7.18. Skutek zastosowania metody ToString do zmiennej typu Punkt PunktXY 1,5
Pola typów obiektowych mogą nie tylko być wykorzystywane jako elementy listy wyświetlanych pól, ale również występować w innych miejscach zapytania. W przykładzie wykorzystano typ Punkt do określenia warunku filtrowania. Ponieważ współrzędne wprowadzono w postaci napisu, zastosowano wbudowaną funkcję konwersji.
358
MS SQL Server. Zaawansowane metody programowania
Należy zauważyć, że przy tak zdefiniowanym filtrze porównaniu poddawana jest wewnętrzna notacja binarna, co oznacza, że aby wyrażenie było prawdziwe, pierwsza współrzędna punktu musi być większa lub równa 2. Jeśli jest większa, druga współrzędna nie odgrywa roli; gdy jest równa, druga współrzędna musi być większa niż 2. Skutek wykonania zapytania przedstawia tabela 7.19. SELECT ID, PunktyXY.ToString() AS PunktyXY FROM Punkty WHERE PunktyXY > CONVERT(Punkt, '2,2');
Tabela 7.19. Zastosowanie konwersji oraz typu użytkownika w klauzuli filtrującej ID
PunktyXY
1
3,4
Lepszym przykładem zastosowania właściwości do budowania wyrażeń jest pokazane w przykładzie porównanie dwóch współrzędnych punktu i wyświetlenie tych rekordów, w których współrzędna Y jest większa niż X. Skutek wykonania tego zapytania pokazuje tabela 7.20. SELECT ID, PunktyXY.ToString() AS PunktyXY FROM Punkty WHERE PunktyXY.X < PunktyXY.Y;
Tabela 7.20. Zastosowanie właściwości typu użytkownika w wyrażeniu filtrującym ID
PunktyXY
1
3,4
2
1,5
3
1,99
Kolejny przykład, poza zastosowaniem właściwości prezentujących każdą ze współrzędnych, pokazuje metodę obliczającą dla każdego z punktów odległość od początku układu współrzędnych. Rezultat otrzymany po wykonaniu tego zapytania pokazuje tabela 7.21. SELECT ID, PunktyXY.X AS PunktX, PunktyXY.Y AS PunktY, PunktyXY.Odleglosc() AS OdlegloscOdPoczatku FROM Punkty;
Tabela 7.21. Zastosowanie właściwości i metody typu użytkownika w zapytaniu wybierającym ID
PunktX
PunktY
OdlegloscOdPoczatku
1
3
4
5
2
1
5
5,09901951359278
3
1
99
99,0050503762308
Kolejny przykład pokazuje zastosowanie metody wyznaczającej odległość punktu, na rzecz którego ona działa, od punktu danego parametrem. Ponieważ wybrano metodę, dla której parametr jest dany jako zmienna obiektowa, to aby można było do niej
Rozdział 7. Typy złożone
359
przekazać wartość daną jako napis, należy dokonać konwersji, stosując jedną z wbudowanych funkcji, np. CONVERT. Rezultat jest pokazany w tabeli 7.22. SELECT ID, PunktyXY.ToString() AS Pkt, PunktyXY.OdlegloscOd(CONVERT(Punkt, '1,99')) AS OdlegloscOdPunktu FROM Punkty;
Tabela 7.22. Zastosowanie metod typu użytkownika w zapytaniu wybierającym ID
Pkt
OdlegloscOdPunktu
1
3,4
95,0210502993942
2
1,5
94
3
1,99
0
Oczywiście liczba przykładów testujących zachowanie utworzonego obiektowego typu użytkownika może być znacznie większa. Poza tym, aby skrócić tworzony kod, zastosowano uproszczenia, co zmniejszyło funkcjonalność typu. Przede wszystkim w definicji współrzędnych zastosowano typ całkowity, a bardziej uzasadnione byłoby zastosowanie danych rzeczywistych. Możliwość stosowania różnych separatorów dziesiętnych w zależności od ustawień narodowych powoduje konieczność odróżnienia w kodzie, który z nich jest używany podczas zapisu do danych binarnych — metoda Parse. Taki sam zabieg wskazany jest przy tworzeniu metody ToString. Ponieważ często stosowanym separatorem dziesiętnym jest przecinek, należy zmienić separator rozdzielający współrzędne na inny, np. średnik lub spację, co wykorzystano we wbudowanym typie Spatial. Drugim niedociągnięciem jest sztywne zastosowanie dwuwymiarowego układu współrzędnych, podczas gdy bardzo często posługujemy się danymi trójwymiarowymi. Można sobie wyobrazić, że będziemy chcieli przechowywać punkt o n wymiarach. W takim przypadku do reprezentowania współrzędnych wygodne jest stosowanie listy, a zastosowanie typu złożonego wymaga opracowania własnych metod serializacji i deserializacji. Wprowadzenie tych zmian powoduje spory wzrost komplikacji kodu, dlatego zdecydowałem się, aby go tu nie prezentować. Stosowanie typów użytkownika niesie ze sobą dużo korzyści związanych z bardzo dużym wzrostem liczby przechowywanych i przetwarzanych struktur. Liczba ta jest ograniczona w zasadzie tylko pomysłowością programisty. Stosowanie typów użytkownika daje również dużą wydajność przetwarzania, co jest niepodważalną zasługą twórców serwera. Z wymienionych poprzednio względów zastosowanie CLR do tworzenia własnych funkcjonalności bazy danych stanowi materiał na oddzielną książkę, co jak sądzę, zostanie wkrótce przeze mnie zrealizowane.
7.5. Elementy proceduralne CLR Poza typami użytkownika, które wprowadzają obiektowość do relacyjnej bazy danych, mechanizm klas CLR pozwala na definiowanie innych elementów bazy danych. Pierwszym z nich jest procedura. Zdefiniujmy klasę pozwalającą na wyznaczenie reszty z dzielenia dwóch liczb. Pierwszym elementem jest dyrektywa kompilatora Microsoft.SqlServer.Server.SqlProcedure, określająca docelowy rodzaj obiektu po
360
MS SQL Server. Zaawansowane metody programowania
stronie MS SQL Server. Zastosowano nazwę klasyfikowaną, ponieważ nie dokonano importu właściwej przestrzeni nazw. Gdyby taka operacja została wykonana (using Microsoft.SqlServer.Server), wystarczyłoby podanie tylko ostatniego elementu nazwy. Zdefiniowana w tej klasie metoda statyczna ma trzy parametry: pierwszy wejściowy, reprezentujący dzielną typu zmiennoprzecinkowego dopuszczającego wartość null, drugi wejściowy, całkowitoliczbowy, reprezentujący dzielnik, oraz trzeci wyjściowy, przedstawiający wynik. Oczywiście z punktu widzenia bazy najwłaściwsze byłoby zastosowanie dla wszystkich parametrów typu zmiennoprzecinkowego z dopuszczalną wartością null. Zastosowanie różnych typów ma w przykładzie odgrywać rolę ilustracyjną. Ciało klasy stanowi instrukcja wyznaczająca wynik. using System; public class wynik { [Microsoft.SqlServer.Server.SqlProcedure] public static void resztap(double? a, int b, out double wyn) { wyn = (double)a % b; } }
Pozostaje utworzenie typu wiążącego ASSEMBLY, dla którego podajemy nazwę, tryb autoryzacji w postaci kwalifikowanej, nazwę biblioteki dll oraz tryb dostępu. CREATE ASSEMBLY funkcja AUTHORIZATION [dbo] FROM 'C:\...\funkcja.dll' WITH PERMISSION_SET = SAFE
W ostatnim kroku tworzymy procedurę T-SQL. Na liście jej parametrów następuje rzutowanie typów występujących po stronie bazy danych na typy klasy C# oraz wskazanie przez nazwę kwalifikowaną nazwy zewnętrznej metody. Nazwa ta składa się z nazwy obiektu ASSEMBLY, nazwy klasy C# i nazwy metody. CREATE Procedure ResztaP(@a float, @b int, @c float OUTPUT) AS EXTERNAL NAME funkcja.wynik.resztap
Tak utworzona procedura może być wykorzystywana na takich samych zasadach, jak gdyby była napisana z użyciem składni T-SQL. Według podobnych zasad możemy utworzyć funkcję zwracającą skalar. Konieczna jest zamiana dyrektywy kompilatora na SqlFunction, ustanowienie metody na zwracającą wartość, np. double, oraz wstawienie do ciała instrukcji return zwracającej policzoną wartość. using System; public class wynik { [Microsoft.SqlServer.Server.SqlFunction] public static double reszta(double? a, int b) { double wyn = (double)a % b; return wyn; } }
Rozdział 7. Typy złożone
361
Jeśli funkcja już istniała, a zostało zmodyfikowane ciało metody ją opisującej, to konieczne jest jej usunięcie, usunięcie elementu ASSEMBLY, który ją zawiera, a następnie ponowne utworzenie tych obiektów w odwrotnej kolejności. Porządek wykonywania operacji jest obowiązkowy, ponieważ nie można usunąć obiektu bazy, który posiada obiekty potomne. DROP FUNCTION reszta GO DROP ASSEMBLY funkcja GO CREATE ASSEMBLY [funkcja] AUTHORIZATION [dbo] FROM 'C:\...\funkcja.dll' WITH PERMISSION_SET = SAFE GO CREATE FUNCTION Reszta(@a float, @b int) RETURNS float AS EXTERNAL NAME funkcja.wynik.reszta
Należy zauważyć, że funkcje i procedury mogą być wykorzystywane zarówno w T-SQL, jak i po stronie Analysis Services. W pierwszym przypadku wskazane jest, aby parametry były typów, które mogą przyjąć wartość null, ale stosowanie takich, które tej cechy nie mają, nie jest błędem. Natomiast dla narzędzi analitycznych stosowanie typów zezwalających na null jest zabronione, co wynika z zasad prowadzenia tego typu przetwarzania [5]. Możliwe jest umieszczenie definicji wielu metod w jednej klasie, co zostało pokazane w kolejnym przykładzie. Poza utworzonymi do tej pory dodana została definicja metody, która ma być enkapsulowana do funkcji zwracającej tabelę. Jej zadaniem jest wyświetlenie zawartości wskazanego parametrem dziennika systemowego. Tym razem dyrektywa dla kompilatora SqlFunction, wskazująca, że jest tworzona funkcja, musi być uzupełniona o wartość atrybutu, który podaje nazwę metody odpowiedzialnej za zasilenie pojedynczego wiersza wynikowej tabeli. Pierwszą metodą jest Dziennik, zwracająca kolekcję wyliczaną IEnumerable. W jej ciele zawarta jest instrukcja zwracająca nową instancję obiektu takiego typu. Zasadnicza część przetwarzania odbywa się w metodzie zasilającej pojedynczy rekord, element kolekcji. Musi mieć ona nazwę zgodną ze zdefiniowaną w dyrektywie poprzedzającej ciała obu metod. Pierwszy parametr wejściowy jest zawsze typu Object i przekazuje do ciała informację o tym, jaki element ma być przetwarzany. Pozostałe parametry, wyjściowe, reprezentują kolejne pola w tabeli wynikowej. Ich liczba, nazwy i typy są uzależnione od tego, jaką informację zamierzamy uzyskać. Pierwsza linia ciała tworzy nową instancję obiektu EventLogEntry przez rzutowanie na właściwy typ wejściowego obiektu. Następne linie ciała, odwołując się do metod zmiennej tego typu, zasilają kolejne pola pojedynczego rekordu. using System; using System.Data.Sql; using Microsoft.SqlServer.Server; using System.Collections; using System.Data.SqlTypes; using System.Diagnostics; public class wynik { [Microsoft.SqlServer.Server.SqlFunction] public static double reszta(double? a, int b)
362
MS SQL Server. Zaawansowane metody programowania { double wyn = (double)a % b; return wyn; } [Microsoft.SqlServer.Server.SqlProcedure] public static void resztap(double? a, int b, out double wyn) { wyn = (double)a % b; } [SqlFunction(FillRowMethodName = "Wypelnij")] public static IEnumerable Dziennik(String logname) { return new EventLog(logname).Entries; } public static void Wypelnij(Object obj, out SqlDateTime CzasWpisu, out SqlChars komunikat, out SqlChars kategoria, out long IdInstancji) { EventLogEntry eventLogEntry = (EventLogEntry)obj; CzasWpisu = new SqlDateTime(eventLogEntry.TimeWritten); komunikat = new SqlChars(eventLogEntry.Message); kategoria = new SqlChars(eventLogEntry.Category); IdInstancji = eventLogEntry.InstanceId; } }
Możemy teraz zgodnie ze znanym schematem utworzyć ASSEMBLY, a następnie funkcję tabelaryczną, dla której definiujemy tabelę wynikową o polach zgodnych co do typu z polami zadeklarowanymi w kolekcji CLR. W przykładzie nazwy pól w obu miejscach są zgodne, ale nie jest to warunek konieczny. CREATE ASSEMBLY funkcja FROM 'C:\...\funkcja.dll' WITH PERMISSION_SET = SAFE GO CREATE FUNCTION CzytajDziennik(@logname nvarchar(100)) RETURNS TABLE (CzasWpisu datetime, Komunikat nvarchar(4000), Kategoria nvarchar(4000), IdInstancji bigint) AS EXTERNAL NAME funkcja.wynik.Dziennik
Jeśli w takim stanie spróbujemy wykonać zapytanie odwołujące się do tej funkcji: SELECT TOP 100 * FROM dbo.CzytajDziennik(N'Security') AS T
Otrzymamy komunikat informujący o niewystarczających uprawnieniach. Jeśli nawet podczas tworzenia lub modyfikowania zmienimy uprawnienia na EXTERNAL_ACCESS lub UNSAFE, sytuacja się powtórzy. CREATE ASSEMBLY funkcja FROM 'C:\...\funkcja.dll' WITH PERMISSION_SET = EXTERNAL_ACCESS --lub = UNSAFE --albo ALTER ASSEMBLY funkcja WITH PERMISSION_SET = UNSAFE
Rozdział 7. Typy złożone
363
Dodatkowo konieczne jest przypisanie uprawnień zewnętrznego dostępu EXTERNAL ACCESS ASSEMBLY do każdego zalogowanego użytkownika roli SQL_Server_logon. Ponadto należy zmodyfikować bazę systemową master, ustawiając opcję TRUSTWORTHY. USE master GRANT EXTERNAL ACCESS ASSEMBLY TO SQL_Server_logon ALTER DATABASE master SET TRUSTWORTHY ON
Po takich zmianach odwołania do utworzonej funkcji zakończą się sukcesem. W przykładzie pokazane zostało odpytywanie o zawartość wybranych dzienników. SELECT --FROM --FROM --FROM
TOP 100 * FROM dbo.CzytajDziennik(N'Application') AS T dbo.CzytajDziennik(N'System') AS T dbo.CzytajDziennik(N'Security') AS T dbo.CzytajDziennik(N'Internet Explorer') AS T
Jak widać z dotychczasowych przykładów, zarówno procedury, jak i oba rodzaje funkcji utworzone za pomocą klas języka wyższego rzędu pozwalają na utworzenie funkcjonalności, których nie da się uzyskać za pomocą czystego T-SQL, ponieważ albo nie oferuje on koniecznych operatorów, albo nie zawiera funkcji systemowych odwołujących się czy to do systemu plików, czy to do obiektów systemu operacyjnego lub innych funkcjonalności, które są możliwe do oprogramowania za pomocą klas np. języka C#. Również doświadczenia z wydajnością, która jest naprawdę wysoka, przemawiają za stosowaniem tego typu rozwiązań. Wszystkie pokazane dotąd przykłady abstrahowały od danych pozyskiwanych w ich ciele z tabel lub widoków schematu relacyjnego. Spróbujmy zatem utworzyć procedurę odwołującą się do bazy. Jej zadaniem będzie policzenie wierszy tabeli danej parametrem. Wynik będzie zwracany przez drugi z parametrów, co widać przy analizie nagłówka metody wiersze. W jej ciele wyzerowano zmienną przeznaczoną na wynik, a następnie za pomocą obiektu SqlConnection zdefiniowano połączenie jako takie, które wynika z aktualnego kontekstu przetwarzania context connection=true. Następnie połączenie zostaje otwarte. Gdybyśmy odwoływali się do innego serwera, należałoby zdefiniować łańcuch połączeniowy ConnectionString. Następnie definiowana jest nowa instancja obiektu SqlCommand, której pierwszym parametrem jest przetwarzane zapytanie, a drugim wykorzystywane, otwarte połączenie. W przykładzie zapytanie zostało zbudowane z konkatenacji statycznego napisu i parametru wejściowego zawierającego nazwę tabeli. Następnie wykonywana jest metoda ExecuteReader, która na skutek wykonania zapytania pozwala odczytywać kolejne zwracane przez nie rekordy. Ponieważ formalnie rekordów może być wiele, odczyt jest wykonywany w pętli while. W przykładzie przetwarzamy zapytanie skalarne, więc wystarczyłoby pojedyncze podstawienie. Jeśli zapytanie zwracałoby większą liczbę kolumn, dla każdej z nich należałoby wykonać podstawienie, zmieniając indeks oraz wybierając właściwą dla typu metodę dostępu do danych, np. GetInt32, GetDouble, GetString itp. using using using using using using using
System; Microsoft.SqlServer.Server; System.Collections; System.Data; System.Data.Sql; System.Data.SqlTypes; System.Data.SqlClient;
364
MS SQL Server. Zaawansowane metody programowania using System.Diagnostics; public class wynik { [Microsoft.SqlServer.Server.SqlProcedure] public static void wiersze(String tabela, out SqlInt32 ile) { ile = 0; SqlConnection conn = new SqlConnection("context connection=true"); conn.Open(); SqlCommand zap = new SqlCommand("SELECT count(*) FROM " + tabela, conn); SqlDataReader zapReader = zap.ExecuteReader(); while (zapReader.Read()) { ile = zapReader.GetInt32(0); } } }
Teraz możemy utworzyć obiekt ASSEMBLY oraz inkapsulować metodę do procedury zgodnie z poniżej zamieszczonym kodem. CREATE ASSEMBLY funkcja AUTHORIZATION [dbo] FROM 'C:\...\funkcja.dll' WITH PERMISSION_SET = SAFE GO CREATE PROCEDURE wiersze(@a nvarchar(33), @b int OUTPUT) AS EXTERNAL NAME funkcja.wynik.wiersze
Wywołanie może się teraz odbyć zgodnie z obowiązującymi zasadami. DECLARE @ile int EXEC wiersze 'Dzialy', @ile OUTPUT PRINT @ile
Na takich samych zasadach możliwe jest zbudowanie funkcji skalarnej. Wystarczy tylko zmienić deklaracje metody ze static void na np. static int, usunąć parametr wyjściowy, a w jego miejsce wprowadzić zmienną lokalną, która będzie przekazywana na zewnątrz w instrukcji return. W MS SQL tworzymy ASSEMBLY i dokonujemy inkapsulacji do funkcji, jak to pokazywano wcześniej. O wiele ciekawsza jest funkcja tabelaryczna odwołująca się do bazy danych. W przykładzie zrealizujemy wyświetlanie osób mających wzrost wyższy niż wartość wskazana parametrem. W tym celu musimy zadeklarować w ciele głównej klasy pomocniczą klasę prywatną ZapResult, która będzie posiadała dwa atrybuty publiczne, których zadaniem będzie reprezentowanie pól przechwyconych z tabeli. W ciele zdefiniowany został dwuparametrowy konstruktor tego obiektu. W dyrektywie kompilatora wskazano, że budowana jest funkcja, a przez jej parametry wskazano, że dostęp do danych jest ograniczony do odczytu. Za wypełnianie rekordami zwracanego typu wyliczeniowego jest odpowiedzialna metoda Pobierz, której definicja znajduje się w dalszej części kodu. Podstawowa klasa ma nazwę wysocy, zwraca typ wyliczany i posiada jeden parametr typu zmiennoprzecinkowego. W jej ciele zadeklarowano nową instancję typu ArrayList, następnie zdefiniowano sposób połączenia, które zostało otwarte, oraz określono przetwarzane zapytanie. W postaci komentarza do kodu pokazano dwie linie, które są odpowiedzialne za poprawne odczytanie zmiennej mini w przypadku zastosowania wskazanego ustawienia międzynarodowego. Następnie zapytanie jest wyko-
Rozdział 7. Typy złożone
365
nywane w określonym uprzednio kontekście, a w pętli while dodawane są kolejne elementy do listy rezultatów. Wykorzystano metodę Add, a atrybuty obiektu odczytywane są za pomocą metod zgodnych z kolejnymi typami pól zapytania GetSqlInt32 oraz GetSqlString. Pozycja na liście jest wskazywana indeksem w wyżej wymienionych metodach. W ciele metody Pobierz najpierw mapowany jest wejściowy parametr zapResultObj do postaci zadeklarowanego obiektu lokalnego ZapResult, a następnie wartości jego atrybutów przepisywane są do zmiennych wyjściowych. using System; using Microsoft.SqlServer.Server; using System.Collections; using System.Data; using System.Data.Sql; using System.Data.SqlTypes; using System.Data.SqlClient; using System.Diagnostics; public class wynik { private class ZapResult { public SqlInt32 idosoby; public SqlString nazwisko; public ZapResult(SqlInt32 IdOsoby, SqlString Nazwisko) { idosoby = IdOsoby; nazwisko = Nazwisko; } } [SqlFunction(DataAccess = DataAccessKind.Read, FillRowMethodName = "Pobierz")] public static IEnumerable wysocy(double mini) { ArrayList resultCollection = new ArrayList(); SqlConnection conn = new SqlConnection("context connection=true"); conn.Open(); SqlCommand zap = new SqlCommand("SELECT IdOsoby, Nazwisko FROM Osoby WHERE Wzrost>" + mini, conn); //String.Format(CultureInfo.GetCultureInfo("en-US"), "{0,0}", mini), conn); //min.ToString(CultureInfo.InvariantCulture.NumberFormat),conn); SqlDataReader zapReader = zap.ExecuteReader(); while (zapReader.Read()) { resultCollection.Add(new ZapResult(zapReader.GetSqlInt32(0), zapReader.GetSqlString(1))); } return resultCollection; } public static void Pobierz( object zapResultObj, out SqlInt32 idosoby, out SqlString nazwisko) { ZapResult zapResult = (ZapResult)zapResultObj; idosoby = zapResult.idosoby; nazwisko = zapResult.nazwisko; } }
366
MS SQL Server. Zaawansowane metody programowania
Proces tworzenia ASSEMBLY i funkcji jest analogiczny do poprzednio opisywanego. CREATE ASSEMBLY funkcja AUTHORIZATION [dbo] FROM 'C:\...\funkcja.dll' WITH PERMISSION_SET = SAFE GO CREATE FUNCTION wysocyf(@mini float) RETURNS TABLE (kto int, Nazwisko nvarchar(4000)) AS EXTERNAL NAME funkcja.wynik.wysocy
Kolejnym elementem proceduralnym, który może zostać utworzony z zastosowaniem techniki CLR, są wyzwalacze. Aby sprawdzić działanie takiego obiektu, utwórzmy najpierw pomocniczą tabelę o dwóch polach: całkowitoliczbowym automatycznie inkrementowanym, które będzie kluczem podstawowym, oraz znakowym. CREATE TABLE TAudit (Id int IDENTITY PRIMARY KEY, Nazwa varchar(15))
Tworzony będzie trigger, którego zadaniem jest śledzenie wszystkich zmian danych w tabeli Dzialy. Tworzymy klasę, w której deklarujemy rodzaj tworzonego obiektu SqlTrigger, a której parametry określają nazwę logiczną (nie jest obowiązkowa), nazwę tabeli, na rzecz której będzie on działał, oraz obsługiwane zdarzenia. W ciele metody deklarujemy pomocnicze zmienne, które będą przechowywały: wartość pola modyfikowanego rekordu oraz wykonywane polecenia SQL. Następnie tworzone są nowe instancje obiektów reprezentujących: kontekst wykonywania wyzwalacza, kanał komunikacyjny i kanał odpowiedzialny za odczyt rekordów. Zasadniczym elementem klasy jest obsługa zdarzeń rozpoznawana w instrukcji warunkowej switch na podstawie kontekstu triggera uzyskiwanego metodą TriggerAction. Jeśli jest nim Insert, odzyskiwane jest połączenie z bazą, które wykorzystuje wyzwalacz, a następnie jest ono otwierane. Definiowane jest zapytanie odpytujące tabelę tymczasową INSERTED, które jest następnie wykonywane, a zwrócony przez nie rekord jest odczytywany, po czym obiekt odpowiedzialny za przetwarzanie i odczyt jest zamykany. W kolejnych liniach tworzone jest zapytanie odpowiedzialne za zasilenie tabeli pomocniczej, którego treść jest przesyłana na uniwersalne wyjście za pomocą metody Send. Następnie zapytanie jest wykonywane i wysyłany jest na wyjście komunikat potwierdzający wstawioną wartość. Po słowie kluczowym brake rozpoczyna się obsługa kolejnego zdarzenia Update. Oba pozostałe zdarzenia są obsługiwane według tego samego schematu. Dodano sprawdzenie za pomocą metody HasRows przetworzenia przynajmniej jednego rekordu. Odczyt modyfikowanych danych wykonywany jest w pętli for. Zrezygnowano z zapisu zmian do tabeli audytowej, ale takie uzupełnienie funkcjonalności nie powinno nastręczać żadnych trudności. Na koniec w sekcji else instrukcji switch przesłano komunikat o braku modyfikacji. using using using using using using
System; System.Data; System.Data.Sql; Microsoft.SqlServer.Server; System.Data.SqlClient; System.Data.SqlTypes;
Rozdział 7. Typy złożone
367
public class CLRTriggers { [SqlTrigger(Name = @"Sprawdz", Target = "Dzialy", Event = "FOR INSERT, UPDATE, DELETE")] public static void Sprawdz() { string Nazwa; SqlCommand polecenie; SqlTriggerContext kontekst = SqlContext.TriggerContext; SqlPipe pipe = SqlContext.Pipe; SqlDataReader reader; switch (kontekst.TriggerAction) // Obsługa wstawiania rekordów { case TriggerAction.Insert: // Uzyskaj połączenie używane przez trigger using (SqlConnection conn = new SqlConnection(@"context connection=true")) { conn.Open(); polecenie = new SqlCommand(@"SELECT * FROM INSERTED;", conn); reader = polecenie.ExecuteReader(); reader.Read(); Nazwa = (string)reader[1]; reader.Close(); polecenie = new SqlCommand(@"INSERT TAudit VALUES ('" + Nazwa + @"');", conn); pipe.Send(polecenie.CommandText); polecenie.ExecuteNonQuery(); pipe.Send("Wstawiles: " + Nazwa); } break; // Obsługa modyfikacji rekordów case TriggerAction.Update: using (SqlConnection conn = new SqlConnection(@"context connection=true")) { conn.Open(); polecenie = new SqlCommand(@"SELECT * FROM INSERTED;", conn); reader = polecenie.ExecuteReader(); if (reader.HasRows) { while (reader.Read()) { Nazwa = (string)reader[1]; pipe.Send(@"zmieniłeś: '" + Nazwa + @"'"); for (int columnNumber = 0; columnNumber < kontekst.ColumnCount; columnNumber++) { pipe.Send("Zmodyfikowano " + reader.GetName(columnNumber) + "? " + kontekst.IsUpdatedColumn(columnNumber). ToString()); } } }
368
MS SQL Server. Zaawansowane metody programowania reader.Close(); } break; // Obsługa kasowania rekordów case TriggerAction.Delete: using (SqlConnection connection = new SqlConnection(@"context connection=true")) { connection.Open(); polecenie = new SqlCommand(@"SELECT * FROM DELETED;", connection); reader = polecenie.ExecuteReader(); if (reader.HasRows) { pipe.Send(@"Usunąłeś wiersze:"); while (reader.Read()) { pipe.Send(@"'" + reader.GetString(1) + @"'"); } reader.Close(); // Alternatywnie, aby wysłać tabelaryczny wynik, możemy zastosować // pipe.ExecuteAndSend(command); } else { pipe.Send("Nie zmieniono żadnych wierszy"); } } break; } } }
Tworzenie ASSEMBLY jest takie samo jak w przypadku wszystkich poprzednich elementów proceduralnych. CREATE ASSEMBLY [trig] AUTHORIZATION [dbo] FROM 'C:\...\triggerCLR.dll' WITH PERMISSION_SET = SAFE
Analogicznie wygląda inkapsulacja klasy do triggera. Należy jednak pamiętać o zapewnieniu zgodności deklarowanych na poziomie MS SQL zdarzeń ze zdarzeniami obsługiwanymi w ciele klasy. Brak takiej zgodności nie prowadzi do powstania błędów składniowych ani błędów przetwarzania, ale spowoduje, że zdarzenie, które nie będzie miało pary, nie zostanie obsłużone. CREATE TRIGGER DzialyAudit ON Dzialy FOR INSERT, UPDATE, DELETE AS EXTERNAL NAME trig.CLRTriggers.Sprawdz
Możemy teraz przetestować działanie wyzwalacza na przykładzie zapytań modyfikujących zawartość tabeli. W przypadku wstawienia do tabeli Dzialy dwóch nowych wierszy, np. za pomocą poniższego skryptu: INSERT INTO Dzialy Values('dodany') INSERT INTO Dzialy Values('Nowy')
Rozdział 7. Typy złożone
369
w zakładce Messages otrzymamy serię komunikatów potwierdzających wykonanie wskazanych zmian. INSERT TAudit VALUES ('dodany'); Wstawiles: dodany (1 row(s) affected) INSERT TAudit VALUES (‘Nowy'); Wstawiles: Nowy (1 row(s) affected)
Podobnie jeśli zmodyfikujemy zawartość wybranych wierszy, zmieniając nazwę działu na pisaną dużymi literami i stosując przykładowe zapytanie UPDATE: UPDATE Dzialy SET Nazwa=UPPER(Nazwa) WHERE IdDzialu>10
seria komunikatów może mieć poniższą postać: zmieniłeś: 'NOWY' Zmodyfikowano IdDzialu? False Zmodyfikowano Nazwa? True zmieniłeś: 'DODANY' Zmodyfikowano IdDzialu? False Zmodyfikowano Nazwa? True (2 row(s) affected)
W tej samej przestrzeni nazw możemy utworzyć kolejną klasę, tym razem dla triggera na bazie danych, którego zadaniem będzie informowanie o wszystkich zdarzeniach wykonywanych w schemacie relacyjnym na poziomie bazy danych, w której został on utworzony. W ciele zadeklarowano zmienną reprezentującą kontekst przetwarzania wyzwalacza. W instrukcji warunkowej switch dla zdarzenia DropTable przesłano na standardowe wyjście najpierw komunikat, a następnie zawartość struktury XML EventData, opisującej jego właściwości, a którą omówiono w podrozdziale 5.5. Sekcja default będzie obsługiwać wszystkie pozostałe zdarzenia, które mogą uruchomić trigger. Poza tekstem komunikatu obsługa jest taka sama jak w poprzedniej sekcji. public static void DDLTrigger() { SqlTriggerContext kontekst = SqlContext.TriggerContext; switch (kontekst.TriggerAction) { case TriggerAction.DropTable: SqlContext.Pipe.Send("Tabela jest kasowana! Dane z EventData:"); SqlContext.Pipe.Send(kontekst.EventData.Value); break; default: SqlContext.Pipe.Send("Inna akcja DDL! Dane z EventData:"); SqlContext.Pipe.Send(kontekst.EventData.Value); break; } }
Podobnie jak poprzednio, należy utworzyć obiekt pośredniczący, a następnie oba triggery, które są opisane w ciele klasy C#. W przypadku drugiego z wyzwalaczy wskazano na zakres działania DATABASE oraz zestaw zdarzeń. W przykładzie zastosowano DDL_DATABASE_LEVEL_EVENTS, które leży najwyżej w hierarchii zdarzeń, co oznacza, że przechwycone zostaną wszystkie możliwe przypadki działań na wskazanym poziomie.
370
MS SQL Server. Zaawansowane metody programowania CREATE ASSEMBLY trig AUTHORIZATION [dbo] FROM 'C:\...\trigerCLR.dll' WITH PERMISSION_SET = SAFE GO CREATE TRIGGER DzialyAudit ON Dzialy FOR INSERT, UPDATE, DELETE AS EXTERNAL NAME trig.CLRTriggers.Sprawdz GO CREATE TRIGGER AuditDDL ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS EXTERNAL NAME trig.CLRTriggers.DDLTrigger
Należy pamiętać, że jeżeli utworzono wcześniej obiekty korzystające z ASSEMBLY, należy je najpierw usunąć, następnie usunąć obiekt pośredniczący, a dopiero potem rozpocząć ponowne tworzenie tych obiektów. Podobnie jak w przypadku triggera działającego na tabeli, możliwe jest sprawdzenie działania wyzwalacza działającego dla poleceń DDL. Dla przypadku tworzenia prostej tabeli, opisanego poleceniem przedstawionym poniżej: CREATE TABLE TT (Id int PRIMARY KEY)
seria komunikatów może mieć poniższą postać: (1 row(s) affected) Inna akcja DDL! Dane z EventData: CREATE_TABLE2013-0921T19:56:54.50752PC22431sadbotest< SchemaName>dboTTTABLECREATE TABLE TT (Id int PRIMARY KEY)
W przypadku usuwania tabeli ze schematu: DROP TABLE TT
otrzymujemy informację o postaci: (1 row(s) affected) Tabela jest kasowana! Dane z EventData: DROP_TABLE2013-0921T20:00:22.17752PC22431sadbotestdboTTTABLEDROP TABLE TT
Jednak jeśli na przykład wykonamy polecenie utworzenia nowego loginu, CREATE LOGIN nowy WITH PASSWORD = 'kajak';
Rozdział 7. Typy złożone
371
to wyzwalacz nie zadziała, ponieważ jest to zdarzenie dotyczące całego serwera — dotyczy poziomu ALL SERVER, co opisano w podrozdziale 5.5. Aby w takim przypadku trigger zadziałał, należałoby go powtórnie utworzyć dla tego zakresu zdarzeń albo utworzyć kolejny. Moim zdaniem elementem, który oferuje najbardziej interesujące funkcjonalności poza typem użytkownika CLR, jest funkcja agregująca utworzona za pomocą tego mechanizmu. Budowę agregatu przeanalizujmy na podstawie funkcji, która zlicza wartości NULL. Warto podkreślić, że wbudowane funkcje agregujące pomijają podczas przetwarzania te wartości; dotyczy to również COUNT. Stąd zaproponowane rozwiązanie stanowi użyteczne rozszerzenie bazy danych. Dla obiektów tego rodzaju konieczne jest zastosowanie dyrektyw Serializable, odpowiadającej za zapis i odczyt z wewnętrznej postaci binarnej, oraz SqlUserDefined Aggregate. W przykładzie zastosowano dwa parametry dla drugiej z nich: pierwszy, określający, że stosowane są w obliczeniach typy natywne, i drugi, określający nazwę logiczną agregatu. Poza tym struktura reprezentująca taki obiekt musi zawierać cztery metody: Init, wykonywaną na początku każdej grupy rekordów, Accumulate, wykonywaną dla każdego z rekordów grupy, a której parametrami
są pola wykorzystywane do obliczeń, Merge, łączącą wyniki obliczeń wykonywanych w procesach równoległych, Terminate, wykonywaną na zakończenie każdego z bloków, a służącą do
ostatecznego sformatowania wyniku. W przykładzie zadeklarowano lokalną zmienną całkowitoliczbową Licznik, która w metodzie inicjującej jest zerowana. Metoda Accumulate ma parametr typu object, ponieważ ma działać dla wszystkich rodzajów pól tabeli. W jej ciele sprawdzane jest, czy bieżący rekord ma wartość null, co może być wykonane zarówno za pomocą klasy DBNull, jak i metody Value. W przypadku pozytywnej odpowiedzi wartość licznika jest inkrementowana. Parametrem wejściowym metody scalającej Merge jest obiekt reprezentujący tworzoną strukturę. W ciele tej metody wartości liczników z procesów równoległych są dodawane. Wykonywane w metodzie Terminate formatowanie wyniku ogranicza się do rzutowania go na docelowy typ, zgodny z reprezentacją liczb całkowitych po stronie SQL, i przekazania go do miejsca wywołania. using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; [Serializable] [SqlUserDefinedAggregate(Format.Native, Name = "PoliczNull")] public struct PoliczNull { private int Licznik; public void Init() { Licznik = 0; } public void Accumulate(object Value)
372
MS SQL Server. Zaawansowane metody programowania { if (Value == DBNull.Value) //alternatywnie if (Value.Equals(null)) Licznik++; } public void Merge(PoliczNull Group) { this.Licznik += Group.Licznik; } public SqlInt32 Terminate() { return new SqlInt32(Licznik); } }
Utworzenie funkcji agregującej również wymaga utworzenia ASSEMBLY, a następnie inkapsulacji struktury obiektowej do obiektu MS SQL. Parametr wejściowy jest typu znakowego, ponieważ każdy z typów może być do niego skonwertowany; wynikiem jest liczba całkowita. CREATE ASSEMBLY Policz FROM 'C:\...\ PoliczNull.dll' WITH PERMISSION_SET = SAFE; GO CREATE AGGREGATE IleNULL(@s varchar(max)) RETURNS int EXTERNAL NAME Policz.PoliczNull;
Kolejnym przykładem funkcji agregującej zrealizowanej w technologii CLR jest odchylenie standardowe opisane zależnością: n
1 n x )2 n i 1 i , n 1
( xi i 1
gdzie: xi określa i-tą wartość zmiennej, a n liczbę tych wartości. Wybór tej wielkości był spowodowany tym, że ma ona swoją realizację wykonaną przez twórców MS SQL, co pozwala na sprawdzenie dokładności wyników otrzymywanych obydwoma metodami, ale przede wszystkim tym, że w zależności występują dwie sumy, z których odpowiadająca za wyznaczenie średniej musi być obliczona jako pierwsza. Taka sytuacja prowadzi do wniosku, że metoda Accumulate powinna być dla każdej grupy rekordów wykonywana dwukrotnie. Niestety, nie jest to możliwe. Rozwiązaniem, które może być zrealizowane, jest takie, w którym wszystkie rekordy grupy są przechowywane we wnętrzu klasy, co pociąga za sobą konieczność stosowania macierzy lub listy. Pierwsze z rozwiązań jest kłopotliwe w realizacji, ponieważ macierze wymagają sztywnego podania maksymalnej liczby elementów, a tego nie jesteśmy w stanie arbitralnie rozstrzygnąć. Listy nie mają tej niekorzystnej cechy, dlatego ten typ został zastosowany w przykładzie. Program ze względu na swoją dość dużą objętość zostanie opisany z podziałem na części. Po zaimportowaniu niezbędnych przestrzeni nazw zastosowano obowiązkowe dyrektywy Serializable oraz SqlUserDefinedAggregate. Ponieważ lista nie jest typem natywnym, konieczne jest zastosowanie parametru Format.UserDefined, który informuje, że za obsłużenie procesu serializacji odpowiada programista. W takim przypadku konieczne jest również określenie za pomocą parametru MaxByteSize naj-
Rozdział 7. Typy złożone
373
większej obsługiwanej przez ten proces paczki bitów. Pozostałe parametry określające niezależność od pojawienia się duplikatów, niezależność od kolejności danych oraz nazwę logiczną nie są obowiązkowe. Ponieważ wymagane jest programistyczne zrealizowanie serializacji, tworzona struktura musi dziedziczyć po interfejsie IBinary Serialize, tak aby możliwe było przeciążenie jego metod Read i Write. Następnie zadeklarowano zmienne posr typu List, która będzie przechowywać wartości pola z kolejnych rekordów. Elementy listy ze względu na zastosowany typ double nie będą mogły przechowywać wartości null, ale przy wyznaczaniu funkcji agregujących nie są one brane pod uwagę. Ponadto zadeklarowano trzy pomocnicze zmienne lokalne. using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using System.Collections; using System.Collections.Generic; using Microsoft.SqlServer.Server; using System.Text; [Serializable] [SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToDuplicates = false, IsInvariantToOrder = false, MaxByteSize = 8000, Name = "OdchylenieStandardowe")] public struct OdchStd : IBinarySerialize { public List posr; private int licznik; // zmienna zliczająca liczbę wierszy niemających wartości NULL private double suma; // zmienna przechowująca sumę pól private double temp; // zmienna pomocnicza
W metodzie Init utworzono nową instancję zmiennej typu List oraz wyzerowano zmienne pomocnicze licznik i suma. Parametrem wejściowym metody Accumulate jest zmienna typu double?, ponieważ wartości pól w tabeli mogą przyjmować wartość NULL. Jednak ze względu na sposób przechowywania danych we wnętrzu klasy oraz zasadę obliczania funkcji agregującej najpierw jest weryfikowane w ciele tej metody to, czy nie została przekazana taka wartość. Jeżeli nie jest ona null, licznik jest inkrementowany, wartość przekazanej zmiennej jest rzutowana na typ double i przypisywana do zmiennej pomocniczej. Następnie suma jest powiększana o skonwertowaną wartość oraz jest dopisywana jako kolejny element na liście. W metodzie łączącej procesy równoległe Merge następuje sumowanie wartości zmiennych licznik i suma, a listy podlegają połączeniu za pomocą metody AddRange. public void Init() { posr = new List(); licznik = 0; suma = 0; } public void Accumulate(double? value) { if (value != null) // wyznaczanie sumy dla pól niemających wartości NULL { licznik++;
374
MS SQL Server. Zaawansowane metody programowania temp = (double)value; suma = suma + temp; posr.Add(temp); } } public void Merge(OdchStd Group) { this.suma += Group.suma; // kiedy obliczenia są równoległe, suma staje się sumą ze // wszystkich procesów this.licznik += Group.licznik; this.posr.AddRange(Group.posr); }
Musimy teraz zająć się serializacją, nadpisując metodę Write z interfejsu IBinary Serialize. Aby proces zapisu był prostszy, metoda będzie zawierała zasadnicze elementy przetwarzania danych. Po wyzerowaniu zmiennej pomocniczej temp zadeklarowano zmienną sr, do której podstawiono obliczoną wartość średnią. Następnie przechodząc przez wszystkie elementy listy, wyznaczono licznik wyrażenia definiującego odchylenie standardowe, a w kolejnych liniach podzielono wynik przez liczbę wierszy i wyciągnięto pierwiastek kwadratowy. Ostatnie dwie linie odpowiadają za zapis w postaci binarnej kolejno dwóch wartości: policzonego odchylenia standardowego i licznika. Metoda Read odpowiada za odczyt z postaci binarnej i zawiera dwa polecenia czytające w takiej samej kolejności, w jakiej zapisano we Write dwie wartości. W metodzie Terminate sprawdzono, czy licznik jest równy zero, w takim przypadku zwracana jest wartość null. W przeciwnym razie zwracana jest policzona wartość odchylenia standardowego rzutowana do docelowego typu double?. public void Write(System.IO.BinaryWriter w) { temp = 0; double sr = suma / licznik; foreach (double d in posr) { temp = temp + Math.Pow((d - sr), 2); } temp = temp / (licznik - 1); temp = Math.Pow(temp, 0.5); w.Write(temp); w.Write(licznik); } public void Read(System.IO.BinaryReader r) { temp = r.ReadDouble(); licznik = r.ReadInt32(); } public double? Terminate() { if (licznik == 0) { return null; } else { return (double?)(this.temp); //zwrócenie ostatecznej wartości obliczeń } } }
Rozdział 7. Typy złożone
375
Podobnie jak w przypadku poprzednio tworzonego obiektu reprezentującego funkcję agregującą, po stronie MS SQL budujemy ASSEMBLY, a następnie dokonujemy inkapsulacji struktury CLR do elementu proceduralnego. DROP AGGREGATE ODCHSTD GO DROP ASSEMBLY [ODCHSTD_a] GO CREATE ASSEMBLY [ODCHSTD_a] AUTHORIZATION [dbo] FROM 'C:\...\odchstd.dll' WITH PERMISSION_SET = SAFE GO CREATE AGGREGATE OdchStd(@value float) RETURNS float EXTERNAL NAME ODCHSTD_a.OdchStd; GO
Następnie możemy porównać dokładność obliczeń wykonanych za pomocą wbudowanej funkcji oraz utworzonego przez nas odpowiednika, wykonując poniższe zapytanie. SELECT Nazwa, STDEV(wzrost), dbo.OdchStd(Wzrost), STDEV(wzrost)/dbo.OdchStd(Wzrost) AS dokladnosc FROM Dzialy LEFT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu GROUP BY Nazwa
Otrzymane wyniki będą się różnić nieznacznie, na poziomie 10e-11. Wykonując zapytania zawierające tylko pojedynczą funkcję agregującą i mierząc czas jego wykonania dla obu realizacji agregatu, stwierdzimy, że również wydajność oprogramowanego algorytmu jest bardzo duża. Oba powyższe wyniki potwierdzają sensowność stosowania omawianego mechanizmu. Do tej pory omawiane funkcje agregujące miały tylko jeden parametr wejściowy. Jednak w praktyce nie istnieje żadne ograniczenie ich liczby. Możemy to zweryfikować, tworząc funkcję agregującą wyznaczającą kowariancje między dwoma zmiennymi, która jest opisywana zależnością: COV( x, y) E( xy) E( x)E( y) E( x E( x)( y E( y)) ,
gdzie E(z) jest wartością oczekiwaną zmiennej z. Estymatorem tej wielkości w populacji jest średnia arytmetyczna. Prowadzi to do przekształcenia wyrażenia do poniższej postaci.
1 n 1 n ( x x )( y i n i i n yi ) i 1 i 1 i 1 COV( x, y) n n
Oczywiście możliwe byłoby napisanie od początku nowej klasy, jednak możliwe jest zamknięcie obu struktur w jednym pliku, dlatego też przedstawiany kod można dopisać do już istniejącego. Ponieważ kod jest dość długi, zostanie podzielony na fragmenty, które będą opisywane oddzielnie. Z drugiej strony idea przetwarzania jest bardzo podobna, dlatego komentarz będzie dotyczył tylko nowych elementów.
376
MS SQL Server. Zaawansowane metody programowania
W tym przypadku parametry dyrektywy SqlUserDefinedAggregate różnią się tylko nazwą logiczną tworzonej struktury. Jako zmienne pomocnicze zadeklarowana została macierz, która będzie przechowywała dwa elementy reprezentujące parę współczynników, dla których liczona jest kowariancja. Obiektem przechowującym zestaw takich par będzie zmienna typu ArrayList. Ponieważ operujemy na parach danych, zadeklarowano pary zmiennych na sumy i wartości tymczasowe. W metodzie Init utworzono nową instancję zmiennej macierzowej, definiując, że będzie składała się z dwóch elementów, oraz nową instancję zmiennej typu ArrayList, której zawartość została wyczyszczona. Zmienne pomocnicze zostały wyzerowane. [Serializable] [SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToDuplicates = false, IsInvariantToOrder = false, MaxByteSize = 8000, Name = "Kowariancja")] public struct Kowariancja : IBinarySerialize { double[] a; public ArrayList posr; private int licznik; private double sumaX; private double sumaY; private double tempX; private double tempY; private double temp; public void Init() { a = new double[2]; posr = new ArrayList(a); posr.Clear(); temp = 0; licznik = 0; sumaX = 0; sumaY = 0; }
W klasie Accumulate sprawdzono, czy obie wartości pary zmiennych są różne od null. Odpowiedź jest pozytywna, więc podstawiono do dwóch elementów macierzy zrzutowane na typ double wartości zmiennych wejściowych, a licznik powiększono o 1. Pomocnicze sumy zostały powiększone o skonwertowane wartości. Do zmiennej typu ArrayList dopisano sklonowaną postać elementów macierzy. Metoda Merge zawiera sumowanie wartości z procesów równoległych oraz dopisanie wartości do listy. public void Accumulate(double? valueX, double? valueY) { if (valueX != null && valueY != null) { licznik++; a[0] = (double)valueX; a[1] = (double)valueY; sumaX = sumaX + a[0]; sumaY = sumaY + a[1]; posr.Add(a.Clone()); } }
Rozdział 7. Typy złożone
377
public void Merge(Kowariancja Group) { this.sumaX += Group.sumaX; this.sumaY += Group.sumaY; this.licznik += Group.licznik; posr.AddRange(Group.posr); }
Podobnie jak w przypadku wyznaczania odchylenia standardowego, właściwe przetwarzanie algorytmu odbywa się w klasie Write, odpowiedzialnej za serializację. Wyznaczane są wartości średnie obu zmiennych, a w pętli foreach obliczany jest licznik wyrażenia, po jej zakończeniu dzielony przez liczbę elementów. Dwie ostatnie linie zapisują wartość policzonego wyrażenia i licznika elementów do postaci binarnej. W klasie Read dokonujemy odczytu z postaci binarnej do zmiennych. Metoda Terminate rozróżnia dwa stany: gdy liczba elementów jest 0, zwracany jest null, w przeciwnym razie zrzutowana jest do docelowego typu double? wartość wyznaczonej kowariancji. public void Write(System.IO.BinaryWriter w) { temp = 0; double srX = sumaX / licznik; double srY = sumaY / licznik; foreach (double[] d in posr) { tempX = (d[0] - srX); tempY = (d[1] - srY); temp = temp + (tempX * tempY); } temp = temp / licznik; w.Write(temp); w.Write(licznik); } public void Read(System.IO.BinaryReader r) { temp = r.ReadDouble(); licznik = r.ReadInt32(); } public double? Terminate() { if (licznik == 0) { return null; } else { return (double?)(this.temp); //zwrócenie ostatecznej wartości obliczeń } } }
Utworzenie obu funkcji agregujących zawartych w tej samej bibliotece dll podlega takim samym zasadom jak w przypadku pojedynczego agregatu. Pomimo to zdecydowałem się na pokazanie całego kodu skryptu, aby ponownie podkreślić obowiązkową kolejność usuwania i tworzenia obiektów.
378
MS SQL Server. Zaawansowane metody programowania DROP AGGREGATE COVAR GO DROP AGGREGATE ODCHSTD GO DROP ASSEMBLY [ODCHSTD_a] GO CREATE ASSEMBLY [ODCHSTD_a] AUTHORIZATION [dbo] FROM 'C:\...\odchstd.dll' WITH PERMISSION_SET = SAFE GO CREATE AGGREGATE OdchStd(@value float) RETURNS float EXTERNAL NAME ODCHSTD_a.OdchStd; GO CREATE AGGREGATE CoVar(@value1 float, @value2 float) RETURNS float EXTERNAL NAME ODCHSTD_a.Kowariancja; GO
Możemy teraz wzorem odchylenia standardowego zastosować oprogramowaną przez nas kowariancję w zapytaniach SQL. Można zauważyć, że również ta wielkość daje się wyznaczyć za pomocą klasycznej funkcji agregującej AVG. Jednak nie będzie to rozwiązanie ogólne. Zachęcam uważnych Czytelników do zmierzenia się z tym zadaniem. Zamiast tego możemy wykonać testy na poziomie współczynnika korelacji Pearsona. Współczynnik ten ma ściśle określony zakres wartości <-1, 1>. Wartości graniczne przedziału wskazują na liniową zależność pomiędzy zmiennymi, różniącą się znakiem współczynnika kierunkowego. Wartość 0 mówi, że nie istnieje zależność między współczynnikami, co możemy sobie wyobrazić w postaci punktów leżących na okręgu lub równomiernie w obszarze koła. Stany pośrednie można przedstawić w postaci spłaszczonej elipsy. Współczynnik ten możemy wyrazić zależnością, w której kowariancja jest skalowana przez odchylenia standardowe obu zmiennych:
corr( x, y)
COV( x, y)
x y
.
Po rozpisaniu elementów wzoru pełna postać zależności daje się wyrazić za pomocą równania:
1 n 1 n ( x x )( y y ) i i i i n i 1 n i 1 i 1 corr( x, y) n n 1 n 1 n 2 ( x x ) ( y yi ) 2 i i i n i 1 n i 1 i 1 i 1 n
.
Pierwszą z zależności wykorzystano do wyznaczenia korelacji w zapytaniu wybierającym, mającym za zadanie pokazanie zgodności wyznaczonych wartości za pomocą funkcji wbudowanej i zrealizowanej na podstawie klasy CLR.
Rozdział 7. Typy złożone
379
SELECT Nazwa, STDEVP(wzrost) AS funOdch2, dbo.OdchStd(Wzrost) AS AssOdch, dbo.Covar(Wzrost, RokUrodz) AS AssCoVar , (AVG(Wzrost * RokUrodz)- AVG(Wzrost)*AVG(RokUrodz))/ (STDEVP(Wzrost)*STDEVP(RokUrodz)) AS Corr, dbo.Covar(Wzrost, RokUrodz)/(STDEVP(Wzrost)*STDEVP(RokUrodz)) AS ASsCorr FROM Dzialy LEFT JOIN Osoby ON Dzialy.IdDzialu=Osoby.IdDzialu GROUP BY Nazwa
Drugie z wyrażeń może być użyte do zbudowania klasy CLR realizującej obliczanie współczynnika korelacji. Ponieważ zarówno licznik wyrażenia, jak i mianownik zostały zrealizowane w dwóch poprzednio omawianych przykładach, uważny Czytelnik nie powinien mieć kłopotu z ich połączeniem w jednolity kod. Przedstawiłem tutaj dużą liczbę przykładów zastosowania języków wyższego rzędu do tworzenia elementów programistycznych T-SQL, a także przykłady ich zastosowania do wprowadzenia obiektowości do bazy relacyjnej. Jednak możliwości tych języków oraz możliwości pokazanej metodologii enkapsulacji metod obiektowych do elementów proceduralnych MS SQL Server są znacznie większe — ich dokładniejszy opis wymagałby osobnej publikacji. Mam nadzieję, że wkrótce uda mi się to zrealizować.
380
MS SQL Server. Zaawansowane metody programowania
Rozdział 8.
Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego 8.1. Klasyfikacja Powróćmy do rozważań dotyczących liniowej separacji klas. Problem jest opisywany tabelą zawierającą współrzędne punktów w dwuwymiarowej przestrzeni euklidesowej oraz pole wskazujące na przynależność do kategorii. Dodatkowo definicja została uzupełniona o automatycznie inkrementowane pole klucza głównego, które nie jest konieczne do rozwiązania problemu, ale zostało wprowadzone, aby zapewnić zgodność z zasadami tworzenia tabel w bazach danych — pierwsza postać normalna. CREATE TABLE Punkty (Id int IDENTITY(1,1) PRIMARY KEY, x real, y real, typ int NOT NULL)
Tak przygotowana tabela może zostać zasilona za pomocą instrukcji INSERT i blokowej definicji wstawianych wierszy. INSERT INTO Punkty VALUES (10, 15, 0), (12, 10, 0), (14, 14, 0), (15, 20, 0), (17, 12, 0) INSERT INTO Punkty VALUES (25, 20, 1), (33, 12, 1), (32, 22, 1), (35, 17, 1), (36, 24, 1) INSERT INTO Punkty VALUES (4, 18, 2), (5, 27, 2), (8, 22, 2), (10, 30, 2), (15, 25, 2) …
382
MS SQL Server. Zaawansowane metody programowania
Jak można zauważyć na podstawie przykładowych danych, liczba klas nie jest ograniczona i formalnie może być ich dowolnie wiele. Dodatkowo są one słabo separowalne za pomocą pojedynczego klasyfikatora liniowego. O ile prezentowane w rozdziale 4. rozwiązania wykorzystujące pojedyncze zapytanie wybierające radzą sobie z pierwszym z problemów, oferując bardzo dużą wydajność otrzymywania wyników dla bardzo dużych wolumenów danych, o tyle drugi z problemów musi zostać rozwiązany w inny sposób. Możliwe jest zastosowanie mechanizmu podziału iteracyjnego, tak że po pierwotnym określeniu granic klas sprawdzana jest ich „czystość”, tzn. czy w zdefiniowanych podobszarach znajdują się tylko elementy jednej klasy. Jeśli taki postulat jest prawdziwy, podobszar zostanie uznany za poprawnie określony i kolejne jego podziały nie będą wykonywane. W przeciwnym razie do dalszej klasyfikacji zostaną wybrane tylko elementy podobszaru i proces podziału zostanie powtórzony. Oczywiście poza zakończeniem procesu w sytuacji uzyskania tylko „czystych” podobszarów możliwe jest jego przerwanie, gdy liczba źle sklasyfikowanych elementów nie przekroczy wskazanego dopuszczalnego poziomu. Pierwszym krokiem prowadzącym do rozwiązania jest zdefiniowanie pomocniczego typu tabelarycznego, który będzie przechowywał współczynniki prostych uzyskanych w kolejnych iteracjach procesu klasyfikacji. Na jego definicję składają się pola identyfikatora, pola określające współczynniki oraz flaga wskazująca na stan podziału. CREATE TYPE TabelaWspolczynnikow AS TABLE (Id int, a FLOAT, b FLOAT, flaga float)
Na tej samej zasadzie określono drugi typ pomocniczy, w tym przypadku decydując się na automatyczną inkrementację pola identyfikatora. CREATE TYPE TabelaWspolczynnikow2 AS TABLE (Id int IDENTITY(1,1), a FLOAT, b FLOAT, flaga float)
Zbudujmy teraz pierwszy element proceduralny rozwiązujący nasz problem w ten sposób, że wykrywane są wszystkie proste wyznaczone przez pary punktów należące do jednej klasy, z których wybierane są te, które dzielą przestrzeń tak, że dają „czysty” podział dla przynajmniej jednej z pozostałych klas. To znaczy, że wszystkie punkty ją wyznaczające znajdują się po jednej jej stronie. Rozpoczynamy od funkcji, która wyznacza współczynniki wszystkich prostych określonej parametrem klasy. Zwraca ona tabelę, której definicja pokrywa się z typem TabelaWspolczynnikow2. Niestety, ze względów składniowych jej deklaracja musi być dana jawnie. Głównym elementem ciała są definicja i wykorzystanie kursora wybierającego kolejne identyfikatory z tabeli opisującej dystrybucje punktów. Został on zadeklarowany jako lokalny z opcjami STATIC FORWARD_ONLY, co powoduje znaczne poprawienie wydajności (podrozdział 5.6). Na podstawie otrzymanych wartości współrzędnych wyznaczane są współczynniki prostych, przy czym za pomocą instrukcji CASE wyróżniono przypadek, w którym współrzędne X są równe, gdyż wtedy równanie prostej ma postać x = b, natomiast w przypadku przeciwnym y = ax + b. Funkcję kończy wykasowanie zawartości tabel pomocniczej oraz zamknięcie i zwolnienie zasobów kursora.
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
383
CREATE FUNCTION szukaj_wsp(@wybor int) RETURNS @wspolczynnikiTmp TABLE (id int IDENTITY(1,1), a FLOAT, b FLOAT, flaga float) AS BEGIN DECLARE @ids TABLE(id INT) DECLARE @tmpid INT DECLARE @pierwszyPunkt TABLE(id INT, x FLOAT, y FLOAT) INSERT INTO @ids SELECT id FROM Punkty WHERE typ = @wybor ORDER BY id ASC DECLARE crID CURSOR LOCAL STATIC FORWARD_ONLY FOR SELECT id FROM @ids OPEN crID FETCH NEXT FROM crID INTO @tmpid WHILE @@FETCH_STATUS = 0 BEGIN DELETE FROM @ids WHERE id = @tmpid INSERT INTO @pierwszyPunkt SELECT id, x, y FROM Punkty WHERE typ = @wybor AND id = @tmpid INSERT INTO @wspolczynnikiTmp SELECT CASE WHEN pp.x <> Punkty.x THEN ((pp.y-Punkty.y)/(pp.x-Punkty.x)) ELSE '0' END AS a, CASE WHEN pp.x <> Punkty.x THEN (pp.y - ((pp.y-Punkty.y)/(pp.x-Punkty.x))*pp.x) WHEN pp.x = Punkty.x THEN pp.x ELSE pp.y END AS b, CASE WHEN pp.x = Punkty.x THEN 0 -- x = const = b ELSE 1 END AS flaga FROM Punkty JOIN @pierwszyPunkt pp ON(1=1) WHERE Punkty.typ = @wybor AND Punkty.id > @tmpid DELETE FROM @pierwszyPunkt FETCH NEXT FROM crID INTO @tmpid END CLOSE crID DEALLOCATE crID RETURN END
Utworzona poprzednio funkcja jest wykorzystywana w kolejnej, której zadaniem jest wskazanie, jak wyznaczone w poprzednim kroku proste, dla klasy wskazanej drugim parametrem, dzielą inne klasy. Ustawiane są zmienne pomocnicze, flagi, których zadaniem jest wyznaczenie liczby atrybutów wskazanej trzecim parametrem klasy. Nie ograniczono się do stwierdzenia, że prosta przecina klasę, aby mieć możliwość zatrzymania procesu podziału w przypadku słabych właściwości dyskryminacyjnych — braku czystej klasy. Aby ułatwić śledzenie kodu, pomocnicze zmienne tabelaryczne
384
MS SQL Server. Zaawansowane metody programowania
określono jako @kolka i @krzyzyki, zgodnie z tym, jak zazwyczaj zaznacza się graficznie atrybuty przynależne do dwóch różnych klas. Funkcja analogicznie do poprzedniej zwraca tabelę o takiej samej strukturze. Tym razem podstawą działania funkcji jest kursor wykorzystujący tabelę będącą skutkiem wykonania poprzedniej funkcji. Tak samo jak poprzednio, został on zadeklarowany jako lokalny z opcjami STATIC FORWARD_ONLY, aby poprawić wydajność przetwarzania. W dwóch instrukcjach CASE ustawiane są flagi odpowiadające wybranym położeniom punktów względem prostej. Sprawdzenie dotyczy zarówno przecinania klasy pierwotnej, jak i zmieniającej się klasy wtórnej. W ciele pętli nawigowania po kursorze zliczane są punkty dla każdego z położeń. Po sprawdzeniu warunków, określających, że klasy nie są dzielone, współczynniki prostych są zapisywane do tablicy zwracanej przez nazwę funkcji. Aby dopuścić istnienie podziałów dających nieczystą klasę, wystarczy zamienić przyrównanie do zera na wartości progowe dane jako stała albo procent z liczebności klasy. Na koniec pomocnicze zmienne są czyszczone, kursor jest zamykany, a zasoby mu przydzielone są zwalniane. CREATE FUNCTION szukaj_prostej_pomiedzy_grupami (@wspolczynnikiTmp TabelaWspolczynnikow READONLY, @grupa1 int, @grupa2 int) RETURNS @proste TABLE (ID int, a FLOAT, b FLOAT, flaga float) AS BEGIN DECLARE @kolka TABLE (id int, x int, y int, spr varchar(10)) DECLARE @krzyzyki TABLE (id int, x int, y int, spr varchar(10)) DECLARE @tmpid2 INT DECLARE @tmpa FLOAT DECLARE @tmpb FLOAT DECLARE @tmpflaga float DECLARE @ilekolek_nad int DECLARE @ilekolek_pod int DECLARE @ilekolek_lewej int DECLARE @ilekolek_prawej int DECLARE @ilekrzyz_nad int DECLARE @ilekrzyz_pod int DECLARE @ilekrzyz_lewej int DECLARE @ilekrzyz_prawej int DECLARE crID2 CURSOR LOCAL STATIC FORWARD_ONLY FOR SELECT * FROM @wspolczynnikiTmp OPEN crID2 FETCH NEXT FROM crID2 INTO @tmpid2, @tmpa, @tmpb, @tmpflaga WHILE @@FETCH_STATUS = 0 BEGIN INSERT INTO @kolka select P.id, P.x, P.y, CASE
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
385
WHEN @tmpflaga = 1 AND ((@tmpa*x + @tmpb) < P.y) THEN 'nad' -- punkt nad prostą y=ax+b WHEN @tmpflaga = 1 AND ((@tmpa*x + @tmpb) > P.y) THEN 'pod' -- punkt pod prostą y=ax+b WHEN @tmpflaga = 1 AND ((@tmpa*x + @tmpb) = P.y) THEN 'prosta' -- punkt na prostej y=ax+b WHEN @tmpflaga = 0 AND (@tmpb < P.x) THEN 'po lewej' -- punkt po lewej od prostej x=b WHEN @tmpflaga = 0 AND (@tmpb > P.x) THEN 'po prawej' -- punkt po prawej od prostej x=b WHEN @tmpflaga = 0 AND (@tmpb = P.x) THEN 'pionowa' -- punkt na prostej x=b END AS spr FROM Punkty AS P WHERE P.typ = @grupa1 INSERT INTO @krzyzyki select P.id, P.x, P.y, CASE WHEN @tmpflaga = 1 AND ((@tmpa*x + @tmpb) < P.y) THEN 'nad' -- punkt nad prostą y=ax+b WHEN @tmpflaga = 1 AND ((@tmpa*x + @tmpb) > P.y) THEN 'pod' -- punkt pod prostą y=ax+b WHEN @tmpflaga = 1 AND ((@tmpa*x + @tmpb) = P.y) THEN 'prosta' -- punkt na prostej y=ax+b WHEN @tmpflaga = 0 AND (@tmpb < P.x) THEN 'po lewej' -- punkt po lewej od prostej x=b WHEN @tmpflaga = 0 AND (@tmpb > P.x) THEN 'po prawej' -- punkt po prawej od prostej x=b WHEN @tmpflaga = 0 AND (@tmpb = P.x) THEN 'pionowa' -- punkt na prostej x=b END AS spr FROM Punkty as P WHERE P.typ = @grupa2 SET @ilekolek_nad = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'nad') AS kol) SET @ilekolek_pod = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'pod') AS kol) SET @ilekolek_lewej = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'po lewej') AS kol) SET @ilekolek_prawej = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'po prawej') AS kol) SET @ilekrzyz_nad = (SELECT COUNT(spr) FROM (SELECT * FROM @krzyzyki WHERE spr = 'nad') AS kol) SET @ilekrzyz_pod = (SELECT COUNT(spr) FROM (SELECT * FROM @krzyzyki WHERE spr = 'pod') AS kol) SET @ilekrzyz_lewej = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'po lewej') AS kol) SET @ilekrzyz_prawej = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'po prawej') AS kol) IF @tmpflaga = 1 BEGIN IF (@ilekrzyz_nad = 0 AND @ilekolek_pod = 0) OR (@ilekrzyz_pod = 0 AND @ilekolek_nad = 0) BEGIN INSERT INTO @proste VALUES(@tmpid2,@tmpa, @tmpb, @tmpflaga) END END ELSE IF (@ilekrzyz_lewej = 0 AND @ilekolek_prawej = 0) OR (@ilekrzyz_prawej = 0 AND @ilekolek_lewej = 0) BEGIN INSERT INTO @proste VALUES(@tmpid2,@tmpa, @tmpb, @tmpflaga) END DELETE FROM @kolka DELETE FROM @krzyzyki FETCH NEXT FROM crID2 INTO @tmpid2, @tmpa, @tmpb, @tmpflaga END CLOSE crID2 DEALLOCATE crID2 RETURN END
386
MS SQL Server. Zaawansowane metody programowania
Kolejna funkcja tabelaryczna ma na celu wykonanie nawigacji po wszystkich występujących w wyjściowej tabeli klasach, różnych od tej, która jest uznawana za bieżącą. Zasada jej funkcjonowania jest podobna do już przedstawionej. Idea jej powstania jest taka, że poza parą grup analizowanych poprzednio przecinanie klas może dotyczyć również i innych, nieanalizowanych bezpośrednio jako para. CREATE FUNCTION usun_kolizyjne_proste (@wspolczynnikiTmp TabelaWspolczynnikow2 READONLY, @ile_grup int) RETURNS @proste TABLE (Id int, a FLOAT, b FLOAT) AS BEGIN DECLARE @kolka TABLE (Id int, x int, y int, spr varchar(10)) DECLARE @i int SET @i = 0 DECLARE @poprawne int SET @poprawne = 0 DECLARE @tmpid2 INT DECLARE @tmpa FLOAT DECLARE @tmpb FLOAT DECLARE @tmpflaga INT DECLARE @ilekolek_nad int DECLARE @ilekolek_pod int DECLARE @ilekolek_nad_grupa int SET @ilekolek_nad_grupa = 0 DECLARE @ilekolek_pod_grupa int SET @ilekolek_pod_grupa = 0 DECLARE @ilekolek_lewej int DECLARE @ilekolek_prawej int DECLARE @ilekolek_lewej_grupa int SET @ilekolek_lewej_grupa = 0 DECLARE @ilekolek_prawej_grupa int SET @ilekolek_prawej_grupa = 0 DECLARE crID3 CURSOR LOCAL STATIC FORWARD_ONLY FOR SELECT * FROM @wspolczynnikiTmp OPEN crID3 FETCH NEXT FROM crID3 INTO @tmpid2, @tmpa, @tmpb, @tmpflaga WHILE @@FETCH_STATUS = 0 BEGIN WHILE @i < @ile_grup BEGIN INSERT INTO @kolka SELECT P.id, P.x, P.y, CASE WHEN @tmpflaga = 1 AND ((@tmpa*P.x + @tmpb) < P.y) THEN 'nad' -- punkt nad prostą y=ax+b WHEN @tmpflaga = 1 AND ((@tmpa*P.x + @tmpb) > P.y) THEN 'pod' -- punkt pod prostą y=ax+b WHEN @tmpflaga = 1 AND ((@tmpa*P.x + @tmpb) = P.y) THEN 'prosta' -- punkt na prostej y=ax+b WHEN @tmpflaga = 0 AND (@tmpb < P.x) THEN 'po lewej' -- punkt po lewej od prostej x=b WHEN @tmpflaga = 0 AND (@tmpb > P.x) THEN 'po prawej' -- punkt po prawej od prostej x=b WHEN @tmpflaga = 0 AND (@tmpb = P.x) THEN 'pionowa' -- punkt na prostej x=b END AS spr
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego FROM Punkty AS P WHERE P.typ = @i SET @ilekolek_nad = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'nad') AS kol) SET @ilekolek_pod = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'pod') AS kol) SET @ilekolek_lewej = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'po lewej') AS kol) SET @ilekolek_prawej = (SELECT COUNT(spr) FROM (SELECT * FROM @kolka WHERE spr = 'po prawej') AS kol) IF @tmpflaga = 1 BEGIN IF @ilekolek_pod = 0 OR @ilekolek_nad = 0 BEGIN SET @poprawne = @poprawne + 1 SET @ilekolek_nad_grupa = @ilekolek_nad_grupa + @ilekolek_nad SET @ilekolek_pod_grupa = @ilekolek_pod_grupa + @ilekolek_pod END END ELSE BEGIN IF @ilekolek_prawej = 0 OR @ilekolek_lewej = 0 BEGIN SET @poprawne = @poprawne + 1 SET @ilekolek_lewej_grupa = @ilekolek_lewej_grupa + @ilekolek_lewej SET @ilekolek_prawej_grupa = @ilekolek_prawej_grupa + @ilekolek_prawej END END SET @i = @i + 1 DELETE FROM @kolka END IF @poprawne = @ile_grup BEGIN IF @ilekolek_nad_grupa <> 0 and @ilekolek_pod_grupa <> 0 BEGIN INSERT INTO @proste VALUES(@poprawne,@tmpa, @tmpb) END END IF @poprawne = @ile_grup BEGIN IF @ilekolek_prawej_grupa <> 0 and @ilekolek_lewej_grupa <> 0 BEGIN INSERT INTO @proste VALUES(@poprawne,@tmpa, @tmpb) END END SET @poprawne = 0 SET @i = 0 SET @ilekolek_nad_grupa = 0 SET @ilekolek_pod_grupa = 0 SET @ilekolek_lewej_grupa = 0 SET @ilekolek_prawej_grupa = 0 FETCH NEXT FROM crID3 INTO @tmpid2, @tmpa, @tmpb, @tmpflaga END CLOSE crID3 DEALLOCATE crID3 RETURN END
387
388
MS SQL Server. Zaawansowane metody programowania
Ostatni element algorytmu to nadrzędna funkcja nawigująca po wszystkich możliwych parach klas. Liczbę klas wyznaczono skalarnym zapytaniem wybierającym i podstawiono do zmiennej, która wyznacza górne ograniczenie licznika dwóch zagnieżdżonych pętli WHILE. W ciele tych pętli wywoływane są funkcje wyznaczające proste dyskryminujące oraz usuwające proste przecinające grupy. Wyeliminowane zostało przetwarzanie dla pary stanowiącej tę samą grupę. W końcowej części ciała funkcji zasilana jest wyjściowa zmienna tabelaryczna. CREATE FUNCTION szukajprostej() RETURNS @proste TABLE (Id int IDENTITY(1,1), a FLOAT, b FLOAT) AS BEGIN DECLARE @wspolczynnikiTmp AS TabelaWspolczynnikow; DECLARE @wspolczynnikiTmp2 TABLE (id int IDENTITY(1,1), a FLOAT, b FLOAT, flaga float) DECLARE @wspolczynnikiTmp3 AS TabelaWspolczynnikow2; DECLARE @i int SET @i = 0; DECLARE @j int SET @j = 0; DECLARE @ile_grup int SET @ile_grup = (SELECT COUNT(p.typ) FROM (SELECT typ FROM Punkty GROUP BY typ) AS p) WHILE @i < @ile_grup BEGIN INSERT INTO @wspolczynnikiTmp SELECT * FROM dbo.szukaj_wsp(@i) SET @i = @i + 1 WHILE @j < @ile_grup BEGIN IF @j = @i SET @j = @j + 1 ELSE BEGIN INSERT INTO @wspolczynnikiTmp2 SELECT a,b,flaga FROM szukaj_prostej_pomiedzy_grupami(@wspolczynnikiTmp, @i, @j) SET @j = @j + 1 END END SET @j = 0; END DELETE FROM @wspolczynnikiTmp INSERT INTO @wspolczynnikiTmp3 SELECT a, b, flaga FROM @wspolczynnikiTmp2 group by a,b, flaga INSERT INTO @proste SELECT a, b FROM usun_kolizyjne_proste(@wspolczynnikiTmp3, @ile_grup) RETURN END
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
389
Wywołanie opracowanego algorytmu sprowadza się do wykonania prostego zapytania wybierającego, którego źródłem jest ostatnia z funkcji tabelarycznych. SELECT * FROM dbo.szukajprostej()
Z premedytacją zamieszczony został pełny kod realizujący wskazany algorytm, tak aby możliwe było prześledzenie występujących w kodzie zjawisk składniowych zastosowanych do jego realizacji programistycznej. Aby pomimo swojej objętości kod był czytelny, nie zastosowano pełnej optymalizacji składni. Jest to widoczne w przypadku powtórzenia części kodu dotyczącego sprawdzania warunków w dwóch funkcjach, co mogłoby zaowocować powstaniem kolejnej funkcji lub procedury. Pomimo zastosowania w ciałach elementów proceduralnych niezbyt wydajnego mechanizmu kursora całość daje dobre rezultaty wydajnościowe nawet dla bardzo rozległych wolumenów danych przy dużej liczbie klas. Dla przykładowych danych rezultat takiej klasyfikacji może wyglądać tak jak na rysunku 8.1. Rysunek 8.1. Przykład działania dyskryminatora liniowego
Rozwinięciem kolejnego zadania przedstawionego w rozdziale 4. jest klasyfikacja za pomocą prostych prostopadłych do odcinków łączących najbliższych sąsiadów dwóch klas. Podobnie jak w poprzednim rozwiązaniu, konieczne jest zbudowanie systemu funkcji realizujących kolejne etapy klasyfikacji dla wielu klas. Tutaj również został wprowadzony pomocniczy typ tabelaryczny, który poza współczynnikami prostej oraz flagą będzie przechowywał odległość między najbliżej leżącymi elementami dwóch wskazanych grup. CREATE TYPE TabelaWspolczynnikow3 AS TABLE (ID int IDENTITY(1,1), odleglosc float, a FLOAT, b FLOAT, flaga float)
Najważniejszym elementem jest ponownie funkcja wyznaczająca podział na dwa podobszary. Jej budowa jest podobna do wykorzystanej w poprzednim algorytmie, a zasadnicza różnica dotyczy wyrażeń wyznaczających współczynniki. Ponadto wyznaczana jest odległość między klasami, która jest przechowywana w oddzielnym polu.
390
MS SQL Server. Zaawansowane metody programowania
Ponownie skorzystano z kursora do nawigowania pomiędzy punktami należącymi do wskazanych klas. CREATE FUNCTION szukajprostej1(@PierwszaKlasa int, @DrugaKlasa int) RETURNS @wspolczynniki_prostopadle_wynik TABLE (Id int IDENTITY(1,1), odleglosc float, a FLOAT, b FLOAT) AS BEGIN DECLARE @wspolczynniki TABLE( Id int IDENTITY(1,1), id1 int, id2 int, x1 float, y1 float, x2 float, y2 float, x3 float, y3 float, odleglosc float, a float, b float, a2 float, b2 float, flaga int) DECLARE @ids table(id int) DECLARE @pierwszyPunkt table(id int, x float, y float) DECLARE @tmpid int DECLARE @wspolczynniki_prostopadle AS TabelaWspolczynnikow3; INSERT INTO @ids SELECT id FROM Punkty WHERE typ = @PierwszaKlasa ORDER BY id ASC DECLARE crID CURSOR LOCAL STATIC FORWARD_ONLY FOR SELECT id FROM @ids OPEN crID FETCH NEXT FROM crID INTO @tmpid WHILE @@FETCH_STATUS = 0 BEGIN INSERT INTO @pierwszyPunkt SELECT id, x, y FROM Punkty WHERE typ = @PierwszaKlasa AND id = @tmpid INSERT INTO @wspolczynniki SELECT pp.id, Punkty.id, pp.x, pp.y, Punkty.x, Punkty.y, (pp.x+Punkty.x)/2 AS x3, (pp.y+Punkty.y)/2 AS y3, SQRT((pp.x-Punkty.x)*(pp.x-Punkty.x)+(pp.y-Punkty.y)*(pp.y-Punkty.y)) AS odleglosc, CASE WHEN pp.x <> Punkty.x THEN ((pp.y-Punkty.y)/(pp.x-Punkty.x)) ELSE '0' END AS a, CASE WHEN pp.x <> Punkty.x THEN (pp.y - ((pp.y-Punkty.y)/(pp.x-Punkty.x))*pp.x) WHEN pp.x = Punkty.x THEN pp.x ELSE pp.y END AS b, CASE WHEN pp.y <> Punkty.y THEN -1*(pp.x-Punkty.x)/(pp.y-Punkty.y) -- a2 = -1/a, gdy punkty nie leżą na prostej pionowej
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
391
ELSE 0 -- a2 = 0 END AS a2, CASE WHEN pp.y <> Punkty.y THEN (pp.y+Punkty.y)/2 + ((pp.x-Punkty.x)/(pp.y-Punkty.y))*((pp.x+Punkty.x)/2) -- b2 = y3 - a2*x3 ELSE (pp.y+Punkty.y)/2 -- b2 = (y1+y2)/2 END AS b2, CASE WHEN pp.x = Punkty.x THEN 0 -- x = const = b ELSE 1 -- y = ax + b END AS flaga FROM Punkty JOIN @pierwszyPunkt pp ON (1=1) WHERE typ = @DrugaKlasa AND Punkty.id > @tmpid DELETE FROM @pierwszyPunkt FETCH NEXT FROM crID INTO @tmpid END CLOSE crID DEALLOCATE crID INSERT INTO @wspolczynniki_prostopadle SELECT odleglosc, a2, b2, flaga FROM @wspolczynniki ORDER BY odleglosc ASC INSERT INTO @wspolczynniki_prostopadle_wynik SELECT TOP 1 odleglosc, a, b FROM usun_kolizyjne_proste1(@wspolczynniki_prostopadle, 2) RETURN END
Pozostałe elementy algorytmu pozostają takie same i składają się z iteracyjnego podziału po źle sklasyfikowanych podobszarach, finalnego usunięcia prostych powodujących „nieczyste” podziały oraz nawigacji po wszystkich parach klas, które zostały zdefiniowane w tabeli źródłowej. W celach testowych możemy teraz wywołać naszą funkcję dla wybranej statycznie pary klas według schematu: SELECT * FROM dbo.szukajprostej1(0, 2)
Przykładowy podział dla testowego zadania podziału dla dwóch klas może mieć postać przedstawioną na rysunku 8.2. Rysunek 8.2. Przykład działania dyskryminatora liniowego według drugiego algorytmu
392
MS SQL Server. Zaawansowane metody programowania
Możliwa jest również szybka realizacja bezpośrednio z zastosowaniem rozszerzenia proceduralnego parametrycznego klasyfikatora k-najbliższych sąsiadów. Zbudujmy ponownie tabelę opisującą położenie punktów w przestrzeni, a następnie zasilmy ją według schematu przykładowymi danymi. IF NOT EXISTS(SELECT * FROM sys.objects WHERE name='Data' AND type='U') BEGIN CREATE TABLE Data (Id int PRIMARY KEY IDENTITY(1,1), Klasa INT NOT NULL, XCoord FLOAT NOT NULL, YCoord FLOAT NOT NULL) GO INSERT INTO Data VALUES(1,109,945) INSERT INTO Data VALUES(1,932,107) INSERT INTO Data VALUES(1,601,380) …
W realizującej algorytm funkcji skalarnej, której parametrami są współrzędne klasyfikowanego punktu oraz liczba sąsiadów decydujących o przypisaniu do grupy, sprawdzono statycznie zgodność z ograniczeniami przeszukiwanego obszaru. W pełnej realizacji sprawdzenie przeprowadzane jest dynamicznie na podstawie zmienności danych wejściowych. Ponadto zweryfikowano, czy zadeklarowana liczba sąsiadów jest nieparzysta, w przeciwnym wypadku dla zadania dwuklasowego istnieje groźba niejednoznacznej klasyfikacji. Kolejnym elementem jest zapytanie wybierające wyznaczające odległości między punktem o współrzędnych danych parametrem a wszystkimi pozostałymi punktami. Do ograniczenia liczby wyznaczanych punktów zastosowano dyrektywę TOP, której parametrem jest liczba analizowanych sąsiadów. Wyniki zapytania zostały przekierowane do pomocniczej zmiennej tabelarycznej. Do określenia klasy zastosowano zapytanie z dwoma podzapytaniami. Na najniższym poziomie wybrano z pomocniczej zmiennej tabelarycznej nazwę klasy oraz liczebność sąsiadów jej odpowiadających. Zestaw rekordów został uporządkowany względem liczebności malejąco. Z tego zapytania wyznaczono pierwszego reprezentanta, osiągając nazwę i odległość najliczniejszej klasy, a na najwyższym poziomie informacje przycięto do określenia klasy, którą jako skalar można przypisać do zmiennej zwracanej przez funkcję. IF EXISTS(SELECT * FROM sys.objects WHERE name='Discriminate' AND type='FN') BEGIN DROP FUNCTION Discriminate END GO CREATE FUNCTION Discriminate (@TargetX FLOAT, @TargetY FLOAT, @FactorK INT=3) RETURNS INT AS BEGIN DECLARE @ReturnClass INT = 0, @minX FLOAT = 10, @minY FLOAT = 10 DECLARE @maxX FLOAT = 800, @maxY FLOAT = 800 IF NOT (@TargetX BETWEEN @minX AND @maxX AND @TargetY BETWEEN @minY AND @maxY AND @FactorK % 2 = 1 AND @FactorK >= 1) RETURN -1 DECLARE @DistanceTable TABLE
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
393
(Distance FLOAT, Klasa INT) INSERT INTO @DistanceTable SELECT TOP (@FactorK) SQRT(SQUARE(@TargetX - D.XCoord) + SQUARE(@TargetY - D.YCoord)) AS Distance, D.Klasa FROM dbo.Data D ORDER BY Distance SET @ReturnClass = (SELECT Results.Klasa FROM ( SELECT TOP 1 R.Klasa, C.Amount, R.Distance FROM @DistanceTable R JOIN (SELECT Klasa, COUNT(Klasa) AS Amount FROM @DistanceTable GROUP BY Klasa) AS C ON R.Klasa = C.Klasa ORDER BY C.Amount DESC, R.Distance ASC ) AS Results) RETURN @ReturnClass END
Aby uzyskać bardziej elegancki kod, utworzono funkcję zewnętrzną, której podstawowym zadaniem jest wychwytywanie wyjątków i generowanie odpowiednich komunikatów. Następnie wywołano funkcję, która odgrywa w zapytaniu rolę filtra, tak aby wyświetlone zostały wszystkie elementy tej klasy, do której został przypisany badany punkt. IF EXISTS(SELECT * FROM sys.objects WHERE name='DisplayDiscriminate' AND type='P') BEGIN DROP PROCEDURE DisplayDiscriminate END GO CREATE PROCEDURE DisplayDiscriminate (@TargetX FLOAT, @TargetY FLOAT, @FactorK INT=3) AS BEGIN IF @FactorK % 2 = 0 RAISERROR('Liczba sąsiadów musi być parzysta', 10, 2) IF @FactorK <= 0 RAISERROR('Liczba sąsiadów musi być większa niż 1', 10, 2) SELECT c.Name AS ClassName, @TargetX AS X, @TargetY AS Y, @FactorK AS k FROM dbo.Classes c WHERE c.Klasa = dbo.Discriminate(@TargetX, @TargetY, @FactorK) END GO
Aby otrzymać ostateczne wyniki, wystarczy w bloku anonimowym wywołać nadrzędną procedurę. EXEC DisplayDiscriminate 100, 100, 3
W realizacji powyższego algorytmu nie został zastosowany kursor, lecz jedynie zapytania wybierające, co powoduje, że algorytm ten jest bardzo szybki, a czas obliczeń praktycznie nie zależy od liczby rekordów analizowanej tabeli. Wydajność przetwarzania, tak jak to już podkreślono, jest wynikiem działania wbudowanych narzędzi
394
MS SQL Server. Zaawansowane metody programowania
optymalizujących przetwarzanie, szczególnie wpływających na tworzenie optymalnych planów wykonania zapytań występujących w ciele elementów proceduralnych. Podkreślić też należy, że w miejscu wywołania odwołujemy się do obiektu skompilowanego. Niebagatelną rolę odgrywają również indeksy, które mogą zostać zdefiniowane na przeszukiwanych tabelach. Pomimo że poprzednie algorytmy wykorzystywały mniej wydajne kursory, ze względu na ich umieszczenie po stronie relacyjnej ich wydajność jest również wysoka, a ich złożoność obliczeniowa jest liniowa.
8.2. Funkcje agregujące definiowane przez użytkownika Podczas omawiania typów użytkownika stwierdzono, że zastosowanie typów różnych od prostej do definiowania ich właściwości powoduje konieczność zdefiniowania przez użytkownika sposobu serializacji, czyli metod, za których pomocą dane binarne są zapisywane i odczytywane ze strumienia. Sposób rozwiązania tego zagadnienia pokazano na przykładzie innego obiektu, który wykorzystuje programowanie CLR, a którym jest funkcja agregująca. Klasa obiektowa, która taką funkcję implementuje, musi zawierać co najmniej cztery metody, których nazwy są obowiązkowe (ze względu na środowisko programistyczne istotna jest wielkość liter): metodę Init — wykonuje się ona przed każdą grupą rekordów, dla której
wyznaczana jest funkcja; metoda ta służy do inicjalizacji wartości zmiennych; metodę Accumulate — jej parametrem jest wartość, dla której wyznaczana
jest agregacja; metoda ta wykonuje się dla każdego rekordu grupy i służy do przeprowadzenia właściwych obliczeń wartości funkcji; metodę Terminate — wykonuje się ona na końcu każdej grupy i służy
do ostatecznego sformatowania wyniku; metodę Merge — musi ona zostać zdefiniowana, ale jej ciało może być puste.
Jeśli nie jest puste, to w przypadku przetwarzania wielowątkowego metoda ta dokonuje połączenia wyników otrzymanych z procesów równoległych. W przypadku serializacji definiowanej przez użytkownika muszą pojawić się dodatkowo metody Read i Write. W pokazywanym przykładzie zrealizowana zostanie funkcja wyznaczająca medianę, zwaną też wartością środkową, wartością przeciętną, drugim kwartylem lub kwantylem rzędu ½. Mediana jest ustalana dla nieparzystej liczby elementów jako wartość leżąca w środku posortowanych wartości. W przypadku parzystej liczby elementów jest średnią z dwóch elementów leżących najbliżej środka. Przykładowy kod zostanie opisany z podziałem na funkcjonalne fragmenty. Na początku klasy zostały zaimportowane właściwe przestrzenie nazw, co umożliwia posługiwanie się krótkimi nazwami właściwości i metod. using using using using
System; System.Data; System.Data.SqlClient; System.Data.SqlTypes;
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
395
using System.Collections; using System.Collections.Generic; using Microsoft.SqlServer.Server;
Potem następują dyrektywy kompilatora, z których pierwsza wskazuje na zastosowanie serializacji, a druga określa, że klasa ma być po stronie serwera interpretowana jako funkcja agregująca — jej parametry wskazują, że serializacja zostanie zdefiniowana przez programistę. Logiczną nazwą tej klasy będzie "mediana", a maksymalna wartość jednocześnie przesyłanych w trakcie serializacji danych nie przekroczy 8000b, co jest najwyższą dopuszczalną wartością. [Serializable] [SqlUserDefinedAggregate(Format.UserDefined, Name = "mediana", MaxByteSize = 8000)]
Zasadniczą część kodu rozpoczyna deklaracja struktury publicznej, która ze względu na definiowanie serializacji przez użytkownika musi dziedziczyć po interfejsie IBinary Serialize. Następnie zadeklarowane zostaną dwie zmienne pomocnicze — pierwsza przeznaczona na wynik, druga, typu List, stanowi tymczasowe miejsce składowania danych z grupy obsługiwanych rekordów. Typ drugiej zmiennej pozwala na składowanie w postaci listy wartości rzeczywistych o nieokreślonej maksymalnej liczbie elementów. public struct Med : IBinarySerialize { private double mediana; private List list;
W klasie Init dokonano wyzerowania wartości wyniku oraz zainicjowano instancję zmiennej będącej listą wartości. public void Init() { mediana = 0; list = new List(); }
Metoda Accumulate jest wykorzystywana tylko do przepisania kolejnych niepustych wartości zmiennej do pomocniczej listy. Jej parametrem jest zmienna o typie double?, który w odróżnieniu od oryginalnego typu double dopuszcza wartości puste, co jest zgodne z ideą przechowywania danych w bazie, a przede wszystkim jest równoważna typowi rzeczywistemu występującemu w SQL. Pomijanie wartości NULL jest uznaną powszechnie cechą wszystkich zaimplementowanych w bazach danych funkcji agregujących. public void Accumulate(double? Value1) { if (Value1 != null) { list.Add((double)Value1); } }
Łączenie wyników z procesów równoległych jest realizowane za pomocą metody AddRange działającej na rzecz pomocniczej listy.
396
MS SQL Server. Zaawansowane metody programowania public void Merge(Med Group) { Group.list.AddRange(this.list); }
Metoda Terminate ma w swoim ciele tylko rzutowanie wynikowej zmiennej na docelowy typ danych obsługiwany przez SQL. Jak widać, nie zawiera ono żadnych obliczeń prowadzących do wyznaczenia mediany; zostały one przejęte przez metodę Write obsługującą serializację. public SqlDouble Terminate() { return new SqlDouble(mediana); }
Metoda Write przeciąża oryginalną metodę interfejsu IBinarySerialize. W jej ciele wykonywane są zasadnicze obliczenia zgodnie z podanymi wcześniej zależnościami. Na koniec, wykorzystując oryginalną metodę Write, dla parametru typu IO.BinaryWriter dokonano serializacji, czyli zapisu do postaci binarnej pojedynczej zmiennej zawierającej wynik. public void Write(System.IO.BinaryWriter w) { if (Convert.ToBoolean(list.Count % 2)) { mediana = list[list.Count / 2]; } else { mediana = (list[list.Count / 2 - 1] + list[list.Count / 2]) / 2; } w.Write(mediana); }
Przeciążenie oryginalnej metody Read pozwala na odczytanie zapisanej binarnie wartości. Niejawnie jest ona przekazywana do metody Terminate, którą omówiono poprzednio. public void Read(System.IO.BinaryReader r) { mediana = r.ReadDouble(); } }
Na tym kończy się definicja struktury, która po skompilowaniu do postaci biblioteki *.dll może zostać inkapsulowana do właściwego typu proceduralnego po stronie serwera SQL. W tym celu należy utworzyć element pośredniczący ASSEMBLY, w którego definicji podajemy informację o tym, kto jest uprawniony do jego wykorzystania w bazie, wskazujemy w postaci nazwy kwalifikowanej nazwę skompilowanej biblioteki oraz sposób odwołania do zasobów. CREATE ASSEMBLY [MEDIANA_a] AUTHORIZATION [dbo] FROM 'C:\...\mediana.dll' WITH PERMISSION_SET = SAFE
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
397
Pozostaje utworzenie funkcji agregującej, w której definicji należy podać parametr zgodny co do typu ze zmienną wejściową oraz po słowie kluczowym RETURNS typ zgodny z typem zmiennej zwracanej za pomocą metody Termminate. Ostatnim elementem polecenia jest wskazanie na używaną strukturę opisującą agregat. Nazwa kwalifikowana składa się z nazwy obiektu ASSEMBLY oraz nazwy struktury na poziomie języka wyższego rzędu, w naszym przypadku C#. CREATE AGGREGATE mediana(@value float) RETURNS float EXTERNAL NAME MEDIANA_a.Med;
Tak opracowana funkcja agregująca może zostać wykorzystana w zapytaniach SQL zarówno zawierających zwykłe grupowanie, jak i wykorzystujących definicję okna logicznego. Kolejnym przykładem obiektowej realizacji w relacyjnej bazie danych funkcji agregującej jest współczynnik korelacji Pearsona, który wyznacza zależność między dwiema wartościami, które możemy interpretować jako współrzędne punktów w przestrzeni dwuwymiarowej. Współczynnik ten może zostać opisany zależnością: n
rxy
x i 1
i
1 n n 1 n xi yi yi n i 1 i 1 n i 1
1 n x x i n i 1 i i 1 n
2
1 n y y i n i 1 i i 1 n
2
.
Wartość tego współczynnika jest skalowana do przedziału obustronnie domkniętego: rxy 1,1 .
Analizując równanie, możemy zauważyć, że licznik opisuje kowariancje dwóch zmiennych, a mianownik jest iloczynem odchyleń standardowych każdej z nich. rxy
COV( x, y)
x y
Jeśli teraz spróbujemy utworzyć strukturę realizującą taką zależność, to w jej ciele musimy wyznaczyć trzy funkcje agregujące. Jeśli jednak wrócimy do równania wyjściowego, to zauważymy, że dodatkowo konieczne jest wyznaczenie wartości średnich obu zmiennych, czyli kolejnych dwóch agregatów. Niestety, nie jest możliwe przekazywanie wyników między funkcją agregującą SQL a wnętrzem klasy, dlatego cały algorytm musimy zawrzeć w jednym kodzie. Przy okazji możemy zauważyć, że w odróżnieniu od standardowych funkcji agregujących ta będzie wyznaczana dla pary parametrów. Tak jak poprzednio, kod rozpoczyna import przestrzeni nazw, dyrektywy dla kompilatora — informujące go, jakiego rodzaju obiekt jest tworzony oraz że serializacja realizowana jest przez użytkownika.
398
MS SQL Server. Zaawansowane metody programowania using System; using System.IO; using System.Collections; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; [Serializable] [SqlUserDefinedAggregate(Format.UserDefined, Name = "WspolKor", MaxByteSize = -1)]
Definicję struktury przeciążającej interfejs IBinarySerialize rozpoczyna deklaracja zmiennych pomocniczych. Dwie pierwsze, typu ArrayList, dla każdej z analizowanych grup rekordów będą przechowywały czasowo wartości pól przekazywanych z bazy. Poza tym zdefiniowano zmienną stanowiącą licznik rekordów oraz dwie zmienne, które będą przechowywały sumy wartości pól z analizowanych rekordów. public struct WspolKor : IBinarySerialize { private ArrayList tabx, taby; private int licznik; private double sumax; private double sumay;
Metoda Init tworzy nowe instancje obu zmiennych ArrayList oraz zeruje pozostałe zmienne pomocnicze. public void Init() { tabx = new ArrayList(); taby = new ArrayList(); sumax = 0; sumay = 0; licznik = 0; }
W metodzie Accumulate, gdy obydwie wartości zmiennych są niepuste, do list dopisywane są kolejne pozycje, sumy są powiększane o ich wartość, a licznik jest inkrementowany. Jak już zaznaczono, pominięcie wartości NULL wynika z cech funkcji agregujących, a zastosowanie typu double? ze zgodności ze sposobem przechowywania danych rzeczywistych po stronie SQL. public void Accumulate(double? x, double? y) { if (x != null && y != null) { tabx.Add((double)x); sumax = sumax + (double)x; taby.Add((double)y); sumay = sumay + (double)y; licznik++; } }
W przykładowej realizacji zdecydowano się na zastosowanie pustej metody Merge, co jest dopuszczalne. public void Merge(WspolKor Group) { }
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
399
Serializacja danych do postaci binarnych jest realizowana w ten sposób, że najpierw zapisujemy dane skalarne, a następnie we wspólnej pętli kolejne elementy obu list. Możliwe jest również zastosowanie dwóch oddzielnych pętli na każdą z list oddzielnie. Zaproponowane rozwiązanie powinno być wydajniejsze. public void Write(BinaryWriter w) { w.Write(licznik); w.Write(sumax); w.Write(sumay); for (int i = 0; i < licznik; i++) { w.Write((double)tabx[i]); w.Write((double)taby[i]); } }
Odczytanie danych z postaci binarnej wymaga utworzenia nowych instancji dla obu list. Następnie zgodnie z kolejnością zapisu odczytywane są skalary, a później w pętli odczytywane są i wstawiane do nowo utworzonych instancji kolejne elementy obu list: public void Read(BinaryReader r) { tabx = new ArrayList(); taby = new ArrayList(); licznik = r.ReadInt32(); sumax = r.ReadDouble(); sumay = r.ReadDouble(); for (int i = 0; i < licznik; i++) { tabx.Add(r.ReadDouble()); taby.Add(r.ReadDouble()); } }
Tym razem zasadnicze przetwarzanie danych wykonywane jest w ciele metody Terminate. Z definicyjnych zależności wyznaczane są średnie obu zmiennych. W pierwszej pętli wyznaczana jest wartość kowariancji zmiennych, czyli licznika definiującego współczynnik korelacji. W kolejnej — dwie wartości odchyleń wariancji (kwadrat odchylenia standardowego), które są czynnikami w mianowniku wyrażenia definiującego wyznaczaną wielkość. Ostatnia linijka zawiera formatowanie zwracanej wartości do docelowego typu SQL. Wykonywane jest właściwe dzielenie, a z iloczynu wariacji wyznaczany jest pierwiastek. public SqlDouble Terminate() { double sredniax = sumax / (double)licznik; double sredniay = sumay / (double)licznik; double x; double y; double sigma = 0; double sigmax = 0; double sigmay = 0; for (int i = 0; i < licznik; i++) { x = (double)tabx[i];
400
MS SQL Server. Zaawansowane metody programowania y = (double)taby[i]; sigma = sigma + (x - sredniax) * (y - sredniay); } for (int i = 0; i < licznik; i++) { x = (double)tabx[i]; sigmax = sigmax + Math.Pow(x - sredniax, 2); y = (double)taby[i]; sigmay = sigmay + Math.Pow(y - sredniay, 2); } return new SqlDouble((sigma / licznik) / (Math.Sqrt((sigmax / licznik) * (sigmay / licznik)))); } }
Tak samo jak poprzednio, przed użyciem klasy po stronie serwera SQL konieczne jest utworzenie obiektu pośredniczącego ASSEMBLY oraz funkcji agregującej. Możliwe jest, aby w jednej bibliotece *.dll zostało utworzonych wiele struktur definiujących funkcje agregujące lub obiektowe typy użytkownika. Jednak należy pamiętać o tym, iż w przypadku modyfikacji klasy trzeba usunąć ASSEMBLY. Natomiast do skutecznego wykonania takiego polecenia konieczne jest uprzednie usunięcie wszystkich elementów proceduralnych, które z niego korzystają. W przypadku biblioteki bardzo rozległej, zawierającej wiele elementów, może to być dość kłopotliwe. Należy zaznaczyć, że obie przykładowe funkcje agregujące nie są zaimplementowane w komercyjnym serwerze, natomiast są bardzo użyteczne w statystycznej analizie wyników. Druga z nich jest powszechnie stosowanym wskaźnikiem do redukcji przestrzeni cech za pomocą analizy głównych składowych (ang. Principal Component Analysis — PCA).
8.3. Analiza sieci powiązań Bardzo istotnym zagadnieniem jest analiza grafów. Szczególne znaczenie zyskały te, które opisują sieci powiązań, np. sieci społecznościowe, połączenia telefoniczne, wymianę poczty elektronicznej itp. Analizy te mają bardzo szerokie zastosowanie np. przy badaniu zjawisk społecznych, monitorowaniu niepożądanych wiadomości (spam), prób wyłudzeń, przeciwdziałaniu i wykrywaniu przestępczości, w tym terroryzmu. Od strony opisu na poziomie bazy danych problem daje się zredukować do jednej tabeli o dwóch polach, które reprezentują połączenia między węzłami. Jest to tzw. krawędziowy opis grafu, a przykładowa tabela może zostać utworzona za pomocą przedstawionego skryptu. IF EXISTS (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Wezly') DROP TABLE Wezly; CREATE TABLE Wezly (Wezel1 int NOT NULL, Wezel2 int NOT NULL);
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
401
Zasilenie danymi możemy zrealizować za pomocą serii instrukcji INSERT, jak pokazano niżej. INSERT INSERT INSERT INSERT …
INTO INTO INTO INTO
Wezly Wezly Wezly Wezly
VALUES(1,2); VALUES(2,3); VALUES(2,4); VALUES(1,5);
Pierwszym etapem jest określenie liczby grup skupionych wokół węzła centralnego. Nie jest to zadanie jednoznacznie określone, ponieważ jest zależne od tego, ile połączonych węzłów wyznacza grupę. Innymi słowy, jeśli połączeń wychodzących z węzła jest mniej niż określona granica, to może być on traktowany na tych samych zasadach, jakby był izolowany i nie miał żadnych połączeń. Możemy zauważyć, że w grafie składającym się z n węzłów liczba różnych połączeń nie może być większa niż: k
n(n 1) . 2
Zakładając, że grupę znajomych oznacza procent wszystkich możliwych połączeń i że r-krotny wzrost liczby grup powoduje r-krotny spadek liczby wszystkich połączeń, możemy przyjąć, że liczbę grup P możemy wyrazić zależnością: P
n(n 1) k , 2m m
w której m jest liczbą wszystkich połączeń (liczność tabeli Wezly). W celu wyznaczenia przedstawionej wyżej zależności utworzona została funkcja skalarna, której parametrem jest procent wszystkich połączeń w grafie, od którego połączone nimi węzły wyznaczają grupę. Po deklaracji zmiennych lokalnych zapytaniem skalarnym wyznaczamy liczbę wierszy w tabeli Wezly, a kolejnym liczbę węzłów w tabeli, które definiują połączenia w grafie. Jako źródło zostały wybrane połączone operatorem UNION dwa zapytania, wybierające przeciwne węzły z notacji krawędziowej. Zapewnia to, że w źródle pojawią się wszystkie węzły, nawet wtedy, gdy jakieś z nich znajdą się tylko w pierwszej czy drugiej kolumnie tabeli. Dodatkowo zastosowany operator mnogościowy zapewnia pominięcie duplikatów. Pozostaje tylko obliczenie pokazanego poprzednio wyrażenia. CREATE FUNCTION policz_liczbe_grup(@wsp real) RETURNS int AS BEGIN DECLARE @liczba_mozliwych_polaczen int DECLARE @liczba_grup int DECLARE @liczba_wezlow int DECLARE @liczba_polaczen int SELECT @liczba_polaczen=COUNT(*) FROM Wezly SELECT @liczba_wezlow=COUNT(*) FROM (SELECT Wezel1 FROM Wezly UNION SELECT Wezel2 FROM Wezly) AS x SET @liczba_mozliwych_polaczen=(@liczba_wezlow-1)*(@liczba_wezlow/2)
402
MS SQL Server. Zaawansowane metody programowania SET @liczba_grup=CEILING((@liczba_mozliwych_polaczen*@wsp)/@liczba_polaczen) RETURN @liczba_grup END
Sprawdzenie działania wykonano w bloku anonimowym, zakładając, że 25% wszystkich połączeń stanowi grupę. DECLARE @liczba_grup int SET @liczba_grup = dbo.policz_liczbe_grup(0.25) PRINT @liczba_grup;
W kolejnym kroku wyznaczymy węzły centralne grup, tzn. takie, które mają największą liczbę połączeń. W rozwiązaniu wykorzystano liczbę grup wyznaczonych przez poprzednio utworzoną funkcję do przycięcia za pomocą dyrektywy TOP liczby rekordów zwracanych przez nazwane w klauzuli WITH zapytanie. Podobnie jak w przypadku funkcji, podstawą są połączone operatorem UNION zapytania zwracające węzły. Zliczenia połączeń dokonano za pomocą funkcji agregującej wyznaczonej nad oknem logicznym. Ostateczne sformatowanie danych jest otrzymywane na skutek złączenia tego zapytania z takim, które działa bezpośrednio na tabeli Wezly. DECLARE @liczba_grup int SET @liczba_grup = dbo.policz_liczbe_grup(0.25) WITH Srodki AS (SELECT TOP(@liczba_grup) y.Srodek AS Srodek, y.liczbaPolaczen FROM (SELECT DISTINCT x.Wezel1 AS Srodek, COUNT(*) OVER (PARTITION BY x.Wezel1) AS liczbaPolaczen FROM (SELECT w.Wezel1 AS Wezel1 FROM (SELECT Wezel1 FROM Wezly UNION SELECT Wezel2 FROM Wezly )AS w JOIN Wezly z ON w.Wezel1=z.Wezel1 OR w.Wezel1=z.Wezel2)AS x )AS y ORDER BY y.liczbaPolaczen DESC) SELECT * FROM Srodki
Teraz spróbujmy utworzyć element proceduralny, który dla każdego z centrów wyznaczonych grup dołączy członków w ten sposób, że ich odległość od centrum nie może przekroczyć określonej odległości liczby węzłów pośrednich i że węzły te będą powiązane z wystarczającą liczbą sąsiadów. Należy zauważyć, że takie rozwiązanie będzie obejmowało również takie przypadki, w których węzeł może być przypisany do przynajmniej dwóch grup, co ilustruje rysunek 8.3. Utworzona funkcja tabelaryczna posiada parametr, który wskazuje na minimalną liczbę węzłów, które mogą wyznaczać grupę. Zwraca ona pola opisujące: jej nazwę przez numer węzła stanowiącego centrum grupy, numer węzła przynależącego do tej grupy, współczynnik przynależności mierzony jako odległość od centrum. CREATE FUNCTION sn(@minimum int) RETURNS @resultTable TABLE (NazwaGrupy int, CzlonekGrupy int, StopienPrzynaleznosci int) AS
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
403
Rysunek 8.3. Przykład podziału sieci powiązań na grupy
W początkowej części ciała funkcji zadeklarowano pomocnicze zmienne tabelaryczne. Możliwe byłoby również zastosowanie uprzednio utworzonych typów tabelarycznych. BEGIN DECLARE @t1Table table (groupCenter int, CzlonekGrupys int) DECLARE @t2Table table (Uczestnik1 int, Uczestnik2 int) DECLARE @t3Table table (Centrum int, Potomek int)
Następuje wyznaczenie centrów grup o liczności większej niż wskazana parametrem. Zastosowano omówiony w tym podrozdziale mechanizm korzystania z operatora UNION do wyznaczenia źródłowego zestawu rekordów. Ponadto dokonano złączenia pozwalającego na uznanie za członków grupy węzłów połączonych ze środkiem za pośrednictwem węzłów pośrednich. Zapytanie to wstawia rekordy do pomocniczej zmiennej tabelarycznej. INSERT INTO @t1Table SELECT SrodekGrupy.Wz AS Grupa, Sasiedzi.Wezel1 AS Wezly FROM ( SELECT Polaczenia.Wezel AS Wz, Polaczenia.CN AS PDegree FROM (SELECT Wezel1 AS Wezel, SUM(ile) AS CN FROM (SELECT Wezel1, COUNT(*) AS ile FROM Wezly GROUP BY Wezel1 UNION ALL SELECT Wezel2, COUNT(*) AS ile FROM Wezly GROUP BY Wezel2 ) AS ilosc_relacji GROUP BY ilosc_relacji.Wezel1 ) AS Polaczenia WHERE Polaczenia.CN >= @minimum ) AS SrodekGrupy JOIN (SELECT tt.Wezel1 FROM (SELECT Wezel1 FROM Wezly UNION SELECT Wezel2 FROM Wezly) AS tt ) AS Sasiedzi ON ((((CAST(SrodekGrupy.Os AS varchar)+
404
MS SQL Server. Zaawansowane metody programowania CAST(Sasiedzi.Wezel1 AS varchar)) IN (SELECT CAST(Wezel1 AS varchar)+CAST(Wezel2 AS varchar) FROM Wezly)))) OR (((CAST(Sasiedzi.Wezel1 AS varchar))+ (CAST(SrodekGrupy.Os AS varchar)) IN (SELECT CAST(Wezel1 AS varchar)+CAST(Wezel2 AS varchar) FROM Wezly)))
Przeszukiwanie wnętrza grup jest wykonywane za pomocą par zagnieżdżonych kursorów, z których zewnętrzny wyszukuje węzły połączone z centrum skupienia grupy. W definicji zastosowano odwołanie do tabeli pomocniczej zasilonej poprzednim zapytaniem. DECLARE cr_siec1 CURSOR FOR SELECT groupCenter, CzlonekGrupys FROM @t1Table DECLARE @GSrodek int DECLARE @GCzlonek int DECLARE @pgn int DECLARE @tmtable TABLE(Uczestnik int) OPEN cr_siec1 FETCH NEXT FROM cr_siec1 INTO @GSrodek, @GCzlonek
Każdy z elementów przeszukiwanych kursorem staje się prowizorycznym centrum grupy, a powiązany z nim — elementem członkiem. Operacja jest powtarzana do końca nawigacji przez zestaw rekordów, a unikalne pary rekordów są dopisywane do kolejnej pomocniczej zmiennej tabelarycznej. SET @pgn = @GSrodek INSERT INTO @tmtable SELECT @GCzlonek WHILE @@FETCH_STATUS = 0 BEGIN IF @pgn <> @GSrodek BEGIN INSERT INTO @t2Table -- wpisanie unikalnych par punktów danej grupy SELECT DISTINCT t1.Uczestnik as m1, t2.Uczestnik as m2 FROM @tmtable AS t1, @tmtable AS t2 WHERE t1.Uczestnik < t2.Uczestnik ORDER BY t1.Uczestnik DELETE @tmtable INSERT INTO @tmtable SELECT @GCzlonek
Następuje zadeklarowanie kolejnego, wewnętrznego kursora, który przeszukuje członków grupy na kolejnych poziomach, realizując tzw. zachłanny algorytm przeszukiwania grafu. Skutki poszukiwań są zapisywane do pary pomocniczych zmiennych tabelarycznych. DECLARE cr_siec2 CURSOR FOR SELECT Uczestnik1, Uczestnik2 FROM @t2Table DECLARE @s2CG1 int DECLARE @s2CG2 int OPEN cr_siec2 FETCH NEXT FROM cr_siec2 INTO @s2CG1, @s2CG2 WHILE @@FETCH_STATUS = 0 BEGIN IF ((CAST(@s2CG1 AS VARCHAR)+CAST(@s2CG2 AS VARCHAR) IN (SELECT CAST(Wezel1 AS VARCHAR)+CAST(Wezel2 AS VARCHAR) FROM Wezly)) OR (CAST(@s2CG2 AS VARCHAR)+CAST(@s2CG1 AS VARCHAR) IN (SELECT CAST(Wezel1 AS VARCHAR)+CAST(Wezel2 AS VARCHAR) FROM Wezly)) ) INSERT INTO @t3Table SELECT @pgn, @s2CG1 INSERT INTO @t3Table SELECT @pgn, @s2CG2 FETCH NEXT FROM cr_siec2 INTO @s2CG1, @s2CG2 END
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
405
Następuje zamknięcie i zwolnienie zasobów wewnętrznego kursora oraz wyczyszczenie pomocniczej zmiennej tabelarycznej. CLOSE cr_siec2 DEALLOCATE cr_siec2 DELETE @t2Table SET @pgn = @GSrodek END ELSE BEGIN INSERT INTO @tmtable SELECT @GCzlonek END FETCH NEXT FROM cr_siec1 INTO @GSrodek, @GCzlonek END INSERT INTO @t2Table -- wpisanie unikalnych par punktów danej grupy SELECT DISTINCT t1.Uczestnik AS m1, t2.Uczestnik AS m2 FROM @tmtable as t1, @tmtable AS t2 WHERE t1.Uczestnik < t2.Uczestnik ORDER BY t1.Uczestnik DELETE @tmtable
Powtórnie otwierany jest kursor wewnętrzny, w którym obsługiwane są elementy warstw zewnętrznych. Kod jest analogiczny do kodu poprzedniego kursora wewnętrznego. DECLARE cr_siec2s CURSOR FOR SELECT Uczestnik1, Uczestnik2 FROM @t2Table DECLARE @s2CG1s int DECLARE @s2CG2s int open cr_siec2s FETCH NEXT FROM cr_siec2s INTO @s2CG1s, @s2CG2s WHILE @@FETCH_STATUS = 0 BEGIN IF ( (CAST(@s2CG1s AS VARCHAR)+CAST(@s2CG2s AS VARCHAR) IN (SELECT CAST(Wezel1 AS VARCHAR)+CAST(Wezel2 AS VARCHAR) FROM Wezly)) OR (CAST(@s2CG2s AS VARCHAR)+CAST(@s2CG1s AS VARCHAR) IN (SELECT CAST(Wezel1 AS VARCHAR)+CAST(Wezel2 AS VARCHAR) FROM Wezly)) ) INSERT INTO @t3Table SELECT @pgn, @s2CG1s INSERT INTO @t3Table SELECT @pgn, @s2CG2s FETCH NEXT FROM cr_siec2s INTO @s2CG1s, @s2CG2s END
Zamykany jest drugi kursor wewnętrzny oraz kursor zewnętrzny, a przydzielone im zasoby zostają zwolnione. CLOSE cr_siec2s DEALLOCATE cr_siec2s CLOSE cr_siec1 DEALLOCATE cr_siec1
Ostatni fragment kodu stanowi określenie stopnia przynależności. W tym celu tworzony jest kursor obsługujący zmienną tabelaryczną wypełnioną danymi zwracanymi przez kursory poprzedniej części kodu, w której zapisane są informacje o centrach oraz członkach wykrytych grup. Nawigując po wszystkich elementach, tworzymy pary węzłów w grupie:
406
MS SQL Server. Zaawansowane metody programowania DECLARE cr_siec3 CURSOR FOR SELECT Centrum, Potomek FROM @t3Table DECLARE @k int -- dla węzła centralnego DECLARE @s int -- dla węzła potomnego DECLARE @control1 int -- jeśli napotkano inną grupę DECLARE @tmtable2 table(fgm int) -- zmienna tabelaryczna FinalCzlonek Grupy DECLARE @pct table (fWezel1 int, fWezel2 int) -- PointCheckTable DECLARE @sCounter int = 0 -- LicznikPolaczenSatelity DECLARE @ZliczTable TABLE(person int, cValue int) DECLARE @OstatniWstawiony int OPEN cr_siec3 FETCH NEXT FROM cr_siec3 INTO @k, @s SET @control1 = @k -- wpisujemy do zmiennej kontrolnej poprzednie centrum skupienia grupy INSERT INTO @tmtable2 SELECT @s -- wstawiamy pierwszego członka do tabeli członków danej grupy WHILE @@FETCH_STATUS = 0 BEGIN IF @control1 <> @k -- kiedy zaczynamy pobierać członków nowej grupy BEGIN INSERT INTO @pct -- wpisz członków poprzedniej grupy do tabeli i zwróć wszelkie możliwe pary SELECT DISTINCT tmt2a.fgm, tmt2b.fgm from @tmtable2 AS tmt2a, @tmtable2 AS tmt2b WHERE tmt2a.fgm <> tmt2b.fgm ORDER BY tmt2a.fgm DELETE @tmtable2 -- usuń członków poprzedniej grupy INSERT INTO @tmtable2 SELECT @s -- wstaw pierwszego członka nowej grupy
Otwierany jest kursor wewnętrzny nawigujący po elementach będących członkami poprzedniej grupy w celu sprawdzenia, czy nie pojawią się ponownie w kolejnej z grup. DECLARE cr_inner CURSOR FOR SELECT fWezel1, fWezel2 FROM @pct DECLARE @v1 int DECLARE @v2 int OPEN cr_inner FETCH NEXT FROM cr_inner INTO @v1, @v2 WHILE @@FETCH_STATUS = 0 BEGIN -- dla każdej pary z grupy występującej w sieci dodaj wpis do @ZliczTable IF ( (CAST(@v1 AS VARCHAR)+CAST(@v2 AS VARCHAR) IN (SELECT CAST(Wezel1 AS VARCHAR)+CAST(Wezel2 AS VARCHAR) FROM Wezly)) OR (CAST(@v2 AS VARCHAR)+CAST(@v1 AS VARCHAR) IN (SELECT CAST(Wezel1 AS VARCHAR)+CAST(Wezel2 AS VARCHAR) FROM Wezly)) ) INSERT INTO @ZliczTable SELECT @v1, 1; FETCH NEXT FROM cr_inner INTO @v1, @v2 END
Zamknięto i zwolniono zasoby kursora wewnętrznego, formatując ostateczny wynik dla bieżącej grupy. Do stopnia przynależności grupy dodano 1, co ma na celu uwzględnienie węzła centralnego. Wyczyszczono pomocnicze zmienne tabelaryczne i wykonano nawigację do kolejnego rekordu dla kursora zewnętrznego. CLOSE cr_inner DEALLOCATE cr_inner INSERT INTO @resultTable SELECT @control1, person, SUM(cValue)+1 FROM @ZliczTable GROUP BY person DELETE @pct SET @control1 = @k END ELSE
Rozdział 8. Problemy rozwiązywane za pomocą rozszerzenia proceduralnego i obiektowego
407
BEGIN INSERT INTO @tmtable2 SELECT @s END FETCH NEXT FROM cr_siec3 INTO @k, @s DELETE @ZliczTable END
Zamknięto i zwolniono zasoby kursora zewnętrznego, a utworzoną tabelę wynikową przekazano do miejsca wywołania funkcji. CLOSE cr_siec3 DEALLOCATE cr_siec3 RETURN END; GO
Sprawdzenie skutków działania utworzonej funkcji tabelarycznej może być wykonane za pomocą zapytania wybierającego, dla którego źródłem jest ta funkcja. SELECT NazwaGrupy, CzlonekGrupy , StopienPrzynaleznosci FROM dbo.sn(5)
Pokazano analizę dla grafu, w którym wszystkie połączenia są równoważne. Jednak możemy ten algorytm rozciągnąć na grafy, w których każde połączenie ma przypisaną dodatkową cechę, wagę, np. odległość między węzłami, przepustowość łącza, liczbę połączeń. W takim przypadku należy w prezentowanym algorytmie wyrażenie analizujące stopień przynależności do węzła zmienić na właściwe, pamiętając tylko o tym, aby zastosowana miara spełniała co najmniej dwa pierwsze warunki metryki: by miała niezmiennik (by odległość między tym samym punktem była równa zero): d(a,a) 0 ,
i by była zwrotna (odległość nie jest zależna od kierunku pomiaru): d(a,b) d(b,a) .
Wskazane, ale nieobowiązkowe jest spełnienie tzw. warunku trójkąta: d(a,c) d(a,b) d(b,c) .
Przytoczone w tym rozdziale rozwiązania problemów numerycznych mają cechy bardzo ważne z punktu widzenia przetwarzania. Przede wszystkim są przechowywane i przetwarzane w miejscu składowania danych na serwerze bazy danych. Powoduje to, że nie jest konieczne przesyłanie zestawów wartości wejściowych do miejsca przetwarzania, co w przypadku dużych wolumenów danych jest bardzo czasochłonne. Wykorzystuje się wbudowane cechy serwera bazy związane z automatyczną optymalizacją kodu, tworzeniem planów wykonania zapytań, co dodatkowo poprawia wydajność. Można ponadto zastosować indeksy, które mogą tę cechę jeszcze bardziej poprawić.
408
MS SQL Server. Zaawansowane metody programowania
Zakończenie Drogi Czytelniku! Mam nadzieję, że zdołałeś przebrnąć przez całość książki i jesteś usatysfakcjonowany jej zawartością. Mam świadomość, że książka ta nie porusza wszystkich aspektów związanych ze środowiskiem, którego dotyczy. Dokonany wybór jest arbitralny i wynika z moich doświadczeń dydaktycznych oraz chęci utrzymania spójności prezentowanego materiału i zachowania sensownej jego objętości. Z premedytacją pominąłem zagadnienia związane z administracją, ponieważ głównym motywem publikacji miało być programowanie. I chociaż trudno wykonywać zadania administracyjne bez używania SQL czy jego rozszerzenia proceduralnego, to jednak stanowią one wydzieloną klasę problemów zasługujących na odrębne omówienie. Również informacje o przetwarzaniu rozproszonym są raczej symboliczne. Jednak aby uwzględnić pełny zakres takich operacji, należałoby również poruszyć problematykę replikacji, systemu powiadamiania Service Broker, co powiększyłoby objętość książki o kolejne dwa rozdziały, dość luźno związane z pozostałą częścią. Część informacji, która jest prezentowana, nie jest pełna — dotyczy to przede wszystkim zagadnień związanych z tworzeniem elementów proceduralnych z zastosowaniem klas języków obiektowych. Ograniczyłem się jedynie do zaprezentowania typów użytkownika tworzonych z zastosowaniem takiego mechanizmu. Całość problematyki zasługuje na odrębną książkę — i w tym zakresie mogę się zobowiązać, że taka pozycja powstanie, ponieważ potencjał, jaki niesie taka metoda programowania, jest bardzo duży. Jednak w pierwszym rzędzie chciałbym przedstawić metody i narzędzia służące zgłębianiu, eksploracji danych (data mining). W szerokim znaczeniu są to sposoby przetwarzania surowych danych na informację, czyli sposoby mówiące o tym, jak do wartości atrybutów pozyskanych z otoczenia dołożyć, na skutek ich przetwarzania, wartość dodaną, która te dane objaśni, wzbogaci lub która odkryje tkwiące w nich niewidoczne na pierwszy rzut oka zależności i powiązania. Mam nadzieję, że tym razem uporam się z opracowaniem materiału dużo szybciej niż podczas pisania tej książki. Chciałbym Cię przekonać, że bazy danych, a szerzej przetwarzanie danych może mieć w sobie dużo uroku. Oczywiście nie bez znaczenia jest fakt, że wiedza z tego zakresu stanowi podstawy do znalezienia atrakcyjnej pracy. Wieloletnie doświadczenia, obserwacja rynku IT oraz rozmowy z wieloma przedstawicielami tej branży potwierdzają, że poza programowaniem, najczęściej obiektowym, wiedza z zakresu przetwarzania transakcyjnego stanowi kanon wiedzy najbardziej poszukiwanej na tym rynku. Również przetwarzanie analityczne znajduje odzwierciedlenie w wielu ofertach pracy. Wiele bocznych dróg wiodących od baz danych stanowi bardzo interesujące wątki
410
MS SQL Server. Zaawansowane metody programowania
przyszłego rozwoju informatyki. Na pewno należy do nich wprowadzenie obiektowości do struktur relacyjnych. Widzę w tym nurcie dużą przyszłość, chociaż wiedza z tego zakresu słabo jeszcze przenosi się na grunt biznesowy. Swego rodzaju zwieńczenie stanowi eksploracja danych. Jest ona stosunkowo rzadko wykorzystywana w komercyjnych rozwiązaniach oferowanych przez firmy, ale prawdopodobnie szybko znajdzie swoje miejsce na rynku, ponieważ rozwój produktów polegający na zwykłej poprawie warstwy prezentacyjnej, poszerzaniu zakresu lub wydajności przetwarzanych danych coraz mniej różnicuje produkty różnych firm. Może to spowodować, że przewagę konkurencyjną osiągną ci producenci, którzy wprowadzą funkcjonalności nieoferowane przez konkurencję, np. związane z „inteligentnym” wnioskowaniem. Cudzysłów jest uzasadniony dużą dozą wątpliwości co do możliwości rzeczywiście inteligentnej pracy urządzeń czy oprogramowania. Pomimo różnych wątpliwości dotyczących rozwoju przetwarzania, szczerze wierzę, że będzie on następował bardzo szybko. Bazy danych, stanowiące jądro przechowywania i przetwarzania, będą się rozwijały równie szybko. Sądzę, że ich znaczenie będzie rosło, co spowoduje zwiększenie zainteresowania wiedzą z tego zakresu.
412
MS SQL Server. Zaawansowane metody programowania
12. Kazienko P., Gwiazda K., XML na poważnie, Gliwice: Helion, 2002, ISBN 83-7197-765-4. 13. Vidhya P.M., Samuel P., Query translation from SQL to XPath, NaBIC 2009, World
Congress on Nature & Biologically Inspired Computing, Coimbatore 2009, s. 1749 – 1752. 14. Krishnamurthy R. et al., Recursive XML schemas, recursive XML queries, and relational
storage: XML-to-SQL query translation, Proceedings 20th International Conference on Data Engineering, 2004, s. 42 – 53. 15. Qiu Peng, Chen Jian-hui, Chang Qing, Study of XML & SQL Server and Application in
Distributed Integration, 8th International Conference on Electronic Measurement and Instruments, ICEMI ’07, Xi’an, 2007, s. 4-369 – 4-372. 16. Atay M., Chebotko A., Schema-Based XML-to-SQL Query Translation Using Interval
Encoding, Eighth International Conference on Information Technology: New Generations (ITNG), Las Vegas, 2011, s. 84 – 89. 17. Chaudhuri S. et al., Storing XML (with XSD) in SQL databases: interplay of logical and
physical designs, Proceedings 20th International Conference on Data Engineering, 2004, ISSN 1041-4347, s. 1595 – 1609. 18. Funderburk J.E., Malaika S., Reinwald B., XML programming with SQL/XML and XQuery,
„IBM Systems Journal” 2002, Volume 41, Issue 4, IBM Corp. Riverton, New Jersey, ISSN 0018-8670, s. 642 – 665. 19. Tellez E.S., Ortiz J., Graff M., PGSYNC: A Multiple-Reader/Single-Writer Table Replication
Tool For High Loaded Distributed Relational SQL Databases, Electronics, Robotics and Automotive Mechanics Conference, 2007, CERMA 2007, Morelos, s. 735 – 739. 20. Larson P.A., Goldstein J., Zhou J., MTCache: transparent mid-tier database caching in
SQL server, Proceedings of 20th International Conference on Data Engineering, 2004, ISBN 0-7695-2065-0, s. 177 – 188. 21. Ajila S.A., Al-Asaad A., Mobile databases — Synchronization & conflict resolution strategies
using SQL server, IEEE International Conference on Information Reuse and Integration (IRI), Las Vegas 2011, ISBN 978-1-4577-0965-4, 978-1-4577-0964-7, s. 487 – 489. 22. Bernstein P.A. et al., Adapting Microsoft SQL server for cloud computing, IEEE 27th
International Conference on Data Engineering (ICDE), Hannover 2011, ISBN 978-1-4244-8958-9, 978-1-4244-8959-6, s. 1255 – 1263. 23. Tajpour A., Massrum M., Heydari M.Z., Comparison of SQL injection detection and
prevention techniques, International Conference on Education Technology and Computer (ICETC), 2010, Volume 5, Shanghai, ISBN 978-1-4244-6367-1, s. 174 – 179. 24. Agata M., Pelikant A., Problemy SQL Injection w bazach danych Oracle, XIII Konferencja
PLOUG, Kościelisko 2007. 25. Droś A., Pelikant A., SQL Injection i inne metody ataków na bazy danych oraz metody
ochrony po stronie aplikacji WWW, VII Krajowe Sympozjum „Modelowanie i Symulacja Komputerowa w Technice”, Łódź 2010, s. 23 – 28. 26. Anley C., Advanced SQL Injection In SQL Server Applications, NGS Software Insight
Security Research (NISR) Publication 2002.
Literatura
413
27. Celko J., Data and Databases, Morgan Kaufmann Publishers 1999, ISBN 978-1-55860-
432-2. 28. Celko J., SQL Programming Style, Morgan Kaufmann Publishers 2005,
ISBN 978-0-12-088797-2. 29. Celko J., SQL Puzzles and Answers, wyd. 2, Morgan Kaufmann Publishers 2006, ISBN
978-0-12-373596-6. 30. Celko J., Data, Measurements and Standards in SQL, Morgan Kaufmann Publishers 2009,
ISBN 978-0-12-374722-8. 31. Chaudhuri S., Narasayya V., An Efficient, Cost-Driven Index Selection Tool for Microsoft
SQL Server, Proceedings of the 23rd VLDB Conference Athens, Greece, 1997, s. 1 – 10. 32. Agrawal S., Chaudhuri S., Narasayya V., Automated Selection of Materialized Views and
Indexes for SQL Databases, Proceedings of the 26th International Conference on Very Large Databases, Cairo 2000. 33. Larson P.A., Hanson E.N., Price S.L., Columnar Storage in SQL Server 2012, Bulletin
of the IEEE Computer Society Technical Committee on Data Engineering, Los Alamitos, California 2012. 34. Strate J., Krueger T., Expert Performance Indexing for SQL Server 2012, Apress Media
LLC Springer 2012, ISBN 978-1-4302-3741-9. 35. Karthik P., Thippa Reddy G., Vanan K., Tuning the SQL Query in order to Reduce Time
Consumption, „IJCSI International Journal of Computer Science Issues” 2012, Volume 9, Issue 4, No. 3, s. 418 – 423. 36. Zak M.J., Efficiently mining frequent trees in a forest, Proceedings of ACM SIGKDD
International Conference on Knowledge Discovery and Data Mining, New York 2002, ISBN 1-58113-567-X, s. 71 – 80. 37. Böhm C., Krebs F., The k-Nearest Neighbour Join: Turbo Charging the KDD Process,
„Knowledge and Information Systems”, Volume 6, Issue 6, Springer-Verlag, ISSN 0219-1377, s. 728 – 749. 38. Enderle J., Hampel M., Seidl T., Joining interval data in relational databases, Proceedings
of the ACM SIGMOD International Conference on Management of Data, New York 2004, ISBN 1-58113-859-8, s. 683 – 694. 39. Dobra A. et al., Processing complex aggregate queries over data streams, Proceedings of
ACM SIGMOD international conference on Management of data, New York 2002, ISBN 1-58113-497-5, s. 61 – 72. 40. Ordonez C., Integrating K-means clustering with a relational DBMS using SQL, „IEEE
Transactions on Knowledge and Data Engineering”, Volume 18, Issue 2, ISSN 1041-4347, s. 188 – 201. 41. Suresh L., Simha J.B., Veluru R., Generating Optimum Number of Clusters Using Median
Search and Projection Algorithms, International Conference on Advances in Computer Engineering (ACE), Bangalore — Karnataka 2010, ISBN 978-1-4244-7154-6, s. 274 – 276. 42. Leinders D., Van den Bussche J., On the complexity of division and set joins in the relational
algebra, Journal of Computer and System Sciences, Volume 73, Issue 4, Elsevier 2007, s. 538 – 549.
414
MS SQL Server. Zaawansowane metody programowania
43. Gillis J.J.M., Van den Bussche J., Induction of Relational Algebra Expressions, „Lecture
Notes in Computer Science” 2010, Volume 5989, Springer Berlin Heidelberg, ISBN 978-3-642-13839-3, s. 25 – 33. 44. Fink R., Olteanu D., Rath S., SPROUT, IEEE International Conference on Data Engineering
(ICDE), Athens 2011, s. 315 – 326. 45. Dalvi N., Schnaitter K., Suciu D., Computing query probability with incidence algebras,
Proceedings of the twenty-ninth ACM SIGMOD-SIGACT-SIGART symposium on Principles of database systems, New York 2010, ISBN 978-1-4503-0033-9, s. 203 – 214. 46. Rantzau R., Mangold C., Laws for Rewriting Queries Containing Division Operators,
Proceedings of the 22nd International Conference on Data Engineering ICDE, Atlanta 2006, ISBN 0-7695-2570-9. 47. Rutkowski L., Computational Intelligence. Methods and Techniques, Springer-Verlag
2008, ISBN 978-3-540-76288-1. 48. Rutkowski L., Metody i techniki sztucznej inteligencji, wyd. 2 rozszerzone, Warszawa:
PWN, 2009, ISBN 978-83-01-15731-9. 49. Ye Xu, Ping W., Campbell A.T., Multi-Instance Metric Learning, IEEE International
Conference on Data Mining (ICDM), Vancouver 2011, s. 874 – 883, ISSN 1550-4786. 50. Lee E.-W., Chae S.-I., New classifier with reduced computational complexity, IEEE
International Conference on Neural Networks, Perth 1995, Volume 2, ISBN 0-7803-2768-3, s. 968 – 973. 51. Fei Wu, Gardarin G., Gradual clustering algorithms, International Conference on Database
Systems for Advanced Applications, Hongkong 2001, ISBN 0-7695-0996-7, s. 48 – 55. 52. Hsiang-Chuan L. et al., Fuzzy C-Mean Algorithm Based on Mahalanobis Distance and
New Separable Criterion, International Conference on Machine Learning and Cybernetics, Hongkong 2007, ISBN 978-1-4244-0973-0, s. 1851 – 1855. 53. Singh R.V., Bhatia M.P.S., Data clustering with modified K-means algorithm, International
Conference on Recent Trends in Information Technology (ICRTIT), Chennai 2011, ISBN 978-1-4577-0588-5, s. 717 – 721. 54. Sun J. et al., Analysis of the Distance Between Two Classes for Tuning SVM Hyperparameters,
„IEEE Transactions on Neural Networks”, Volume 21, Issue 2, ISSN 1045-9227, s. 305 – 318. 55. Kowalczyk A., Pelikant A., Fuzzy clustering in relational databases, XII International
Conference System Modelling and Control, Zakopane 2007. 56. Kowalczyk-Niewiadomy A., Pelikant A., Zagadnienia grupowania w kontekście
budowania zapytań rozmytych [w:] Bazy danych Rozwój metod i technologii, Architektura, metody formalne i zaawansowana analiza danych, Warszawa: Wydawnictwa Komunikacji i Łączności, 2008, s. 175 – 186. 57. Rueda L., Oommen B.J., On optimal pairwise linear classifiers for normal distributions:
the two-dimensional case, „IEEE Transactions on Pattern Analysis and Machine Intelligence” 2002, ISSN 0162-8828, s. 274 – 280.
416
MS SQL Server. Zaawansowane metody programowania
Science” 2009, Volume 5739, Springer Berlin Heidelberg, ISBN 978-3-642-03972-0, 978-3-642-03973-7, s. 179 – 193. 72. Shao F., Novak A., Shanmugasundaram J., Triggers over XML views of relational data,
International Conference on Data Engineering, Tokyo 2005, ISSN 1084-4627, ISBN 0-7695-2285-8, s. 483 – 484. 73. Izquierdo J.L.C., Molina J.G., An Architecture-Driven Modernization Tool for Calculating
Metrics, Software, „IEEE Software, Software Evolution”, Volume 27, Issue 4, ISSN 0740-7459, s. 37 – 43. 74. Sadiq S., Orlowska M., Sadiq W., Data flow and validation in workflow modelling,
Proceedings of the 15th Australasian Database Conference, Volume 27, Darlinghurst 2004, s. 207 – 214. 75. Ko H.F., Nicolici N., Resource-Efficient Programmable Trigger Units for Post-Silicon
Validation, IEEE European Test Symposium, Seville 2009, ISBN 978-0-7695-3703-0, s. 17 – 22. 76. Wanat P., Pelikant A., Obsługa acyklicznych grafów skierowanych (drzew) z poziomu
języka zapytań SQL, „Zeszyty Naukowe Wyższej Szkoły Informatyki w Łodzi” 2007, Vol. 6, Nr 1, ISSN 1643-0689, s. 91 – 101. 77. Dillenbourg P., Jermann P., Designing Integrative Scripts, Scripting Computer-Supported
Collaborative Learning, Computer-Supported Collaborative Learning, Volume 6, Springer US, 2007, ISBN 978-0-387-36947-1, 978-0-387-36949-5, s. 275 – 301. 78. Chaudhuri S. et al., Optimizing queries with materialized views, Proceedings of the
Eleventh International Conference on Data Engineering, Taipei 1995, ISBN 0-8186-6910-1, s. 190 – 200. 79. Hartmann S., Kirchberg M., Link S., Design by example for SQL table definitions with
functional dependencies, „The International Journal on Very Large Data Bases” 2012, Volume 21, Issue 1, Springer-Verlag, ISSN 1066-8888, s. 121 – 144. 80. Sheetlani J., Gupta V.K., Concurrency Issues of Distributed Advance Transaction Process,
„Research Journal of Recent” 2012, Vol. 1 (ISC-2011), ISSN 2277-2502, s. 426 – 429. 81. Schuldt H., Alonso G., Schek H.-J., Concurrency Control and Recovery in Transactional
Process, PODS ’90 Philadelphia PA, 1999, s. 316 – 326. 82. Botan I. et al., Transactional Stream Processing, Proceedings of the 15th International
Conference on Extending Database Technology, EDBT 2012, New York 2012, ISBN 978-1-4503-0790-1, s. 204 – 215. 83. Strom R., Dorai C., Providing transactional quality of service in event stream processing
middleware, IEEE/IFIP International Conference on Dependable Systems & Networks, Lisbon 2009, ISBN 978-1-4244-4422-9, s. 135 – 144. 84. Cabral B., Marques P., Exception Handling: A Field Study in Java and .NET [w:] ECOOP
2007 — Object-Oriented Programming, Lecture Notes in Computer Science, Volume 4609, Springer Berlin Heidelberg, 2007, ISBN 978-3-540-73588-5, 978-3-540-73589-2, s. 151 – 175.
Literatura
417
85. Entwisle S. et al., A Model Driven Exception Management Framework for Developing
Reliable Software Systems, IEEE International Conference on Enterprise Distributed Object Computing, Hongkong 2006, ISSN 1541-7719, ISBN 0-7695-2558-X, s. 307 – 318. 86. Acheson A. et al., Hosting the .NET Runtime in Microsoft SQL Server, Proceedings of the
ACM SIGMOD International Conference on Management of Data, New York 2004, ISBN 1-58113-859-8, s. 860 – 865. 87. Bernstein P.A., Newcomer E., Principles of Transaction Processing, Morgan Kaufmann
Publishers 2009, ISBN 978-1558606234. 88. Reinwald B. et al., Heterogeneous query processing through SQL table functions, 15th
International Conference on Data Engineering, Sydney 1999, ISSN 1063-6382, ISBN 0-7695-0071-4, s. 336 – 373. 89. Buneman P. et al., Principles of programming with complex objects and collection types,
„Theoretical Computer Science” 1995, Volume 149, Issue 1, Elsevier Science Publishers Ltd. Essex, ISSN 0304-3975, s. 3 – 48. 90. Chong E.I. et al., An Efficient SQL-based RDF Querying Scheme, VLDB Proceedings
of the 31st international conference on Very large data bases, Trondheim 2005, ISBN 1-59593-154-6, s. 1216 – 1227. 91. Celko J., Thinking in Sets: Auxiliary, Temporal, and Virtual Tables in SQL, Morgan
Kaufmann Publishers 2008, ISBN 978-0-12-374137-0. 92. Celko J., Trees and Hierarchies in SQL for Smarties, Morgan Kaufmann Publishers 2012,
ISBN 978-0-12-387733-8. 93. Libkin L., Wong L., On the Power of Incremental Evaluation in SQL-like Languages,
Research Issues in Structured and Semistructured Database Programming, „Lecture Notes in Computer Science”, Volume 1949, Springer Berlin Heidelberg, 2000, ISBN 978-3-540-44543-2, s. 17 – 30. 94. Tropashko V., Nested Intervals Tree Encoding in SQL, „ACM SIGMOD Record”,
Volume 34, Issue 2, New York 2005, ISSN 0163-5808, s. 47 – 52. 95. Stroe I.D., Rundensteiner E.A., Ward M.O., Database and Expert Systems Applications,
„Lecture Notes in Computer Science” 2000, Volume 1873, Springer Berlin Heidelberg, ISBN 978-3-642-03572-2, s. 784 – 793. 96. Blakeley J.A. et al., .NET database programmability and extensibility in Microsoft SQL
Server, Proceedings of the ACM SIGMOD International Conference on Management of Data, New York 2008, ISBN 978-1-60558-102-6, s. 1087 – 1098. 97. Egenhofer M.J., Spatial SQL: a query and presentation language, „IEEE Transactions
on Knowledge and Data Engineering” 1994, ISSN 1041-4347, s. 86 – 95. 98. Zhou S. et al., Seamless integration of SQL Server spatial data on GIS platform,
International Conference on Geoinformatics, Beijing 2010, ISBN 978-1-4244-7301-4, s. 1 – 5. 99. Tzouridis E., Brefeld U., Learning Shortest Paths in Word Graphs, Proceedings of the
German Workshop on Knowledge Discovery and Machine Learning (KDML), Bamberg 2013, s. 113 – 116.
418
MS SQL Server. Zaawansowane metody programowania
100. Pelikant A., Zastosowanie typów obiektowych w przetwarzaniu analitycznym, „Roczniki
Kolegium Analiz Ekonomicznych” 2012, z. 25, SGH Warszawa, ISSN 1232-4671, s. 231 – 250. 101. Pelikant A., Metody reprezentacji atrybutów w zadaniach zgłębiania danych, VI Krajowe
Sympozjum „Modelowanie i Symulacja Komputerowa w Technice”, Łódź 2008, „Zeszyty Naukowe Wyższej Szkoły Informatyki w Łodzi” 2008, Vol. 7, Nr 1, ISSN 1643-0689, s. 101 – 105. 102. Pawłowski J., Pelikant A., Zapis plików grafiki wektorowej do bazy danych MS SQL,
„Zeszyty Naukowe Wyższej Szkoły Informatyki w Łodzi” 2011, Vol. 10, Nr 1, ISSN 1643-0689, s. 90 – 109. 103. Pitchaimalai S.K., Ordonez C., Garcia-Alvarado C., Efficient Distance Computation Using
SQL Queries and UDFs, IEEE International Conference on Data Mining Workshops, Pisa 2008, ISBN 978-0-7695-3503-6, s. 533 – 542. 104. Pardede E, Wenny Rahayu J., Taniar D., Mapping Methods and Query for Aggregation
and Association in Object-Relational Database using Collection, Proceedings of the IEEE International Conference on Information Technology: Coding and Computing, Las Vegas 2004, ISBN 0-7695-2108-8, s. 539 – 543. 105. Pardede E., Rahayu J.W., Taniar D., New SQL standard for object-relational database
applications, Conference on Standardization and Innovation in Information Technology, Delft 2003, ISBN 0-7803-8172-6, s. 191 – 203. 106. Mi-Yeon Kim, Jung-Min Seo, Chang-Joo Moon, SQL Extension for Multidatabase System,
International Conference on Computational Science and its Applications, Kuala Lumpur 2007, ISBN 0-7695-2945-3, s. 283 – 289. 107. Pilny P., Pelikant A., Typy użytkownika CLR. Wprowadzenie obiektowości do relacyjnej
bazy danych, „Zeszyty Naukowe Wyższej Szkoły Informatyki w Łodzi” 2012, Vol. 11, Nr 2, ISSN 1643-0689, s. 51 – 81. 108. Konopka E., Pelikant A., Funkcje i typy użytkownika CLR w zadaniach statystycznych,
„Zeszyty Naukowe Wyższej Szkoły Informatyki w Łodzi” 2012, Vol. 11, Nr 2, ISSN 1643-0689, s. 5 – 30. 109. Kowalczyk A., Pelikant A., Implementation of Automatically Generated Membership
Functions Based on Grouping Algorithms, The IEEE Region 8 Eurocon 2007 Conference September 9 – 12, Warsaw 2007, ISBN 978-1-4244-0813-9, s. 835 – 840. 110. Sharp J., Microsoft Visual C# 2012. Krok po kroku, Gliwice: Helion, 2012,
ISBN 978-8-3754-1074-7.
Skorowidz A analiza grafów, 400 sieci powiązań, 400 aplikacja ISQL, 189 OSQL, 189 ASCII, 39 atak na bazę, 235 atomowość, 303 automatyczna inkrementacja, 112 autoryzacja, 20, 24
B bazy systemowe master, 25 model, 26 msdb, 25 tempdb, 26 bitowa różnica symetryczna, 38 blokowanie rekordów, 295, 296 błąd, 243 przetwarzania, 318 użytkownika, 241, 245
C CLR, 359
D dbo, database owner, 32, 237 definicja instancji serwera, 18 definiowanie typów, 170 dekodowanie danych, 93
diagram bazy danych, 34 relacyjny, 33, 157 dołączanie bazy, 32 dyrektywa, Patrz także, polecenie, słowa kluczowe ADD, 141 CURRENT OF, 295 DISTINCT, 67 ELEMENT, 89 ENCRYPTION, 152 EXPLICIT, 87 HIDE, 89 ON DELETE, 118 ON DELETE NO ACTION, 117 ON UPDATE, 118 OUT, 231 OUTPUT, 231 PARTITION BY, 70 PATH, 90 RAW, 91 ROOT, 93 SCHEMABINDING, 152, 185 SCROLL_LOCKS, 294 TOP, 150 TOP 5, 46 TYPE, 91 VIEW_METADATA, 152–154 XMLSCHEMA, 92 dyskryminator liniowy, 389 dystrybucja punktów, 211 dzielenie przestrzeni, 215, 216 relacyjne bez reszty, 198 relacyjne z resztą, 200
E edycje serwera, 10 elementy formatujące, 138 proceduralne CLR, 359 składowe, 10 EPSG, 348
F filtr, 45, 150 filtrowanie, 40, 49 flaga IDENTITY_INSERT, 98 format XML, 94 formatowanie stylów, 135 funkcja, 236, Patrz także metoda /text(), 140 @@CURSOR_ROWS, 285 @@FETCH_STATUS, 280 AVG(), 58 COLUMNS_UPDATED(), 252 COUNT(), 137, 230 CUME_DIST(), 71 CURSOR_STATUS(), 284, 291 DATEPART(), 75 EXIST(), 237 FIRST_VALUE(), 71 getdate(), 114 IDENTITY(), 99, 112, 125 LAG(), 71 LAST_VALUE(), 71 LEAD(), 71 LEN(), 60 NEWID(), 110, 125
420
MS SQL Server. Zaawansowane metody programowania
funkcja NTILE(), 70 PERCENT_RANK(), 71 RAND(), 221 RANK(), 70 ROW_NUMBER(), 69 SUM(), 48 UPPER(), 252 XACT_STATE(), 319 funkcje agregujące, 49, 73, 83, 394 tabelaryczne, 386
G generator liczb pseudolosowych, 221 generowanie błędów, 247 gęsty ranking, 70 grupy błędów, 243, 321
H hurtownia danych, 11
I IIS, Internet Information Services, 11 iloczyn bitowy, 38 kartezjański, 64 ilustracja złączenia, 275 indeks grupujący CLUSTERED, 187 indeksy unikalne, 174 informacje o błędzie, 318 inkrementacja, 84 instalacja SQL Server 2012, 14 instancja serwera, 18 instrukcja warunkowa if, 137 interfejs IBinarySerialize, 398 ISO, 287 izolacja, 303
J język SQL, 35
K klauzula FROM, 40, 51 GROUP BY, 57 HAVING, 58, 61, 207 INCLUDE, 183 INTO, 94, 101 OFFSET, 85 ORDER BY, 37, 50, 173 WHERE, 45, 60, 100, 139 WITH, 82, 151 klucz cykliczny, 179 główny, 105 obcy, 116, 122, 177 podstawowy, 103 komponenty MS SQL Server, 22 komunikat o błędzie, 115, 184, 228, 243, 259 o braku modyfikacji, 366 kursory, 280
L liczba transakcji, 65 logowanie, 25
Ł łączenie dyrektyw, 288
M metakod, 189 metanotacja, 188 metaskładnia, 287 metoda, Patrz także funkcja Accumulate, 394, 395, 398 GetDescendant, 333 GetRoot, 255 Init, 394 MakeValid, 348 Merge, 394 modify, 136 nodes, 140 query, 139 STBoundary, 340 STBuffer, 342 STDifference, 347 STIntersection, 346 STNumPoints, 343
STOverlaps, 344 Terminate, 394, 396 value, 140 Write, 396 metody typu obiektowego, 329 migawka, 304 modyfikacja danych, 102 danych XML, 137 perspektyw, 103 tabel, 103, 141 MSSQLSERVER, 18
N narzędzia klienckie, 188 narzędzie Reporting Services, 11 negacja bitowa, 38
O obsługa wyjątków, 316 odchylenie standardowe, 372, 378 odcinek, 213 odłączanie bazy, 31 OGC, Open Geospatial Consortium, 347 ograniczenie NOT NULL, 112, 120 UNIQUE, 105, 144 okno Messages, 242 określanie okna, 74 OLAP, 11 opcje uruchamiania usług, 19 operator ALL, 79 BETWEEN, 42, 62 CASE, 47, 163, 225 COMPUTE, 77, 78 CROSS JOIN, 53 EXCEPT, 81 EXISTS, 79, 146, 154 FULL JOIN, 52 iloczynu, 41 IN, 42, 56 INTERSECT, 81 IS NULL, 41 JOIN, 51 LIKE, 43 przeczenia, 41 sumy, 41 UNION, 204 UNION ALL, 80, 162
Skorowidz
421
operatory logiczne, 40 relacyjne, 40 specjalne, 40
P partycja, 72 partycjonowanie logiczne, 76 PCA, Principal Component Analysis, 400 perspektywa sys.trigger_event_types, 255 perspektywy, 148 perspektywy słownikowe, 156 pierwsza postać normalna, 381 pliki *.ddl, 302 *.ldf, 32 * mdf, 32 podsumowanie, 66–68 podział sieci, 403 polecenie, Patrz także słowa kluczowe @@NESTLEVEL, 234 ADD CONSTRAINT, 144 ALTER TABLE, 141, 144 CREATE FUNCTION, 130 CREATE PROCEDURE, 227 CREATE SYNONIM, 241 CREATE TABLE, 103, 315 CREATE VIEW, 148, 164 DELETE, 100, 149, 250 DROP CONSTRAINT, 144 DROP TABLE, 103, 125, 315 DROP VIEW, 150 EXEC, 230–233 FETCH, 283, 292 FETCH NEXT FROM, 280 GOTO, 227 IF EXISTS, 392 INSERT INTO, 95, 96, 102 RAISERROR, 241 replace value of, 136 SAVE TRANSACTION, 313 SELECT, 96 SET LANGUAGE, 246 TRUNCATE TABLE, 101 UPDATE, 100, 142, 249, 252 WAITFOR, 226 WAITFOR DELAY, 132 poziom dostępu READ COMMITTED, 304 READ UNCOMMITTED, 304
REPEATABLE READ, 304 SERIALIZABLE, 304 SNAPSHOT, 304 procedury składowane, 227 wyzwalane, 247 proste rozdzielające klasy, 217 przetwarzanie transakcyjne, 303 punkty trzech klas, 217
R redundancja danych, 86 relacje, 33 Reporting Services, 22 rodzaje autoryzacji, 24 rozszerzenie obiektowe, 381 proceduralne Transact-SQL, 221
S samozłączenie tabeli, 212 schemat relacyjny, 195, 198, 202, 205 sekcja CATCH, 322 silnik analityczny, 11 bazy danych, 10 składowanie triggerów, 249, 272 typów użytkownika, 325 skrypt Java, 135 testujący, 260 słowa kluczowe, Patrz także polecenie ABSOLUTE, 283 ADD, 141 ALL, 40, 79 ALL SERVER, 259 ALTER TABLE, 141, 144 AND, 40 ANY, 40 AS, 52, 127, 299 ASSEMBLY, 375 BEGIN CATCH, 316 BETWEEN, 40, 62 CACHE, 147 CASCADE, 119, 123 CASE, 47, 163, 225, 382 CATCH, 322
CHECK, 180 CLOSE, 289 COMPUTE, 77 CONSTRAINT, 106, 115, 144 CONTINUE, 224 CREATE, 130, 148, 227 CROSS JOIN, 53 CURSOR, 290 CYCLE, 147 DEALLOCATE, 289 DECLARE, 147, 221 DEFAULT, 167, 240 DELAY, 226 DELETE, 100, 149, 250 DISABLE, 186 DISTINCT, 67 DROP, 103, 150, 172 DYNAMIC, 288 ELEMENT, 89 ELSE, 222 ENCRYPTION, 152 EXCEPT, 81 EXEC, 230–233 EXISTS, 40, 79 EXPLICIT, 87 FAST_FORWARD, 288 FETCH, 283, 292 FIRST, 283 FOR, 248 FORCESCAN, 305 FORCESEEK, 305 FOREIGN KEY, 116 FORWARD_ONLY, 287 FROM, 51 FULL JOIN, 52, 54 GLOBAL, 287 GO, 94, 325 GOTO, 227 GROUP BY, 57 GROUPING SETS, 68 HASH JOIN, 53 HAVING, 58, 61, 207 HIDE, 89 HOLDLOCK, 306 IF, 221 IGNORE_CONSTRAINTS, 306 IGNORE_TRIGGERS, 306 IN, 40, 58 INCLUDE, 183 INCREMENT BY, 147 INDEX, 305 INNER JOIN, 52, 54 INSERT INTO, 95, 102
422 słowa kluczowe INTERSECT, 81 INTO, 101, 299 IS NOT NULL, 40 IS NULL, 40 JOIN, 51 KEEPDEFAULTS, 305 KEEPIDENTITY, 305 KEYSET, 287 LAST, 283 LEFT JOIN, 54 LIKE, 40 LOCAL, 287 MAXVALUE, 147 MERGE JOIN, 53 MINVALUE, 147 NEXT, 283 NO CACHE, 147 NO CYCLE, 147 NO MAXVALUE, 147 NOEXPAND, 305 NOLOCK, 306 NOT, 40 NOT NULL, 112, 120 NOWAIT, 306 OFFSET, 85 OPTIMISTIC, 288 OR, 40 ORDER BY, 58 OUT, 231 OUTPUT, 231, 292 OVER, 69 PAGLOCK, 306 PARTITION BY, 70 PATH, 90 PRIMARY KEY, 103, 107, 112 PRINT, 226, 230 PRIOR, 283 RAISERROR, 241 RAW, 91 READ_ONLY, 288 READCOMMITTED, 306 READCOMMITTEDLOCK, 306 READPAST, 306 READUNCOMMITTED, 306 REBUILD, 186 RELATIVE, 283 REORGANIZE, 186 REPEATABLEREAD, 306 RETURN, 235, 236, 238 RETURNS, 239 RIGHT JOIN, 54
MS SQL Server. Zaawansowane metody programowania ROOT, 93 ROWLOCK, 306 SCHEMABINDING, 152 SCROLL_LOCKS, 288 SELECT, 35, 36 SERIALIZABLE, 306 SET, 221, 290 SET DEFAULT, 121 SET LANGUAGE, 246 SET NULL, 120, 123 SOME, 40 SPATIAL_WINDOW _MAX_CELLS, 306 START WITH, 147 STATIC, 287 TABLE, 239 TABLOCK, 306 TABLOCKX, 306 TOP, 150 TRUNCATE TABLE, 101 TYPE, 91 TYPE_WARNING, 288 UNION ALL, 80, 84, 162 UNIQUE, 105, 106, 144 UPDATE, 100, 101, 142, 249 UPDLOCK, 307 VALUES, 95, 96 VARYING, 292 VIEW, 148 VIEW_METADATA, 152, 153 WAITFOR, 226 WAITFOR DELAY, 132 WHEN, 47 WHERE, 51, 57, 60 WHILE, 223 with, 136 WITH, 82, 83, 151 WITH CUBE, 67 XLOCK, 307 XMLSCHEMA, 92 sortowanie, 36, 69, 182 spójność, 303 SQL, 193 SQL injection, 235 SQL Server, 9 SQL Server Management Studio, 24 SRID, 348 struktura EVENT_INSTANCE, 261 obiektów, 167, 168, 169 sufiks DESC, 45 suma bitowa, 38
symbole formatowania zmiennych, 245 synonimy, 241
T tabela Audyt_ddl, 264 Autoryzacja, 271 Logowanie, 267 Mecze, 279 przestawna, 80 Towar, 297 Transact-SQL, 221 transakcje, 303, 307 trigger, 247 trwałość, 304 tworzenie bazy, 28–30 filtrów, 39 indeksów, 171 kluczy obcych, 177 macierzy, 63 okna logicznego, 76 perspektyw, 103, 152 procedur, 228 przedziałów, 63 raportów, 11 sekwencji, 147 tabel, 103 typu użytkownika, 167 typ ASSEMBLY, 360 binary, 111 char, 110 CLUSTERED, 172, 181 datetime2, 109 datetimeoffset, 109 decimal, 109 EventTag_TSQLCommand, 262 float, 108 geography, 334 geometry, 110, 211, 334 hierarchyid, 110, 329 image, 111 nchar, 110 NONCLUSTERED, 183 ntext, 111 numeric, 109 nvarchar, 110 table, 111 text, 111 time, 109