Professional Documents
Culture Documents
Od autora . ....................................................................................... 5
Jeśli już przeczytałeś moją poprzednią książkę, wiesz, że jedną z podstawowych za-
sad, której staram się przestrzegać, jest prostota wywodu. Dlatego książka ta zawiera
wiele krótkich przykładów, które starałem się precyzyjnie opisać. Również przykła-
dowy schemat bazy danych jest prosty, tak aby nie obciążać czytelnika koniecznością
zapamiętywania nazw wielu tabel i pól w nich zawartych. Przyświecała mi zasada po-
dobno wygłoszona przez Einsteina — „rób wszystko tak prosto, jak to możliwe, ale nie
prościej”. Mam nadzieję, że takie odczucie będzie podczas lektury tej książki towa-
rzyszyło również czytelnikowi. Jeśli jednak obawiasz się, że prostota jest równoważna
z trywialnością, jesteś w błędzie. Naprawdę trudne są rzeczy, które na pierwszy rzut
oka wyglądają na proste. Co może być trudnego we wzorze e = mc2? Może nie w tej
skali, ale najtrudniejsze zadania z SQL dotyczą niewielu tabel. Znam wiele zadań doty-
czących przetwarzania danych zawartych w prostej, pojedynczej tabeli, które nie są
trywialne. Niektóre z nich znajdziesz w tej książce. „Kiedy ludzie mówią, że coś jest
oczywiste, oznacza to wielką lukę w ich argumentacji oraz świadomość, że sprawy
wcale oczywiste nie są” — Terry Pratchett (Prawda).
l
l.p
al
k4
o
bo
.e
w
w
w
Rozdział 1.
Wstęp
Prezentowana Państwu książka dotyczy zagadnień projekto�nia baz danych w śro
ę lig oraz wskazane
dowisku Oracle. Wywód będzie prowadzony w oparciu o r
zostaną rozbieżności z wcześniejszymi realizacjami, zc��
�
�
lnym uwzględnieniem
l�g.. Wynika to głów�e �e zmiany funkcjona��ości ��, Wki klienta w wersji dla �d
rmmstratora. Odwołame się do starszych wersJI wy�� aktu, że bardzo często poJa
wiają się one w praktyce komercyjnej. Głównym��eMotem wywodu będzie tworzenie
�
w tym środowisku zapytań w języku SQL�� gólnym podkreśleniem elementów
składni i funkcjo�aln?ści, które odróżn �� stos �wany � h w � nnych �e�e�ach
� _
baz danych. Drugim Istotnym elementeł��•.,nążki będzie omowieme możhwosci za
stosowania przy tworzeniu opro a = ma baz danych rozszerzenia proceduralnego
- PL/SQL. Mottem przewod · podręcznika będą słowa Alberta Einsteina:
"Rób wszystko tak prosto, j �� lko możliwe, ale nie prościej", do czego przy
budowie oprogramowania szc�nie gorąco zachęcam.
W przeważającej c ęś
cyjnego (rysunek l
�� � o u korzystam z prostego trójtabelowego schematu rela-
isującego organizację firmy w postaci działów (Działy), jej
pracowników (O b raz ich wypłat (Zarobki).
Rysunek 1.1. NAGRODY ZAROBKI
Podstawowy schemat P NIDNAGRODY P NIDZAROSKU
F NIDOSOSY
bazy danych F A IDOSOSY
A OPIS
OSOBY -< A SRUTIO
P NIDOSOSY
� - A DATAWYP
F A IDDZIALU l
A NAZWISKO
A IMIE
A ROKURODZ
A WZROST
A DATAZATR
F A IDSZ EFA
DZIALY
P N IDDZIALU
A OPIS
10 Część l + Oracle SQL
Jak łatwo zauważyć, w nazwach tabel oraz pól nie są używane narodowe znaki diakty
tyczne, co powinno być powszechną praktyką Nazwy te nie zawierają również spacji
jako separatorów pomiędzy ich częściami (w tej książce separatory w nazwach nie będą
w ogóle stosowane). Jeśli programista używa bardzo rozbudowanego nazewnictwa (opi
sowego), zaleca się stosowanie jako znaku separacji podkreślnika.
A CENA
Y
PRODUCENCI
...
FAKTURY
A NAZWAPRODUCENTA
P N IDFAKTURY
F A IDMIASTA
F A IDOSOBY
F A IDKLIENTA
A DATA
MIASTA
OSOBY • P N IDMIASTA
F A IDDZIALU A MIASTO
A NAZWISKO \V
A IMIE
l
A ROKURODZ
A WZROST
•
WOJEWODZTWA
A DATAZATR
P �l IDWOJEWODZTWA
• A WOJEWODZTWO
ZAROBKI
P N IDZAROBKU
F A IDOSOSY
A BRUTTO
A DATAWYP
Organizacja serwera
Pierwszy element bezpośrednio związany z omawianym środowiskiem to formalny
sposób organizacji serwera. Może on być podzielony na dwie zsynchronizowane ze
sobą gałęzie: logiczną i fizyczną (rysunek 1.3).
Rozdział 1. + Wstęp 11
Typ Opis
VARCHAR2(rozmiar) Ciąg znaków o zmiennej długości. Maksymalna długość: 4000 znaków,
minimalna: l znak Specyfikacja maksymalnej długości jest niezbędna.
DATE Data od l stycznia 4712 roku p.n.e. do 31 grudnia 9999 roku n.e.
CHAR(rozmiar) Ciąg o stałej długości. Maksymalny rozmiar: 2000 bajtów. Standardowy: l bajt.
NCHAR(rozmiar) Ciąg o stałej długości. Maksymalny rozmiar określony przez liczbę bajtów
na znak: 2000 bajtów. Standardowy: l bajt.
CLOB Obiekt zawierający duże ilości tekstu (do 4GB), gd � eden znak jest
reprezentowany przez jeden bajt.
BFILE Zawiera lokalizację binarnego pliku prz � wanego na zewnątrz bazy danych.
Maksymalny rozmiar 4GB.
,.L
LONG Ciąg znaków o zmiennej dług � "lr7ksymalna długość: 2GB.
RAW(rozmiar) Czyste dane o długości rów� �e bajtów. Maksymalna długość: 4000 bajtów.
LONG RAW Czyste dane o długo � nej liczbie bajtów. Maksymalna długość: 2GB.
Rysunek 1.3.
Organizacja
� Organizacja bazy danych
serwera Dracle LOGICZNA FIZYCZNA
12 Część l + Oracle SQL
Rysunek 1.4. �
Widok ekranu lo a · do konsoli serwera
o
Lo gged inAsSYSTEM
Databa5e ln5tance: oradell
Latest Data Collected From T arget 02-Feb-2008 12:16:59 o'clock CEl ( Refresh l 1/iew Data l Automa tic ally (60 sec) v.l
General
{f :6
Host CPU Active Sessions SQL Res:r;:!onse Time
o'clock
1 % .. � 1.0 6 1.0
Status !:!Q
UpSince 02-Feb-2008 10:59:03 CEl .Wait .
LatestCollection
D
Diagnostic Summary
A DM Findings
Alert Log
{l)i
O
NoORA- errors
Q.
Space Summary
Database Size (GB)
ProblemTablespaces
Segment Advisor Recommendations
1.578
�
Q
High Availabilitv
Ins:tanceRecoveryTime(s:ec)
Last Backup
Usable Flash RecoveryArea(0h)
53
n(
100
a
�
Activelncidents
PolicyiJiolations V O FlashbackDatabaseLCQJing
Datab.!lse Inst�nce Health Dump Area Used(0/o) TI
lm
(No alerts)
�Related Alerts
��·� -
Policy__'llolations
_________..
Rysunek 1.5. Widok konsoli serw era Gracle po za!ogowani
LCQJedinAs:SYSTEM
G
Datafiles �
RollbackSeqments Job Cl�sses
� �
�-
Archive o s lq Win odw roups
MiaratetoASM Global Attributes
MakelablespacelocallyManaqed �enancelas:ks:
E nteroriseManaqerU:ser:s NotificationSchedule
Related Linko
Acce:s:s Advisor Centr�l AlertHistory
Alertlog Contents AIIMetrics BaselineMetrirThresho!ds
Blackouts: EM SOLHistory Job s
�dPolicySettincJs MetricCollectionErrors: M'O"nitorimConfiquration
Monitor in Mernory AccessMode PolicyGroups SchedulerCentral
SOLWorksheet TaraetProperties User-DefinedMetrics
tablespace). Ponadto, aby użytkownik mógł korzystać z konsoli oraz końcówek klienta,
powinien mieć dodane uprawnienia systemowe Select Any Dictionary, pozwalające
odczytywać tabele oraz perspektywy słownika danych. Zawartość tych obiektów wy-
korzystywana jest do wyświetlania informacji o systemie (używanych przez konsolę
i końcówkę klienta) oraz do budowania w obrębie serwera czy schematu drzew ele-
mentów koniecznych do poprawnego funkcjonowania końcówek klienta. W dalszej
części książki tworzone będą perspektywy, do tworzenia których rola RESOURCE nie
ma uprawnień, co jest dziwne, gdyż daje ona prawo do generowania wszystkich ele-
mentów proceduralnych — procedur, funkcji, pakietów. Dlatego już na początku wy-
daje się słusznym dodanie uprawnienia Create View.
Rysunek 1.7. Konsola serwera w widoku SQL Worksheet w fazie po wykonaniu zapytania wybierającego
Poza obejrzeniem na tej stronie wyników możliwe jest także obejrzenie planu wyko-
nania zapytania (rysunek 1.8), pozwalającego na ocenę sposobu jego formalnej reali-
zacji, wydajności oraz na ewentualną jego optymalizację.
Rysunek 1.8. Konsola serwera w widoku SQL Worksheet w stanie wygenerowanego planu wykonania
zapytania
Jednym z zainstalowanych w ten sposób narzędzi jest Oracle SQL Developer (rysu-
nek 1.9), którego uruchomienie pozwala na połączenie się z dowolnym serwerem wi-
dzianym w sieci. Należy tylko zdefiniować nazwę serwera (service), nazwę hosta
(komputera), na którym jest on zainstalowany, port nasłuchu (domyślnie 1521) oraz
dane użytkownika, który się do niego loguje. Kolejne uruchomienie wymaga tylko
wprowadzenia danych identyfikujących użytkownika.
Oracle SQL Developer pozwala zdefiniować większą liczbę połączeń, czy to do róż-
nych serwerów, różnych instancji bazy tego samego serwera, czy też do tej samej bazy
dla różnych użytkowników. Rzadko zdarzają się kłopoty podczas nawiązywania połą-
czenia z poziomu klienta w przypadku poprawnego łączenia się z poziomu konsoli.
Najczęstszy jest stan, w którym źle został zdefiniowany dla końcówki klienta plik kon-
figuracyjny nasłuchu tnsnames.ora — tzn. nie został on „przepisany” z poziomu serwera.
W takim przypadku należy albo dokonać jego ręcznego poprawienia, albo, co łatwiejsze,
Rozdział 1. ♦ Wstęp 17
ORACLE11 =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = komputer)(PORT = 1521))
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = oracle11)
)
)
Rysunek 1.10. Oracle SQL Developer po wykonaniu zapytania wybierającego do zakładki Results
Rysunek 1.11. Oracle SQL Developer po wykonaniu zapytania wybierającego do zakładki Script Output
Rysunek 1.12. Oracle SQL Developer po wygenerowaniu planu wykonania zapytania wybierającego
Jeżeli zdecydujecie się na starszą wersję oprogramowania (10g lub 9), co może być
uzasadnione tym, że w zastosowaniach praktycznych jeszcze dominują te wersje, pro-
ces instalacji będzie przebiegał podobnie. Nieco odmienny jest natomiast zestaw na-
rzędzi. Co prawda konsola w postaci aplikacji WWW jest bardzo podobna do tej z re-
alizacji 11 — trochę węższy zakres funkcjonalności — jednak narzędzia instalowane
razem z końcówką kliencką są już zupełnie inne.
20 Część I ♦ Oracle SQL
Rysunek 1.15. Widok drzewa obiektów: wszystkie kategorie obiektów górnego poziomu oraz rozwinięte
drzewo dla schematu pojedynczego użytkownika
Rysunek 1.16.
Widok SQL Worksheet
w stanie logowania
Kolejnym wariantem końcówki klienckiej jest SQL*Plus (rysunek 1.18). Proces logo-
wania się do niego jest kopią tego, z czym mieliśmy do czynienia przy okazji SQL
Worksheet. Jest to proste narzędzie do zadawania zapytań SQL wyposażone w prosty
edytor liniowy, co w znacznym stopniu utrudnia posługiwanie się nim, zwłaszcza kiedy
używa go mało wprawny programista (rysunek 1.19). Prosty edytor sprawia, że za-
pytania muszą być pisane bezbłędnie, nie dając szansy powrotu do już zatwierdzonego
kodu. Wymusza on stosowanie znaku średnika jako symbolu końca polecenia i sygnału
do rozpoczęcia przetwarzania danych, co uniemożliwia pisanie bardziej złożonych
skryptów. Możliwe jest za to ich załadowanie z wcześniej przygotowanego pliku tek-
stowego.
Również prezentacja wyników w tym narzędziu nie jest szczególnie atrakcyjna (rysu-
nek 1.19), dlatego stosuje się je najczęściej przy wykonywaniu niektórych zadań ad-
ministracyjnych i przetwarzaniu wsadowym.
Rozdział 1. ♦ Wstęp 23
Elementem wspólnym dla wszystkich realizacji serwera bazy danych Oracle jest SQL
Plus (rysunek 1.20). To proste narzędzie jest podstawą w administracji serwerem. Lo-
gowanie odbywa się po uruchomieniu aplikacji w obrębie edytora liniowego. W przy-
padku logowania się do pojedynczej instancji bazy danych lub pojedynczego serwera
wystarczającą informacją jest podanie nazwy użytkownika i jego hasła.
Rysunek 1.20.
SQL Plus w stanie
logowania do
pojedynczej instancji
bazy danych
Rysunek 1.21.
SQL Plus w stanie
logowania ze
wskazaniem instancji
bazy danych
Dla przykładu pokazany został plik konfiguracji połączenia tnsnames.ora (listing 1.2)
dla dwóch serwerów bazy danych zlokalizowanych na różnych komputerach. Jeżeli
oba serwery są zainstalowane na tym samym komputerze, należy tylko zmienić jego
nazwę. Podobnie wyglądała będzie sprawa w przypadku dwóch instancji bazy danych
tego samego serwera. Warto zauważyć, że proces nasłuchu dla różnych baz może się
odbywać zarówno na różnych portach (konieczne przy osobnych realizacjach serwera
zainstalowanych na tym samym komputerze), jak i na tym samym porcie.
Listing 1.2. Przykładowa zawartość pliku konfiguracyjnego tnsnames.ora dla definicji połączenia
z więcej niż jednym serwisem
# tnsnames.ora Network Configuration File:
# C:\oracle\client\network\admin\tnsnames.ora
# Generated by Oracle configuration tools.
ORACLE =
(DESCRIPTION =
Rozdział 1. ♦ Wstęp 25
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = komp1)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = oracle)
)
)
ORACLE11 =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = komp2)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = oracle11)
)
)
Rysunek 1.22.
SQL Plus w stanie
logowania po
wykonaniu zapytania
wybierającego
Zapytania w Oracle powinny być kończone średnikiem. Nie jest to wymóg ścisły,
gdyż zarówno PLSQL Worksheet, jak i Oracle Developer radzą sobie z brakującymi
średnikami na końcu zapytań. Da się jednak wskazać takie przypadki, kiedy przetwa-
rzanie nie jest wykonywane (bez żadnego komentarza o błędzie) tylko dlatego, że zo-
stał pominięty ten element składniowy. Można, parafrazując znaną wypowiedź, stwier-
dzić, że zapytania ze średnikiem na końcu wykonują się lepiej.
Pomimo że zarówno w nazwach tabel, pól, jak i w poleceniach czy operatorach Oracle nie
rozróżnia wielkości liter, w całej książce stosowana będzie notacja, w której stałe
elementy składniowe zapytań (instrukcje, klauzule, operatory) zapisywane będą du-
żymi literami (kapitalikami). Pozostałe elementy (nazwy pól i tabel, aliasy) nie będą
28 Część I ♦ Oracle SQL
oznaczane w ten sposób. Taka forma jest tylko i wyłącznie wynikiem chęci zachowania
przejrzystości kodu, co jest szczególnie istotne przy bardziej złożonym przetwarzaniu.
Jeżeli chodzi o rozróżnianie wielkości liter w nazwach obiektów, to istnieje jeden wy-
jątek, występujący wtedy, kiedy nazwa jest ujęta w cudzysłów. W takim przypadku
następuje rozróżnienie wielkości liter. Tak więc nazwy tabeli OSOBY, Osoby, osoby
są sobie równe (równoważne), natomiast nazwy "OSOBY", "Osoby", "osoby" są różne.
Wydawać się może, że jest to przypadek czysto akademicki, jednak jeśli w nazwie
pola występuje jako separator spacja, to aby cały ciąg znaków był interpretowany jako
nazwa, należy go ująć w cudzysłów. Jak widać, jest to kolejny powód, aby nie stoso-
wać spacji jako separatora, gdyż nie tylko powoduje to konieczność pamiętania o cu-
dzysłowach, ale jeszcze, na skutek rozróżniania wielkości liter, musimy dokładnie
pamiętać, jak nazwa została zapisana. Podobną uwagę można przyjąć dla narodowych
znaków diakrytycznych, które nie zawsze muszą być poprawnie interpretowane bez
ujęcia nazwy w cudzysłów. Poza przypadkami nieprzestrzegania zasad nazewniczych
przez twórcę bazy danych, cudzysłowy w nazwach obiektów pojawiają się wtedy, gdy
używamy narzędzi do migracji danych lub automatycznych generatorów zapytań. Sto-
sowanie tych narzędzi nie należy do rzadkości.
Jeżeli chcemy wybrać wszystkie pola danej relacji (tabeli), to ich pełną listę możemy
zastąpić znakiem gwiazdki.
SELECT * FROM Osoby;
W zapytaniu wybierającym możliwe jest wyświetlenie nie tylko zawartości pól, ale
również wyrażeń wynikających z działań na tych polach (tabela 2.1) oraz wyników
zwracanych przez funkcję. W prezentowanym przypadku będzie to iloczyn pól oraz
zamiana liter pola na duże.
Dla pól wynikowych będących skutkiem takich operacji opłacalnym staje się ich alia-
sowanie (przypisywanie im nazw zastępczych). Jeśli tego nie zrobimy, Oracle jako
nazwę dla wyniku przyjmie zapis wyrażenia, dzięki któremu został on obliczony, co
jest mało wygodne, zwłaszcza przy próbie odwołania się do takiego pola, czy to z za-
pytania nadrzędnego, czy z końcówki klienta. Aliasy pól definiujemy po słowie klu-
czowym AS lub też po prostu separując nazwę pola i aliasu spacją. Ten drugi sposób
może prowadzić do pomyłek. Jeśli w naszej tabeli istnieją dwa pola, Nazwisko i Imie,
to zapis:
SELECT Nazwisko Imie
FROM Osoby;
spowoduje wyświetlenie wszystkich nazwisk, ale nazwą pola będzie Imie. Pominięcie
przecinka przy separowaniu pól może więc prowadzić nie do błędu składniowego, ale
do niepożądanego aliasowania. Uzasadnia to postulat aliasowania z zastosowaniem
słowa kluczowego AS. Ponieważ „aliasowanie po spacji” było pierwotną metodą zapropo-
nowaną w tym środowisku, a użytkownicy Oracle należą raczej do konserwatystów,
bardzo często widzieć będziemy kody ze stosowanym tym właśnie sposobem.
Częste i praktyczne jest łączenie pól znakowych w jeden ciąg. Operatorem konkatenacji
w Oracle jest podwójny znak more ||. Ponieważ łączone w ten sposób pola nie są sepa-
rowane, często dodajemy separator, np. spację.
SELECT Nazwisko || ' ' || Imie AS Osoba
FROM Osoby;
Zamiast spacji w roli separatora można użyć znaków specjalnych poprzez odwołanie
się do ich kodów ASCII i zamianę ich funkcją CHR na znak. W prezentowanym przy-
kładzie zastosowany został znak tabulacji CHR(9).
SELECT Nazwisko || CHR(9) || Imie AS Osoba
FROM Osoby;
Domyślnym kierunkiem sortowania jest kierunek rosnący, jeżeli jednak chcemy go jawnie
podać, używamy sufiksu ASC — ascending.
SELECT Nazwisko, Imie FROM Osoby
ORDER BY RokUrodz ASC;
Aby uzyskać malejący kierunek sortowania, używamy sufiksu DESC i podanie go jest
obowiązkowe.
SELECT Nazwisko, Imie FROM Osoby
ORDER BY RokUrodz DESC;
30 Część I ♦ Oracle SQL
Jeżeli w obrębie cechy (pola), względem której sortujemy, znajdują się takie same
wartości, to w tym zakresie kolejność rekordów jest wymuszona kolejnością wpisy-
wania (narzuconą przez indeks). Dlatego możliwe jest sortowanie względem kolejnej
cechy, wymienionej w obrębie klauzuli ORDER BY po przecinku. Kierunek sortowania
dla każdej z cech ustalany jest oddzielnie.
SELECT Nazwisko, Imie FROM Osoby
ORDER BY RokUrodz ASC, Wzrost DESC;
Jeśli wyrażeniu lub polu został nadany alias, to może on pojawić się w klauzuli sortują-
cej, podobnie jak można użyć do sortowania funkcji, wyrażenia.
SELECT RokUrodz*wzrost AS Iloczyn FROM Osoby
ORDER BY Iloczyn;
jednak nie ma ona nic wspólnego z sortowaniem. Wyświetla ona opis pól tabeli (DESC
— describe). Uwaga: w zapytaniu tym przeznaczono bardzo dużo znaków na nazwę
pola (chociaż wymagania środowiska ograniczają ją do co najwyżej 30 znaków), dla-
tego pozostałe informacje — typ pola, możliwość przyjmowania wartości NULL — wy-
świetlane są daleko po prawej stronie ekranu lub zawijane, w zależności od używanej
końcówki klienta.
być dla niego uzyskana. W takim przypadku zawartość pola pozostaje „pusta” — ściśle
mówiąc, ma ona wartość NULL (nieznana, niezidentyfikowana). W ten sposób wcho-
dzimy w zakres logiki trójwartościowej, gdzie dwa dotychczasowe stany (TRUE i FALSE)
zostają uzupełnione o wartość NULL. Musimy więc wprowadzić nowe definicje opera-
torów logicznych obowiązujących w tym przypadku. Przedstawiono je w postaci ta-
belarycznej — tabele 2.3 – 2.5.
to
(NULL = NULL) ⇒ NULL
32 Część I ♦ Oracle SQL
Kolejnym operatorem jest operator listy IN, który zwraca TRUE, kiedy porównywana
wartość jest równa któremukolwiek elementowi listy. Separatorem elementów jest zawsze
przecinek. Operator ten jest stosowany bardzo często z listą dynamiczną (podzapytanie),
co zostanie pokazane później.
SELECT Nazwisko, Imie FROM Osoby
WHERE RokUrodz IN (1960, 1970, 1980);
Operator ten może być wykorzystywany nie tylko dla pól typu numerycznego, ale rów-
nież dla pól znakowych.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko IN ('Kowalski', 'Nowak');
Tak samo jak poprzednio, może zostać on zastąpiony przez wielokrotne przyrównanie
do elementów listy.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko = 'Kowalski'
OR Nazwisko = 'Nowak'
Jednak w przypadku pól znakowych daje o sobie znać cecha Oracle — rozróżnianie
wielkości liter. W związku z nią poprzednie zapytania wykryją tylko nazwiska (Ko-
walski, Nowak) rozpoczynające się od dużej litery. Aby ominąć tę właściwość i wy-
świetlić poszukiwane nazwiska bez względu na wielkość użytych w nich znaków, ko-
Rozdział 2. ♦ Zapytania wybierające 33
Dla tego operatora wprowadzone zostały znaki specjalne, które zawiera tabela 2.6.
Symbol % zastępuje dowolny ciąg znaków (w tym ciąg pusty), stąd kolejny przykład
pokazuje wyświetlanie nazwisk i imion osób, których nazwisko rozpoczyna się od
frazy KOW (KOWALSKI, KOWALCZYK, KOWALEWSKI itp.). Należy zwrócić
uwagę na to, że zgodność wielkości liter dotyczy tylko jawnie podanej frazy, więc
wyświetlone zostałyby również te nazwiska, dla których tylko ona pisana jest dużymi
literami (KOWalski, KOWaLczYk, KOWalewSKI itp.). Ponieważ symbol ten zastępuje
również pusty ciąg znaków, to jeśli w tabeli Osoby byłaby osoba o nazwisku KOW,
jej dane również zostałyby wypisane.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko LIKE 'KOW%';
Złożeniem dwóch poprzednich przypadków jest filtr, w którym znak specjalny zasto-
sowano zarówno przed, jak i po frazie. W tym przykładzie zostaną wyświetlone na-
zwiska, w których w dowolnym miejscu występuje fraza KOW, czyli zawierające ją
34 Część I ♦ Oracle SQL
Do tej pory tworzone były, ze względu na przejrzystość wywodu, tylko proste warunki
filtrujące. Oczywiście dopuszczalne jest używanie bardziej złożonych wyrażeń poprzez
wiązanie elementarnych warunków przy wykorzystaniu operatorów logicznych. Należy
Rozdział 2. ♦ Zapytania wybierające 35
zauważyć, że nie zostały wprowadzone żadne priorytety dla operatorów, dlatego zło-
żone wyrażenia analizowane są zawsze, począwszy od lewej strony. Zmianę kolejno-
ści sprawdzania warunków można uzyskać, łącząc je nawiasami według ogólnych zasad,
a wtedy jako pierwsze sprawdzane są warunki najbardziej zagnieżdżone.
SELECT ListaPól FROM Osoby
WHERE warunek1 AND warunek2 OR warunek3 OR … AND …;
pól, w których nie zdefiniowano wartości, czyli RokUrodz IS NULL. Dodatkowo w przy-
kładzie zastosowano prezentowaną poprzednio funkcję ROWNUM w celu ograniczenia
liczby wyświetlanych wierszy. Wyznaczone na podstawie instrukcji CASE pole może
mieć nadany alias (w tym przypadku Info), ale, tak samo jak w przypadku innych ob-
liczanych pól, jest to tylko dobry zwyczaj, a nie konieczność.
SELECT Nazwisko, RokUrodz,
CASE
WHEN RokUrodz >= 1980 THEN 'Młody'
WHEN RokUrodz >= 1970 AND RokUrodz < 1980 THEN 'Dość młody'
WHEN RokUrodz >= 1960 AND RokUrodz < 1970 THEN 'Średni'
WHEN RokUrodz < 1960 THEN 'Stary'
ELSE
'Nie wiem!!!'
END AS Info
FROM Osoby
WHERE ROWNUM < 20;
Ponieważ po pierwszym spełnionym warunku nie są już sprawdzane kolejne, niżej po-
łożone, poniższe zapytanie jest równoważne poprzedniemu:
SELECT Nazwisko, RokUrodz, CASE
WHEN RokUrodz >= 1980 THEN 'Młody'
WHEN RokUrodz >= 1970 THEN 'Dość młody'
WHEN RokUrodz >= 1960 THEN 'Średni'
WHEN RokUrodz < 1960 THEN 'Stary'
ELSE
'Nie wiem!!!'
END AS Info
FROM Osoby
WHERE ROWNUM < 20;
W celu wyznaczania grup, względem których stosowane są funkcje agregujące (tabela 2.7),
wykorzystujemy klauzulę GROUP BY. W zaprezentowanym przykładzie pokazane są
jedynie sumy i nie wiadomo, komu je wypłacono, w związku z czym możemy w części
głównej wymienić pole grupujące — IdOsoby.
SUM suma ∑x
AVG wartość średnia ∑x
n
MAX maksimum
MIN minimum
n∑ x 2 − (∑ x )
2
VARIANCE wariancja
n(n − 1)
n∑ x 2 − (∑ x )
2
odchylenie
STDDEV
standardowe n(n − 1)
COUNT zlicz n
Jak widać, w części głównej zapytania agregującego nie musi występować pole (pola)
grupowania, ale jeżeli pojawi się tam jakiekolwiek pole bez funkcji agregującej, to
musi się ono znaleźć w klauzuli GROUP BY. Tak jak przy każdym z pól, tak i przy polu
z funkcją agregującą może pojawić się alias nazwy. W części głównej zapytania może
wystąpić wiele funkcji agregujących — dotyczyć mogą one wielu pól i możliwe jest
wykonywanie na nich działań algebraicznych.
SELECT IdOsoby, SUM(Brutto) AS Razem, AVG(Brutto) AS Srednio,
COUNT(IdZarobku) AS Ile, MAX(Brutto)/AVG(Brutto) AS Wskaznik
FROM Zarobki GROUP BY IdOsoby
Czy zawsze zwrócone zostaną trzy takie same liczby? Jeżeli przypomnimy sobie in-
formacje dotyczące wartości NULL, oczywistym stanie się, że nie, przy czym różnica
może dotyczyć trzeciego z wyprowadzanych pól. Pierwsza liczba zawsze pokaże, ile
jest wszystkich rekordów (gwiazdka oznacza wszystkie pola — jeśli nie ma żadnego
pola, nie ma rekordu), druga również zlicza je wszystkie (IdOsoby jako klucz podsta-
wowy określa rekord, ponadto z definicji nie może mieć wartości NULL). Inaczej rzecz
się ma w przypadku zliczania pola niebędącego kluczem podstawowym. Jeżeli nie
38 Część I ♦ Oracle SQL
Wyświetlone zostaną rekordy, których suma przekroczy 400, czyli te, które wskazano
w tabeli 2.9.
Rozdział 2. ♦ Zapytania wybierające 39
Tabela 2.9. Skutek wykonania zapytania agregującego z filtrowaniem na poziomie grupy rekordów
Kowalski Nowak Janik
100 100 300
100 200 200
100 100
100
RAZEM 400 400 500
Niewyświetlane Niewyświetlane Wyświetlane
W takim przypadku przykładowe wyniki będą wyglądały tak jak pokazano w tabeli 2.10.
Należy zwrócić uwagę, że klauzula ORDER BY znajduje się na końcu, a sortowanie może się
odbywać zarówno w oparciu o pole grupujące, jak i o funkcję agregującą, przy czym
nie musi być to ta sama funkcja, która występuje w części głównej, ani ta, która jest
używana z klauzulą filtrującą HAVING.
40 Część I ♦ Oracle SQL
gdzie Nazwisko jest polem tabeli Osoby, natomiast Brutto polem tabeli Zarobki. Za-
pytanie takie jest poprawne składniowo, a skutkiem jego wykonania jest iloczyn kar-
tezjański (krotka) na wartościach pól wybranych ze wszystkich rekordów obu tabel.
Wyświetlone zostaną więc dla każdego nazwiska wszystkie wartości brutto. W więk-
szości przypadków interesuje nas, aby dla danego Nazwiska zostały podane tylko warto-
ści Brutto jego zarobków (wynagrodzeń). To zadanie realizowane jest poprzez złączenie
tabel — najczęściej na polach zawierających takie same dane (pomiędzy kluczem
głównym jednej a kluczem obcym drugiej tabeli). Pierwszą metodą realizacji złączenia
jest zastosowanie znanej już klauzuli filtrującej WHERE.
SELECT Nazwisko, Brutto FROM Osoby, Zarobki
WHERE Osoby.IdOsoby = Zarobki.IdOsoby
Warunkiem złączenia jest najczęściej równa wartość pól. Ponieważ z reguły mają one
taką samą nazwę w obu tabelach, konieczne jest użycie ich nazwy kwalifikowanej.
Warunkiem koniecznym realizacji złączenia jest natomiast tylko zgodność typów pól.
Ponieważ w produktach Microsoftu wszystkie zmienne są przekazywane przez typ
wariant (dopasowujący się do typu danych), zgodność typu w tym przypadku można
traktować szerzej jako możliwość automatycznej konwersji między typami danych.
Pomimo tak elastycznego traktowania zgodności danych w produktach Microsoftu
wskazane byłoby, aby łączone ze sobą pola były dokładnie tego samego typu. Pozwala to
na wyeliminowanie ewentualnych konfliktów przy innych operacjach na danych. Według
takiego samego schematu możemy złączyć dowolną liczbę tabel. Na przykład, chcąc
uzyskać informację o nazwach działów, nazwiskach pracowników, którzy w nich pra-
cują, oraz o ich zarobkach, możemy wykonać następujące zapytanie:
SELECT Opis, Nazwisko, Brutto FROM Osoby, Zarobki, Dzialy
WHERE Osoby.IdOsoby = Zarobki.IdOsoby
AND Dzialy.IdDzialu = Osoby.IdDzialu
Zwróćmy uwagę na to, iż kolejność wymieniania tabel, z których pochodzić mają dane,
jest dowolna, tak samo jak dowolna jest kolejność warunków złączenia między tabe-
lami w klauzuli WHERE. Wszystkie one połączone są operatorem iloczynu logicznego AND.
Tak realizowane złączenie zostało zastąpione przez bardziej wydajny mechanizm sto-
sujący operator JOIN (w MS SQL występujący w pełnej postaci INNER JOIN). Użycie
takiej formy złączenia powoduje, że informacja o osobach i ich zarobkach jest uzy-
skiwana dzięki wykonaniu zapytania:
SELECT Nazwisko, Brutto
FROM Zarobki JOIN Osoby
ON Osoby.IdOsoby = Zarobki.IdOsoby
Rozdział 2. ♦ Zapytania wybierające 41
Pełna nazwa operatora złączenia brzmi INNER JOIN. Złączenie trzech tabel może zostać
natomiast zrealizowane w następujący sposób:
SELECT Opis, Nazwisko, Brutto
FROM Zarobki JOIN Osoby
ON Osoby.IdOsoby = Zarobki.IdOsoby
JOIN Dzialy
ON Dzialy.IdDzialu = Osoby.IdDzialu
W przypadku więcej niż jednej tabeli konieczne jest zachowanie porządku łączenia,
czyli możemy łączyć je, począwszy od najbardziej ogólnej, a skończywszy na najbardziej
szczegółowej (Działy — Osoby — Zarobki), albo rozpoczynając od najbardziej „pod-
rzędnej” (Zarobki — Osoby — Działy). Żaden inny porządek nie jest dopuszczalny,
o czym należy szczególnie pamiętać, łącząc większą liczbę tabel.
Operator złączenia JOIN oprócz swej wersji podstawowej ma też inne postaci. Możemy
powiedzieć, że możliwe jest wskazanie „kierunku” realizacji połączenia między ta-
belami. Jeśli zastosujemy wariant RIGHT JOIN, to z tabeli występującej po prawej stro-
nie operatora zostaną wyprowadzone wszystkie rekordy, nawet jeśli w tabeli po jego
lewej stronie nie istnieją wiersze, dla których prawdziwe jest wyrażenie łączące. Z ta-
beli po lewej stronie operatora pobrane zostaną natomiast tylko te rekordy, dla których
wyrażenie łączące jest prawdziwe. Mówimy o prawdziwości wyrażenia, a nie wprost
o równości rekordów, ponieważ możliwe jest zrealizowanie złączenia nierównościo-
wego — opartego na praktycznie dowolnym operatorze innym niż równość. W poka-
zanym przykładzie wyświetlone zostaną nazwiska wszystkich pracowników oraz dla
każdego z nich informacja, czy otrzymał jakiekolwiek wypłaty (wartości Brutto). Jeżeli
pracownik nie pobierał żadnego wynagrodzenia, w polu brutto wyświetlona zostanie
wartość NULL.
SELECT Nazwisko, Brutto
FROM Zarobki RIGHT JOIN Osoby
ON Osoby.IdOsoby = Zarobki.IdOsoby
Zmieniając kierunek złączenia na LEFT JOIN, otrzymamy wszystkie wypłaty oraz ich
właścicieli. Jeżeli wypłata nie ma właściciela (wartość NULL w pozycji IdOsoby tabeli
Zarobki) lub podano wartość spoza zakresu występującego w tabeli Osoby w pozycji
Nazwisko, przy wartości Brutto pojawi się NULL.
SELECT Nazwisko, Brutto
FROM Zarobki LEFT JOIN Osoby
ON Osoby.IdOsoby = Zarobki.IdOsoby
Złożeniem tych dwóch operatorów jest operator FULL JOIN, przy zastosowaniu którego
wyświetlone zostaną wszystkie rekordy z obu tabel oraz rekordy, dla których praw-
dziwe jest wyrażenie łączące. Innymi słowy wartości NULL mogą wystąpić w obu wy-
świetlanych polach.
SELECT Nazwisko, Brutto
FROM Zarobki FULL JOIN Osoby
ON Osoby.IdOsoby = Zarobki.IdOsoby
42 Część I ♦ Oracle SQL
Zamieniając strony pól w klauzuli WHERE, również zmieniamy kierunek złączenia. W ta-
kim rozwiązaniu niemożliwe było osiągnięcie złączenia odpowiadającego FULL JOIN.
Pomimo że rozwiązanie to jest stare, to, zważywszy na konserwatyzm programistów
Oracle, często jeszcze występuje w kodach.
Analogicznie, jeśli tabele są łączone wszystkimi polami, których nazwy są takie same,
możliwe jest zastosowanie operatora NATURAL JOIN bez podania pól łączących, ponie-
waż zdeterminowane są one przez nazwy pól tabel.
SELECT Nazwisko, Brutto
FROM Osoby NATURAL JOIN Zarobki;
Taka realizacja jest zawsze skuteczna, jeśli łączymy dwie tabele o zgodnych nazwach
pól podlegających temu złączeniu, jednakże w przypadku łączenia większej liczby ta-
bel operatorem NATURAL JOIN można uzyskać niepożądane efekty. Spróbujmy wyświe-
tlić nazwę działu, nazwisko oraz wypłaty brutto dla każdego z pracowników.
Rozdział 2. ♦ Zapytania wybierające 43
Ostatnim zrealizowanym wariantem złączenia jest użycie operatora CROSS JOIN, który
powoduje wyświetlenie krotki, czyli połączenia każdego rekordu pierwszej tabeli z każ-
dym rekordem drugiej. Wprowadzenie tej konstrukcji do standardu jest nieco dziwne,
zwłaszcza kiedy zdamy sobie sprawę z tego, że zastąpienie go przecinkiem daje do-
kładnie taki sam efekt.
SELECT Nazwisko, Brutto
FROM Osoby CROSS JOIN Zarobki;
Rysunek 2.1.
Graficzna prezentacja
działania operatora
listy
Jeśli będziemy chcieli wyszukać nazwiska, zapewniając zgodność zarówno ich, jak
i imion, to wydaje się słusznym sprawdzenie zgodności na dwóch listach. Niestety,
przedstawione poniżej rozwiązanie, pomimo poprawności składniowej, jest błędne.
Dopuszcza mianowicie sytuację, w której poszukiwane elementy pochodzą z różnych
rekordów tabeli słownikowej, co ilustruje rysunek 2.2.
Rysunek 2.2.
Graficzna prezentacja Osoby TTT
działania operatora Kowalski Karol
listy Kowalski Jan
Nowak Jan
Problem ten można łatwo rozwiązać, stosując do porównania z listą nie pojedyncze
pole, ale konkatenację dwóch lub większej liczby pól.
SELECT Nazwisko, Imie, RokUrodz FROM Osoby
WHERE Nazwisko || Imie IN
(SELECT Nazwisko|| Imie FROM ttt);
Takie rozwiązanie jest poprawne, natomiast wadą jego jest to, że w przypadku po-
równywania zgodności pól różnych typów przed dokonaniem konkatenacji należy je
poddać konwersji na typ znakowy (funkcja TO_CHAR). Dodanie kolejnej operacji po-
woduje wydłużenie czasu trwania obliczeń, co przy dużych wolumenach danych może
być kłopotliwe. Innym rozwiązaniem, charakterystycznym dla Oracle (tylko na tej
platformie dopuszczalnym), jest porównanie przy użyciu operatora IN listy pól z ma-
cierzą generowaną przez podzapytanie. Prezentuje to kolejny fragment kodu.
SELECT Nazwisko, Imie, RokUrodz FROM Osoby
WHERE (Nazwisko, Imie) IN
(SELECT Nazwisko, Imie FROM ttt);
Rysunek 2.3.
Graficzna
prezentacja działania
wielokrotnego
złączenia
Rozdział 2. ♦ Zapytania wybierające 45
Taki sam efekt możemy uzyskać, stosując operator NATURAL JOIN, który daje nam uprosz-
czenie zapisu, ale „ukrywa” pola łączące. Wykorzystywane są, zgodnie z definicją,
wszystkie pola o takich samych nazwach.
SELECT Nazwisko, Imie, RokUrodz,
FROM Osoby NATURAL JOIN ttt;
Należy zauważyć, że użyte do realizacji złączenia pola o zgodnych nazwach nie mogą
mieć nazw kwalifikowanych. Takie samo rozwiązanie można uzyskać, stosując ope-
rator USING.
SELECT Nazwisko, Imie, RokUrodz,
FROM Osoby JOIN ttt
USING(Nazwisko, Imie, RokUrodz);
Tak jak poprzednio, niedopuszczalne jest użycie nazw kwalifikowanych dla pól wy-
korzystanych w realizacji złączenia — wymienionych na liście operatora USING. W tym
przypadku jednak, ponieważ jawnie je wskazujemy, możemy ograniczyć się tylko do
niektórych z nich, na przykład porównując jedynie zgodność nazwiska i imienia.
SELECT Nazwisko, Imie, Osoby.RokUrodz,
ttt.RokUrodz
FROM Osoby JOIN ttt
USING(Nazwisko, Imie);
Pola, które są tu wymienione na liście operatora USING, nie mogą mieć nazw kwalifi-
kowanych, jednak pominięte na tej liście pole RokUrodz musi już mieć taką nazwę.
Korzystając z właściwości operatora złączenia i używając jego postaci LEFT JOIN, mo-
żemy wyszukać rekordy (również z dokładnością do tych samych trzech cech), które
wchodzą w skład tabeli Osoby i nie należą do tabeli ttt — innymi słowy dane osób,
które są zawarte w tabeli Osoby, a nie występują w ttt. Przekłada się to w teorii mno-
gości na różnicę obu zbiorów (Osoby–ttt). Poza zrealizowaniem złączenia należy spraw-
dzić, czy którekolwiek z pól odejmowanej tabeli ma wartość NULL. W przykładzie zwe-
ryfikowano ten warunek dla wszystkich pól odejmowanej tabeli ttt, ale wobec znanej
już zależności: (NULL = NUL) ⇒ NULL, uważny czytelnik zauważy ich nadmiarowość.
W porównaniu z poprzednim zapytaniem usunięta została druga linia (przekreślona),
ponieważ i tak nie niesie ona ze sobą żadnej interesującej informacji — gdyby pozo-
stała ona w kodzie, wszystkie wymienione w niej pola we wszystkich rekordach za-
wierałyby wartości NULL.
SELECT Osoby.Nazwisko, Osoby.Imie, Osoby.RokUrodz
,ttt.Nazwisko, ttt.Imie, ttt.RokUrodz
FROM Osoby LEFT JOIN ttt
ON Osoby.Nazwisko = ttt.Nazwisko
AND Osoby.Imie = ttt.Imie
AND Osoby.RokUrodz = ttt.RokUrodz
46 Część I ♦ Oracle SQL
Aby wyznaczyć różnicę (ttt–Osoby), wystarczy zmienić kierunek złączenia oraz spraw-
dzić, czy pole z tabeli Osoby ma wartość NULL.
Aby pokazać zastosowanie zapytania skalarnego w celu stworzenia warunku dla klau-
zuli HAVING, skonstruowano zapytanie wyświetlające identyfikator osoby oraz wartość
średniej wypłaty brutto dla tych przypadków, w których uśredniona pensja pracownika
jest wyższa niż średnia wypłata w firmie.
SELECT IdOsoby, AVG(Brutto) FROM Zarobki
GROUP BY IdOsoby
HAVING AVG(Brutto) > (SELECT AVG(Brutto) FROM Zarobki);
Jak widać, oba operatory mogą zostać zastąpione przez wyznaczenie kolejnej funkcji
agregującej, co powoduje, że w praktyce są one bardzo rzadko wykorzystywane.
Rozdział 2. ♦ Zapytania wybierające 47
Możliwe jest pominięcie tych powtórek poprzez zastosowanie funkcji ROWNUM oraz DECODE
według zasad, jakie pokazano przy numerowaniu rekordów w grupie. Niestety powo-
duje to jeszcze większą komplikację i tak dość złożonego zapytania. Dlatego atrak-
cyjnym rozwiązaniem jest możliwość stosowania innej definicji grupowania, która
zdecydowanie upraszcza zapis. Jeśli klasyczną klauzulę grupującą zastąpimy za po-
mocą GROUP BY ROLLUP, to uzyskamy interesujące dodatkowe podsumowania na pozio-
mie (poziomach) nadrzędnym — w prezentowanym przykładzie dla działów; tabela 2.12.
Ponadto wyprowadzone zostało tu podsumowanie bez podziału na grupy — suma dla
całej firmy.
Taki sam efekt możemy uzyskać w każdym z omawianych przykładów przy użyciu
operatora USING. Dopuszczalne jest również stosowanie większej liczby funkcji agre-
gujących (dowolnego zestawu spośród wszystkich zdefiniowanych w środowisku), a pod-
sumowaniu może podlegać wiele pól oraz dowolne wyrażenia algebraiczne.
SELECT Nazwa, Nazwisko, SUM(Brutto)
FROM Dzialy JOIN Osoby USING(IdDzialu)
JOIN Zarobki USING(IdOsoby)
GROUP BY GROUPING SETS (Nazwa, Nazwisko, IdOsoby);
Wszystkie wyżej wymienione funkcje grupujące mogą być wzbogacane poprzez do-
dawanie nadrzędnego pola grupującego (tabela 2.15). Dopiero w obrębie tej grupy za-
chodziło będzie wtedy właściwe wielopoziomowe wyznaczanie funkcji agregującej. Pole
nadrzędne może być również jednym z pól listy dla ROLLUP, CUBE lub GROUPING SETS.
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, ROLLUP (Nazwa, Nazwisko);
Tabela 2.15. Skutek wykonania zapytania z opcją grupowania ROLLUP i z dodatkowym, nadrzędnym
poziomem grupowania
Nazwa Nazwisko Razem
Dyrekcja Polakow 999
Dyrekcja Kowalski 2775
Handlowy Jakow 333
Handlowy Kowalik 888
Handlowy Nowicki 3108
Techniczny Kow 2109
Techniczny Kowalczyk 1554
Administracja Janik 777
Administracja Nowak 1998
Administracja Jasiński 1554
Administracja Baranowski 777
Dyrekcja 3774
Handlowy 4329
Techniczny 3663
Administracja 5106
Dyrekcja 3774
Handlowy 4329
Techniczny 3663
Administracja 5106
Tabela 2.16. Skutek wykonania zapytania z opcją grupowania ROLLUP, z dodatkowym, nadrzędnym
poziomem grupowania oraz z zastosowaniem funkcji GROUP_ID
Nazwa Nazwisko Razem GR
Dyrekcja Polakow 999 0
Dyrekcja Kowalski 2775 0
Handlowy Jakow 333 0
Handlowy Kowalik 888 0
Handlowy Nowicki 3108 0
Techniczny Kow 2109 0
Techniczny Kowalczyk 1554 0
Administracja Janik 777 0
Administracja Nowak 1998 0
Administracja Jasiński 1554 0
Administracja Baranowski 777 0
Dyrekcja 3774 0
Handlowy 4329 0
Techniczny 3663 0
Administracja 5106 0
Dyrekcja 3774 1
Handlowy 4329 1
Techniczny 3663 1
Administracja 5106 1
W tego typu funkcjach agregujących pojawiają się pola o wartości NULL dla podsumo-
wań dla nadrzędnych poziomów. Stan taki wykrywa funkcja GROUPING(pole), zwra-
cająca wartość zero, gdy na danym poziomie pole zawiera NULL, oraz jeden, kiedy ma
ono wartość różną od NULL. Funkcja ta w połączeniu z funkcją DECODE pozwala na wy-
pełnienie pustych miejsc zdefiniowanymi wyrażeniami, co poprawia czytelność re-
zultatów (tabela 2.18). W przedstawionym przykładzie dla podsumowania dotyczącego
całej firmy wyprowadzany jest napis FIRMA, a dla podsumowania dotyczącego działu
napis Wszyscy. Dodatkowo równolegle prezentowane są pola bez zastosowania funkcji
GROUPING(pole).
SELECT
DECODE(GROUPING(Nazwa), 1, 'FIRMA', Nazwa) AS Dzial,
DECODE(GROUPING(Nazwisko), 1, 'Wszyscy', Nazwisko)
AS Pracownik,
Nazwa, Nazwisko, SUM(Brutto) AS Razem
FROM Dzialy JOIN Osoby
ON Dzialy.IdDzialu = Osoby.IdDzialu
JOIN Zarobki
ON Osoby.IdOsoby = Zarobki.IdOsoby
GROUP BY ROLLUP (Nazwa, Nazwisko);
56 Część I ♦ Oracle SQL
Tabela 2.17. Skutek wykonania zapytania z opcją grupowania CUBE, z dodatkowym, nadrzędnym
poziomem grupowania oraz z zastosowaniem funkcji GROUP_ID
Nazwa Nazwisko Razem GR
Dyrekcja Polakow 999 0
Dyrekcja Kowalski 2775 0
Handlowy Jakow 333 0
Handlowy Kowalik 888 0
Handlowy Nowicki 3108 0
Techniczny Kow 2109 0
Techniczny Kowalczyk 1554 0
Administracja Janik 777 0
Administracja Nowak 1998 0
Administracja Jasiński 1554 0
Administracja Baranowski 777 0
Dyrekcja Polakow 999 1
Dyrekcja Kowalski 2775 1
Handlowy Jakow 333 1
Handlowy Kowalik 888 1
Handlowy Nowicki 3108 1
Techniczny Kow 2109 1
Techniczny Kowalczyk 1554 1
Administracja Janik 777 1
Administracja Nowak 1998 1
Administracja Jasiński 1554 1
Administracja Baranowski 777 1
Dyrekcja 3774 0
Handlowy 4329 0
Techniczny 3663 0
Administracja 5106 0
Dyrekcja 3774 1
Handlowy 4329 1
Techniczny 3663 1
Administracja 5106 1
Tabela 2.18. Skutek wykonania zapytania z opcją grupowania ROLLUP i z kodowaniem pustych pól
przy użyciu funkcji DECODE oraz GROUPING(pole)
Dzial Pracownik Nazwa Nazwisko Razem
Dyrekcja Polakow Dyrekcja Polakow 999
Dyrekcja Kowalski Dyrekcja Kowalski 2775
Dyrekcja Wszyscy Dyrekcja 3774
Handlowy Jakow Handlowy Jakow 333
Handlowy Kowalik Handlowy Kowalik 888
Handlowy Nowicki Handlowy Nowicki 3108
Handlowy Wszyscy Handlowy 4329
Techniczny Kow Techniczny Kow 2109
Techniczny Kowalczyk Techniczny Kowalczyk 1554
Techniczny Wszyscy Techniczny 3663
Administracja Janik Administracja Janik 777
Administracja Nowak Administracja Nowak 1998
Administracja Jasiński Administracja Jasiński 1554
Administracja Baranowski Administracja Baranowski 777
Administracja Wszyscy Administracja 5106
FIRMA Wszyscy 16872
Tabela 2.19. Skutek wykonania zapytania z opcją grupowania ROLLUP i z kodowaniem pustych pól
przy użyciu funkcji GROUPING(pole) oraz GROUPING_ID(ListaPol)
Gr_nazwa_
Gr_nazwa Gr_nazwisko Gr_nazwisko_nazwa Nazwa Nazwisko Razem
nazwisko
0 0 0 0 Dyrekcja Polakow 999
0 0 0 0 Dyrekcja Kowalski 2775
0 1 1 2 Dyrekcja 3774
0 0 0 0 Handlowy Jakow 333
0 0 0 0 Handlowy Kowalik 888
0 0 0 0 Handlowy Nowicki 3108
0 1 1 2 Handlowy 4329
0 0 0 0 Techniczny Kow 2109
0 0 0 0 Techniczny Kowalczyk 1554
0 1 1 2 Techniczny 3663
0 0 0 0 Administracja Janik 777
0 0 0 0 Administracja Nowak 1998
0 0 0 0 Administracja Jasiński 1554
0 0 0 0 Administracja Baranowski 777
0 1 1 2 Administracja 5106
1 1 3 3 16872
58 Część I ♦ Oracle SQL
SELECT
GROUPING(Nazwa) GR_Nazwa,
GROUPING(Nazwisko) GR_Nazwisko,
GROUPING_ID(Nazwa, Nazwisko) GR_Nazwa_Nazwisko,
GROUPING_ID(Nazwisko, Nazwa) GR_Nazwisko_Nazwa,
Nazwa, Nazwisko, SUM(Brutto) AS Razem
FROM Dzialy JOIN Osoby
ON Dzialy.IdDzialu = Osoby.IdDzialu
JOIN Zarobki
ON Osoby.IdOsoby = Zarobki.IdOsoby
GROUP BY ROLLUP (Nazwa, Nazwisko);
Tradycyjne funkcje agregujące, jak na przykład SUM(), AVG(), COUNT(), tak jak to po-
kazano poprzednio, wyliczają dla każdej grupy pojedynczą wartość wynikową. Dzięki
rozszerzeniu składni języka SQL w realizacji Oracle 10g możliwe jest wyznaczanie
wartości funkcji grupowej oddzielnie dla każdego rekordu grupy (a nie tylko raz dla
niej całej). W celu skorzystania z takiej możliwości klauzula GROUP BY zostaje zastą-
piona przez PARTITION BY, której składnia jest następująca:
FUNKCJA_AGREGUJACA() OVER (PARTITION BY Kolumna)
Klauzula PARTITION umożliwia prostszy zapis wielu zapytań analitycznych, które wy-
magały użycia podzapytań. Poniżej przedstawiony został przykład zastosowania wy-
rażenia PARTITION (tabela 2.20) w celu otrzymania wartości funkcji grupowej SUM()
oddzielnie dla każdego rekordu.
SELECT IdOsoby, Brutto, SUM(Brutto)
OVER (PARTITION BY IdOsoby) Razem
FROM Zarobki;
Rozdział 2. ♦ Zapytania wybierające 59
gdzie:
Kolumna1 to kolumna (lub wyrażenie) łącząca rekordy w grupy, wewnątrz
których przesuwa się okno (pominięcie PARTITION BY oznacza, że okno
przesuwa się przez całą tabelę).
Kolumna2 to kolumna (lub wyrażenie) porządkująca rekordy wewnątrz grupy.
DESC zmienia porządek przesuwania okna na od wartości największej
do najmniejszej.
Wyrażenie1 określa położenie początku okna.
Wyrażenie2 określa położenie końca okna.
Podobnie definiowane jest okno fizyczne, lecz zamiast słowa kluczowego RANGE sto-
sujemy słowo ROWS. Ponadto nie wykorzystuje ono wyrażeń INTERVAL. Ruchome okno
fizyczne może zostać użyte do policzenia sumy bieżącej, traktowanej jako suma wszyst-
Rozdział 2. ♦ Zapytania wybierające 61
Tabela 2.24. Skutek wykonania zapytania wyświetlającego sumy bieżące w obrębie grupy (partycji)
IdOsoby DataWyp Brutto Razem
1 02/01/03 999 999
1 03/12/07 111 1110
1 03/12/17 333 1443
1 04/01/07 111 1554
1 04/11/19 666 2220
1 05/03/13 555 2775
2 02/11/22 444 444
2 03/02/14 888 1332
2 03/05/05 666 1998
3 02/02/07 222 222
3 02/07/03 999 1221
3 05/10/11 888 2109
4 03/03/08 222 222
4 04/01/03 555 777
Tabela 2.25. Skutek wykonania zapytania wyświetlającego funkcje agregujące w obrębie grupy (partycji)
SUM Średnia Średnia sumy Procent Procent
IdDzialu Nazwisko
(BRUTTO) wszystkich dla działu z całości w dziale
1 Kowalski 2664 1509,6 2664 ,1764706 1
2 Janik 1776 1509,6 1591 ,1176471 ,3720930
2 Nowak 1998 1509,6 1591 ,1323529 ,4186047
2 Zieliński 999 1509,6 1591 ,0661765 ,2093023
3 Kow 1443 1509,6 1258 ,0955882 ,3823529
3 Kowalczyk 2220 1509,6 1258 ,1470588 ,5882353
3 Pawlak 111 1509,6 1258 ,0073529 ,0294118
4 Nowicki 1998 1509,6 1998 ,1323529 1
5 Kowal 1221 1509,6 943,5 ,0808824 ,6470588
5 Kowalski 666 1509,6 943,5 ,0441176 ,3529412
Tabela 2.27. Skutek wykonania zapytania numerującego rekordy w obrębie grupy (partycji)
po ujednoliceniu sposobu sortowania w każdej z nich
IdDzialu Nazwisko Brutto Numer Numer_w_dziale Numer_wyplaty
1 Kowalski 555 1 1 1
1 Kowalski 333 2 2 2
1 Kowalski 111 3 3 3
1 Kowalski 111 4 4 4
1 Kowalski 666 5 5 5
1 Kowalski 999 6 6 6
1 Polakow 999 7 7 1
2 Nowak 888 8 1 1
2 Nowak 666 9 2 2
2 Nowak 444 10 3 3
2 Janik 555 11 4 1
2 Janik 222 12 5 2
2 Jasiński 888 13 6 1
2 Jasiński 666 14 7 2
2 Baranowski 444 15 8 1
2 Baranowski 333 16 9 2
3 Kow 222 17 1 1
3 Kow 888 18 2 2
3 Kow 999 19 3 3
3 Kowalczyk 777 20 4 1
3 Kowalczyk 777 21 5 2
Poza znaną już funkcją numerującą wiersze wprowadzono kolejny zestaw funkcji ana-
litycznych, których nazwy prezentuje tabela 2.28. W większości przypadków nazwa
wskazuje na posiadaną przez nie funkcjonalność.
Tabela 2.31. Skutek wykonania zapytania wyświetlającego parametry rozkładu wartości pola RokUrodz
w obrębie każdego z działów
Odchylenie Stand
IdDzialu Wariancja Wariancja Populacji Odchylenie Stand
Populacji
1 21,3333333 14,2222222 4,61880215 3,77123617
2 75,8095238 64,9795918 8,70686647 8,06099199
3 53,7666667 44,8055556 7,33257572 6,69369521
4 52,0181818 47,2892562 7,21236312 6,87671842
5 8 4 2,82842712 2
VAR_POP(RokUrodz) OVER
(PARTITION BY IdDzialu ) AS WariancjaPopulacji,
STDDEV_SAMP(RokUrodz) OVER
(PARTITION BY IdDzialu ) AS OdchylenieStand,
STDDEV_POP(RokUrodz) OVER
(PARTITION BY IdDzialu ) AS OdchylenieStandPopulacji
FROM Osoby ORDER BY IdDzialu;
Podobnie możemy wyznaczyć te same parametry rozkładu w skali całej firmy bez po-
działu na partycje (tabela 2.32).
Tabela 2.32. Skutek wykonania zapytania wyświetlającego parametry rozkładu wartości pola RokUrodz
w obrębie każdego z działów
Wariancja Wariancja Populacji Odchylenie Stand Odchylenie Stand Populacji
45,8344828 44,3066667 6,77011689 6,65632531
SELECT
VAR_SAMP(RokUrodz) AS Wariancja,
VAR_POP(RokUrodz) AS WariancjaPopulacji,
STDDEV_SAMP(RokUrodz) AS OdchylenieStand,
STDDEV_POP(RokUrodz) AS OdchylenieStandPopulacji
FROM Osoby;
Tabela 2.34. Skutek wykonania zapytania wyświetlającego kowariancję RokUrodz i pola Wzrost
IdDzialu Kowariancja Kowariancja Populacji
1 –,48 –,32
2 ,805714286 ,690612245
4 ,066272727 ,060247934
5 0
3 ,1895 ,1516
Rozdział 2. ♦ Zapytania wybierające 69
SELECT IdDzialu,
COVAR_SAMP(RokUrodz, Wzrost) AS Kowariancja,
COVAR_POP(RokUrodz,Wzrost) AS KowariancjaPopulacji
FROM Osoby
GROUP BY IdDzialu;
gdzie:
FUNKCJA() — nazwa funkcji rankingowej.
Kolumna1 — kolumna (lub wyrażenie) grupująca rekordy ujęte w rankingu;
brak wyrażenia PARTITION BY oznacza, że rankingowi podlegają wszystkie
rekordy tabeli.
Kolumna2 — kolumna (lub wyrażenie) porządkująca rekordy wewnątrz grupy;
podanie tego parametru jest obowiązkowe.
DESC — ustala porządek rankingu w kierunku malejących wartości.
NULLS FIRST — powoduje, że wartości puste trafiają na początek rankingu.
NULLS LAST — powoduje, że wartości puste znajdują się na końcu rankingu
(stan domyślny).
Przykładem funkcji z tej rodziny jest funkcja RANK(), która oblicza miejsce w rankingu
każdego wiersza zwracanego przez zapytanie zgodnie z klauzulą ORDER BY; wiersze
kodowane są kolejnymi liczbami naturalnymi (tabela 2.35). Jeśli dwie pozycje mają taką
samą wartość, następna wartość funkcji rankingu jest zwiększana o liczbę powtórzeń
— pozycje równe mają w rankingu to samo miejsce.
SELECT IdDzialu, Nazwisko, Brutto,
RANK() OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC) Rank
FROM Osoby JOIN Zarobki USING(IdOsoby);
Ponieważ zarówno NULL > A ⇒ NULL, jak i NULL < A ⇒ NULL, konieczne jest zdefi-
niowanie, jakie miejsce będą miały pozycje o nieokreślonych wartościach. Sposób
umieszczenia wartości NULL w rankingu określa parametr o dwóch wartościach: NULLS
LAST — domyślnie, oraz NULLS FIRST.
SELECT IdDzialu, Nazwisko, Brutto,
RANK() OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC NULLS FIRST) Rank
FROM Osoby JOIN Zarobki USING(IdOsoby);
70 Część I ♦ Oracle SQL
Sposób określenia miejsca wartości NULL na liście jest taki sam jak w funkcji RANK().
Funkcja ta może być również używana z parametrem (tabela 2.38). W takim przypadku
zliczane są tylko niepowtarzające się wystąpienia wartości znajdującej się powyżej lub
poniżej wartości progowej, w zależności od zdefiniowanego w obrębie okna kierunku
sortowania.
SELECT DENSE_RANK(250) WITHIN GROUP
(ORDER BY Brutto DESC) "Powyżej 250"
FROM Zarobki;
72 Część I ♦ Oracle SQL
Jak widać, liczba rekordów plasujących się powyżej progu (tabela 2.38) jest wobec czę-
stego powtarzania się wartości znacznie mniejsza od liczby wyświetlonej przy użyciu
funkcji RANK(wartosc) (tabela 2.36). Kolejną funkcją z tej rodziny jest PERCENT_RANK(),
która oblicza miejsce w rankingu dla każdego wiersza zwracanego przez zapytanie
zgodnie z klauzulą ORDER BY i koduje go liczbami z przedziału (0, 1>. Sposób okre-
ślenia miejsca wartości NULL na liście jest taki sam jak w funkcjach poprzednio omó-
wionych (tabela 2.39). Również i ta funkcja może zostać użyta z parametrem, ale spo-
sób jej działania nie różni się istotnie od funkcji RANK() — zamiast liczby rekordów
powyżej czy poniżej progu obliczany jest ich udział w liczbie wszystkich rekordów
zawartych w oknie.
Funkcja CUME_DIST() oblicza względną pozycję bieżącego rekordu w obrębie ich grupy
i zwraca wartość z przedziału (0, 1> (tabela 2.40).
Tabela 2.41. Skutek działania funkcji analitycznej CUME_DIST() oraz jej reprezentacja analityczna
IdDzialu Nazwisko Brutto Ułamkowy rozkład Obliczone
1 Polakow 999 ,285714286 ,285714286
1 Kowalski 999 ,285714286 ,285714286
1 Kowalski 666 ,428571429 ,428571429
1 Kowalski 555 ,571428571 ,571428571
1 Kowalski 333 ,714285714 ,714285714
1 Kowalski 111 1 1
1 Kowalski 111 1 1
2 Nowak 888 ,222222222 ,222222222
Różnice wartości rankingu ułamkowego dla obu sposobów jego tworzenia przedstawia
rysunek 2.4.
0.6
CUME_DIST()
PERCENT_RANK()
0.4
0.2
0.0
0 200 400 600 800 1000
Sposób określania miejsca wartości NULL na liście jest taki sam jak w funkcji RANK().
Funkcja CUME_DIST() może być również używana z parametrem (tabela 2.42).
SELECT CUME_DIST(250) WITHIN GROUP
(ORDER BY Brutto DESC) "Jaki ułamek powyżej 250"
FROM Zarobki;
Rozdział 2. ♦ Zapytania wybierające 75
Tabela 2.43. Rezultat wykonania zapytania wyświetlającego skutek działania funkcji agregującej
w obrębie grupy (partycji)
IdDzialu Nazwisko Brutto Numer Zakresowy rozkład
1 Kowalski 999 1 1
1 Kowalski 666 2 1
1 Kowalski 555 3 2
1 Kowalski 333 4 3
1 Kowalski 111 5 4
2 Zieliński 999 1 1
2 Janik 999 2 1
2 Nowak 888 3 2
2 Nowak 666 4 2
2 Janik 555 5 3
2 Nowak 444 6 3
2 Janik 222 7 4
3 Kow 888 1 1
3 Kowalczyk 777 2 1
3 Kowalczyk 777 3 2
3 Kowalczyk 666 4 2
3 Kow 333 5 3
3 Kow 222 6 3
3 Pawlak 111 7 4
4 Nowicki 888 1 1
4 Nowicki 666 2 2
4 Nowicki 333 3 3
4 Nowicki 111 4 4
5 Kowal 777 1 1
5 Kowalski 555 2 2
5 Kowal 444 3 3
5 Kowalski 111 4 4
Jeśli CRN = FRN, wówczas wynikiem jest wartość znajdująca się w wierszu RN; w prze-
ciwnym przypadku jest nim wartość wyrażenia: (CRN – RN)*(wartość z wiersza CRN)+
(RN–FRN)*(wartość z wiersza FRN)
gdzie:
FUNKCJA() — nazwa funkcji raportującej,
Rozdział 2. ♦ Zapytania wybierające 77
Dla parametru 0.5 w przypadku grup rekordów zawierających nieparzystą liczbę ele-
mentów funkcja wskazuje element środkowy, natomiast dla parzystej liczby argumentów
wskazywana jest średnia wartości środkowych, analogicznie jak przy wyznaczaniu
wartości modalnej. Dla parametrów różnych od 0.5 wybrany zostanie albo element
o randze procentowej wskazany tym ułamkiem, albo odpowiednia średnia ważona z naj-
bliższych „sąsiadów”, kiedy ranga nie wskazuje dokładnie żadnego elementu.
gdzie:
wyrażenie1 — rodzaj funkcji agregującej lub określenie kolumny
jako argument funkcji WIDTH_BUCKET;
DGranica — określa dolny zakres przedziału;
GGranica — określa górny zakres przedziału;
LPrzedz — liczba przedziałów, które chcemy otrzymać w wyniku.
Jeszcze bardziej złożony jest przypadek, kiedy dynamicznie chcemy określić górne i dolne
ograniczenie dla wyrażenia czy funkcji agregującej. W takim przypadku konieczne
jest zastosowanie dwupiętrowego podzapytania. Najpierw wyznaczana jest wtedy war-
tość tej funkcji na odpowiednim poziomie grupowania, a dopiero w zapytaniu nadrzęd-
Rozdział 2. ♦ Zapytania wybierające 81
nym, za pomocą funkcji MIN i MAX, określane są granice zmienności tego wyrażenia
(tabela 2.49). Komentarz dotyczący konieczności nieznacznej zmiany trzeciego para-
metru pozostaje bez zmian.
∑ ⎜ x − n ∑ x ⎟⎜ y
i =1 ⎝
i i i
− ∑ yi ⎟
n i=1 ⎠
n∑ xi yi − ∑ xi ∑ yi
a=
i =1 ⎠⎝ a = i=1 i =1 i =1
2 2
n
⎛ 1 n
⎞ n
⎛ n
⎞
∑ ⎜
i =1 ⎝
x i − ∑ i
n i=1 ⎠
x ⎟ n ∑i =1
xi
2
− ⎜ ∑ i
⎝ i=1 ⎠
x ⎟
1⎛ n n
⎞
b= ⎜ ∑ yi − a ∑ xi ⎟
n ⎝ i=1 i =1 ⎠
1980
1976
1972
1968
1964
1.6 1.7 1.8 1.9 2
Pomimo dużego rozrzutu i słabej korelacji wartości wybranych pól, dość łatwo zauwa-
żyć tendencję wyznaczaną przez narysowaną na podstawie obliczonych współczynni-
ków prostą. Podobny efekt otrzymamy, zmieniając kolejność parametrów w funkcjach
REGR_SLOPE oraz REGR_INTERCEPT (tabela 2.52, rysunek 2.6). Tym razem wartości pola
RokUrodz odpowiadają wartościom osi X.
Rysunek 2.6. 2
Przykład wizualizacji
regresji liniowej
1.9
1.8
1.7
1.6
1964 1968 1972 1976 1980 1984 1988
Przydatne w analizie mogą być również dodatkowe funkcje pozwalające lepiej poznać
badaną zmienność funkcji, np.:
REGR_COUNT — liczba wartości uwzględnianych w wyznaczaniu regresji liniowej.
REGR_AVGX — średnia wartość pola będącego odpowiednikiem wartości X badanej za-
leżności.
REGR_AVGY — średnia wartość pola będącego odpowiednikiem wartości Y badanej za-
leżności.
REGR_R2 — wyznacza tzw. współczynnik determinacji nazywany czasem współczynnikiem
dopasowania. Może on przyjąć wartości z przedziału <0, 1> i w ogólnym przypadku
jest kwadratem współczynnika korelacji — REGR_R2 = POWER(CORR(expr1,expr2),2)
(tabela 2.53). Jeśli jednak wariancja populacji pierwszego parametru jest zerowa, REGR_R2
jest z definicji równe 0 (aby uniknąć nieoznaczoności), natomiast wartość 1 przyjmuje,
kiedy zerowa jest wariancja populacji drugiego parametru.
Ponieważ zmienne, dla których wyliczano regresję, nie były dobrze skorelowane, to
i parametry określające jej jakość nie są specjalnie małe. Dużo większa wartość
Regr_Sxx niż Regr_Syy wynika stąd, że wartości kolumny RokUrodz są praktycznie
o dwa rzędy większe niż odpowiadające im wartości w kolumnie Wzrost. Lepsza ocena
wyników wymagałaby dokonania standaryzacji do podobnego zakresu zmienności, np.
wyrażenia wzrostu w centymetrach lub sprowadzenia zmienności do zakresu 0 – 1
dzięki zastosowaniu funkcji rankingowej PERCENT_RANK w odniesieniu do obu kolumn.
Ten drugi sposób standaryzacji powinien skutkować bardziej miarodajnymi wynikami.
∑ (x − x )⋅ (y − y )
n
i i
C( X ,Y )
rxy = i =1
=
∑ (x − x ) ⋅ ∑ (y )
n 2 n 2 sx s y
i i
−y
i =1 i =1
gdzie:
Tabela 2.57. Skutek wykonania zapytania wyświetlającego współczynnik korelacji pola RokUrodz
względem pola Wzrost; dla porównania wyświetlono współczynnik korelacji wyznaczony w oparciu
o definicję, kowariancję oraz odchylenia standardowe obu pól
Korelacja Oblicz Kowariancja Stdev_W Stdev_R
,357077751 ,373896914 ,211734694 ,085075715 6,65632531
n
6∑ d i2
rs = 1 − i =1
n(n 2 − 1)
gdzie:
d i — odległość (j, l = 1, …, m) między rangami cech j oraz l w i-tej parze elementów.
Prezentowany wzór jest poprawny tylko w przypadku, gdy nie ma rang związanych,
czyli nie istnieją wartości powtarzające się. Dla ogólnego przypadku konieczne jest
stosowanie wyrażenia o postaci:
E (rank ( X ) ⋅ rank (Y )) − E (rank ( X )) ⋅ E (rank (Y ))
rs =
E (rank 2 ( X )) − E 2 (rank ( X )) ⋅ E (rank 2 (Y )) − E 2 (rank (Y ))
gdzie:
E oznacza średnią arytmetyczną;
Rank() jest funkcją rankingową (z reguły stosowany jest tzw. ranking gęsty
uwzględniający pozycje ex aequo, który stanowi średnia arytmetyczna
ze wszystkich pozycji zajmowanych przez równe pary).
Jak widać, współczynnik korelacji Spearmana jest jeszcze niższy niż współczynnik kore-
lacji Pearsona, co potwierdza przypuszczenie o słabym związku między analizowa-
nymi zmiennymi.
SELECT CORR_K(Wzrost, RokUrodz) as Korelacja_K,
CORR_K(Wzrost, RokUrodz,'ONE_SIDED_SIG') AS
ONE_SIDED_SIG,
CORR_K(Wzrost, RokUrodz,'TWO_SIDED_SIG') AS
TWO_SIDED_SIG
FROM Osoby;
Rozdział 2. ♦ Zapytania wybierające 87
Tabela 2.59. Skutek wykonania zapytania wyświetlającego współczynnik korelacji tau Kendalla pola
RokUrodz względem pola Wzrost oraz jednostronna i dwustronna istotność korelacji
Korelacja_K ONE_SIDED_SIG TWO_SIDED_SIG
,224965918 ,046475016 ,092950033
Dla tak przyjętych oznaczeń współczynnik tau Kendalla możemy wyznaczyć jako:
P−Q
rK =
P +Q +T
ponieważ jeżeli przez n oznaczymy liczebność próby, to
⎛ n ⎞ n(n − 1)
P + Q + T = ⎜⎜ ⎟⎟ =
⎝ 2⎠ 2
CLEAR BREAKS
CLEAR COMPUTES
CLEAR COLUMNS
zapisana przy pomocy czterech cyfr. Po przecinku muszą być wyświetlone dwie cyfry,
a jeśli ułamek jest krótszy, dodawane są zera; jeśli jest dłuższy, zaokrąglany jest zgodnie
z zasadami ogólnymi. Przed przecinkiem obowiązkowo powinna znajdować się jedna
cyfra). Możliwe jest również nadanie kolumnie nazwy zastępującej zarówno nazwę pola,
jak i alias dla niego lub wyrażenia; występujący w napisie znak | (more) powoduje, że
dalszy ciąg nazwy pola wyświetlany jest w kolejnej linii. Wyrównanie JUSTIFY jest
ostatnim elementem definicji kolumny. Nie muszą w niej występować wszystkie ele-
menty, ale dowolna ich kombinacja. W praktyce najczęściej wykorzystujemy forma-
towanie, ponieważ domyślne formatowanie liczb daje zazwyczaj zbyt dużo cyfr po
przecinku, natomiast przy wyprowadzaniu pól o zdefiniowanej bardzo dużej szerokości
często mamy kłopot z obejrzeniem wyniku. P, natomiast przy polach o bardzo krótkich
zmiennych znakowych Oracle potrafi z kolei „przycinać” nazwę pola lub aliasu. Do-
datkowymi elementami są możliwości ustawienia nagłówka zapytania, raportu TITLE
(również wielolinijkowego) oraz jego stopki BTITLE. Jeżeli nie wyczyścimy definicji
formatowania, będą one wpływały na sposób wyświetlania kolejnych zapytań. Moż-
liwe jest wyczyszczenie definicji łamania BREAKS, kolumn COLUMNS i podsumowań
COMPUTES poleceniem CLEAR. Wyczyszczenie tytułu i stopki następuje tylko przez podanie
pustego ciągu znaków.
Obowiązuje tutaj zgodność pozycyjna — pola w łączonych tabelach mogą mieć różne
nazwy, ale na odpowiadających sobie pozycjach powinny mieć takie same typy lub
typy, do których możliwa jest automatyczna konwersja. Liczba kolumn w obu zapy-
taniach musi być taka sama. Często, jeśli łączymy tabele o różnej liczbie kolumn, do
zapytania wyświetlającego zawartość tabeli o mniejszej liczbie pól dodajemy stałe:
albo wartość NULL, albo (dla liczb) 0, albo (dla pól znakowych) pusty ciąg znaków ''.
Jeśli chcemy, aby powtarzające się rekordy nie były eliminowane, zmieniamy operator
na UNION ALL i otrzymujemy dokładne połączenie rekordów obu tabel (Osoby∪ttt).
SELECT Nazwisko, Imie FROM Osoby
UNION ALL
SELECT Nazwisko, Imie FROM ttt;
Tracimy jednak w ten sposób porządkowanie rekordów, które tym razem wyświetlane
są w kolejności łączonych tabel — najpierw wszystkie z pierwszego zapytania, a póź-
niej z drugiego.
90 Część I ♦ Oracle SQL
Przy pomocy operatora UNION (w obu klonach) możliwe jest łączenie rekordów po-
chodzących z wielu tabel. Jeśli nazwy pól na odpowiednich pozycjach nie są zgodne,
to o nazwie pól wynikowych decydują nazwy pól lub aliasy pierwszego z zapytań.
Ponieważ wyznaczanie różnicy zbiorów nie jest operacją przemienną, inny rezultat
otrzymamy, zmieniając kolejność zapytań połączonych operatorem MINUS; tym razem
jest to ttt–Osoby.
SELECT Nazwisko, Imie FROM ttt
MINUS
SELECT Nazwisko, Imie FROM Osoby;
Operator MINUS musi łączyć dokładnie dwa zapytania. Ich część wspólną (iloczyn) wy-
znaczamy, stosując operator INTERSECT — w naszym przykładzie Osoby∩ttt.
SELECT Nazwisko, Imie FROM Osoby
INTERSECT
SELECT Nazwisko, Imie FROM ttt;
Podobnie jak poprzednio, operator INTERSECT musi łączyć dokładnie dwa zapytania.
Ponieważ operacja jest przemienna, ich kolejność nie ma wpływu na wynik. Wyjąt-
kiem mogą być nazwy kolumn w wyniku, kiedy w zapytaniach składowych się one
różnią — tak jak w przypadku operatorów UNION i MINUS, o nazwach wynikowych de-
cydują nazwy stosowane w pierwszym zapytaniu.
W starszych realizacjach Oracle jedyną końcówką klienta był SQL Plus, który oferował
tylko edytor liniowy. Pomimo swojej dość archaicznej postaci jest on stosowany do
dzisiaj, głównie dla zadań administracyjnych — zwłaszcza związanych z uruchamia-
niem, odzyskiwaniem lub konfigurowaniem instancji. Jest niezastąpionym narzędziem
dla wszystkich operacji wykonywanych w stanie NOMOUNT, najbardziej podstawowym
dla instancji, jednak i z poziomu SQL oferuje ciekawe, niedostępne w innych narzę-
dziach funkcjonalności. Jedną z nich jest zapytanie z parametrem, gdzie dowolny
napis rozpoczynający się od znaku & jest traktowany jako parametr. Konieczne jest poda-
nie jego wartości przy każdym wywołaniu tak zdefiniowanego zapytania.
SELECT Nazwisko, Wzrost FROM Osoby
WHERE Wzrost > &minimum;
Rozdział 2. ♦ Zapytania wybierające 91
Jeśli napis poprzedzimy podwójnym znakiem &&, to żądanie podania wartości pojawi
się tylko przy pierwszym jego wywołaniu w obrębie sesji. W kolejnych wywołaniach
przyjmowana będzie podstawiona wtedy wartość. Dopiero zamknięcie sesji i otwarcie
kolejnej przywróci stan poprzedni.
SELECT Nazwisko, Wzrost FROM Osoby
WHERE Wzrost > &&minimum;
Jak widać, w przypadku odwoływania definicji parametru korzystamy tylko z jego nazwy,
bez poprzedzających ją znaków & lub &&. Przejście do poprzedniej postaci wywołania
(pojedynczy znak & i taka sama nazwa parametru) &minimum nie zmienia jednak spo-
sobu jego działania. W SQL Worksheet, który był „naturalną” końcówką klienta przez
trzy wersje Oracle, funkcjonalność ta nie występowała — dopiero wprowadzenie SQL
Developera przywróciło możliwość korzystania z zapytań z parametrem. Rozważmy teraz
zapytanie wybierające, w którym poprzez parametr przekazywana jest nazwa tabeli.
SELECT * FROM &tabela;
Rysunek 2.7.
Przetwarzanie
zapytania
z parametrem
w środowisku SQL
Developera
92 Część I ♦ Oracle SQL
Istotnym faktem jest to, że wyszukiwanie symbolu & w celu wykrycia parametru od-
bywa się również w łańcuchach znakowych. Ilustruje to zastosowanie parametru w ope-
ratorze LIKE.
SELECT * FROM Osoby WHERE UPPER(Nazwisko) LIKE '&kto';
Parametr zdefiniowany według tych reguł może zostać użyty w każdym typie zapytania,
np. w zapytaniu wstawiającym wiersze INSERT. Przeszukiwanie pod kątem występowania
znaku & w całym zapytaniu może być jednak problematyczne, np. wtedy, kiedy chcemy
wpisać nazwę firmy 'Kowalski & s-ka'. W takim przypadku nie pozostaje nam nic
innego, jak podać znak & w postaci jego kodu ASCII — 'Kowalski ' || chr(38) ||
' s-ka'. Na szczęście znak ten nie jest w języku polskim szczególnie często używany.
Pełną funkcjonalność parametrów w SQL możemy uzyskać, stosując dodatkowo po-
lecenia DEFINE i UNDEFINE. Przeanalizujmy to w oparciu o kolejny przykład.
DEFINE mini = &pierwszy;
SELECT * FROM Osoby WHERE Wzrost > &&mini ;
UNDEFINE mini;
Polecenie DEFINE pozwala na przypisanie parametrowi wartości. Może być nią stała,
ale również inny parametr, w naszym przypadku o nazwie pierwszy. W takiej sytuacji
pojawia się okno typu INPUT BOX związane z podaniem wartości dla tego parametru,
ale nie pojawi się takie okno dla parametru mini, ponieważ został on już zainicjowany
programistycznie. Dzieje się tak zarówno w przypadku parametru typu &, jak i &&. Ini-
cjalizacja jednego parametru przy pomocy innego ma sens wtedy, kiedy piszemy zło-
żony skrypt, w którym parametr o tej samej nazwie występuje wielokrotnie. Inicja-
lizacja z użyciem parametru na początku skryptu spowoduje, że pytanie o wartość
pojawi się tylko raz, w miejscu inicjalizacji. Ostatnia linia przykładowego skryptu
powoduje usunięcie wartości przypisanej do parametru. Polecenie UNDEFINE, które
z reguły występuje na końcu skryptu, może zostać użyte również w innej jego części.
Pamiętać jednak należy, że wtedy parametr będzie wymagał ponownej inicjalizacji, czy
to poleceniem DEFINE, czy też bezpośrednio z poziomu okna INPUT BOX.
Ciekawą cechą Oracle jest możliwość zapisywania skutku wykonania zapytania wy-
bierającego — lub skryptu zawierającego takie zapytania — do pliku tekstowego
(ASCII). Zapewnia to wykonanie polecenia SPOOL, którego parametrem jest nazwa
pliku (z jego ścieżką). Jeśli nie podamy jej rozszerzenia, domyślnym będzie *.lst.
SET HEADING OFF
SET PAGESIZE 0
SET FEEDBACK OFF
SET TRIMSPOOL ON
SPOOL file_name.txt
<SQL statement>
SPOOL OFF
Zakończenie zapisywania do pliku następuje dzięki poleceniu SPOOL OFF (tabela 2.60).
Również zamknięcie sesji kończy ten proces. Dla zapytań zwracających tabelę, w której
długość rekordu jest większa niż 80 znaków, należy ustawić odpowiednio dużą wartość
LINESIZE — liczbę znaków w linii. W innym wypadku Oracle będzie zawijać linie po
każdych 80 znakach (domyślne ustawienie LINESIZE), co powoduje, że plik staje się
trudny do analizy lub odczytania przez inne aplikacje. Ogólna postać polecenia SPOOL
jest następująca:
Rozdział 2. ♦ Zapytania wybierające 93
SPO[OL] [file_name[.ext]
[CRE[ATE] | REP[LACE] | APP[END]] | OFF];
Dla polecenia SPOOL, ale również do określenia sposobu wyświetlania wyników za-
pytania na standardowym urządzeniu wyjściowym, przydatne może być ustawienie
flag, z czego skorzystano w prezentowanym poprzednio kodzie:
SET ECHO [ ON | OFF ] — powoduje wyświetlanie lub niewyświetlanie kodu
wykonywanych zapytań — nie dla każdego sposobu wywoływania działa
poprawnie.
SET FEEDBACK [ ON | OFF ] — powoduje wyświetlanie lub niewyświetlanie
informacji o liczbie przetworzonych wierszy.
SET HEADING [ ON | OFF ] — powoduje wyświetlanie lub niewyświetlanie
nagłówków kolumn wykonywanego zapytania.
SET PAGESIZE liczba — ustala długość strony w liniach; wartość 0 skutkuje
brakiem łamania strony.
SET TRIMSPOOL [ ON | OFF ] — powoduje przycinanie lub nieprzycinanie
końcowych spacji w linii oraz pustych linii na końcu pliku.
Przeciwnie, jeśli w pliku tekstowym mamy zapisane zapytanie lub skrypt SQL, to
uruchomienie go możliwe jest przy użyciu polecenia START. Domyślnym rozszerzeniem
pliku jest *.sql.
START C:\temp\wyk.txt;
Dokładnie tak samo uruchomienie skryptu zawartego w pliku możemy uzyskać, wpi-
sując zamiast słowa kluczowego START znak @ (at).
@C:\temp\wyk.txt;
94 Część I ♦ Oracle SQL
Podstawowe zapytanie, które wyświetli relacje podległości w firmie, opiera się na ope-
ratorze CONNECT BY, w którym, stosując operator PRIOR, wskazujemy kierunek do po-
przedniego rekordu oraz pola realizujące samozłączenie.
SELECT LEVEL, Idosoby, Nazwisko, IdSzefa
FROM Osoby CONNECT BY PRIOR IdOsoby = IdSzefa;
Zamiana kolejności pól w klauzuli CONNECT BY skutkuje wskazaniem jako pola niższego
w hierarchii identyfikatora szefa. Powoduje to, że drzewo wyświetlane jest w kierunku
do korzenia (zawsze jedna gałąź). Przy zastosowanym warunku startowym IdSzefa IS
NULL zwrócony zostanie tylko jeden rekord.
SELECT LEVEL, IdOsoby, Nazwisko, IdSzefa
FROM Osoby CONNECT BY PRIOR IdSzefa = IdOsoby
START WITH IdSzefa IS NULL ORDER BY LEVEL;
Tabela 2.61. Skutek wykonania zapytania wyświetlającego sumę zarobków dla każdego poziomu
w hierarchii
LEVEL SUM(Brutto)
1 2664
2 17205
4 129870
3 91575
Wstawianie danych wykonywane jest za pomocą polecenia INSERT, które może zostać
zapisane w dwóch podstawowych wariantach. W pierwszym przypadku, kiedy poda-
jemy wartości pól, po nazwie tabeli występuje słowo kluczowe VALUES, a po nim lista
wartości separowanych przecinkami. Wstawianie wartości do odpowiednich kolumn
odbywa się zgodnie z kolejnością tych kolumn w definicji tabeli. W tym przypadku
musimy podać wartości dla każdej z nich.
INSERT INTO Nowa
VALUES ('KOWAL', 'JAN', 1966);
Podobnie jak w poprzedniej grupie, przy stosowaniu zapytania wybierającego jako źró-
dła danych dla tabeli możemy ograniczyć liczbę zasilanych nimi kolumn dzięki użyciu
listy wybranych pól tabeli docelowej występującej po jej nazwie.
INSERT INTO Nowa(Imie, Nazwisko)
SELECT Imie, Nazwisko FROM Osoby
WHERE RokUrodz <1960
ORDER BY Nazwisko DESC;
Ponieważ, jak to już powiedziano wcześniej, w obu wariantach składni mamy możliwość
zasilania danymi tylko wybranych kolumn, to aby przetwarzanie zakończyło się suk-
cesem, pomijane podczas zasilania kolumny muszą spełniać jeden z trzech postulatów:
Muszą mieć możliwość przyjmowania wartości NULL — warunek najczęściej
stawiany w praktyce.
Jeśli nie mają takiej możliwości, muszą mieć ustaloną wartość domyślną.
Muszą być automatycznie inkrementowane — co stanowi odpowiednik
wartości domyślnej, ale definiowany programistycznie.
Jeśli żaden z powyższych postulatów nie zostanie spełniony, próba wstawienia nowych
danych zakończy się błędem przetwarzania.
Możliwa jest modyfikacja w jednym zapytaniu zawartości więcej niż jednego pola.
W takim wypadku po słowie kluczowym SET pojawia się lista wyrażeń modyfikujących
rozdzielonych przecinkami. Wykonanie przedstawionego zapytania spowoduje prze-
pisanie zawartości kolumn Imie i Nazwisko, tak że będą one zapisane tylko przy użyciu
dużych liter.
UPDATE Nowa
SET Nazwisko = UPPER(Nazwisko),
Imie = UPPER(Imie);
Usuwanie danych znajdujących się w tabeli dokonywane jest poprzez wykonanie po-
lecenia DELETE. Zwykle w zapytaniu takim stosowana jest klauzula WHERE, ogranicza-
jąca zakres usuwanych wierszy do tych, dla których wyrażenie w niej umieszczone daje
wartość TRUE (prawda). W przedstawionym przykładzie usuwane są rekordy, dla któ-
rych nie zdefiniowano wartości pola RokUrodz.
DELETE FROM Nowa
WHERE RokUrodz IS NULL;
Wykonanie polecenia DELETE polega na tym, że dla każdego rekordu sprawdzana jest,
o ile istnieje, klauzula WHERE, a następnie wiersz, dla którego zawarte w niej wyrażenie
jest prawdziwe, jest usuwany. Można powiedzieć, że usuwanie następuje wiersz po
wierszu, co w przypadku bardzo dużych tabel może być czasochłonne. Szybsze jest
wykonanie polecenia TRUNCATE — usuwanie odbywa się w nim poprzez kasowanie
wskaźnika do pierwszego wiersza. Zysk będzie tym większy, im większej tabeli to
dotyczy.
TRUNCATE TABLE Nowa;
Tabela 3.1. Parametry polecenia TRUNCATE określające jego oddziaływanie na przydzielone tabeli
zasoby dyskowe
DROP STORAGE Zwalnia wszystkie przydzielone tabeli ekstenty powyżej wartości MINEXTENTS.
Znacznik wysokiej wody jest kasowany i ustawiany na pierwszy blok tabeli;
jest to klauzula domyślna.
REUSE STORAGE Usuwa dane, natomiast przestrzeń zarezerwowana dla tabeli pozostaje bez zmian.
Również znacznik wysokiej wody nie zmienia swojego położenia.
Wyjaśnienia wymagają dwa wprowadzone przy okazji pojęcia. Ekstent jest najmniej-
szym elementem logicznym, o który można zwiększyć lub zmniejszyć przydział miej-
sca na dysku — rysunek 1.3. Znacznik wysokiej wody pokazuje natomiast najwyższy
poziom zajętości przestrzeni przez dane wprowadzone do tabeli. Jeśli jest on ustawiony
na poziomie jej ostatniego wiersza, to dodanie nowych rekordów powoduje jego pod-
niesienie. Jeśli jednak usuwamy rekordy, wskaźnik ten nie zmienia swojej pozycji. Ko-
lejne wstawiane wiersze zaczną go podnosić dopiero wtedy, gdy liczba nowo wprowa-
dzonych rekordów przekroczy liczbę usuniętych.
Rozdział 4.
Zapytania
tworzące tabele
Do tej pory zakładaliśmy, że na poziomie schematu użytkownika dana jest już struk-
tura relacyjna. Jest oczywistym, że, posiadając odpowiednie uprawnienia, tabele może
tworzyć również użytkownik. Podstawowe składniowe ograniczenia związane z ich ge-
nerowaniem są następujące:
długość nazwy pola tabeli nie może przekraczać 30 znaków;
liczba kolumn w ramach jednej tabeli nie może przekroczyć 1000.
Tworzenie tabeli dokonuje się za pomocą polecenia CREATE TABLE, po którym nastę-
puje jej nazwa. Następnie występuje definicja struktury tabeli ujęta w nawiasy zwy-
czajne. Na definicję tę składa się lista definicji pól tej tabeli separowanych nawiasami.
Minimalną definicję pola tworzy jego nazwa oraz typ. Przykładem jest polecenie two-
rzące tabelę o trzech polach: dwóch znakowych, Nazwisko i Imie, oraz jednym nume-
rycznym, całkowitoliczbowym RokUrodz.
CREATE TABLE Nowa
(Nazwisko varchar2(15),
Imie varchar2(15),
RokUrodz number(4));
Do tak zdefiniowanej tabeli można wpisać dowolne dane; jedynym ograniczeniem jest
ich typ i długość pola. Dokładnie rzecz biorąc, ograniczenie typu znakowego sprowadza
się tylko do liczby znaków, ponieważ może on zawierać w sobie dane innych typów.
W tabeli można również zdefiniować dodatkowe ograniczenia, tzw. więzy integralności.
Integralność danych może zostać wymuszona przy pomocy:
więzów integralności (więzy statyczne jawne) definiowanych na poziomie
tworzenia tabeli, którym poświęcony jest ten rozdział;
wyzwalaczy (więzy dynamiczne niejawne) omówionych w rozdziale
poświęconym programowaniu z wykorzystaniem języka PL/SQL.
104 Część I ♦ Oracle SQL
Tabela może posiadać co najwyżej jeden klucz podstawowy; nie jest możliwe stwo-
rzenie dwóch lub większej ich liczby w obrębie jednej tabeli. Jednakże da się stworzyć
klucz podstawowy oparty na więcej niż jednym jej polu, czyli złożony klucz główny.
Oznacza to, że para (lub więcej) pól tworzących ten klucz musi być w obrębie tabeli uni-
kalna oraz że żadne z tych pól nie może przyjąć wartości NULL. W przypadku gdy ograni-
czenie dotyczy więcej niż jednego pola, jego definicja nie może pojawić się przy defi-
nicji żadnego spośród pól, ale musi zostać podana w obrębie klauzuli CONSTRAINT.
Próba wykonania polecenia tworzącego tabelę w przypadku istnienia tabeli o tej samej
nazwie zakończy się niepowodzeniem z jednoczesnym wyświetleniem komunikatu
o błędzie. Konieczne jest wtedy usunięcie istniejącego obiektu poleceniem DROP TABLE.
Należy zauważyć, że wraz z usunięciem logicznej definicji tabeli kasowana jest rów-
nież jej postać fizyczna, czyli wszystkie zawarte w niej dane.
DROP TABLE Nowa;
CREATE TABLE Nowa
(Nazwisko varchar2(15),
Imie varchar2(15),
CONSTRAINT klucz PRIMARY KEY (Nazwisko, Imie)
);
Należy zwrócić uwagę na to, że w klauzuli CONSTRAINT podawana jest nazwa ograni-
czenia, która musi być unikalna w skali schematu użytkownika (można to traktować
jako unikalność w skali bazy danych). W przypadku tworzenia ograniczenia jako ele-
mentu definicji pola, tak jak to było w poprzednim przykładzie, jego unikalna nazwa
jest generowana przez system. Oracle dopuszcza pominięcie nazwy ograniczenia w przy-
padku definiowania go w klauzuli CONSTRAINT; wtedy również system nadaje nazwę
unikalną. Kolejnym elementem tej klauzuli jest nazwa ograniczenia, w analizowanym
Rozdział 4. ♦ Zapytania tworzące tabele 105
przypadku klucza głównego — PRIMARY KEY, po którym ujęta w nawias występuje se-
parowana przecinkami lista pól ten klucz stanowiących. Możliwe jest, aby lista była
ograniczona do jednego elementu — w takim przypadku ograniczenie to jest równo-
ważne temu, które jest tworzone przy definicji pola. Jedyną różnicę stanowi możliwość
nadania mu nazwy. Definicja ograniczenia z zastosowaniem klauzuli CONSTRAINT może
wystąpić w dowolnym miejscu tabeli, jednakże nie wcześniej niż definicje podstawowe
(nazwa i typ) wszystkich znajdujących się w niej kolumn, co zostanie pokazane póź-
niej. Pomimo to zwyczajowo takie definicje ograniczeń umieszcza się na końcu defi-
nicji tabeli.
Oprócz ograniczenia unikalności i klucza głównego, do pola można przypisać też ogra-
niczenie związane z możliwością przyjmowania przez nie wartości nieokreślonej. Ogra-
niczenie NULL, które jest stanem domyślnym definicji kolumny i w związku z tym z re-
guły jest pomijane, pozwala na przyjmowanie przez wartości pola stanu nieokreślonego.
Przeciwstawne ograniczenie NOT NULL zabrania natomiast przyjmowania tej wartości. Oba
te ograniczenia muszą być definiowane bezpośrednio przy definicji kolumny; nie jest
możliwe zastosowanie ich w obrębie klauzuli CONSTRAINT. Podobnie w obszarze tej klau-
zuli nie można stosować ograniczenia definiującego wartość domyślną DEFAULT. Wy-
stępujące w definicji pola ograniczenie DEFAULT powoduje, że w momencie wstawiania
rekordu poleceniem INSERT i przypisywania temu polu wartości NULL (bez względu na to,
czy jawnie, czy też na skutek pominięcia tego pola na liście pól uzupełnianych) za-
miast wartości niezdefiniowanej jest tam wstawiane wyrażenie występujące po słowie
kluczowym DEFAULT. Wartość domyślna nie jest wykorzystywana w przypadku aktu-
alizacji zawartości pola poleceniem UPDATE, w związku z czym możliwe jest przypisa-
nie do niego w ten sposób wartości nieokreślonej, o ile nie występuje ograniczenie
NOT NULL. Kolejnym z ograniczeń jest sprawdzające zgodność wprowadzanej do pola
wartości ograniczenie CHECK. Wyrażenie to, występując przy kolumnie, pozwala użyć
do zdefiniowania wyrażenia sprawdzającego (umieszczonego w nawiasie po nazwie
ograniczenia) nazwy sprawdzanego pola, stałej oraz określonych w środowisku ope-
ratorów. W przedstawionym przykładzie tworzona jest tabela o nazwie Zlec, w której
pole nr jest kluczem głównym, pole Nazwisko nie może mieć wartości NULL, dla pola
Imie przypisano wartość domyślną 'Brak', natomiast wartości pola m_v nie mogą być
mniejsze od 10.
DROP TABLE Zlec;
CREATE TABLE Zlec
(nr number(3) PRIMARY KEY,
106 Część I ♦ Oracle SQL
Możemy przyjąć, że w definicji tabeli Zlec potrzebujemy stworzyć dwa pola stanowiące
dolne i górne ograniczenie jakiejś cechy. Załóżmy, że polami tymi są m_v (ogranicze-
nie dolne) oraz mm_v (ograniczenie górne), które określają minimalny i maksymalny
koszt wykonania zlecenia (miarą może być zarówno czas, jak i szacunkowy koszt wy-
rażony w jednostkach monetarnych). Jeśli zachowamy poprzednie ograniczenie nało-
żone na pole m_v, to dla pola mm_v możemy zastosować ograniczenie niepozwalające mu
przekroczyć wartości 300. Oba ograniczenia zostały podane przy definicji kolumny.
DROP TABLE Zlec;
CREATE TABLE Zlec
(nr number(3) PRIMARY KEY,
Nazwisko varchar2(15) NOT NULL,
Imie varchar2(15) DEFAULT 'Brak',
m_v number(3) CHECK (m_v > 10) ,
mm_v number(3) CHECK (mm_v < 300)
);
Rolę bardzo ważną z punktu widzenia teorii relacyjnego modelu danych, utrzymania
spójności danych w połączonych tabelach oraz dla codziennej praktyki tworzenia baz
danych spełnia ograniczenie klucza obcego. Rozważmy tabelę Osoby przechowującą
informacje o pracownikach. Nie dając żadnych ograniczeń, w pole IdDzialu możemy
wpisać dowolną liczbę całkowitą, czyli przypisać pracownika do nieistniejącego działu.
Możliwe jest również wpisanie w to pole wartości NULL, co pociągnie za sobą fakt, że
taki pracownik nie będzie przypisany do żadnego z działów. Jeśli chcemy zapobiec pierw-
szemu z tych przypadków, możemy zastosować ograniczenie FOREIGN KEY, w którym
wymieniamy pole tworzonej tabeli oraz, po słowie kluczowym REFERENCES, nazwę ta-
beli nadrzędnej i pole w niej występujące, z którym wartości wprowadzane do pola
tworzonej tabeli będą porównywane. Tabela ta musi już istnieć w bazie danych. W na-
szym przypadku chcemy zapewnić, aby w pole IdDzialu tabeli Osoby można było wpi-
sać numery działów, które występują w polu IdDzialu tabeli Dzialy, czyli aby pra-
Rozdział 4. ♦ Zapytania tworzące tabele 107
cownik mógł zostać przypisany tylko do istniejącego działu. Ponieważ podczas kon-
struowania tego ograniczenia musimy odwołać się do pola innego niż definiowane,
konieczne jest użycie klauzuli CONSTRAINT. Widzimy również, że definicja ogranicze-
nia została wprowadzona bezpośrednio po definicji pola, którego dotyczy. Nie możemy
jej umieścić wcześniej, ale możliwe jest przesunięcie jej w dowolne miejsce poniżej
tutaj przedstawionego.
DROP TABLE Osoby;
CREATE TABLE Osoby
(nr number(3) PRIMARY KEY,
IdDzialu number(3),
CONSTRAINT fk FOREIGN KEY(IdDzialu)
REFERENCES Dzialy(IdDzialu),
Nazwisko varchar2(15) NOT NULL,
Imie varchar2(15) DEFAULT 'Brak');
Zwyczajowo pole tabeli nadrzędnej, do którego odwołujemy się, definiując klucz obcy,
jest polem jej klucza podstawowego. Nie jest to jednak warunek konieczny. Konieczne
jest, aby pole to miało ustanowiony indeks unikalny (tworzenie indeksów zostanie omó-
wione w dalszej części książki), co w przypadku klucza podstawowego jest automatycznie
zapewnione. Zdefiniowanie klucza obcego nie eliminuje jednak możliwości wpisania
w określające go pole wartości NULL. Musimy to wykonać jawnie, wpisując ogranicze-
nie NOT NULL przy definicji kolumny. Konsekwencją stosowania ograniczenia klucza
obcego — poza zapewnieniem zgodności wartości z tymi w kolumnie nadrzędnej — jest,
przy zdefiniowaniu braku możliwości wpisania w pole wartości NULL, częściowa blo-
kada usuwania rekordów z tabeli nadrzędnej. Nie jest możliwe usunięcie z niej tych
rekordów, do których odwołuje się chociażby jeden rekord tabeli podrzędnej (z dodat-
kową blokadą przypisania NULL). Przenosząc to na grunt naszego przykładu — nie jest
możliwe usunięcie działu, w którym pracuje jakikolwiek pracownik. Aby usunąć taki
dział, trzeba przenieść pracownika do innego (zmienić IdDzialu na inne dostępne),
a dopiero potem skasować wpis o dziale z tabeli Dzialy. W Oracle możliwe jest od-
blokowanie usuwania takich rekordów przez zastosowanie klauzuli ON DELETE CASCADE.
Powoduje ona, że w momencie usuwania działu, do którego przypisani są pracownicy,
dotyczące ich wpisy są automatycznie usuwane z tabeli Osoby; są oni kasowani razem
z działem, w którym pracują. Z punktu widzenia praktyki baz danych, taka kaskadowa
akcja, zwłaszcza w przypadku wielostopniowego powiązania kluczem obcym i kaso-
wania rekordów z tabeli znajdującej się na samym szczycie powiązań, może doprowa-
dzić do daleko idących spustoszeń w danych. Dlatego, moim zdaniem, używanie klauzuli
ON DELETE CASCADE, jako potencjalnie niebezpiecznej, powinno być przy budowaniu
rozwiązań praktycznych zabronione. Można powiedzieć, że baza danych jest miejscem
do gromadzenia danych, a nie do ich usuwania.
DROP TABLE Osoby;
CREATE TABLE Osoby
(nr number(3) PRIMARY KEY,
IdDzialu number(3) NOT NULL,
CONSTRAINT fk FOREIGN KEY(IdDzialu)
REFERENCES Dzialy(IdDzialu) ON DELETE CASCADE,
Nazwisko varchar2(15) NOT NULL,
Imie varchar2(15) DEFAULT 'Brak'
);
108 Część I ♦ Oracle SQL
Możliwe jest również stosowanie klucza obcego odwołującego się do listy pól tabeli
nadrzędnej. W takim przypadku albo stanowią one w tej tabeli klucz wielokrotny, albo
są elementami tego samego indeksu. Przykładem może być sytuacja, kiedy kluczem
głównym dla pracowników jest kombinacja PESEL i NIP, a tabelą podrzędną są ich
wypłaty.
DROP TABLE Osoby;
CREATE TABLE Osoby
(NIP number(10),
PESEL number(10),
IdDzialu number(3) NOT NULL,
CONSTRAINT fk FOREIGN KEY(IdDzialu)
REFERENCES Dzialy(IdDzialu),
Nazwisko varchar2(15) NOT NULL,
Imie varchar2(15) DEFAULT 'Brak',
CONSTRAINT pk PRIMARY KEY(NIP, PESEL)
);
CREATE TABLE Zarobki
(IdZarobku number(10),
NIP number(10),
PESEL number(10),
Brutto number(12, 4),
CONSTRAINT fk1 FOREIGN KEY(NIP, PESEL)
REFERENCES Osoby(NIP, PESEL)
);
Także dla CHECK możliwe jest stosowanie funkcji systemowych. Ważnym ograniczeniem
jest fakt, że nie można odwoływać się do funkcji niedeterministycznych, czyli takich,
których wynik jest zależny od chwili ich wywołania. Należą do nich funkcje daty oraz
wszystkie odwołujące się do generatora liczb pseudolosowych.
DROP TABLE Nowa;
CREATE TABLE Nowa
(nr number(3) PRIMARY KEY,
Data date DEFAULT Sysdate,
CONSTRAINT spr CHECK(date = sysdate)
);
Na platformie Oracle możliwe jest stworzenie tabeli, której struktura oparta jest na ta-
beli już istniejącej. Tworzymy ją, zamieniając w poleceniu CREATE TABLE przedsta-
wiane dotąd metody definicji pól słowem kluczowym AS, po którym stosowane jest
zapytanie wybierające pola z tabeli źródłowej.
CREATE TABLE nazwa_tabeli
AS SELECT [pole1, pole2, ...|*]
FROM nazwa_tabeli_zrodlowej ;
Stosowanie innych klauzul, np. sortowania czy filtrowania, także jest dozwolone i po-
woduje, że do tworzonej tabeli wynikowej przepisywane są odpowiednie wiersze źró-
dła lub źródeł (ponieważ zapytanie może odwoływać się do wielu tabel). Przy takim
sposobie tworzenia tabeli, w oparciu o dane pochodzące z różnych tabel źródłowych,
mówimy o niej również zmaterializowana perspektywa.
Do tej pory tabele tworzone były w domyślnej przestrzeni tabel użytkownika. Ponie-
waż jednak może on mieć dostępnych wiele przestrzeni tabel, podczas tworzenia ta-
beli można wskazać na dowolną spośród nich.
CREATE TABLE Dzialy
(IdDzialu NUMBER(10) PRIMARY KEY,
Nazwa VARCHAR2(20))
TABLESPACE Users;
110 Część I ♦ Oracle SQL
Taką samą możliwość mamy wtedy, kiedy tworzymy tabelę opierającą się na zapyta-
niu wybierającym — korzystającym ze struktury i danych innych tabel.
CREATE TABLE test
TABLESPACE Users
AS
SELECT * FROM Dzialy;
Usuwanie tabeli, jak to już pokazano, następuje na skutek wykonania zapytania DROP
TABLE z opcjonalnym użyciem CASCADE CONSTRAINTS.
DROP TABLE nazwa_tabeli
[CASCADE CONSTRAINTS];
Opcja CASCADE CONSTRAINTS umożliwia usunięcie tabeli, do której odnoszą się ograni-
czenia integralnościowe FOREIGN KEY. Jeśli istnieje tabela połączona kluczem obcym
z usuwaną, to pominięcie tej opcji spowoduje błąd przetwarzania, a tabela nie zostanie
usunięta.
W celu dodania kolumny do istniejącej tabeli w zapytaniu tym należy użyć opcji ADD,
po której powinna nastąpić definicja dodawanego pola stworzona według zasad przed-
stawionych w podrozdziale dotyczącym tworzenia tabel.
ALTER TABLE Dzialy
ADD kod number(3);
Jeśli podczas pracy z tabelą okaże się, że proponowana długość pola jest zbyt mała,
możemy ją zwiększyć, używając opcji MODIFY.
ALTER TABLE Dzialy
MODIFY kod number(5);
Gdy będziemy chcieli zmienić typ zmiennej z numerycznego na znakowy, np. z number(5)
na char(5), to kolumna, na której będziemy wykonywać tę operację, musi być pusta.
W tym przypadku, aby nie utracić danych, dobrym pomysłem jest przepisanie ich do
pomocniczej tabeli, a po zmianie typu zwrotne przepisanie na poprzednie miejsce z za-
stosowaniem polecenia UPDATE. To samo dotyczy próby „skrócenia” pola, jeśli dane są
dłuższe niż ograniczenie docelowe.
Rozdział 4. ♦ Zapytania tworzące tabele 111
Podobnie jak dodania opcji blokowania wartości NULL dla kolumny, możemy dokonać
dopisania do niej wartości domyślnej.
ALTER TABLE Dzialy
MODIFY kod DEFAULT 0;
Usunięcie tej wartości może odbyć się przez jawne przypisanie do ograniczenia
DEFAULT wartości nieokreślonej NULL.
ALTER TABLE Dzialy
MODIFY kod DEFAULT NULL;
Możliwe jest również podanie pustego ciągu znaków, i to bez względu na typ danych
zmienianej kolumny.
ALTER TABLE Dzialy
MODIFY kod DEFAULT '';
Usunięcia kolumny z tabeli możemy dokonać, posługując się opcją DROP COLUMN. Należy
pamiętać, że usunięcie kolumny jest równoważne utracie wszystkich danych w niej
zawartych i powinno być wykonywane ze szczególną rozwagą.
ALTER TABLE Dzialy
DROP COLUMN kod;
Usuwanie kolumny może zostać wykonane przez jej wstępne zablokowanie na skutek
zastosowania opcji SET UNUSED. Od tej chwili kolumna ta jest niedostępna dla żadnego
użytkownika, ale fizycznie istnieje jeszcze w definicji tabeli.
ALTER TABLE Dzialy
SET UNUSED (kod);
powoduje jej fizyczne usunięcie. Metoda ta jest wykorzystywana, kiedy usuwanych jest
wiele kolumn, a tabela, na której wykonywana jest taka operacja, jest duża. Najpierw
zaznaczamy wtedy wszystkie przeznaczone do usunięcia kolumny, a następnie zostają
one wykasowane dzięki użyciu opcji DROP UNUSED COLUMNS. Z reguły usuwanie wstęp-
nie zaznaczonych do usunięcia kolumn odbywa się w czasie najmniejszego obciąże-
nia bazy, ponieważ sam ten proces może absorbować sporo zasobów.
Możliwe jest wznowienie usuwania dzięki zastosowaniu opcji CONTINUE. W celu uzy-
skania informacji o częściowo usuniętych i zablokowanych kolumnach możemy odwo-
łać się do perspektywy.
SELECT * FROM USER_PARTIAL_DROP_TABS;
Rozdział 4. ♦ Zapytania tworzące tabele 113
Należy zaznaczyć, że nie ma możliwości wycofania się z blokady kolumny; nie ist-
nieje opcja odwrotna do SET UNUSED COLUMN. Jedyną dopuszczalną operacją na takich
kolumnach jest ich usunięcie.
Należy pamiętać, że podczas ustanawiania ograniczenia dla tabeli dane w niej wystę-
pujące są walidowane, w związku z czym polecenie zakończy się powodzeniem tylko
wtedy, gdy dane występujące w kolumnie (kolumnach), których to ograniczenie doty-
czy, są z nim zgodne. Usunięcie ograniczenia odbywa się przy pomocy opcji DROP
CONSTRAINT z podaniem jego nazwy. Konsekwentne stosowanie dla ograniczeń nazw
użytkownika znacznie ułatwia tego typu operacje.
ALTER TABLE Nowa
DROP CONSTRAINT kl;
zwłaszcza gdy tworzone są tabele o bardzo dużej liczbie kolumn i stosunkowo nie-
wielkiej liczbie istotnych z punktu widzenia dalszego przetwarzania ograniczeń. Schemat
ten jest powszechnie stosowany przez generatory skryptów automatycznie budujących
zapytania tworzące tabele. Modyfikacje tabel mogą dotyczyć nie tylko ich struktury,
ale również sposobu alokacji. Pierwszym przykładem może być zapytanie powodujące
zwolnienie nieużywanej w segmencie przestrzeni powyżej znacznika wysokiej wody
(high water mark). Dla przypomnienia, jest to wskaźnik ostatniego bloku, jaki był
kiedykolwiek wykorzystywany do przechowywania danych tabeli. Wartość tego wskaź-
nika nie ulega zmianie przy usuwaniu z niej rekordów.
ALTER TABLE nazwa_tabeli
DEALLOCATE UNUSED [KEEP rozm K/M];
Klauzula KEEP określa liczbę bajtów, jaka ma zostać zostawiona powyżej znacznika wy-
sokiej wody. Pominięcie jej oznacza, że zwalniane są wszystkie bloki powyżej tego
wskaźnika.
Zmiana nazwy kolumny nie powoduje zmiany typu ani zdefiniowanych na niej ogra-
niczeń. Oczywiście zapisane dane również pozostają bez zmian.
Dodatkowe informacje
Dla każdego wiersza tabeli, bez względu na jej strukturę, zdefiniowane ograniczenia itp.,
Oracle tworzy identyfikator, który na potrzeby wewnętrzne silnika bazy danych koduje in-
formację o alokacji tabeli oraz danego jej wiersza. Dane o tym identyfikatorze możemy
uzyskać, odwołując się w zapytaniu wybierającym do funkcji ROWID (tabele 4.1 i 4.2).
SELECT ROWID FROM Dzialy;
Tak jak wspomniano w poprzednim rozdziale, zmieniane mogą być również parame-
try składowania tabeli. Przedstawione w tym fragmencie przykłady da się przenieść
wprost na zapytania tworzące tabelę, jednak aby ich nie dublować, zaprezentowane zo-
staną tylko dla polecenia ALTER TABLE; jest to tym bardziej uzasadnione, że są stosun-
kowo rzadko wykorzystywane.
Rozdział 4. ♦ Zapytania tworzące tabele 115
Tabela 4.1. Wartości przyjmowane przez ROWID dla wierszy tabeli Dzialy
ROWID
AAAMVhAAKAAAAAyAAA
AAAMVhAAKAAAAAyAAB
AAAMVhAAKAAAAAyAAC
AAAMVhAAKAAAAAyAAD
AAAMVhAAKAAAAAyAAE
AAAMVhAAKAAAAAyAAF
AAAMVhAAKAAAAAyAAG
AAAMVhAAKAAAAAyAAH
Istnieje możliwość jawnego wskazania pliku danych, w którym mają być przydzielane
tabeli kolejne ekstenty.
ALTER TABLE test
ALLOCATE EXTENT
(SIZE 500K
DATAFILE 'E:\ORA\ORADATA\ORCL\AP.ORA');
116 Część I ♦ Oracle SQL
Ponieważ, jak wiemy, użytkownikowi może być przypisanych wiele przestrzeni tabel,
z których dokładnie jedna jest domyślna, istnieje możliwość przeniesienia tabeli po-
między należącymi do niego przestrzeniami tabel.
ALTER TABLE test
MOVE TABLESPACE inna;
niczenia separowanych kropką. Sprawa wydaje się prosta, gdyż nie można wpisać ta-
kich samych wartości do kolumny klucza podstawowego. Spróbujmy zmienić dla tej
tabeli walidację ograniczenia na odroczoną (DEFERRED).
SET CONSTRAINT pk DEFERRED;
Komunikat zwracany po wykonaniu polecenia potwierdza ten stan rzeczy: Więzy zo-
stały określone. W takim przypadku wprowadzenie w pole klucza dwóch takich samych
wartości nie wywołuje komunikatu o błędzie.
INSERT INTO Osoba VALUES(1,'Jan');
INSERT INTO Osoba VALUES(1,'Karol');
1 wiersz został utworzony.
1 wiersz został utworzony.
Jak widać, wykrycie naruszenia więzów przy próbie zatwierdzenia transakcji polece-
niem COMMIT powoduje nie tylko wyświetlenie komunikatu, ale również jej wycofanie,
czyli powrót do stanu sprzed próby wstawienia jakiegokolwiek wiersza. Możliwe jest
przywrócenie sprawdzania więzów przed zatwierdzeniem transakcji — natychmiastowo,
bez odroczenia — dzięki wykonaniu polecenia:
SET CONSTRAINT pk IMMEDIATE;
118 Część I ♦ Oracle SQL
Jeśli chcemy dokonać tego dla wszystkich więzów schematu, możemy zastosować za-
miast nazwy słowo kluczowe ALL.
SET CONSTRAINT ALL IMMEDIATE;
Do tej pory mówiliśmy tylko o odraczaniu walidacji więzów, jednakże mogą one być
również czasowo blokowane. Dla przykładu możemy spowodować wyłączenie klucza
podstawowego w tabeli Osoby. Opcja CASCADE jest wymuszona istnieniem powiąza-
nych rekordów i klucza obcego po stronie tabel podrzędnych.
ALTER TABLE Osoby DISABLE PRIMARY KEY CASCADE;
Przywrócenie klucza podstawowego dla tej tabeli może zostać uzyskane dzięki pole-
ceniu:
ALTER TABLE Osoby ENABLE PRIMARY KEY;
Wiąże się to z wykonaniem w tym momencie walidacji danych. Możliwe jest również
czasowe blokowanie procesu walidacji przy odblokowanym (lub zablokowanym) ogra-
niczeniu, np.:
ALTER TABLE Dzialy
ENABLE NOVALIDATE UNIQUE(Nazwa) ;
Możliwa jest również zmiana statusu ograniczenia za pomocą odwołania się do nazwy
ograniczenia wcześniej utworzonego.
Rozdział 4. ♦ Zapytania tworzące tabele 119
Tabela 4.3. Wpływ rodzaju więzów na dane zapisane w bazie i do niej wstawiane
Stan, w jakim znajdują się więzy
DISABLE ENABLE
Rodzaj danych DISABLE VALIDATE ENABLE VALIDATE
NOVALIDATE NOVALIDATE
Istniejące mogą nie spełniać nie są dozwolone mogą nie spełniać muszą spełniać
ograniczenia operacje UPDATE ograniczenia ograniczenia
i DELETE
Wstawiane mogą nie spełniać nie jest dozwolona muszą spełniać muszą spełniać
ograniczenia operacja INSERT ograniczenia ograniczenia
Sekwencja
Problemem, na który szybko natrafiają początkujący programiści Oracle, jest brak typu
danych zapewniającego automatyczną inkrementację. Pierwszym pomysłem na jego
rozwiązanie jest stworzenie sekwencji SEQUENCE. Obiekt ten posiada możliwość zmiany
generowanej wartości w sposób określony przez jego parametry. Nie jest on związany
z żadnym innym obiektem, ale stanowi oddzielny byt. Podstawowe polecenie tworzące
sekwencję wymaga tylko podania jej nazwy.
CREATE SEQUENCE Licznik;
Tak stworzony obiekt będzie przy wywołaniu generował kolejne liczby całkowite,
począwszy od 1. Każda następna wartość będzie o jeden większa od poprzedniej. Moż-
liwe jest jednak stworzenie sekwencji, w której definiowana jest większa liczba para-
metrów. Opcja START WITH ustanawia wartość, od której będą generowane kolejne
liczby, a INCREMENT BY wartość, o którą różnią się dwie liczby wygenerowane kolejno.
Opcja MAXVALUE określa najwyższą wartość, która może zostać wygenerowana, zaś
MINVALUE najmniejszą. Wszystkie te parametry mogą być liczbami całkowitymi (również
ujemnymi), stąd wartości opcji START WITH i MINVALUE nie są synonimami. Parametr
CYCLE (NOCYCLE) określa, czy po osiągnięciu ograniczenia górnego lub dolnego nastąpi
ponowienie generacji liczb, począwszy od drugiego z ograniczeń. Ponowne rozpoczę-
cie cyklu powoduje generowanie kolejnych numerów według dotychczasowych zasad
(wykorzystane wartości nie są nigdzie zapamiętywane). W przypadku osiągnięcia ogra-
niczenia przy opcji NOCYCLE próba kolejnego wywołania sekwencji powoduje wyświe-
tlenie komunikatu o błędzie. W prezentowanym przykładzie pokazano generowanie se-
kwencji z pełnym wykazem parametrów, których wartości są ustawione na domyślne.
CREATE SEQUENCE Licznik
INCREMENT BY 1 START WITH 1
MAXVALUE 1.0E28 MINVALUE 1 CYCLE
CACHE 20 NOORDER;
120 Część I ♦ Oracle SQL
Drugą metodą sekwencji jest CURRVAL, która powoduje wyświetlenie jej bieżącej war-
tości. Wywołanie tej metody nie powoduje zmiany stanu sekwencji. Do jej pokazania
zastosowano dostępną dla każdego użytkownika tabelę pomocniczą DUAL znajdującą się
w schemacie SYS. Konieczność odwołania się do tej tabeli wynika z faktu, że Oracle
nie potrafi wykonać zapytania wybierającego, w którym nie podano źródła. Taki sam
mechanizm jest stosowany do wyświetlania wielkości skalarnych.
SELECT Licznik.CURRVAL FROM SYS.DUAL
Sekwencja może być modyfikowana przy użyciu polecenia ALTER SEQUENCE. Zmianie
mogą podlegać wszystkie parametry za wyjątkiem pozycji startowej START WITH.
ALTER SEQUENCE Licznik
INCREMENT BY 1
MAXVALUE 1.0E28 MINVALUE 1 CYCLE
CACHE 20 NOORDER;
Aby zmienić wartość, od której generowane są przez sekwencję liczby, należy ją naj-
pierw usunąć poleceniem DROP SEQUENCE, a następnie utworzyć ponownie.
DROP SEQUENCE Licznik;
Perspektywy
Do tej pory źródłem danych dla zapytań była ich materialna postać, czyli tabela lub
złączenie tabel. Zamiast odwołania do takiej formy danych, możemy odpytywać ich
dynamiczną postać, czyli perspektywę, nazywaną często widokiem. Perspektywę two-
rzymy poleceniem CREATE VIEW, którego pierwszym parametrem jest jej nazwa. Po słowie
kluczowym AS następuje definicja widoku, na którą składa się dowolne poprawne skła-
dniowo zapytanie wybierające. Jeżeli perspektywa o danej nazwie już istnieje, należy
ją uprzednio usunąć poleceniem DROP. W prezentowanym przykładzie perspektywa za-
wiera dwa pola tych rekordów tabeli Osoby, dla których wartość RokUrodz jest więk-
sza niż 1970.
DROP VIEW mlodzi;
CREATE VIEW mlodzi
AS
SELECT Idosoby, RokUrodz FROM Osoby
WHERE RokUrodz > 1970;
W przypadku stosowania opcji CHECK możliwe jest użycie dyrektywy CONSTRAINT w celu
nazwania ograniczenia związanego ze sprawdzeniem klauzuli WHERE. W przedstawio-
nym przykładzie ograniczenie uzyska nazwę ogr.
CREATE OR REPLACE VIEW mlodzi
AS
SELECT Idosoby, RokUrodz FROM Osoby
WHERE RokUrodz>1970
WITH CHECK OPTION CONSTRAINT ogr;
Jeśli znamy nazwę ograniczenia, to, podobnie jak w przypadku tabel, możemy jej użyć
w celu odwołania się do niego, np. w przypadku usuwania tego ograniczenia. Należy
zauważyć, że jeśli jawnie nie przypiszemy opcji CHECK nazwy, zostanie ona nadana przez
system.
ALTER VIEW mlodzi DROP CONSTRAINT ogr;
Jeśli nie jest spełniony w stosunku do pomijanych pól żaden z powyższych warunków,
próba wykonania zapytania INSERT zakończy się niepowodzeniem. Bardzo istotnym po-
wodem tworzenia perspektyw jest potrzeba przyznania użytkownikowi (operatorowi)
praw zarówno do części kolumn, jak i tylko do wybranych wierszy tabeli. Perspektywy
mogą być również wykorzystywane w celu poprawy wydajności przetwarzania, po-
nieważ już na etapie ich tworzenia generowany jest plan wykonania zapytania, które
jest zawarte w ich definicji.
Bardzo ważnym źródłem informacji o strukturze bazy danych oraz w zasadzie o wszyst-
kim, co dotyczy obiektów bazy, użytkowników czy organizacji, są tabele systemowe.
Wygodną metodą korzystania z nich jest jednak odwoływanie się nie bezpośrednio do
nich, ale do opartych na nich perspektyw systemowych. Przykładem może być uzyskanie
informacji o wszystkich obiektach stworzonych przez użytkownika za pośrednictwem
perspektywy USER_OBJECTS (tabela 4.4). W prezentowanym przykładzie dodatkowo
sformatowana została kolumna z nazwą obiektu, ponieważ w oryginalnej postaci
zawiera ona bardzo dużą liczbę znaków, co w większości przypadków jest nadmiarowe.
COLUMN Object_Name FORMAT A25;
SELECT Object_Name, Object_Type FROM USER_OBJECTS;
Jednak nie wszystkie obiekty dostępne dla użytkownika muszą być przez niego stwo-
rzone. Aby uwzględnić obiekty własne oraz te, do których użytkownik ma prawo,
możemy skorzystać z perspektywy ALL_OBJECTS (tabela 4.4).
COLUMN Object_Name FORMAT A25;
SELECT Owner, Object_Name, Object_Type
FROM ALL_OBJECTS;
124 Część I ♦ Oracle SQL
Jeśli interesują nas wszystkie obiekty w bazie, możemy odwołać się do perspektywy
DBA_OBJECTS (tabela 4.4).
COLUMN Object_Name FORMAT A25;
SELECT Owner, Object_Name, Object_Type
FROM DBA_OBJECTS;
Perspektywa ta zawiera bardzo dużą ilość informacji i zastosowanie jej bez jakiego-
kolwiek filtrowania spowoduje, że czas przetwarzania będzie dość długi.
Tabela 4.5. Pierwsze rekordy słownika zawierającego informacje o nazwach perspektyw systemowych
— komentarze pozostawiono w pisowni oryginalnej
TABLE_NAME COMMENTS
ALL_ALL_TABLES Description of all objects and relational tables accessible to the user
USER_AUDIT_SESSION All audit trail records concerning CONNECT and DISCONNECT
USER_AUDIT_STATEMENT Audit trail records concerning grant, revoke, audit, noaudit and alter system
••• •••
Tabela 4.7. Zakres działania perspektyw — xxx symbolizuje element nazwy wskazujący na przeznaczenie
perspektywy
DBA_xxx (wszystkie obiekty)
być perspektywa V$INSTANCE (tabela 4.9), która, zgodnie z nazwą, zawiera informacje
o instancji serwera Oracle. Poniżej przedstawiono zapytanie wyświetlające nazwy
kolumn w jej definicji.
DESC V$INSTANCE;
DESC V$SESSION_CONNECT_INFO;
Przykłady perspektyw typu V$* można by w tym miejscu mnożyć, ale ponieważ są one
ściśle związane z administracją, a nie z programowaniem, pozostanę przy tym okrojo-
nym przeglądzie. Chciałbym natomiast przedstawić jeszcze kilka perspektyw o nazwach
zaczynających się co prawda od prefiksu DBA_, ale pełniących podobną rolę jak per-
spektywy V$_ oraz nieposiadających swoich odpowiedników na niższych poziomach.
Takim przykładem może być perspektywa DBA_FREE_SPACE przechowująca informacje
o wolnej pamięci w każdej z przestrzeni tabel instancji serwera (tabela 4.13).
DESC DBA_FREE_SPACE;
Ciekawe, zwłaszcza dla początkującego adepta platformy Oracle, może być spraw-
dzenie zawartości perspektywy DBA_USERS (tabela 4.15). Zawiera ona informacje o na-
zwach użytkowników, datach ich utworzenia (dla domyślnych administratorów bazy
SYS i SYSTEM są to daty opracowania realizacji serwera przez firmę) oraz przydzielonych
im przestrzeniach tabel. Dodatkowo możliwe jest odczytanie hasła każdego z użytkow-
ników. Występuje ono co prawda w formie zakodowanej algorytmem nieodwracalnym
(nie można na jej podstawie odtworzyć postaci źródłowej), ale daje również użytkowni-
kom o uprawnieniach administracyjnych możliwość podszycia się pod innych użytkow-
ników systemu. Jedynym, które nie jest kodowane, jest hasło logowań anonimowych
— napis anonymous nie jest hasłem, ale oznacza jego brak. Dodatkowo wyświetlana
jest informacja o statusie konta, który może przyjąć trzy wartości:
OPEN — otwarte, aktywne,
LOCKED — zablokowane, nieaktywne,
EXPIRED & LOCKED — przeterminowane i zablokowane.
Indeksy
Ważnym elementem poprawy jakości przetwarzania jest wprowadzenie w tabelach in-
deksów. Są one strukturami, które mają za zadanie nadanie rekordom znaczników po-
zwalających na ich szybką identyfikację względem wybranego w definicji indeksu atry-
butu lub ich listy. Możemy mówić, że indeks wprowadza dodatkową „numerację” wierszy
w oparciu o kolumnę lub ich listę, tak jak zostałyby one wyprowadzone przez zapyta-
nie wybierające, w którego klauzuli ORDER BY umieszczono by taki zestaw kolumn.
W Oracle mamy możliwość stworzenia dwóch typów indeksów: B-drzewa lub bitma-
powego. Pierwszy z nich, zgodnie z nazwą, jest zorganizowany w postaci drzewa, w któ-
Rozdział 4. ♦ Zapytania tworzące tabele 131
rym na szczycie znajduje się korzeń zawierający wskaźniki do drugiego poziomu indeksu.
Ten zawiera bloki ze wskaźnikami do poziomu następnego. Na najniższym poziomie
znajdują się liście ze wskaźnikami do rekordów tabeli. Schematyczny obraz organiza-
cji tego indeksu przedstawia rysunek 4.1. Liście drzewa połączone są w listę dwukie-
runkową, co ułatwia odczyt indeksu zarówno w kierunku wartości rosnących, jak i ma-
lejących. Jak widać, organizacja taka pozwala na znaczne przyspieszenie dostępu do
rekordów.
Rysunek 4.1.
Struktura indeksu
typu B-drzewo
W definicji indeksu nie może powtórzyć się żadna kombinacja kolumn, natomiast
kolejność pól na liście jest istotna dokładnie z tych samych przyczyn, dla których ma
znaczenie kolejność pól w klauzuli ORDER BY. Indeks usuwamy poleceniem DROP
INDEX, w którym podajemy jego nazwę. Dowodzi to tego, że nazwy indeksów w obrębie
schematu bazy danych muszą być unikalne.
DROP INDEX dzial;
Wymaga to jednak, aby wartości pola (lub ich lista) były unikalne w obrębie tabeli.
Warunek ten jest sprawdzany w momencie tworzenia indeksu i jeśli nie jest prawdziwy,
przetwarzanie kończy się komunikatem o błędzie — ORA-01452: nie można CREATE
UNIQUE INDEX; znaleziono duplikaty klucza — i indeks nie zostaje utworzony.
Tak jak każdy obiekt bazy (segment), indeksy wymagają przydzielenia im pamięci.
Zwykle zdajemy się na ustawienia domyślne nadawane przez Oracle, jednak, tak samo
jak przy tworzeniu tabel, parametry składowania mogą być podawane jawnie podczas
tworzenia indeksów. W przykładzie przedstawiono przydział zasobów podczas
tworzenia indeksu unikatowego, jednak identyczne opcje dostępne są przy tworzeniu
indeksu nieunikatowego oraz przy modyfikacji każdego z wymienionych typów.
CREATE UNIQUE INDEX nazwa_indeksu
ON nazwa_tabeli (pole1, pole2, .....)
[PCTFREE liczba] [PCTUSED liczba]
[INITRANS liczba] [MAXTRANS liczba]
[TABLESPACE nazwa_ przestrzeni]
[NOSORT]
STORAGE (parametry składowania);
Rozdział 4. ♦ Zapytania tworzące tabele 133
Jako jedno z niewielu środowisk Oracle dopuszcza budowanie indeksów nie tylko
opartych na polach lub ich listach, ale również na wyrażeniach bazujących na tych
polach (możliwe jest użycie dowolnych operatorów i funkcji deterministycznych zde-
finiowanych w Oracle). Ma to znaczenie w przypadku tworzenia złożonych zapytań
raportujących, w których występuje duża liczba operacji opartych na takich wyraże-
niach — chodzi głównie o filtrowanie i sortowanie. W praktyce najrzadziej pojawiają
się złączenia korzystające z pól wyliczanych, ale te na pewno powinny mieć zdefinio-
wany indeks.
CREATE INDEX nazwa_indeksu
ON nazwa_tabeli (wyrażenie)
[PCTFREE liczba] [PCTUSED liczba]
[INITRANS liczba] [MAXTRANS liczba]
[TABLESPACE nazwa_przestrzeni]
[NOSORT]
STORAGE (parametry_składowania);
Jest oczywistym, że nie jest możliwe zmniejszenie zasobów poniżej poziomu, który
indeks rzeczywiście wykorzystał. Ponieważ podczas wykonywania operacji modyfi-
kujących zawartość tabeli zmienia się informacja przechowywana w indeksach, ko-
nieczne jest dokonywanie ich „konserwacji”. Najczęściej wykorzystywaną metodą jest
zarządzanie indeksami przez ich przebudowanie. Celami osiąganymi dzięki przebu-
dowaniu indeksu są:
odzyskanie miejsca zabieranego przez usunięte rekordy;
przeniesienie indeksu do nowej przestrzeni tabel;
zmiana atrybutów fizycznego miejsca przechowywania;
ustawienie na nowo parametrów wykorzystania miejsca.
COALESCE
Jak widać na rysunku 4.2, taki proces scalania nie powoduje usunięcia z przestrzeni
pustych ekstentów; dąży się tylko do maksymalnego wypełnienia ekstentów już wy-
korzystanych lub połączenia pustych. Rzeczywiste usunięcie niewykorzystanej prze-
strzeni ma miejsce tylko w przypadku usunięcia i ponownego utworzenia indeksu.
Podstawowe różnice dla metod przebudowy indeksu pokazuje tabela 4.16. Należy pa-
miętać, że wszystkie operacje na indeksach powodują obciążenie zarówno zasobów, jak
i procesora, dlatego dla dużych tabel z wieloma indeksami ich przebudowa dowolną me-
todą powinna być wykonywana w okresie możliwie najmniejszego obciążenia bazy.
Tabela 4.16. Podstawowe różnice dla przebudowy indeksu metodą usunięci i ponownego stworzenia
oraz przez zastosowanie opcji REBUILD
Usunięcie i stworzenie Użycie opcji REBUILD
Można zmienić nazwę indeksu Nie można zmienić nazwy indeksu
Można zmienić UNIQUE na non-UNIQUE i odwrotnie Nie można zmienić UNIQUE na non-UNIQUE i odwrotnie
Można przełączyć indeks pomiędzy B-drzewem Nie można przełączyć indeks pomiędzy B-drzewem
i bitmapą i bitmapą
Potrzebne jest miejsce na tylko jedną kopię indeksu Potrzebne jest miejsce na dwie kopie indeksu
Konieczne jest sortowanie, jeśli dane istnieją Nigdy nie jest konieczne sortowanie
Indeks jest tymczasowo niedostępny dla zapytań Indeks pozostaje dostępny dla zapytań
Nie można użyć tej metody, jeśli indeks jest Można użyć tej metody dla indeksu stosowanego
stosowany do zachowania ograniczenia do zachowania ograniczenia
Poza przebudową indeksów ważnym elementem jest analiza ich wykorzystania, pozwa-
lająca na ocenę celowości ich utworzenia. Wyniki analizy indeksów możemy otrzymać
przez odpytanie perspektywy INDEX_STATS.
SELECT * FROM INDEX_STATS;
Omawiając indeksy, skupiłem się na ich pozytywnej roli, pozwalającej znacznie przy-
spieszyć wykonywanie zapytań wybierających. Należy jednak pamiętać, że stosowanie
indeksów może okazać się bronią obosieczną. W stopniu podobnym do tego, w jakim
przyspieszają wyszukiwanie rekordów, potrafią także spowolnić wykonywanie zapytań
modyfikujących dane. Najmniejsze spowolnienie dotyczy usuwania rekordów, ponieważ
wiąże się to tylko z usunięciem dotyczących ich wpisów i nie pociąga za sobą żadnej
dodatkowej akcji (brakujące wpisy nie są uzupełniane) czy reorganizacji. Dużo więk-
sze opóźnienia wiążą się natomiast z wstawianiem nowych rekordów lub z modyfikacją
już istniejących. Dlatego, poza indeksami tworzonymi automatycznie na więzach in-
tegralnościowych, nie powinno się dodatkowo tworzyć zbyt wielu indeksów „trwałych”,
tzn. takich, które istnieją w bazie przez długi czas bez względu na to, jakie operacje są
na niej wykonywane. Zwyczajowo poprzedza się stworzeniem odpowiednich indeksów
złożone zapytania wybierające, zwykle związane z raportowaniem i zajmujące dużo
czasu. Wykonanie takich zapytań, o ile to możliwe, przekłada się na czas mniejszego
obciążenia bazy, stosując mechanizm zadań. Całość zadania umieszcza się z kolei we
wnętrzu transakcji gwarantującej odpowiedni poziom blokad. Dobór najbardziej opty-
malnych (praktycznie zawsze quasi-optymalnych) indeksów jest zadaniem trudnym
i powinien być weryfikowany praktycznie. Najczęściej testowanie rozwiązań jest wy-
konywane nie na bazie produkcyjnej, ale na specjalnej instancji zawierającej dane te-
stowe (zwykle przekopiowany podzbiór rzeczywistych danych z bazy produkcyjnej).
Rozdział 5.
Dodatkowe
funkcjonalności SQL
Tabela 5.1. Podstawowa postać generowania znaczników XML dla pojedynczych pól każdego rekordu
Nazwiska Imiona
<?xml version="1.0"?> <?xml version="1.0"?>
<NAZWISKO>Kowalski</NAZWISKO> <IMIE>Jan</IMIE>
<?xml version="1.0"?> <?xml version="1.0"?>
<NAZWISKO>Nowak</NAZWISKO> <IMIE>Karol</IMIE>
Jak widać, ten sposób prezentacji jest daleki od wygenerowania poprawnego zbioru XML
zawierającego pełny zakres danych dostępnych w tabeli. Poprawę wyglądu generowanego
pliku możemy osiągnąć przez zastosowanie złożenia funkcji SYS_XMLAGG(SYS_XMLGEN()),
dla którego parametrem będzie nazwa kolumny (tabela 5.2). Należy zwrócić uwagę,
138 Część I ♦ Oracle SQL
że nadrzędny znacznik będzie miał zawsze postać <ROWSET>. Tym razem wszystkie re-
kordy będą stanowiły jedno pole tabeli wynikowej, co przy eksporcie do pliku z użyciem
polecenia SPOOL da poprawny, chociaż zawierający tylko jedno pole (jeden znacznik)
plik XML.
SELECT XMLELEMENT("Pracownik",
XMLELEMENT("Nazwisko", Nazwisko),
XMLELEMENT("Imie", Imie)) AS Rekordy
FROM Osoby
WHERE IdOsoby<3;
SELECT XMLELEMENT("Pracownik",
XMLATTRIBUTES(IdOsoby AS ID, Nazwisko),
XMLELEMENT("Imie", Imie)) AS Rekordy
FROM Osoby
WHERE IdOsoby<3;
Tabela 5.5. Skutek zastosowania funkcji XMLELEMENT oraz XMLATTRIBUTES z zawartym w niej
zapytaniem skorelowanym do wygenerowania pliku XML z atrybutami
Rekordy
<Pracownik IDOSOBY="1" NAZWISKO="Kowalski"><Imie>Jan</Imie><Dzial IDDZIALU="1"
DZIAL="Dyrekcja"></Dzial></Pracownik>
<Pracownik IDOSOBY="2" NAZWISKO="Nowak"><Imie>Karol</Imie><Dzial IDDZIALU="2"
DZIAL="Administracja"></Dzial></Pracownik>
SELECT XMLELEMENT("Pracownik",
XMLATTRIBUTES(IdOsoby, Nazwisko),
XMLELEMENT("Imie", Imie),
XMLELEMENT("Dzial", XMLATTRIBUTES(IdDzialu,
(SELECT nazwa FROM Dzialy
WHERE Dzialy.IdDzialu = Osoby.IdDzialu) as Dzial)))
AS Rekordy
FROM Osoby
WHERE IdOsoby<3;
SELECT XMLELEMENT("Pracownik",
XMLForest(Nazwisko, Imie, Wzrost))AS Rekordy
FROM Osoby
WHERE IdOsoby<3;
140 Część I ♦ Oracle SQL
Podobnie jak XMLForest, również XMLCOLATTVAL dla utworzenia struktury XML zwią-
zanej z pojedynczym rekordem potrzebuje jako parametru tylko listy pól (tabela 5.7).
Odmienna jest tylko postać wynikowego pliku, w którym nazwę kolumny stanowi atry-
but standardowego znacznika o nazwie COLUMN.
SELECT XMLELEMENT("Pracownik",
XMLCOLATTVAL(Nazwisko, Imie, Wzrost))AS Rekordy
FROM Osoby
WHERE IdOsoby<3;
SELECT XMLELEMENT("Dzial",
XMLAGG(XMLELEMENT("Pracownik", Nazwisko)))
AS Dzialy
FROM Osoby
WHERE IdOsoby<6
GROUP BY IdDzialu;
Jeżeli parametry funkcji wymagają podania pojedynczej wartości, a tak jest w przy-
padku stosowania funkcji XMLELEMENT lub XMLAGG, to możliwe jest dokonanie konkate-
nacji pól (połączenia łańcuchów) — tabela 5.9. Połączenie takie wykonywane jest za
pomocą funkcji XMLCONCAT. Należy jednak pamiętać, że, analogicznie do zwykłej funkcji
CONCAT, musi mieć ona dokładnie dwa parametry, w związku z czym połączenie więcej
niż dwóch pól wymaga jej zagnieżdżenia.
Tabela 5.9. Skutek zastosowania funkcji XMLAGG do połączenia pól podczas generowania pliku XML
Pracownik
<Nazwisko>Kowalski</Nazwisko><Imie>Jan</Imie>
<Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie>
<Nazwisko>Kow</Nazwisko><Imie>Piotr</Imie>
<Nazwisko>Janik</Nazwisko><Imie>Paweł</Imie>
<Nazwisko>Kowalczyk</Nazwisko><Imie>Jarosław</Imie>
Kolejne rekordy wpisywane są do tej tabeli według takich samych reguł, z zachowa-
niem wymagania unikalnej wartości pola klucza podstawowego.
INSERT INTO Pracownicy (IDPracownika, Dane)
VALUES (2,
XMLTYPE('
142 Część I ♦ Oracle SQL
<PRecord>
<Nazwisko>Nowak</Nazwisko>
<Imie>Karol</Imie>
<RokUrodz>1977</RokUrodz>
<Wzrost>1.81</Wzrost>
<DataZatr>1998/03/05</DataZatr>
</PRecord>')
);
W ten sposób w tabeli mamy dwa rekordy, które zawierają informacje w postaci znacz-
nikowej o dwóch pracownikach firmy. Tabelę taką możemy odpytać, stosując proste
zapytanie wybierające. W celu ograniczenia szerokości wyświetlanej kolumny zastoso-
wano dodatkowo formatowanie A50, powodujące, że liczba znaków w pojedynczym
wierszu nie może przekroczyć tej wartości. W przypadku pola typu XMLTYPE dotyczy
to szerokości pojedynczej pary znaczników wraz z zawartością (tabela 5.10).
Tabela 5.10. Skutek wykonania prostego zapytania wybierającego do tabeli zawierającej pole typu
XMLTYPE
IdPracownika Dane
1 <PRecord>
<Nazwisko>Kowalski</Nazwisko>
<Imie>Jan</Imie>
<RokUro
2 <PRecord>
<Nazwisko>Nowak</Nazwisko>
<Imie>Karol</Imie>
<RokUrodz>197
Niestety, przy tak sformułowanym zapytaniu pole może wyświetlić tylko standardową
liczbę 69 znaków i dlatego zawartość pola typu XMLTYPE o nazwie Dane urywa się przed
zakończeniem danych w nim zawartych, bez uwzględniania końców znaczników. Po
prostu wypełnianie pola jest kończone wraz z wypisaniem ostatniego dopuszczalnego,
69. znaku, wliczając w to znaki przejścia do kolejnej linii. Aby wymusić wypisywanie
dłuższej zawartości, musimy ustawić flagę SET LONG na dostatecznie dużą wartość.
W naszym przypadku nadmiarowo przyjęto 100000, co sprawi, że wypisywanie za-
wartości pola będzie teraz ograniczone do takiej liczby znaków (tabela 5.11). Dla pól
zawierających mniejszą ich liczbę wypisywanie zakończy się wraz z osiągnięciem ostat-
niego występującego w nich znaku. Efekt takiego postępowania możemy prześledzić,
wykonując poniżej zamieszczony skrypt.
Poza wyświetlaniem całej zawartości pola, w przypadku pól zawierających typ znaczni-
kowy możliwe jest wskazanie dowolnego poziomu, od którego rozpocznie się wyświetla-
nie. Efekt taki uzyskujemy na skutek zastosowania funkcji EXTRACT, której pierwszym
Rozdział 5. ♦ Dodatkowe funkcjonalności SQL 143
Tabela 5.11. Skutek wykonania prostego zapytania wybierającego do tabeli zawierającej pole typu
XMLTYPE przy fladze SET LONG ustawionej na 100000
IdPracownika Dane
1 <PRecord>
<Nazwisko>Kowalski</Nazwisko>
<Imie>Kowalski</Imie>
<RokUrodz>1980</RokUrodz>
<Wzrost>1.77</Wzrost>
<DataZatr>2001/02/10</DataZatr>
</PRecord>
2 <PRecord>
<Nazwisko>Nowak</Nazwisko>
<Imie>Karol</Imie>
<RokUrodz>1977</RokUrodz>
<Wzrost>1.81</Wzrost>
<DataZatr>1998/03/05</DataZatr>
</PRecord>
parametrem jest nazwa kolumny zawierającej dane w formacie XML, natomiast drugim
definicja poziomu w hierarchicznej strukturze. Definiowanie poziomu początkowego
odbywa się poprzez wskazanie, od najwyższego, wszystkich poziomów podrzędnych,
aż do interesującego nas znacznika (łącznie z wszystkimi potomnymi). Poszczególne
poziomy oddzielane są znakiem backslash (/). W prezentowanym przykładzie wyświe-
tlane są tylko pojedyncze znaczniki <Nazwisko> (tabela 5.12), ponieważ nie zostały
zdefiniowane żadne znaczniki w nich zagnieżdżone (nie istnieją znaczniki potomne).
Tabela 5.12. Skutek wykonania zapytania wybierającego do tabeli zawierającej pole typu XMLTYPE,
z definicją poziomu zagnieżdżenia, z którego będą wyświetlane znaczniki
IdPracownika NazwiskoXML
2 <Nazwisko>Nowak</Nazwisko>
3 <Nazwisko>Kowalski</Nazwisko>
1 <Nazwisko>Kowalski</Nazwisko>
Możliwe jest również wyświetlenie wartości znajdującej się między znacznikami okre-
ślonego poziomu. W tym celu należy zastosować funkcję EXTRACTVALUE (tabela 5.13),
której parametry są takie same jak w przypadku funkcji EXTRACT. EXTRACTVALUE wymaga,
aby węzeł był prosty, bez poziomów potomnych (bez zagnieżdżenia). Jeśli warunek ten
nie jest spełniony, generowany jest komunikat o błędzie.
144 Część I ♦ Oracle SQL
Tabela 5.13. Skutek wykonania zapytania wybierającego do tabeli zawierającej pole typu XMLTYPE,
z definicją poziomu zagnieżdżenia, z którego będą wyświetlane wartości znaczników prostych
IdPracownika NazwiskoXML
2 Nowak
3 Kowalski
1 Kowalski
Szerszy zakres możliwości oferuje funkcja XMLQuery (tabela 5.14). Przede wszystkim
możliwe jest tu filtrowanie pól XML według dowolnego ze znaczników oraz ich po-
rządkowanie. Funkcja ta wymaga jednego argumentu, którym jest łańcuch z popraw-
nym składniowo zapytaniem do struktury XML. Zawiera ono definicję pętli, w której
nazwa zmiennej inkrementowanej zaczyna się od znaku $, a zakres zmian wyznacza
wskazanie odpowiedniego poziomu znaczników. Klauzule WHERE i ORDER BY definiowane
są tu analogicznie do klasycznego zapytania. Konieczne jest określenie zakresu zwra-
canej zmiennej — w przykładzie zastosowano pełną nazwę zmiennej inkrementowanej,
co prowadzi do wyświetlenia wszystkich znaczników wraz z zawartością na zdefi-
niowanym poziomie zagnieżdżenia. Poza tym łańcuchem należy podać w klauzuli
PASING BY nazwę analizowanego pola XML oraz wskazać, że zwracana ma być jego
zawartość. Poniżej wyświetlono wszystkie dane pracowników o nazwisku Kowalski.
Uporządkowanie ich według imienia miałoby praktyczne znaczenie tylko wtedy, gdyby
w obrębie danych jednego pracownika istniały dwa znaczniki <Imie>. W naszym przy-
padku o porządkowaniu rekordów decyduje zewnętrzne, zwykłe zapytanie wybierające.
Należy zwrócić uwagę na to, że jeśli w danym rekordzie nie są zawarte dane osoby
o nazwisku Kowalski, funkcja XMLQuery zwraca wartość NULL, choć rekord w nadrzęd-
nym zapytaniu wybierającym jest wyprowadzany, a jego eliminację można osiągnąć
tylko za pomocą zewnętrznej klauzuli WHERE. Jeżeli interesuje nas tylko wybrany ze-
staw znaczników, należy go zdefiniować po słowie kluczowym RETURN w postaci po-
łączenia (konkatenacji). Operatorem odpowiedzialnym za łączenie składowych znacz-
ników jest znak | (more). Są one definiowane jako elementy struktury zmiennej
inkrementowanej (licznika kursora), w naszym przypadku $i. W prezentowanym przy-
kładzie wyświetlane są znaczniki <Nazwisko> i <Imie> wszystkich osób o nazwisku
Kowalski (tabela 5.15).
Tabela 5.15. Zastosowanie funkcji XMLQuery do filtrowania pól XML oraz zwracania wybranej
listy znaczników
IdPracownika NazwiskoXML
2
3 <Nazwisko>Kowalski</Nazwisko><Imie>Karol</Imie>
1 <Nazwisko>Kowalski</Nazwisko><Imie>Jan</Imie>
Tabela 5.16. Równoważny symboliczny i literałowy zapis operatorów dla funkcji XMLQuery
= eq
> gt
>= ge
< lt
<= le
!= ne
Tabela 5.17. Zastosowanie funkcji XMLQuery do filtrowania pól XML oraz zwracania wybranej
listy znaczników, z niepoprawnym odwołaniem się do znacznika <Imie>
IdPracownika NazwiskoXML
3 <Nazwisko>Kowalski</Nazwisko>
1 <Nazwisko>Kowalski</Nazwisko>
SELECT * FROM
(SELECT IdPracownika,
XMLQuery(
'FOR $i IN /PRecord/Nazwisko
WHERE $i /Nazwisko = "Kowalski"
ORDER BY $i/Imie
RETURN $i/Nazwisko | $i/Imie'
PASSING BY VALUE Dane
RETURNING CONTENT) NazwiskoXML
FROM Pracownicy)
WHERE NazwiskoXML IS NOT NULL;
Poza możliwością wyświetlania zawartości pól typu XMLType istnieje możliwość wy-
konywania zapytań modyfikujących ją. Przykładem jest modyfikacja danych na po-
ziomie wskazanego znacznika dzięki zastosowaniu funkcji UPDATEXML, której pierwszym
parametrem jest nazwa kolumny zawierającej dane XML, drugim poziom oraz nazwa
wskaźnika, natomiast trzecim nowa wartość. Wskazanie zmienianego rekordu odbywa
się, podobnie jak w zwykłym zapytaniu modyfikującym, przez użycie klauzuli WHERE.
Prezentowany przykładowy skrypt składa się z trzech zapytań: pierwsze wyświetla iden-
tyfikator rekordu oraz zawartość znaczników <Nazwisko> i <Imie> przed zmianą (ta-
bela 5.18), drugie zmienia zawartość znacznika <Imie>, a trzecie wyświetla takie same
informacje jak zapytanie pierwsze, ale po dokonaniu zmiany (tabela 5.19).
EXTRACTVALUE(dane,'/PRecord/Nazwisko') AS NazwiskoXML,
EXTRACTVALUE(dane,'/PRecord/Imie') AS ImieXML
FROM Pracownicy WHERE IdPracownika = 1;
UPDATE Pracownicy
SET dane = UPDATEXML(Dane,
'/PRecord/Imie/text()','Jerzy')
WHERE IdPracownika = 1;
SELECT IdPracownika,
EXTRACTVALUE(dane,'/PRecord/Nazwisko') AS NazwiskoXML,
EXTRACTVALUE(dane,'/PRecord/Imie') AS ImieXML
FROM Pracownicy WHERE IdPracownika = 1;
UPDATE Pracownicy
SET dane = INSERTCHILDXML(dane,
'/PRecord', 'IdSzefa',
XMLType('<IdSzefa>1</IdSzefa>'))
WHERE IdPracownika = 2;
UPDATE Pracownicy
SET dane = APPENDCHILDXML(dane,
'/PRecord',
XMLType('<IdSzefa>1</IdSzefa>'))
WHERE EXTRACTVALUE(dane,
'/PRecord/Imie')='Jerzy';
Tabela 5.24. Skutek sprawdzenia istnienia węzła /PRecord/IdSzefa dla danych testowych
IdPracownika Wezel
2 1
3 0
1 1
Dokładnie tak samo jak pojedynczy węzeł, za pomocą funkcji APPENDCHILDXML można
dodać do istniejących danych XML wiele wskaźników równocześnie, w tym także za-
gnieżdżone. W prezentowanym przykładzie dopisywany jest poziom <Wyplaty>, w obrę-
bie którego dopisywane będą kolejne wartości <Brutto> (tabela 5.25). W pierwszym
kroku stworzony został właściwy poziom oraz pierwsza wypłata brutto. Skrypt zawiera
również zapytania pokazujące stan przed zmianą oraz po jej wprowadzeniu.
UPDATE Pracownicy
SET dane = APPENDCHILDXML(dane,
'/PRecord',
XMLType('<Wyplaty><Brutto>1111</Brutto></Wyplaty>'))
WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';
UPDATE Pracownicy
SET dane = INSERTXMLBEFORE(dane,
'/PRecord/Wyplaty/Brutto[1]',
XMLType('<Brutto>999</Brutto>'))
WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';
UPDATE Pracownicy
SET dane =
DELETEXML(Dane,
'/PRecord/Wyplaty/Brutto')
WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';
Oprócz wybierania zawartości pola XML dostępna jest też funkcja TABLE (tabela 5.30),
wyświetlająca tę zawartość tak, że każdy z wskaźników, począwszy od zdefiniowanego
poziomu, np. wskazanego w drugim parametrze funkcji EXTRACT, stanowi oddzielny rekord.
SELECT XMLDIFF(
XMLTYPE('
<Osoba><Nazwisko>Kowalski</Nazwisko><Imie>Jan</Imie>
</Osoba>
'),
XMLTYPE('
<Osoba><Nazwisko>Kowalski</Nazwisko></Osoba>
')
) xxx
FROM DUAL;
Rozdział 6.
PL/SQL
Podstawy składni
O języku PL/SQL, który jest zaimplementowany na platformie Oracle, zwykło się mó-
wić, że jest proceduralnym rozszerzeniem języka SQL, jednakże spora część progra-
mistów przychyla się raczej do twierdzenia, że PL/SQL jest proceduralnym językiem
programowania, który zawiera w sobie elementy składni SQL. Spór ten wydaje się aka-
demicki, zwłaszcza że praktycznie zawsze używamy końcówek klienckich PL/SQL, bez
względu na to, czy zadajemy proste zapytanie do bazy, czy też tworzymy bardziej za-
awansowany kod proceduralny. Tym niemniej autorowi bliższy jest drugi sposób pa-
trzenia na ten język. W PL/SQL zdefiniowane zostały znaki specjalne (tabela 6.1), z których
większość jest już czytelnikowi znana z części poświęconej SQL. Na uwagę zasługują
znak przypisania := oraz symbole komentarzy: jednoliniowego – oraz wieloliniowego
/* ... */.
Poza obowiązkowymi elementami deklaracji zmiennej, które stanowią jej nazwa oraz
typ, możliwe jest zdefiniowanie ograniczenia przyjmowania wartości NULL oraz przy-
pisanie wartości początkowej (czasami nazywanej domyślną). Wartość początkowa
zmiennej może zostać zastąpiona (nadpisana) w kodzie programu dowolną inną war-
tością (z ograniczeniami wynikającymi jedynie z typu zmiennej i ograniczenia NOT NULL).
Należy pamiętać, że niezainicjowana zmienna domyślnie jest inicjowana wartością NULL.
Podobnie do deklaracji zmiennej wygląda ogólna postać deklaracji stałej.
<nazwa stałej> constant < typ_danych > := <wartość>;
Jedyną różnicą jest to, że stała musi mieć przypisaną wartość, a ta nie może być NULL.
Wartości przypisanej do stałej nie można zmieniać w programie. Przykłady deklaracji
zmiennych i stałych zawiera poniżej prezentowany fragment kodu.
licz number (5) NOT NULL := 1111;
nazwa varchar2(30);
wzrost real(5.2) := 1.75;
Pi constant real := 3.14159;
dzisiaj date: = SYSDATE;
stan boolean;
a,b,c number;
Należy zauważyć, że jeżeli definiujemy kilka zmiennych takiego samego typu, mo-
żemy to wykonać w jednej linii, a listę tych zmiennych oddzielamy przecinkami. Inicjo-
wanie zmiennych może odbywać się za pomocą wartości stałych lub wbudowanych funk-
cji — w przykładzie zastosowano funkcję SYSDATE zwracającą bieżącą datę systemową.
W przypadku definiowania zmiennej, zamiast odwoływać się do typów podstawowych,
możliwe jest odwołanie się do typów wynikających z definicji innych obiektów bazy
(pól, rekordów). W takiej sytuacji deklaracja ma postać ogólną:
< nazwa_zmiennej > <obiekt> %TYPE [not null] [:= < wartość_pocz >];
Po słowie kluczowym DECLARE występuje sekcja deklaracji, w której może być zadekla-
rowana dowolna liczba zmiennych i stałych. Sekcja zawarta w klamrze słów kluczowych
BEGIN oraz END stanowi ciało bloku anonimowego. W jego obszarze może znajdować się
Rozdział 6. ♦ PL/SQL 157
Jak widać, tak proste zadanie wymaga całkiem rozbudowanego skryptu. W przykładzie
zastosowano pustą sekcję deklaracji. W takim przypadku słowo kluczowe DECLARE mo-
głoby zostać w skrypcie pominięte, jednakże użyto go tu, ponieważ sekcja deklaracji
jest jedyną, której zawartość może być pusta. Inne sekcje wymagają istnienia chociaż
pojedynczej instrukcji w ich obrębie. W celu wyprowadzenia tekstu użyta została pro-
cedura PUT_LINE, która jest elementem wbudowanego pakietu DBMS_OUTPUT. Jeżeli
używamy końcówki klienta z wersji 10. lub starszej (SQL*Plus Worksheet lub SQL Plus),
ograniczenie się do trzech ostatnich linijek przykładu nie przyniosłoby spodziewanego
skutku. Otrzymalibyśmy komunikat Procedura PL/SQL została przetworzona pomyślnie,
natomiast tekst komunikatu nie zostałby wyświetlony. Wynika to z faktu, że w stanie
domyślnym standardowe urządzenie wyjściowe jest zablokowane (trywializując, zablo-
kowane jest wyświetlanie na monitorze). W celu jego odblokowania musimy zmienić
flagę systemową, wykonując polecenie SET SERVEROUTPUT ON;, które powinno być
pierwszym poleceniem skryptu. Teoretycznie stan aktywności wyjścia powinien utrzy-
mywać się do końca sesji (przelogowanie, wyłączenie końcówki klienta), jednak w prak-
tyce okazuje się, że czasami ponowna blokada uniwersalnego wyjścia pojawia się podczas
jej trwania. Dlatego we wszystkich prezentowanych przykładach polecenie „odbloko-
wania monitora” jest pierwszą linią kodu. W przypadku stosowania końcówki klienta
z wersji 11. (SQL Navigator) używanie polecenia SET SERVEROUTPUT prowadzi do po-
wstania błędu przetwarzania (rysunek 6.1).
Rysunek 6.1.
Widok SQL
Developera
po wykonaniu
bloku anonimowego
z aktywnym
poleceniem
odblokowania
standardowego
wyjścia
Rysunek 6.2.
Widok SQL
Developera ze
wskazaniem sposobu
odblokowania
standardowego
wyjścia
Jak pokazano w przykładzie, instrukcja inkrementująca zmienną licz wykona się tylko
wtedy, gdy przed instrukcją warunkową zmienna ta będzie większa niż 18. Instrukcje
warunkowe mogą być w sobie zagnieżdżane, jak to pokazuje kolejny przykład.
IF var1 > 10 THEN
IF var2 < var1 THEN
var2 : = var1 + 20;
END IF;
END IF;
Rysunek 6.3.
SQL Developer
po poprawnym
wykonaniu bloku
anonimowego
z wyłączonym
poleceniem
odblokowania
standardowego wyjścia
i jego odblokowaniem
za pomocą ikony
(widok zakładki
DBMS Output)
Rysunek 6.4.
SQL Developer
po poprawnym
wykonaniu bloku
anonimowego
z wyłączonym
poleceniem
odblokowania
standardowego wyjścia
i jego odblokowaniem
za pomocą ikony
(widok zakładki
Script Output)
różną od TRUE (FALSE lub NULL), nie była wykonywana żadna instrukcja — ciało instruk-
cji warunkowej było pomijane. Możliwe jest zastosowanie sekcji ELSE, której instrukcje
wykonają się, kiedy warunek nie będzie prawdziwy.
IF var1 > 10 THEN
var2 : = var1 + 20;
ELSE
var2 : = var1 * var1;
END IF;
Dla wartości zmiennej var1 większych niż 10 wykona się instrukcja przypisująca zmiennej
var2 wartość zmiennej var1 powiększoną o 20. Dla zmiennej var1 mniejszej niż 10 lub
mającej wartość NULL zmienna var2 będzie jej drugą potęgą (zapisano w postaci ilo-
czynu). Zamiast stosowania instrukcji warunkowej w celu sprawdzenia wielu różnych
warunków, możliwe jest użycie jednej lub wielu sekcji ELSEIF. Warunki sprawdzane są
po kolei i wykonywane są instrukcje dla tego z nich, którego wartość jest prawdziwa.
Jeżeli żaden z warunków nie jest prawdziwy, wykonywane są instrukcje występujące
w sekcji ELSE — o ile ta istnieje.
160 Część II ♦ ORACLE PL/SQL
Należy pamiętać o tym, że aby skutecznie wyjść z pętli, zmienna, na której opierając się,
budujemy warunek, musi być zmieniana (najczęściej jest inkrementowana). Równie
ważne jest nadanie jej wartości początkowej, albo podczas deklaracji, albo, jak uczy-
niono w przykładzie, przed rozpoczęciem pętli. W przeciwnym przypadku początkowa
wartość zmiennej będzie wynosić NULL i inkrementacja nie spowoduje jej zmiany.
O wiele częściej niż pętli bezwarunkowej używamy konstrukcji, w której składnię wbu-
dowano warunek, na przykład pętli WHILE warunek ... LOOP ... END LOOP.
cut : = 1;
WHILE cut < = 100 LOOP
…
cut : = cut + 1; -- inkrementacja licznika
END LOOP;
W przypadku poprzednich pętli nie zawsze panujemy nad liczbą wykonanych przebie-
gów, zwłaszcza kiedy warunek zakończenia przetwarzania jest złożony. Gdy chcemy,
aby instrukcje zawarte w pętli wykonały się dokładnie określoną liczbę razy, stosujemy
konstrukcję FOR ... IN ... LOOP ... END LOOP. Po słowie kluczowym FOR występuje
zmienna, która jest inkrementowana, począwszy od pierwszej wartości występującej
po słowie kluczowym IN, aż do osiągnięcia drugiej wartości. Licznik zmienia się zaw-
sze o jeden. Wartości wyznaczające zakres zmian mogą być zarówno stałymi, jak i zmien-
nymi lub też stanowić wyrażenia.
Rozdział 6. ♦ PL/SQL 161
W przypadku CONTINUE WHEN warunek instrukcje występujące po niej wykonają się tylko
wtedy, kiedy warunek nie zostanie spełniony. Rezultat wykonania skryptu pokazuje,
że dla wartości i > 2 komunikaty Przed nie mają par w postaci komunikatów Po.
Przed CONTINUE i = 1
Po CONTINUE i = 1
Przed CONTINUE i = 2
Po CONTINUE i = 2
Przed CONTINUE i = 3
Przed CONTINUE i = 4
Instrukcja CONTINUE może być stosowana dla każdego rodzaju pętli, jaki możemy zde-
finiować w PL/SQL.
Tak jak to już wcześniej zauważono, za wyjątkiem sekcji deklaracji wszystkie inne
muszą zawierać przynajmniej jedną instrukcję. Podobnie sprawa wygląda z instrukcjami
warunkowymi czy pętlami — nie jest dozwolona pusta zawartość. Czasami jednak po-
trzebne jest zbudowanie „zaślepek”, czyli pustego kodu w ich obrębie. W takim przy-
padku musimy jawnie „powiedzieć” silnikowi bazy: „Nie rób nic”, czyli użyć pustej
instrukcji NULL.
IF licz >= 10 THEN
NULL;
ELSE
...
END IF;
Jak widać, literał NULL ma w PL/SQL wiele znaczeń — wartość logiczna, brak wartości,
pusta instrukcja. Ostatnim podstawowym elementem składniowym jest instrukcja skoku
bezwarunkowego GOTO.
GOTO etykieta;
…
<<etykieta>>
…
Zamiast słowa kluczowego IS można stosować tożsame słowo kluczowe AS. Ciałem
procedury może być dowolny zestaw instrukcji PL/SQL oraz dowolne zapytania SQL,
za wyjątkiem zapytania wybierającego SELECT.
W przypadku środowisk baz danych wydaje się jednak, że argumenty związane z or-
ganizacją kodu nie są tu najważniejsze. Dla procedur składowanych najpóźniej przy
pierwszym ich wykonaniu generowany jest plan wykonania zapytań wchodzących w ich
skład, ich kod jest wstępnie optymalizowany, a następnie są one prekompilowane. W ten
sposób na serwerze przechowywana jest procedura w dwóch postaciach: przepisu na
jej utworzenie CREATE PROCEDURE oraz skompilowanego kodu. Przy każdym następnym
wykonaniu odwołujemy się już do postaci skompilowanej. Sprawia to, że procedury są
wykonywane szybciej i wydajniej niż ten sam kod w formie skryptu. Sprawa nie jest tak
oczywista w przypadku często wykonywanych zapytań. Plany wykonania każdego za-
pytania przechowywane są w pamięci współdzielonej (SGA — System Global Area) i je-
żeli wykonamy je ponownie, wykorzystana zostanie jego przetworzona postać. Należy
jednak pamiętać, że zgodność dwóch zapytań sprawdzana jest z dokładnością do znaku,
nie jest natomiast analizowana semantyka. Poza tym informacje o planach zapytań są
nadpisywane na stare definicje w momencie wyczerpania zasobów pamięci współdzie-
lonej. Bez względu na te uwagi możemy jednak przyjąć, że wydajność przetwarzania
procedury składowanej jest większa niż w przypadku równoważnego jej skryptu (bloku
anonimowego). Jeżeli chcemy usunąć definicję procedury, możemy użyć następującego
polecenia:
164 Część II ♦ ORACLE PL/SQL
Jak widać, ciało procedury zawiera zapytanie aktualizujące UPDATE zgodne ze składnią
SQL. Jej wywołanie może odbyć się na przykład z wnętrza bloku anonimowego o postaci:
BEGIN
up;
END;
Jeżeli chcemy użyć w definicji procedury więcej niż jednego parametru, ich lista po-
winna być rozdzielona przecinkami. W przykładzie przedstawiono procedurę, która
wstawia do tabeli Dodatki wiersze zawierające sumę wartości brutto i stałej danej dru-
gim parametrem procedury Dodatek dla osoby, której identyfikator zawiera pierwszy
z parametrów Num.
CREATE OR REPLACE PROCEDURE Dodaj
(Num number, Dodatek number)
IS
BEGIN
INSERT INTO Dodatki
SELECT Brutto+Dodatek FROM Zarobki
WHERE IdOsoby = Num;
END;
Oczywiście procedura może zawierać więcej niż jeden parametr typu OUT. Załóżmy,
że oprócz liczby osób o wzroście większym od wartości progowej chcemy wyznaczyć
ich średni wzrost. Możemy zastosować dwa zapytania agregujące, które określą intere-
sujące nas wartości, jednak bardziej wydajne jest użycie podwójnego przypisania w zapyta-
niu SELECT ... INTO. Jeśli zwraca ono jeden wiersz, to po słowie kluczowym INTO mu-
simy umieścić tyle zmiennych, ile wyrażeń jest wyznaczanych w jego pierwszej części.
Rozdział 7. ♦ Procedury składowane 167
Jeżeli jednak chcemy przy tak zdefiniowanych parametrach odwołać się do wartości do-
myślnej, to musimy zastosować wywołanie nazewnicze o postaci parametr=>zmienna,
gdzie parametr jest nazwą parametru formalnego procedury, a zmienna jest wartością
aktualną tego parametru w miejscu, z którego ją wywołujemy. Notację tę możemy
odczytywać jako „parametr staje się zmienną”. W przypadku takiego wywołania
zmienna, która nie została w nim wymieniona, będzie miała przypisaną wartość domyślną.
Gdyby w definicji procedury pominięta w wywołaniu zmienna nie miała zdefiniowanej
wartości domyślnej, to takie wywołanie spowodowałoby wyświetlenie komunikatu
o błędzie.
SET SERVEROUTPUT ON;
DECLARE
ile number;
BEGIN
wysocy(ile => ile);
DBMS_OUTPUT.PUT_LINE(ile);
END;
Oczywiście bardziej ogólne jest wywołanie nazewnicze, które, bez względu na to, na
których pozycjach znajdują się wartości domyślne, jest zawsze poprawne.
SET SERVEROUTPUT ON;
DECLARE
ile number;
BEGIN
wysocy(ile => ile);
DBMS_OUTPUT.PUT_LINE(ile);
END;
Wywołanie nazewnicze jest dopuszczalne również wtedy, kiedy podajemy pełny ze-
staw parametrów. W takim przypadku kolejność ich wymieniania nie odgrywa żadnej roli.
SET SERVEROUTPUT ON;
DECLARE
ile number;
BEGIN
wysocy(mm=>1.8, ile=>ile);
DBMS_OUTPUT.PUT_LINE(ile);
END;
Do wersji 11. możliwe było stosowanie albo tylko wywołania pozycyjnego, albo na-
zewniczego — jednoczesne użycie obu tych typów nie było dozwolone. Od wersji 11.
istnieje już taka możliwość. Przeanalizujmy to na przykładzie procedury o trzech pa-
rametrach numerycznych. Pierwsze dwa są typu IN, a trzeci typu OUT. W ciele proce-
dury wartość parametru wyjściowego c jest wyznaczana w postaci ilorazu dwóch pierw-
szych parametrów.
CREATE OR REPLACE PROCEDURE dziel
(a real, b real, c OUT real)
AS
BEGIN
c:=a/b;
END;
Rozdział 7. ♦ Procedury składowane 169
Nie każde wywołanie mieszane jest jednak dopuszczalne. Dozwolone są tylko takie
przypadki, w których pierwsze na liście parametry podstawiane są pozycyjnie, a po-
zostałe nazewniczo. Kolejny przykład pokazuje niepoprawne użycie wywołania mie-
szanego, gdzie błąd polega na tym, że środkowy parametr jest dany nazewniczo, czyli,
inaczej mówiąc, że po parametrze danym nazewniczo istnieje choć jeden dany pozy-
cyjnie.
DECLARE
res real;
BEGIN
dziel(10, b => 9, res);
DBMS_OUTPUT.PUT_LINE('Wynik ' || res);
END;
Przy okazji prezentowania tego przykładu chcę przedstawić nieco archaiczne wywo-
łanie procedury z zastosowaniem polecenia CALL. Poprawnymi parametrami są tu na-
zwy funkcji operujących na zmiennych łańcuchowych: UPPER — przepisanie łańcucha
do postaci pisanej tylko dużymi literami, LOWER — przepisanie łańcucha do postaci pi-
sanej tylko małymi literami, INITCAP — przepisanie łańcucha do postaci pisanej od
dużej litery. Poprawne jest również użycie pustego ciągu znaków '' lub ciągu skła-
dającego się z samych spacji ' '. Zaletą tego typu wywołania jest możliwość za-
stosowania bezpośrednio po nim zapytania wybierającego, które ma za zadanie spraw-
dzić poprawność wykonania procedury.
CALL exe_tekst('UPPER');
SELECT * FROM osoby;
Możemy przekształcić procedurę, tak aby w jej ciele użyć wywołania poprzednio utwo-
rzonej procedury generującej błąd przetwarzania o nazwie Blad.
CREATE OR REPLACE PROCEDURE exe_tekst
(typ varchar2)
IS
zap varchar2(111);
BEGIN
IF UPPER(typ) NOT IN('UPPER', 'LOWER', 'INITCAP') THEN
Blad;
ELSE
zap:= 'UPDATE osoby SET Nazwisko=' || typ || '(Nazwisko)';
EXECUTE IMMEDIATE zap;
END IF;
END exe_tekst;
Błędy mogą się jednak pojawiać podczas przetwarzania nie tylko na skutek celowej
działalności programisty, ale mogą być też spowodowane nie zawsze dającymi się
przewidzieć zdarzeniami, błędnymi wywołaniami, nieodpowiednimi parametrami etc.
Możemy mówić wtedy o sytuacji wyjątkowej — o powstaniu wyjątku. Takie zdarzenia
mogą zostać w PL/SQL obsłużone, oprogramowane.
172 Część II ♦ ORACLE PL/SQL
Rozważmy przykład procedury, której zadaniem jest określenie, czy pracownik o danym
numerze identyfikacyjnym IdOsoby, wskazanym parametrem num, istnieje w tabeli
Osoby. Jeśli tak, drugi z parametrów (status) ma przyjąć wartość 1; w przypadku
przeciwnym 0. W celu realizacji tego zadania zastosowano zapytanie wybierające zwra-
cające do zmiennej pomocniczej kto identyfikator osoby. Jeżeli pracownik o danym
identyfikatorze istnieje, wartość zwrócona przez zapytanie i wartość parametru będą
takie same, jeśli jednak takiego pracownika nie ma, zapytanie wybierające nie zwróci
żadnego wiersza. Spowoduje to, że próba podstawienia pod zmienną kto zakończy się
błędem przetwarzania: Nie znaleziono żadnych wierszy. Stan ten możemy wykorzy-
stać, wprowadzając sekcję EXCEPTION i oprogramowując wyjątek NO_DATA_FOUND, któ-
rego obsługa wykonywana jest według schematu WHEN nazwa_wyjątku THEN instrukcje.
W naszym przypadku obsługa wyjątku zawiera podstawienie odpowiedniej wartości pod
zmienną status oraz wypisanie komunikatu.
CREATE OR REPLACE PROCEDURE czy_jest
(num IN NUMBER, status OUT NUMBER)
IS
kto NUMBER;
BEGIN
SELECT IdOsoby INTO kto
ROM Osoby WHERE IdOsoby = num;
IF (kto = num) THEN
status := 1;
DBMS_OUTPUT.PUT_LINE ('Pracownik istnieje');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
status := 0;
DBMS_OUTPUT.PUT_LINE('Pracownik nie istnieje');
WHEN OTHERS THEN
NULL;
END;
Istnieje formalna możliwość obsłużenia dwóch lub więcej wyjątków w tym samym
miejscu sekcji ich obsługi. W tym celu po słowie kluczowym WHEN łączymy nazwy sym-
boliczne wyjątków operatorem logicznym OR.
CREATE OR REPLACE PROCEDURE czy_jest
(num IN NUMBER, status OUT NUMBER, ok OUT NUMBER)
IS
BEGIN
ok := 0;
SELECT COUNT(IdOsoby) INTO status
FROM Osoby WHERE IdOsoby = num;
IF (status = 1) THEN
DBMS_OUTPUT.PUT_LINE ('Pracownik istnieje');
ELSE
DBMS_OUTPUT.PUT_LINE('Pracownik nie istnieje');
END IF;
EXCEPTION
WHEN INVALID_NUMBER OR NO_DATA_FOUND THEN
Rozdział 7. ♦ Procedury składowane 175
ok := 11;
DBMS_OUTPUT.PUT_LINE('Błąd wartości');
WHEN OTHERS THEN
ok := 99;
DBMS_OUTPUT.PUT_LINE('Błąd przetwarzania');
END;
Nie zawsze jest tak, że wyjątek musi wiązać się z formalnym błędem przetwarzania.
Czasami wygodnym jest, aby pewne sytuacje nieprowadzące do błędów formalnych
były traktowane jako wyjątkowe. Mówimy wtedy o wyjątkach użytkownika, które muszą
zostać zdefiniowane, wykryte i które powinny zostać obsłużone. W prezentowanym
przykładzie za sytuację wyjątkową będziemy chcieli uznać fakt, że nie ma osób o wzro-
ście wyższym od progu danego parametrem. Jak widać, taka sytuacja nie spowoduje
powstania błędu przetwarzania — trzeba więc ją wykryć.
CREATE OR REPLACE PROCEDURE licz
(mini NUMBER, ile out INT)
IS
brakuje EXCEPTION;
BEGIN
SELECT COUNT(IdOsoby) INTO ile FROM Osoby
WHERE Wzrost > mini;
IF (ile = 0) THEN
RAISE brakuje;
END IF;
EXCEPTION
WHEN brakuje THEN
RAISE;
WHEN OTHERS THEN
NULL;
END;
Deklaracji wyjątku użytkownika, tak samo jak każdej innej zmiennej, dokonujemy
w sekcji deklaracji i nadajemy mu typ EXCEPTION (jak widać, to słowo kluczowe pełni
podwójną rolę: jest określeniem typu oraz sygnalizuje początek sekcji obsługi wyjątków).
W przykładzie zdefiniowano wyjątek o nazwie brakuje. Po zliczeniu osób o wzroście
wyższym od wartości progowej instrukcją warunkową sprawdzono, czy ich liczba jest
równa 0, po czym dla takiego przypadku poleceniem RAISE nazwa_wyjątku ustawiono
(wygenerowano) błąd użytkownika. Obsługi wyjątku użytkownika dokonujemy na ta-
kich samych zasadach jak wyjątków wbudowanych (systemowych). W przykładzie za-
stosowano drugą, po minimalnej obsłudze NULL, najczęściej spotykaną metodę — użycie
polecenia RAISE, które odpowiada za wygenerowanie wyjątku. Spowoduje to propa-
gację wyjątku do miejsca, z którego procedura została wywołana. Można powiedzieć,
że nie da się z sekcji obsługi wyjątków przenieść się do tej samej sekcji, więc wyjątek
zgłoszony w sekcji obsługi wyjątków musi zostać obsłużony „piętro wyżej” — w pro-
cedurze wywołującej.
Możemy również, zamiast stosować do tworzenia funkcji składnię CREATE FUNCTION, sko-
rzystać ze składni rozbudowanej CREATE OR REPLACE FUNCTION, która pozwala na jej
stworzenie lub nadpisanie na istniejącej już funkcji o tej samej nazwie. Widok SQL
Developera po wykonaniu zapytania tworzącego funkcję przedstawia rysunek 8.1.
Jedyną różnicą w ciele funkcji w porównaniu z procedurą jest to, że zawiera ona (z re-
guły przy końcu ciała) co najmniej jedno słowo kluczowe RETURN, po którym umiesz-
czona jest zwracana przez nią wartość (dopuszczalna jest zarówno stała, jak i zmienna).
W prezentowanym przykładzie zastosowano zmienną pomocniczą ile. Ponieważ słowo
kluczowe RETURN kończy przetwarzanie funkcji (powoduje wyjście z niej), albo jest ono
dokładnie ostatnią instrukcją jej ciała, albo jest umieszczane w sekcjach instrukcji wa-
runkowej (instrukcji warunkowych).
Ponieważ funkcja zawsze zwraca wartość przez nazwę, jej wywołanie z bloku anoni-
mowego jest równoznaczne z przypisaniem zwracanej wartości pod zmienną lub wy-
korzystaniem jako parametru innej funkcji.
SET SERVEROUTPUT ON;
DECLARE
ile NUMBER;
BEGIN
ile:=liczf(1.8);
DBMS_OUTPUT.PUT_LINE (ile);
DBMS_OUTPUT.PUT_LINE (liczf(1.9));
END;
Rysunek 8.2. SQL Developer po wykonaniu bloku anonimowego — zakładka Script Output
Rozdział 8. ♦ Funkcje w PL/SQL 181
Rysunek 8.3. SQL Developer po wykonaniu bloku anonimowego — zakładka DBMS Output
Podobnie wygląda sprawa, jeśli stosujemy końcówkę klienta w wersji 10g lub starszej,
czyli SQL*Plus Worksheet (rysunek 8.5).
Rysunek 8.5.
SQL*Plus Worksheet
po wykonaniu skryptu
anonimowego
ile INT;
BEGIN
SELECT COUNT(IdOsoby) INTO Ile FROM Osoby
WHERE Wzrost > mini;
ilep := ile;
RETURN ile;
END;
Tak samo jak parametr typu OUT, na liście parametrów może się pojawić parametr typu
IN OUT, który może zostać wykorzystany do dwukierunkowego przekazywania danych
do funkcji i z niej. Dla przejrzystości przykładu zachowano zmienną pomocniczą ile,
jednak możliwa jest jej całkowita eliminacja i zastąpienie jej zmienną mini.
CREATE OR REPLACE FUNCTION Liczf
(mini IN OUT NUMBER)
RETURN INT
IS
ile INT;
BEGIN
SELECT COUNT(IdOsoby) INTO ile FROM Osoby
WHERE Wzrost > mini;
mini := ile;
RETURN ile;
END;
— takiego, jaki ma pole IdOsoby z tabeli Zarobki. Taka definicja w wielu przypad-
kach skutecznie chroni przed zmianą typu danych w definicji tabeli.
CREATE OR REPLACE FUNCTION razem
(kto IN Zarobki.IdOsoby%TYPE)
RETURN NUMBER
IS
policz NUMBER;
BEGIN
SELECT SUM(Brutto) INTO policz FROM zarobki
WHERE idosoby=kto;
RETURN policz;
END;
Jeżeli funkcja zwraca wartość tylko przez nazwę (nie ma żadnego parametru typu OUT
lub IN OUT — wszystkie są typu IN), to może zostać wykorzystana w zapytaniu wy-
bierającym. W prezentowanym przykładzie zastosowanie funkcji razem pozwala wy-
eliminować z zapytania złączenie i grupowanie oraz nie widać w sposób jawny funk-
cji agregującej. Powoduje to znaczne uproszczenie zapisu — niestety, rzadko kiedy
wiąże się to ze wzrostem wydajności (w większości przypadków raczej tracimy).
SELECT Nazwisko, razem(IdOsoby) FROM Osoby;
Jednak odwoływanie się w sekcji obsługi wyjątków do policzonej wartości może oka-
zać się nieskuteczne, ponieważ błąd przetwarzania może się pojawić przed jej wyzna-
czeniem lub na skutek błędu wartość ta może być nieokreślona. Stąd dobrym pomysłem
na funkcję zawierającą obsługę wyjątków jest idea, aby przez nazwę przekazywała ona
stan wykonania (0 — poprawnie, <>0 — błąd), a policzona wartość była przekazywana
przez parametr typu OUT.
CREATE OR REPLACE FUNCTION Liczf
(mini NUMBER DEFAULT 1.8, ile OUT Number) RETURN INT
IS
Brakuje EXCEPTION;
BEGIN
SELECT COUNT(IdOsoby) INTO ile FROM Osoby
WHERE Wzrost > mini;
IF (ile = 0) THEN
RAISE brakuje;
END IF;
RETURN 0;
EXCEPTION
WHEN brakuje THEN
RETURN 99;
WHEN OTHERS THEN
RETURN SQLCODE;
END;
Przy okazji prezentowania metod tworzenia funkcji warto zamieścić tu dwa ciekawe
przykłady zastosowania wbudowanych funkcji Oracle. W pierwszym realizowana jest
konwersja liczby naturalnej danej numerycznie na napis w języku angielskim.
SELECT TO_CHAR(TO_DATE(12345, ’J’), ’JSP’) FROM DUAL
SELECT
TO_NUMBER(SUBSTR(TO_CHAR(SYSDATE, 'yyyymmdd'),1,4)) AS Rok
FROM DUAL;
RETURN SQLCODE;
END;
FUNCTION liczf2(ile OUT INT) RETURN INT
IS
BEGIN
SELECT COUNT(Idosoby) INTO ile FROM Osoby
WHERE rokUrodz > (SELECT AVG(RokUrodz)
FROM Osoby);
RETURN 0;
EXCEPTION
WHEN OTHERS THEN
RETURN SQLCODE;
END;
END;
Wywołanie obiektu pakietu odbywa się zawsze poprzez podanie nazwy kwalifikowanej,
na którą składa się nazwa pakietu, kropka separująca oraz nazwa obiektu (funkcji lub
procedury). Pozostałe elementy wywołania, takie jak lista parametrów oraz przypisa-
nie funkcji do zmiennej, są takie same jak w przypadku takich obiektów zdefiniowa-
nych poza pakietem.
SET SERVEROUTPUT ON;
DECLARE
ile INT;
blad INT;
BEGIN
blad:=pakiet.liczf2(ile);
DBMS_OUTPUT.PUT_LINE(ile);
DBMS_OUTPUT.PUT_LINE(blad);
blad:=pakiet.liczf1(1970, ile);
DBMS_OUTPUT.PUT_LINE(ile);
DBMS_OUTPUT.PUT_LINE(blad);
END;
Zmodyfikujmy poprzedni przykład w taki sposób, aby nazwy obu funkcji były jedna-
kowe — liczf. Pomimo takiej zmiany pakiet jest tworzony poprawnie.
CREATE OR REPLACE PACKAGE pakiet
IS
FUNCTION liczf(mini INT, ile OUT INT) RETURN INT;
FUNCTION liczf(ile OUT INT) RETURN INT;
END;
Rozdział 9. ♦ Pakiety 189
Sytuacja, kiedy dwa różne elementy mają taką samą nazwę, jest dobrze znana w obiek-
towych językach programowania. Nosi tam nazwę przeciążenia i dotyczy klas. W PL/SQL
również mamy do czynienia z przeciążeniem, ale zjawisko to może występować tylko
w obrębie pakietu. Obiekty znajdujące się w tym samym pakiecie są przeciążone, jeśli
są tego samego rodzaju (funkcja, procedura) i mają takie same nazwy, ale różne listy
parametrów. Te ostatnie uznajemy za odmienne, jeśli mają różną liczbę elementów
albo przy takiej samej ich liczbie typ przynajmniej jednego parametru różni się od typu
parametru znajdującego się na tej samej pozycji na drugiej liście. Z powyżej przyto-
czonych wymagań wynika, że można przeciążyć procedurę procedurą albo funkcję
funkcją (nie jest możliwe przeciążenie procedury funkcją i odwrotnie). Jest to spo-
strzeżenie ważne o tyle, że w części z przygotowanych przez Oracle skryptów gene-
rujących pakiety mamy do czynienia z tym, że istnieją np. dwie funkcje oraz dwie
procedury, wszystkie o takiej samej nazwie. Funkcje są dokładnymi odpowiednikami
procedur i to użytkownik powinien przed wykonaniem skryptu zadecydować, czy chce
korzystać z przeciążonych wersji jednych, czy drugich. Kolejna uwaga dotyczy różnicy
typów dwóch parametrów — jest ona rozumiana w szerszym znaczeniu. Jak wiemy,
listy parametrów formalnych nie zawierają długości typów, stąd nie ma rozróżnienia
między liczbą całkowitą integer czy rzeczywistą real — obie i tak mają wewnętrzny typ
number (numeryczny). To samo dotyczy zmiennych typów char, varchar, nchar, nvarchar;
190 Część II ♦ ORACLE PL/SQL
wszystkie one są ogólnego typu znakowego. Stąd wniosek, że możemy przeciążać wza-
jemnie typy liczbowe, znakowe i daty. Kolejna konkluzja dotyczy tylko sposobu prze-
ciążania funkcji. Każda z nich zwraca wartość przez nazwę (typ tej wartości jest defi-
niowany w nagłówku funkcji po słowie kluczowym RETURN). Otóż nie jest możliwe
przeciążenie funkcji tylko na skutek zmiany typu zwracanej przez nazwę wartości. Próba
takiego przeciążenia kończy się komunikatem o błędzie. W prezentowanym przykła-
dzie ciała pakietu, oprócz dwóch funkcji przeciążonych liczf, zdefiniowano blok ini-
cjalny zawierający pustą instrukcję oraz prostą obsługę wyjątku OTHERS przez usta-
wienie błędu — RAISE. Rola bloku inicjalnego zostanie przybliżona przy omawianiu
kolejnej modyfikacji pakietu.
Wywołanie pakietu zawierającego funkcje lub procedury przeciążone nie różni się ni-
czym od wywołania zwykłego pakietu i tak jak poprzednio wymaga stosowania nazw
kwalifikowanych. O tym, która z funkcji zostanie wykonana, decyduje liczba para-
metrów lub ich typy podane w wywołującym te funkcje bloku.
SET SERVEROUTPUT ON;
DECLARE
ile INT;
blad INT;
BEGIN
blad:=pakiet.liczf(ile);
DBMS_OUTPUT.PUT_LINE(ile);
DBMS_OUTPUT.PUT_LINE(blad);
blad:=pakiet.liczf(1970, ile);
DBMS_OUTPUT.PUT_LINE(ile);
DBMS_OUTPUT.PUT_LINE(blad);
blad:=pakiet.liczf(2270, ile);
DBMS_OUTPUT.PUT_LINE(ile);
DBMS_OUTPUT.PUT_LINE(blad);
END;
W definicji pakietu, poza nagłówkami zawartych w nim funkcji (procedur), mogą zna-
leźć się też deklaracje zmiennych. Tak zadeklarowane, są one zmiennymi globalnymi
i możliwe jest odwoływanie się do nich spoza pakietu.
CREATE OR REPLACE PACKAGE pakiet
IS
globalna INT;
FUNCTION liczf(mini INT, ile OUT INT) RETURN INT;
FUNCTION liczf(ile OUT INT) RETURN INT;
END;
Przy pierwszym wywołaniu globalna ma wartość NULL, ponieważ nie wywołano żad-
nego elementu proceduralnego. Kolejne wywołanie wyświetla podstawioną w części
inicjalizacyjnej pakietu wartość. Nie jest ona zmieniana aż do końca sesji, chyba że
zrobiono to jawnie w procedurze (funkcji) pakietu lub w skrypcie wywołującym.
Załóżmy teraz, że nasz pakiet zostanie wzbogacony o kolejną funkcję, tym razem
nieprzeciążoną, o nazwie zmien, która ma dwa znakowe parametry wejściowe i zwraca
wartość typu INT.
CREATE OR REPLACE PACKAGE pakiet
IS
globalna INT;
FUNCTION liczf(mini INT, ile OUT INT) RETURN INT;
FUNCTION liczf(ile OUT INT) RETURN INT;
FUNCTION zmien(pole VARCHAR, kod CHAR) RETURN INT;
END;
staci pisanej dużymi literami, aby zapewnić niewrażliwość na sposób jego wprowadzania,
a przez nazwę zwraca odpowiednią funkcję znakową. Dekodowanie wykonywane jest
dzięki zastosowaniu zagnieżdżonych instrukcji warunkowych. W przypadku wprowa-
dzenia znaku spoza dopuszczalnego zakresu zwracana jest wartość pusta NULL. Należy
zauważyć, że funkcja dekoduj nie została zadeklarowana w pakiecie, a tylko występuje
w jego ciele. O takich funkcjach (procedurach) mówimy, że są wewnętrznymi funk-
cjami (procedurami) pakietu. Można je wywoływać tylko z ciała pakietu, w którym
zostały zdefiniowane, natomiast nie można tego dokonać spoza niego — są tam „nie-
widoczne”.
CREATE OR REPLACE PACKAGE BODY pakiet
IS
FUNCTION liczf(mini INT, ile OUT INT)RETURN INT
...
END;
FUNCTION liczf(ile OUT INT)RETURN INT
...
END;
FUNCTION dekoduj(kod CHAR) RETURN VARCHAR;
FUNCTION zmien(pole VARCHAR, kod CHAR) RETURN INT
IS
fun VARCHAR(11);
ttt VARCHAR(200);
BEGIN
Fun := dekoduj(UPPER(kod));
ttt := 'UPDATE Osoby SET ' || pole || '=' || fun || '(' || pole || ')';
EXECUTE IMMEDIATE ttt;
RETURN 0;
EXCEPTION
WHEN OTHERS THEN
RETURN 1;
END;
FUNCTION dekoduj(kod CHAR) RETURN VARCHAR
IS
fun VARCHAR(11);
BEGIN
IF (kod=’U’) THEN
fun:=’UPPER’;
ELSIF (kod=’L’) THEN
fun:=’LOWER’;
ELSIF (kod=’I’) THEN
fun:=’INITCAP’;
ELSE
fun:=NULL;
END IF;
RETURN fun;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
BEGIN
globalna := 12;
globalna_wew := 11;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END;
194 Część II ♦ ORACLE PL/SQL
Zwyczajowo jako wartość inicjującą generator pseudolosowy stosuje się wartość czasu
systemowego odczytaną z maksymalną dokładnością. Formalnie funkcja SEED jest prze-
ciążona do postaci SEED(ciąg_znaków), która jednak rzadko jest wykorzystywana prak-
tycznie. Zamiast ograniczać się do przedziału <0, 1), możemy dzięki zastosowaniu funk-
cji VALUE(min, max) zmienić ten zakres w prosty sposób na <min, max).
SET SERVEROUTPUT ON;
DECLARE
aaa NUMBER;
BEGIN
DBMS_RANDOM.SEED (TO_CHAR(SYSDATE,’MM-DD-YYYY HH24:MI:SS’));
aaa := DBMS_RANDOM.VALUE(1, 10);
DBMS_OUTPUT.PUT_LINE(aaa);
END;
Na tej podstawie jesteśmy w stanie zbudować funkcję, która będzie generowała nie
ciąg znaków o zadanej długości, ale o zrandomizowanej długości nieprzekraczającej n
znaków.
CREATE OR REPLACE FUNCTION gener (maksi NUMBER)
RETURN Varchar2
IS
196 Część II ♦ ORACLE PL/SQL
ile INT;
lancuch varchar2(1111);
BEGIN
SELECT DBMS_RANDOM.VALUES(1, maksi+1) INTO Ile FROM DUAL;
SELECT DBMS_RANDOM.STRING(’U’, ile) INTO lancuch FROM DUAL;
RETURN lancuch;
END;
Problem, który został przy okazji zasygnalizowany, nie należy do zagadnień czysto aka-
demickich. W praktyce bardzo często zależy nam na prowadzeniu badań, np. nad wy-
dajnością przetwarzania zapytań, na podstawie danych zbliżonych do rzeczywistych.
Z reguły badania takie nie mogą być realizowane na bazie produkcyjnej, ponieważ
mogłoby to prowadzić do zaburzenia przetwarzania transakcji związanych z jej rze-
czywistymi zadaniami. Często wydziela się i przenosi do innego schematu (nierzadko
instancji) pewien zakres danych rzeczywistych, co może być z kolei kłopotliwe ze
względów prawnych (ustawa o ochronie danych osobowych lub inne obostrzenia do-
stępu do informacji poufnych). W takim przypadku dobrze byłoby posiadać narzędzie
do generowania danych, które dobrze odwzorowywałoby je zarówno co do struktury
relacyjnej, jak i dystrybucji wartości (rozkład nazwisk zgodny z częstotliwością ich wy-
stępowania, rozkład miast zgodny z gęstością zaludnienia, normalny rozkład wartości
parametrów etc.). Istnieją komercyjne programy, które realizują wiele z tych postula-
tów, np. DataFactory firmy Quest Software, jednak trudno wyobrazić sobie narzędzie
w pełni spełniające wszystkie możliwe kryteria generacji. Zainteresowanych odsyłam
do lektury książki S. Wajrych, A. Pelikant: Automatyczne generowanie zawartości bazy
danych, PLOUG 2007, gdzie przedstawiono opracowane w tym celu narzędzie oraz
omówiono problemy związane z właściwą generacją danych testowych. Problem ten może
mieć jeszcze istotniejsze znaczenie przy budowaniu i testowaniu hurtowni danych oraz
tworzeniu modeli zgłębiania danych (mam nadzieję szybko opracować książkę poświę-
coną tej tematyce).
Rozdział 10.
Procedury wyzwalane
Wykonanie obu dotychczas omawianych elementów programistycznych (funkcji i pro-
cedur), zarówno stworzonych jako oddzielny byt, jak i tych zawartych w pakietach,
było związane z odwołaniem się do nich z poziomu jednostki nadrzędnej — wywoła-
niem np. ze skryptu lub innej procedury (funkcji). Bardzo przydatne byłyby obiekty,
których wykonanie wiązałoby się z wystąpieniem pewnych zdarzeń. Pojęcie progra-
mowania sterowanego zdarzeniami nie jest nowe i może ono być stosowane zarówno
na poziomie języków wyższego rzędu, jak i rozszerzeń proceduralnych SQL. Obiekty,
których wykonanie po stronie serwera bazy danych jest zależne od zdarzeń w nim za-
chodzących, nazywamy procedurami wyzwalanymi — triggerami. Podstawową ideą
jest, aby triggery były wyzwalane na skutek wystąpienia modyfikacji danych spowo-
dowanych wykonaniem poleceń SQL: INSERT, UPDATE oraz DELETE. Załóżmy, że chcemy,
aby wszystkie pola Nazwisko były zawsze pisane dużymi literami. W tym celu utwórzmy
trigger up_tr, który będzie wykonywany przed wystąpieniem zdarzenia INSERT (wsta-
wianie nowego wiersza) w tabeli Osoby.
CREATE OR REPLACE TRIGGER up_tr
BEFORE INSERT
ON Osoby
BEGIN
UPDATE Osoby SET Nazwisko = UPPER(Nazwisko);
END;
Jak widać, ciałem tego triggera jest zapytanie aktualizujące dane UPDATE. Z punktu
widzenia składni w Oracle zawartość triggera może stanowić dowolny zestaw zapytań,
za wyjątkiem zapytań wybierających, oraz wszystkie instrukcje PL/SQL. Podobnie jak
przy tworzeniu procedur składowanych, podczas tworzenia (modyfikacji) procedury
wyzwalanej możemy użyć pary instrukcji DROP TRIGGER ...; oraz CREATE TRIGGER ...;
lub zastosować, jak to ma miejsce w przykładzie, składnię rozszerzoną CREATE OR REPLACE
TRIGGER ...;. Zwróćmy uwagę na to, że w zapytaniu UPDATE nie zastosowano klau-
zuli WHERE, co spowoduje, że w odpowiedzi na wstawienie pojedynczego rekordu zo-
staną zmodyfikowane wszystkie rekordy. Jest to pomysł dość problematyczny, zwa-
żywszy na to, że obsługiwana triggerem tabela Osoby może być duża, np. może
zawierać milion rekordów, więc wstawienie jednego wiersza spowoduje dodatkowe
przetworzenie miliona wierszy, co na pewno pogorszy wydajność. Ponadto w większości
przypadków modyfikacje te są niepotrzebne, ponieważ poprzednie wstawienie wiersza
198 Część II ♦ ORACLE PL/SQL
New - 11 Nowy
Old -
Oznacza to, że podczas zmieniania wartości pól zmienna rekordowa :OLD zawiera stare
wartości pól modyfikowanego rekordu, natomiast zmienna :NEW zawiera ich nowe war-
tości. Spróbujmy teraz zmodyfikować wiele rekordów. Zrealizujemy to dzięki rezygnacji
z klauzuli WHERE w zapytaniu UPDATE, dzięki czemu zmienione zostaną wszystkie rekordy
tabeli Dzialy.
BEGIN
UPDATE Dzialy SET Nazwa = UPPER(Nazwa);
END;
Podobnie jak w pierwszym oraz drugim przykładzie ograniczono się do usunięcia jed-
nego wiersza, stosując klauzulę WHERE.
Rozdział 10. ♦ Procedury wyzwalane 201
New -
Old - 11 NOWY
Tym razem zmienna rekordowa :OLD zawiera wartości pól kasowanego wiersza, a zmienna
:NEW jest pusta. Możemy sprawdzić, że usunięcie więcej niż jednego wiersza spowo-
duje cykliczne pojawianie się komunikatów — ciało triggera będzie przetwarzane dla
każdego usuwanego wiersza. Podobny wniosek można również wyciągnąć, wstawiając
jednocześnie wiele wierszy oraz stosując składnię INSERT ... SELECT. Najbardziej ogól-
nym wnioskiem jest jednak fakt, że dla każdej z omawianych operacji zmienna :NEW
zawiera nowe, a :OLD stare wartości pól, zaś ciało triggera wykona się tyle razy, ile
wierszy zostanie przetworzonych zapytaniem.
Oczywiście nie wyklucza to użycia zmiennych :NEW lub :OLD nie tylko do tworzenia
wyrażeń bezpośrednio podstawianych do zmiennej — można z nich korzystać także
w celu tworzenia wyrażeń w zapytaniach, np. w klauzuli WHERE.
CREATE OR REPLACE TRIGGER up_tr
AFTER INSERT
ON Osoby
FOR EACH ROW
BEGIN
UPDATE Osoby SET Nazwisko = UPPER(:NEW.Nazwisko)
WHERE Nazwisko = :NEW.Nazwisko;
END;
Takie podejście nie jest jednak specjalnie uzasadnione. Po pierwsze, w dalszym ciągu
grozi nam zapętlenie, jeśli zdarzenie wyzwalające trigger jest powtórzone w jego ciele.
Tym razem wystąpi ono na poziomie pierwszego przetwarzanego rekordu. Ponadto
można powiedzieć, że jest to powielenie akcji, która i tak może zostać zrealizowana
przy podstawianiu wyrażenia do zmiennej.
W przypadku triggera, który zapewnia pisanie nazwisk tylko dużymi literami, możemy
również wykorzystać jego warunkowe wykonanie. Można zauważyć, że wykonywa-
nie triggera, kiedy podczas wprowadzania danych pominięto pole nazwisko (NULL),
jest bezcelowe.
CREATE OR REPLACE TRIGGER up_tr
BEFORE INSERT OR UPDATE OF Nazwisko
ON Osoby
FOR EACH ROW
WHEN (New.Nazwisko IS NOT NULL)
BEGIN
:NEW.Nazwisko := UPPER(:NEW.Nazwisko);
END;
Niestety, w praktyce raczej będziemy chcieli, aby tak podstawowe informacje jak na-
zwisko były zawsze podawane, stąd tego typu warunek może być stosowany do ob-
sługi mniej newralgicznych danych.
dla niego jawnie wartości. Taka metodologia tworzenia wartości domyślnych jest ko-
nieczna wtedy, kiedy opisywane są one złożonymi wyrażeniami, szczególnie odno-
szącymi się do zawartości innych pól.
CREATE OR REPLACE TRIGGER nowy_kod
BEFORE INSERT
ON Dzialy
FOR EACH ROW
WHEN (New.kod IS NULL)
DECLARE
New_Kod number;
BEGIN
:New.Kod := SUBSTR(:New.Nazwa, 1, 2);
END;
powiedzieć, że funkcja NVL zamienia pierwszy parametr, kiedy wynosi on NULL, na inną
wartość. Takie rozwiązanie jest nieuniknione, jeśli chcemy, aby trigger działał popraw-
nie również dla pustej tabeli, ponieważ w takim przypadku funkcja MAX(IdDzialu)
zwraca właśnie wartość NULL. Próba jej inkrementacji o dowolną wartość również spo-
woduje otrzymanie NULL, ponieważ NULL + A = NULL. W takiej sytuacji inkrementacja
nigdy się nie rozpocznie, co w przypadku, gdy pole IdDzialu jest kluczem podstawo-
wym, skończy się wyświetleniem komunikatu o błędzie.
Tym razem zapytanie SELECT ... INTO posłużyło nam do przypisania kolejnej warto-
ści generowanej przez sekwencję do zmiennej pomocniczej New_Num. Zmianę wartości
sekwencji uzyskano dzięki użyciu metody NEXTVAL. Możemy zastanowić się, jakie za-
stosowanie może mieć rozpoczęcie przez sekwencję generowania wartości większych
niż 1. Przyczyną pojawiającą się w praktyce może być fakt tworzenia takiego triggera
dla tabeli, w której już istnieje kilka rekordów, a poprzednio generowanie wartości klu-
cza odbywało się w inny sposób, np. ręcznie.
W tym miejscu muszę pokazać pewne zmiany związane ze sposobem odwoływania się
do sekwencji wprowadzone w wersji 11. Oracle. Jeśli mamy stworzoną sekwencję o na-
zwie licznik, to do tej pory odwołanie się do jej bieżącej czy kolejnej wartości było
Rozdział 10. ♦ Procedury wyzwalane 205
Od wersji 11. możliwe jest bezpośrednie odwoływanie się do sekwencji podczas przypi-
sywania jej kolejnej lub bieżącej wartości do zmiennej. Równie dobrze możemy odwo-
łać się do niej, wyświetlając jej wartości za pomocą procedury DBMS_OUTPUT.PUT_LINE.
Obie sytuacje przedstawia prosty blok anonimowy stanowiący przekształcenie poprzed-
niego przykładu.
DECLARE
zm int;
BEGIN
zm:= licznik.nextval;
DBMS_OUTPUT.PUT_LINE(zm);
DBMS_OUTPUT.PUT_LINE(licznik.currval);
END;
W ciele triggera zapewniono przede wszystkim to, że nazwiska i imiona zapisywane będą
tylko dużymi literami (capitalic), natomiast dynamicznie tworzony login składa się
z pięciu pierwszych liter nazwiska oraz pierwszej litery imienia. Oczywiście w przy-
padku krótszych nazwisk będą wykorzystywane wszystkie litery. Podczas tworzenia
loginu ponownie użyto funkcji UPPER, co jest działaniem nadmiarowym, ponieważ
w poprzednich liniach kodu już zapewniono używanie tylko dużych liter. Niestety,
takie generowanie nazw użytkowników nie gwarantuje ich unikalności. Zapewnienie
unikalności loginów oraz zastąpienie polskich znaków łacińskimi (stosowane są tu rów-
nież polskie znaki diakrytyczne) zostało zrealizowane przy użyciu procedury, w której
zastosowano kursor. Ponieważ pojęcie kursora oraz jego zastosowanie zostanie omó-
wione w dalszej części wywodu, w tym miejscu muszę ograniczyć się tylko do pod-
stawowej funkcjonalności opracowanego rozwiązania.
Czasowa blokada triggera jest w praktyce stosowana podczas masowego zasilania ta-
beli danymi, np. przy wykonywaniu zapytania INSERT INTO ... SELECT .... W więk-
szości przypadków mamy wtedy pewność, że wprowadzane dane są pełne i poprawne,
stąd działanie wyzwalacza powoduje tylko spowolnienie przetwarzania, nie dając w za-
mian żadnych korzyści.
Definiując trigger, możemy nie chcieć posługiwać się domyślnymi nazwami zmiennych
:NEW lub :OLD. Może to wynikać z tego, że w tabeli mamy pola o takich samych na-
zwach i pozostawienie domyślnego stanu ograniczy czytelność kodu. W takim przy-
padku możemy zastosować w definicji wyzwalacza klauzulę REFERENCING, w której
podajemy aliasy dla domyślnych nazw pól. Przedstawione zostało to na przykładzie
triggera tworzącego loginy dla studentów.
CREATE OR REPLACE TRIGGER N_log
BEFORE INSERT OR UPDATE OF Imie, Nazwisko
ON Studenci
REFERENCING OLD AS Stary NEW AS Nowy
FOR EACH ROW
BEGIN
:Nowy.Imie := UPPER(:Nowy.Imie);
:Nowy.Nazwisko := UPPER(:Nowy.Nazwisko);
:Nowy.Login := UPPER(SUBSTR(:Nowy.Nazwisko, 1, 5))
|| UPPER(SUBSTR(:Nowy.Imie, 1, 1));
END;
W powyższym przykładzie domyślne nazwy OLD i NEW zastąpione zostały nazwami Stary
i Nowy. Podobnie jak w klauzuli WHEN, również w REFERENCING stosowane są nazwy bez
poprzedzającego je dwukropka (nie są zmiennymi wiązania). Można aliasować nazwy
obu zmiennych, ale dopuszczalne jest aliasowanie tylko jednej (dowolnej) z nich. Jak
widać w przykładzie, w ciele triggera nie ma żadnego odwołania do zmiennej Stary,
dlatego ten alias w klauzuli REFERENCING można było pominąć. Ustanowienie aliasu
dla którejkolwiek ze zmiennych powoduje, że w ciele triggera możemy posługiwać się
tylko nową nazwą. Próba zastosowania nazwy domyślnej zakończy się komunikatem
o błędzie podczas tworzenia procedury wyzwalanej.
Jak to już zostało pokazane, trigger może zostać wyzwolony każdym z trzech zapytań
zmieniających dane w tabeli — INSERT, UPDATE oraz DELETE. Możliwe jest również za-
stosowanie wszystkich zapytań w jednym triggerze. W takim przypadku może być
wskazane rozpoznanie, która z tych akcji spowodowała uruchomienie wyzwalacza,
i, w zależności od tego, wykonanie odpowiednich działań (instrukcji). W tym celu
można zastosować instrukcje warunkowe testujące wartości trzech funkcji o nazwach
INSERTING, UPDATING oraz DELETING, które przyjmują wartość TRUE w przypadku zaist-
nienia odpowiednich zdarzeń. Przy pozostałych zdarzeniach zwracają one wartość FALSE.
CREATE OR REPLACE TRIGGER jaka_akcja
BEFORE INSERT OR UPDATE OR DELETE
ON Studenci
BEGIN
IF INSERTING THEN
DBMS_OUTPUT.PUT_LINE('Wstawianie');
END IF;
IF DELETING THEN
DBMS_OUTPUT.PUT_LINE('Usuwanie');
END IF;
IF UPDATING THEN
DBMS_OUTPUT.PUT_LINE('Aktualizowanie');
END IF;
END;
Następnie tworzymy dwa kolejne triggery, również dla każdego wiersza, wykonywane
przed instrukcją INSERT. Różnią się one od poprzedniego zastosowaniem wprowadzo-
nej w wersji 11. klauzuli FOLLOWS. Po niej umieszczana jest nazwa triggera, po którym
wykona się trigger definiowany tą klauzulą.
CREATE OR REPLACE TRIGGER trig2
BEFORE INSERT ON Test FOR EACH ROW FOLLOWS trig1
BEGIN
SELECT NVL(max(id),0)+1 INTO :NEW.id FROM Test;
:NEW.opis := :NEW.opis || ' trig2';
END;
/
CREATE OR REPLACE TRIGGER trig3
BEFORE INSERT ON Test FOR EACH ROW FOLLOWS trig2
BEGIN
SELECT NVL(max(id),0)+1 INTO :New.id FROM Test;
:NEW.opis := :NEW.opis || ' trig3';
END;
Jak widać, trigger trig2 wykona się po trig1, a trig3 po trig2. Jeśli zmienimy ko-
lejność tworzenia triggerów, przy tworzeniu pierwszego pojawi się komunikat o błę-
dzie kompilacji (informujący, że trigger odwołuje się do nieistniejącego obiektu), ale
przyczyna jego wystąpienia zniknie w momencie utworzenia kolejnego triggera. Wy-
konajmy teraz instrukcję powodującą wstawienie pojedynczego wiersza, a następnie
sprawdźmy zawartość naszej tabeli.
INSERT INTO Test(opis) VALUES('wstawiam');
SELECT * FROM Test;
Tak jak przy każdym bardziej rozbudowanym kodzie, również stosując triggery, ła-
two o popełnienie błędu. Podobnie jak w przypadku procedur, funkcji czy pakietów,
wyzwalacz zawierający błędy składniowe zostanie utworzony, a fakt istnienia błędu zo-
stanie zasygnalizowany komunikatem Wyzwalacz został utworzony z błędami. Taka in-
formacja jest z reguły zbyt uboga, aby dało się szybko znaleźć miejsce pomyłki. Roz-
ważmy przypadek wyzwalacza z umyślnie wprowadzonym błędem. Czy jesteś w stanie,
Czytelniku, szybko wskazać, gdzie został on popełniony?
CREATE OR REPLACE TRIGGER A_LOG
BEFORE INSERT OR UPDATE OF IMIE, NAZWISKO
ON STUDENCI
210 Część II ♦ ORACLE PL/SQL
Jeśli chcemy dowiedzieć się więcej o błędach wyzwalacza, który został wcześniej utwo-
rzony, możemy jawnie wskazać na nazwę tego triggera.
SHOW ERRORS TRIGGER a_log;
Oczywiście możliwe jest też jawne odwołanie się do informacji o błędach innych wcze-
śniej utworzonych elementów proceduralnych. Pełna składnia wywołania ma następującą
postać:
SHOW ERRORS [{ FUNCTION | PROCEDURE | PACKAGE | PACKAGE BODY | TRIGGER | VIEW |
TYPE | TYPE BODY | DIMENSION | JAVA SOURCE | JAVA CLASS } [schemat.][nazwa]
Druga tabela ma, oprócz klucza, przechowywać nazwę modyfikowanej kolumny oraz
jej starą i nową wartość.
Rozdział 10. ♦ Procedury wyzwalane 211
poprzez użycie funkcji UPDATING z parametrem, którym jest jego nazwa. W obu przy-
padkach wpisywany jest klucz główny w tabeli nadrzędnej oraz, z wykorzystaniem
zmiennych rekordowych :NEW oraz :OLD, stara i nowa wartość tych pól. Wybór kolumn
do śledzenia jest arbitralny. Można sobie wyobrazić powtórzenie tego fragmentu kodu
dla wszystkich pól tabeli. Powielenie tego triggera dla wszystkich tabel schematu, ze
zmianą statycznych nazw tabel i pól występujących w jego ciele, zapewniłoby śledzenie
wszystkich zachodzących zmian danych. Pomimo pozornej dużej użyteczności tego
rozwiązania ma ono wielce prozaiczną, ale bardzo istotną praktyczną wadę. Gdybyśmy
rzeczywiście chcieli śledzić wszystkie zmiany w obrębie schematu, to przyrost infor-
macji zawartej w tabelach audytowych byłby większy, niż przyrost danych w tabelach
schematu. Spowodowałoby to lawinowy przyrost objętości tych tabel, a co za tym
idzie, całej bazy danych. Jedynym środkiem zaradczym byłoby stworzenie na tych ta-
belach triggerów „pilnujących” rozmiaru i w razie jego przekroczenia kasujących naj-
starsze wpisy. Niestety, wtedy śledzenie zmian dotyczyłoby tylko ograniczonego prze-
działu czasowego. W praktyce wyobrażam sobie, że śledzenie zmian na poziomie rekordu
może dotyczyć tylko wybranych tabel (a raczej pojedynczej tabeli) oraz tylko najbar-
dziej „inwazyjnej” operacji kasowania.
Pomimo że prezentowane do tej pory przykłady dotyczyły triggerów dla tabel, możliwe
jest też tworzenie triggerów dla perspektyw. Aby miały one szansę wykonania, per-
spektywy muszą pozwalać na wykonywanie zapytań modyfikujących INSERT, UPDATE
oraz DELETE — to znaczy muszą być oparte na danych pochodzących z jednej tabeli
i nie zawierać w sobie żadnych wyrażeń, funkcji, agregacji czy grupowania. Istnieje
jednak pewien typ wyzwalacza dedykowany specjalnie dla perspektyw. Aby go prze-
analizować, stwórzmy perspektywę Stud wybierającą wszystkie pola z tabeli Studenci.
CREATE OR REPLACE VIEW Stud
AS
SELECT Imie, Indeks, Login, Nazwisko FROM Studenci
WHERE Nazwisko IS NOT NULL
WITH READ ONLY
Formalnie rzecz ujmując, możliwe jest zastosowanie triggera o pustym ciele, jednak
ze względu na to, że PL/SQL „nie lubi” elementów programistycznych bez ciała, można
użyć instrukcji pustej NULL.
CREATE OR REPLACE TRIGGER st_tr
INSTEAD OF DELETE
ON Stud
Rozdział 10. ♦ Procedury wyzwalane 213
BEGIN
NULL;
END;
Według takich samych zasad możliwa jest dezaktywacja wszystkich triggerów dla
perspektywy. Tego typu zabieg najczęściej wykorzystywany jest przy masowym ko-
piowaniu danych, w przypadku kiedy mamy uzasadnione przypuszczenie (pewność),
że spełniają one wszystkie ograniczenia na nie nałożone, ze szczególnym uwzględ-
nieniem danych, które tworzone są dynamicznie.
Rozdział 10. ♦ Procedury wyzwalane 215
Poza triggerami dla tabel, perspektyw bardzo ważne miejsce w przetwarzaniu speł-
niają wyzwalacze definiowane dla bazy danych. Ich zasięg może obejmować zarówno
schemat użytkownika, który taki trigger tworzy, jak i, w przypadku użytkowników
z uprawnieniami DBA (DataBase Admin), całą instancję bazy danych. Zbudujmy wy-
zwalacz, który zablokuje możliwość usunięcia jakiegokolwiek obiektu w schemacie
użytkownika.
CREATE OR REPLACE TRIGGER dla_system
BEFORE DROP
ON SCHEMA
BEGIN
RAISE_APPLICATION_ERROR
(-20555, 'Próba wykasowania : ' || USER || ' - ' || UID);
END dla_system;
Zastosowany został trigger wyzwalany przed zdarzeniem DROP. Jak widać, blokadę uzy-
skano na skutek wywołania w ciele triggera procedury ustanawiającej błąd użytkow-
nika na poziomie aplikacji. Można byłoby osiągnąć podobny efekt, stosując zadekla-
rowany w ciele triggera błąd użytkownika. Sensowne jest również użycie sekcji obsługi
wyjątków w ciele wyzwalacza, jak to pokazano poprzednio. Skutkiem wykonania za-
pytania usuwającego dowolny obiekt będzie komunikat:
ORA-00604: wystąpił błąd na poziomie 1 rekurencyjnego SQL
ORA-20555: Próba wykasowania: TESTOWY - 136
ORA-06512: przy linia 2
Oczywiście usuwanie obiektu nie zostanie wykonane, ponieważ zanim zostanie roz-
poczęte, pojawi się wymuszony triggerem błąd przetwarzania. W komunikacie TESTOWY
jest nazwą użytkownika, w którego schemacie wyzwalacz został utworzony. Zarówno
nazwa użytkownika, jak i jego identyfikator, zależą od schematu oraz sesji, której próby
kasowania dokonano. Dla pozostałych schematów instancji serwera trigger nie będzie
uruchamiany. Podobny wyzwalacz mógłby zostać utworzony z opcją ON DATABASE.
W takim przypadku blokada usuwania obiektów dotyczyłaby całej instancji Oracle.
Tym razem budowany wyzwalacz będzie wykonywał się po zalogowaniu (AFTER LOGON)
dla całej instancji bazy (ON DATABASE), czyli będzie rejestrowane każde logowanie.
216 Część II ♦ ORACLE PL/SQL
Należy zwrócić uwagę na to, że zdarzenie obsługiwane przez trigger dotyczy tylko
udanych połączeń z bazą — próby nieudane nie powodują uruchomienia wyzwalacza!
Pociąga to za sobą wniosek, iż nie jest możliwe zastosowanie tego triggera jako narzę-
dzia do wykrywania wszystkich ataków przeprowadzanych na instancję. Zapisane zo-
stałyby tylko ataki zakończone powodzeniem — zalogowaniem się do bazy — więc
stałoby się to zdecydowanie zbyt późno. Z mojego punktu widzenia trigger ten może
być używany jako automatycznie generowana lista obecności na zajęciach.
Jak widać na przykładzie tabeli 10.2, przez wyzwalacze obsługiwane są wszystkie istotne
z punktu widzenia bazy danych zdarzenia. Większość z nich występuje w postaci pary
zdarzeń BEFORE i AFTER, są jednak wydzielone trzy, które mogą być obsługiwane albo
tylko przed, albo tylko po ich wystąpieniu. Pierwszym jest zdarzenie LOGON, które wy-
konuje się tylko po poprawnym (pozytywnie zweryfikowanym) zalogowaniu, czyli,
jak pisałem poprzednio, nie może służyć do wykrywania prób ataku. Również tylko po
zdarzeniu SUSPEND może wykonywać się trigger związany z wyczerpaniem się zasobów.
Rozdział 10. ♦ Procedury wyzwalane 219
Mówiąc inaczej, nie może on antycypować tego faktu i w związku z tym może tylko
i wyłącznie działać ex post. Kolejne zdarzenie — LOGOFF, związane z wylogowywa-
niem, może wyzwalać trigger tylko przed zamknięciem połączenia z sesją. Trywiali-
zując, można powiedzieć, że po wylogowaniu nie ma już „miejsca”, zasobów, które
mógłby wykorzystać wyzwalacz do przetwarzania. Niestety, należy również zauwa-
żyć, że mogą pojawić się sytuacje, kiedy ta procedura nie będzie miała możliwości
220 Część II ♦ ORACLE PL/SQL
wykonania się. Są one związane z gwałtownym zamknięciem sesji, gdy brak czasu na
dokończenie transakcji. Podstawowym przypadkiem jest wyłączenie komputera przy
pomocy wtyczki, co, niestety, czasami ma miejsce nawet wśród studentów informatyki.
Jest to świadectwo co najmniej beztroski w przypadku przerywania pracy komputera,
na którym zainstalowano tylko końcówkę klienta, ponieważ spowoduje to gwałtowne
zamknięcie jedynie tych jego sesji, które uruchomione zostały z tego komputera. Gor-
szym przypadkiem jest wyłączenie w ten sposób urządzenia, na którym został zain-
stalowany serwer, ponieważ nie tylko przerywane są wtedy natychmiastowo wszystkie
sesje, ale również gwałtownie zatrzymywana jest instancja serwera Oracle. Podobna
sytuacja ma miejsce wtedy, kiedy z przyczyn administracyjnych musimy zatrzymać
serwer poleceniem SHUTDOWN IMMEDIATE lub, jeszcze bardziej radykalnie, poleceniem
SHUTDOWN ABORT (bez mała równoważnym wyciągnięciu wtyczki).
Omawialiśmy do tej pory zdarzenia, które mogą być obsługiwane zarówno z poziomu
instancji bazy danych, jak i z poziomu schematu. Istnieje jednak taka grupa zdarzeń,
które mogą zostać obsłużone tylko z poziomu DATABASE — są to zdarzenia najbar-
dziej istotne z punktu widzenia funkcjonowania instancji serwera. Ich wykaz wraz z krót-
kim opisem zawiera tabela 10.3.
222 Część II ♦ ORACLE PL/SQL
Jak pokazano w tabeli 10.3, żadne z tych zdarzeń nie jest obsługiwane zarówno po,
jak i przed jego wystąpieniem. Co oczywiste, zdarzenie STARTUP może zostać obsłu-
żone tylko po uruchomieniu serwera, ponieważ przed nim serwer jeszcze nie może
wykonać żadnych operacji. Podobnie jest ze zdarzeniem SHUTDOWN — nie możemy wy-
konywać kodu podczas zamykania serwera, a tym bardziej po jego zatrzymaniu. W przy-
padku wystąpienia błędu (SERVERERROR) oraz zmiany roli (DB_ROLE_CHANGE) możliwe
jest tylko działanie post factum, ponieważ inaczej mielibyśmy do czynienia z przewi-
dywaniem przyszłości. Podobnie jak dla zdarzenia LOGOFF, trigger dla zatrzymywania
bazy może nie zostać wykonany. Ma to miejsce w przypadku awaryjnego, natychmia-
stowego zatrzymania serwera, np. poleceniem SHUTDOWN ABORT lub przy okazji odcię-
cia zasilania. Również nie wszystkie błędy mogą wyzwolić trigger, ponieważ albo nie
są one rzeczywistymi błędami, albo są zbyt poważne, aby kontynuować dalsze prze-
twarzanie. Przykłady takich błędów wraz ze skróconym opisem zawiera tabela 10.4.
Teraz możliwe jest utworzenie wyzwalacza dla zdarzenia AFTER SERVERERROR dla instancji
bazy danych. Zadeklarowane zostały zmienne pomocnicze mające przechowywać treść
polecenia, które spowodowało wystąpienie błędu (w postaci wewnętrznej — zmienna
sql_text, która jest zmienną tabelaryczną predefiniowanego typu ora_name_list_t,
a także po konwersji do zmiennej łańcuchowej — zmienna polec) oraz treść komuni-
katu o błędzie — kom.
CREATE OR REPLACE TRIGGER Blady_T
AFTER SERVERERROR ON DATABASE
DECLARE
sql_text ora_name_list_t;
kom varchar2(2000) := NULL;
polec varchar2(2000) := NULL;
BEGIN
FOR depth IN 1 .. ora_server_error_depth
LOOP
kom := kom || ora_server_error_msg(depth);
END LOOP;
FOR i IN 1 .. ora_sql_txt(sql_text)
LOOP
polec := polec || sql_text(i);
END LOOP;
INSERT INTO Blady (dt, kto, msg, stmt)
VALUES (SYSDATE, ora_login_user, kom, polec);
END;
W ciele tego triggera odwołujemy się do tzw. funkcji zdarzeń (events functions), które
mogą dostarczyć wielu informacji o zdarzeniu czy też o sesji, instancji Oracle. Pierw-
sza z nich, ora_server_error_depth, pokazuje liczbę elementów na stosie błędów
i została wykorzystana jako górne ograniczenie inkrementacji licznika pierwszej pętli.
W ciele pętli składany jest w zmiennej kom — z pojedynczych wpisów na stosie
błędów, odczytywanych za pomocą funkcji ora_server_error_msg — zestaw ko-
munikatów o błędzie. Wskaźnikiem końca inkrementacji drugiej pętli jest funkcja
ora_sql_txt(sql_text), która przez nazwę zwraca liczbę wierszy w zmiennej tabela-
rycznej sql_text typu ora_name_list_t . Zmienna ta w każdym wierszu zawiera
64-znakowy fragment polecenia SQL, które spowodowało wyzwolenie triggera (w na-
szym przypadku wystąpienie błędu serwera). Poszczególne wiersze są w tej pętli łączone
i zapisywane do zmiennej znakowej polec. Ostatnia instrukcja ciała triggera powo-
duje wpisanie odczytanych informacji do poprzednio utworzonej tabeli. W celu okre-
ślenia chwili dokonania wpisu wykorzystywana jest funkcja SYSDATE odczytująca datę
i czas systemowy, natomiast nazwę użytkownika odczytujemy za pomocą funkcji
ora_login_user. Oczywiście w tym przypadku jesteśmy w stanie użyć zarówno funk-
cji SYS_CONTEXT('USERENV', 'SESSION_USER'), jak i prostej funkcji USER. Zdecydowano
się na taką realizację, aby pokazać w praktycznym przykładzie kilka funkcji zdarzeń.
Po utworzeniu takiego triggera pozostaje tylko sprawdzenie jego działania. W tym
celu wykonano dwa błędne zapytania: pierwsze próbujące wyświetlić zawartość nie-
istniejącej tabeli, a drugie próbujące zmienić wartość nieistniejącej kolumny w ist-
niejącej tabeli. Skutek działania triggera w tych dwóch przypadkach przedstawia ta-
bela 10.5. Również z niej można odczytać dokładną składnię wykonywanych zapytań
testujących działanie wyzwalacza.
224 Część II ♦ ORACLE PL/SQL
Niestety, nie wszystkie popełniane błędy zapisują tak precyzyjne informacje. W przy-
padku błędów kompilacji elementów proceduralnych komunikaty na stosie dotyczą dzia-
łania procesów serwera, a nie samych błędów. Inaczej mówiąc, nie należy się spodzie-
wać wpisów podobnych do otrzymanych na skutek wykonania polecenia SHOW ERRORS.
Długość łańcucha, który stanowi kolejne wiersze tabeli (64 znaki), uzasadnia koniecz-
ność podziału niektórych informacji, jak chociażby tekstu zapytania SQL, na wiele wier-
szy. W innych przypadkach kolejne wiersze stanowią pojedynczą informację — np.
lista użytkowników.
Oczywiście funkcje zdarzeń mogą być również wywoływane dla schematu czy bazy
danych poza ciałem triggerów, zarówno w blokach anonimowych, jak i procedurach,
funkcjach czy też triggerach innych typów. Również przy takim wywołaniu mogą one
przekazać interesującą nas informację, np.: ora_database_name, ora_instance_num czy
też ora_login_user. Niestety, w większości przypadków nie zwrócą żadnych danych,
ponieważ „zapisanie” informacji niezbędnej do wyświetlenia niepustych wartości jest
związane z wystąpieniem w bazie danych konkretnych zdarzeń. W związku z tym będą
one użyteczne tylko w ciałach niektórych triggerów. Przypisanie tych funkcji do po-
tencjalnych zdarzeń „zapisujących” dla tych funkcji informacje nie powinno nastręczyć
trudności, czy to dzięki analizie opisu z tabeli 10.5, czy też intuicyjnej analizie nazwy
funkcji.
matu (struktura drzewiasta w lewym oknie), a druga z nich jest sesją poleceń SQL.
Należy zauważyć, że otwarcie wielu zakładek SQL dla tego samego użytkownika nie
powoduje otwarcia kolejnych sesji (wszystkie zakładki są obsługiwane przez tę samą
sesję). Jeżeli już ustaliliśmy, która z sesji jest dla nas interesująca (dla pewności zaw-
sze można wybrać wszystkie sesje tego samego użytkownika), możemy wykonać za-
pytanie, które wyświetli nam tekst aktualnie wykonywanego polecenia SQL. W tym celu
odpytamy perspektywę systemową v$sqlarea, dla której informacje o adresie i warto-
ści funkcji haszującej pobierzemy w postaci listy z perspektywy v$session, w której
w klauzuli WHERE umieścimy wybrany identyfikator sesji.
SELECT sql_text
FROM v$sqlarea
WHERE (address, hash_value) IN
(SELECT sql_address, sql_hash_value
FROM v$session
WHERE sid = 108);
W celu zastosowania kursora konieczne jest jego otwarcie w ciele bloku anonimowego,
procedury czy funkcji. Jeżeli już zakończyliśmy pracę z nim, powinien on zostać za-
mknięty, co powoduje zwolnienie zasobów przez niego wykorzystywanych. Pozostawie-
nie otwartego kursora sprawia, że przydzielone mu zasoby są utrzymywane. W przy-
padku rekurencyjnego otwierania wielu kursorów może to prowadzić do niekontrolowanego
wzrostu zapotrzebowania na pamięć, co może zaowocować np. wystąpieniem błędu
lub kłopotami z przetwarzaniem, w skrajnym przypadku utrudnieniem w dostępie lub
całkowitą blokadą instancji serwera. W ramach jednej deklaracji kursor może być wie-
lokrotnie otwierany i zamykany, jednakże pary OPEN ... CLOSE dotyczące tego samego
kursora muszą być rozłączne. Innymi słowy, nie można otworzyć już otwartego kur-
sora ani zamknąć już zamkniętego. Po jego otwarciu kursor znajduje się przed pierw-
szym rekordem z ich zestawu zdefiniowanego zapytaniem wybierającym. Jego pod-
stawowe wykorzystanie przedstawia kolejny listing.
DECLARE
ww NUMBER;
nn VARCHAR(15);
CURSOR sledz IS SELECT Nazwisko, RokUrodz FROM Osoby;
BEGIN
OPEN sledz;
FETCH sledz INTO nn, ww;
WHILE sledz%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE (nn || ' ' || ww);
FETCH sledz INTO nn, ww;
END LOOP;
CLOSE sledz;
END;
230 Część II ♦ ORACLE PL/SQL
Wielokrotnie podczas przetwarzania kursora możemy odwoływać się do wielu pól de-
finiującego go zapytania, a w jego deklaracji do zapytania zawierającego wszystkie
pola tabeli lub perspektywy. Wygodnie jest wtedy użyć jako zmiennej typu rekordo-
wego definiowanego atrybutem %ROWTYPE. Zastosowanie tego atrybutu powoduje, że
wszystkie pola źródłowego obiektu (rekordu), które zawiera zmienna, są zgodne z nią
co do nazwy, jak również co do typu. W kolejnym przykładzie zastosowane zostało
takie podejście do nawigacji po wszystkich rekordach tabeli Osoby.
DECLARE
rec Osoby%ROWTYPE;
CURSOR sledz IS SELECT * FROM Osoby;
BEGIN
OPEN sledz;
FETCH sledz INTO rec;
WHILE sledz%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(SLEDZ%ROWCOUNT);
DBMS_OUTPUT.PUT_LINE(rec.Nazwisko || ' ' ||
rec.RokUrodz);
FETCH sledz INTO rec;
END LOOP;
CLOSE sledz;
END;
Rozdział 11. ♦ Kursory 231
Po słowie kluczowym INTO polecenia FETCH występuje wtedy zmienna rec, zadekla-
rowana jako zmienna rekordowa. Odwołanie się do konkretnych jej pól odbywa się
poprzez nazwę kwalifikowaną składającą się z nazwy zmiennej i nazwy pola (lub aliasu
pola) tabeli lub perspektywy. Takie podejście powoduje ponadto, że kursor jest mniej
czuły na zmianę typów pól tabeli. Niestety, nie dotyczy to operacji na tych polach
wykonywanych, bowiem zmiana pola znakowego na numeryczne lub odwrotnie zmie-
nia zestaw dopuszczalnych operatorów. Problem ten nie dotyczy zmiany skali (rozmiaru
pola), z którą to sytuacją mamy najczęściej do czynienia w praktyce. Zamiast stosować
do nawigacji między rekordami pętlę WHILE, możemy użyć pętli FOR. Tak jak w poprzed-
nim przykładzie, jako zestaw rekordów zadeklarowano zapytanie zawierające wszystkie
pola z tabeli Osoby.
SET SERVEROUTPUT ON;
DECLARE
CURSOR sledz IS SELECT * FROM Osoby;
BEGIN
FOR rec IN sledz
LOOP
DBMS_OUTPUT.PUT_LINE(sledz%ROWCOUNT);
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
END LOOP;
END;
Jako licznik pętli użyta została zmienna rec, a zakres zmienności tej pętli wyznacza ze-
staw rekordów kursora sledz. W przypadku takiego sposobu nawigacji kursor otwie-
rany jest automatycznie, niejawnie, wraz z rozpoczęciem pętli oraz jest automatycznie,
niejawnie, zamykany. W związku z tym nie może on być otwierany lub zamykany w spo-
sób jawny. Automatycznie inkrementowana zmienna rec jest takiego samego typu jak
zestaw rekordów zwracany przez kursor, dlatego nie może być ona jawnie zadeklaro-
wana (prowadziłoby to do powtórzenia deklaracji). Tak jak to ma miejsce w każdej innej
pętli FOR, zmienna będąca jej licznikiem nie może być modyfikowana (nie można przy-
pisywać jej żadnych wartości). Wartości pól dla bieżącego położenia kursora odczy-
tywane są jako pola zmiennej licznika rec. W ciele pętli, tak jak poprzednio, wypro-
wadzono zawartość dwóch pól połączonych konkatenacją wraz ze znakiem spacji.
Dodatkowo wyprowadzony został, na skutek zastosowania atrybutu %ROWCOUNT kursora,
licznik wierszy.
Nawet jeśli w omawianym przypadku zastosujemy w definicji pętli FOR atrybut REVERSE,
to i tak nawigacja odbywać się będzie tylko do przodu, co jeden rekord zestawu.
SET SERVEROUTPUT ON;
DECLARE
CURSOR sledz IS SELECT * FROM Osoby;
BEGIN
FOR rec IN REVERSE sledz
LOOP
DBMS_OUTPUT.PUT_LINE(sledz%ROWCOUNT);
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
END LOOP;
232 Część II ♦ ORACLE PL/SQL
Kolejną interesującą opcją deklaracji kursora jest FOR UPDATE. Nie powoduje ona oczy-
wiście tego, że możliwe jest bezpośrednie modyfikowanie danych przez przypisanie
ich do zmiennej rekordowej, ale tak zadeklarowany kursor dostarcza metody CURRENT OF,
która wskazuje na przetwarzany nim w danym momencie rekord z zestawu.
SET SERVEROUTPUT ON;
DECLARE
CURSOR sledz IS SELECT * FROM Osoby FOR UPDATE;
BEGIN
FOR rec IN sledz
LOOP
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
IF (rec.RokUrodz IS NULL)THEN
UPDATE Osoby SET RokUrodz=0
WHERE CURRENT OF sledz;
END IF;
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
END LOOP;
END;
W deklaracji kursora możliwe jest określenie parametrów oraz ich wartości domyślnych.
Wykonujemy to w sposób analogiczny do określania parametrów formalnych procedury,
w tym jednak przypadku wartość domyślna jest ustanawiana po znaku przypisania :=.
Parametr oczywiście nie musi mieć określonej wartości domyślnej. Przy większej licz-
bie parametrów ich definicje oddzielamy przecinkiem.
SET SERVEROUTPUT ON;
DECLARE
CURSOR sledz (nrdzialu NUMBER :=2) IS
SELECT * FROM Osoby WHERE IdDzialu=nrdzialu;
BEGIN
FOR rec IN sledz
LOOP
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
END LOOP;
END;
Jeśli w definicji pętli FOR użyjemy tylko nazwy kursora, to wykorzystana zostanie do-
myślna wartość parametru — w analizowanym przykładzie 2. Zestaw rekordów bę-
dzie dotyczył tylko działu o takim identyfikatorze. Podobnie, jeśli nie podamy wartości
Rozdział 11. ♦ Kursory 233
parametru przy otwarciu kursora instrukcją OPEN, użyta zostanie jej wartość domyślna.
W przypadku gdy użyjemy w deklaracji kursora parametru z wartością domyślną, to
i tak możliwe będzie użycie przy odwołaniu się do niego wartości innej niż ona.
SET SERVEROUTPUT ON;
DECLARE
CURSOR sledz (nrdzialu NUMBER :=2) IS
SELECT * FROM Osoby WHERE IdDzialu=nrdzialu;
BEGIN
FOR rec IN sledz(3)
LOOP
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
END LOOP;
END;
Jeśli w definicji pętli FOR użyjemy nazwy kursora z umieszczoną w nawiasie wartością
parametru (lub listą wartości separowanych przecinkami, kiedy jest ich więcej niż jeden),
to kursor zostanie otwarty dla tej wartości. Można powiedzieć, że jawnie podana war-
tość parametru nadpisuje się na domyślną. W omawianym przykładzie parametr jest
równy 3, co odpowiada rekordom pracowników pracujących w trzecim dziale. Podobny
efekt otrzymamy, jeśli po nazwie kursora podamy w nawiasie wartości parametrów
w instrukcji OPEN.
DECLARE
CURSOR sledz (nrdzialu NUMBER :=2) IS
SELECT * FROM Osoby WHERE IdDzialu=nrdzialu;
rec Osoby%ROWTYPE;
BEGIN
OPEN sledz(3);
FETCH sledz INTO rec;
WHILE sledz%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(sledz%ROWCOUNT);
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' || rec.RokUrodz);
FETCH sledz INTO rec;
END LOOP;
CLOSE sledz;
END;
Podobnie jak przy wywoływaniu procedur lub funkcji, tak i w przypadku kursora za-
miast pozycyjnego podstawienia wartości parametrów możemy używać podstawienia
nazewniczego.
SET SERVEROUTPUT ON;
DECLARE
CURSOR sledz (nrdzialu NUMBER :=2) IS
SELECT * FROM Osoby WHERE IdDzialu=nrdzialu;
BEGIN
FOR rec IN sledz (nrdzialu=>3)
LOOP
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
END LOOP;
END;
234 Część II ♦ ORACLE PL/SQL
Wywołanie nazewnicze składa się z nazwy parametru, znaku => oraz przypisywanej
mu wartości. W naszym przypadku do parametru przypisano wartość 3 i przeszuki-
wane będą tylko rekordy pracowników działu o takim identyfikatorze. Takie wywoła-
nie jest bardziej ogólne, ponieważ umożliwia podstawianie wartości do parametrów
w dowolnej kolejności. Pozwala to między innymi na odwołanie się do wartości do-
myślnej dowolnego parametru na liście, niekoniecznie do wartości domyślnych koń-
cowych parametrów, jak to ma miejsce przy wywołaniu pozycyjnym.
Nie zawsze konieczne jest zdefiniowanie źródła rekordów w deklaracji kursora. Moż-
liwość uniknięcia tego pojawia się, kiedy używamy kursora referencyjnego. Kursor tego
rodzaju musi zostać najpierw zadeklarowany jako typ danych REF CURSOR, a następnie
określamy obiekt stanowiący instancję tego typu. W definicji typu możliwe jest okre-
ślenie po słowie RETURN rodzaju danych, które ten kursor zwraca. Najczęściej jest to
zmienna rekordowa, np. Osoby%ROWTYPE. Przy takim typie kursora definicja zestawu re-
kordów jest przeniesiona do miejsca jego otwarcia i określana jest po słowie kluczo-
wym FOR.
DECLARE
TYPE pracownicy_type IS
REF CURSOR RETURN Osoby%ROWTYPE;
pracownik pracownicy_type;
pracownicy_rec Osoby%ROWTYPE;
BEGIN
OPEN pracownik FOR SELECT * FROM Osoby;
FETCH pracownik INTO pracownicy_rec;
WHILE pracownik%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(pracownicy_rec.Nazwisko);
FETCH pracownik INTO pracownicy_rec;
END LOOP;
CLOSE pracownik;
END;
Przy deklarowaniu typu kursora referencyjnego nie musimy podawać jawnie typu da-
nych, które będzie on zwracał. Typ zwracanej zmiennej jest ściśle określony przez ze-
staw rekordów podawany podczas otwierania instancji kursora takiego typu. Taki
przypadek, będący analogiem poprzedniego przykładu, przedstawiono w kolejnym
fragmencie kodu — bloku anonimowym.
DECLARE
TYPE pracownicy_type IS REF CURSOR;
pracownik pracownicy_type;
pracownicy_rec Osoby%ROWTYPE;
BEGIN
OPEN pracownik FOR SELECT * FROM Osoby;
FETCH pracownik INTO pracownicy_rec;
Rozdział 11. ♦ Kursory 235
WHILE pracownik%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(pracownicy_rec.Nazwisko);
FETCH pracownik INTO pracownicy_rec;
END LOOP;
CLOSE pracownik;
END;
Pełna siła tego rozwiązania objawia się wtedy, kiedy zmienna zap jest składana z frag-
mentów łańcuchów znakowych dających w efekcie dynamicznie definiowane zapytanie
wybierające. Takie właśnie kursory referencyjne są często stosowane w procedurach
operujących na tabelach, których nazwy są elementami ich wywołania. Przykłady kodu
prezentującego takie podejście zostaną zamieszczone w rozdziale poświęconym dyna-
micznemu SQL.
Podobny pod względem idei działania do kursora referencyjnego jest kursor niejawny.
Jest to konstrukcja, która w poleceniu pętli FOR, po słowie kluczowym IN, ma zawarte
w nawiasie zapytanie wybierające. Poza brakiem jawnej deklaracji zmiennej typu CURSOR,
działanie takiej pętli niczym nie różni się od nawigacji jawnie zdefiniowanego kursora
przy użyciu pętli FOR.
SET SERVEROUTPUT ON;
BEGIN
FOR rec IN (SELECT * FROM Osoby)
LOOP
DBMS_OUTPUT.PUT_LINE (rec.Nazwisko || ' ' ||
rec.RokUrodz);
END LOOP;
END;
236 Część II ♦ ORACLE PL/SQL
W ciele bloku anonimowego najpierw otwarty został kursor res dla zestawu rekordów
zawierającego wszystkie rekordy z tabeli Osoby. Nawigacja tym kursorem odbywa
się w pętli WHILE przy zastosowaniu polecenia FETCH. We wnętrzu tej pętli otwierany
jest kursor res1, zawierający rekordy z tabeli Osoby, dla których szefem jest rekord
opisujący osobę i aktualnie przetwarzany nadrzędnym kursorem res.idosoby. Jak wi-
dać, filtr zawarty w klauzuli WHERE definicji kursora res1 jest zmieniany przy każdej
zmianie rekordu kursora nadrzędnego. Po otwarciu kursor jest przetwarzany w ciele
wewnętrznej pętli WHILE. Do wyprowadzenia informacji wykorzystano procedurę
DBMS_OUTPUT.PUT_LINE, wyświetlającą nazwisko szefa — res.nazwisko, oraz nazwi-
sko jego podwładnego — res1.nazwisko. Jeżeli pracownik nie ma już podwładnych
(liści drzewa), to kursor res1 zawiera pusty zestaw rekordów i nie będzie przetwarzany
w pętli ze względu na zawsze fałszywy atrybut res1%FOUND. Przykładowy początek
wyników uzyskanych na skutek przetwarzania tego skryptu zawiera tabela 11.2.
Rozdział 11. ♦ Kursory 237
się w drzewie. Tam gdzie pracownik nie będzie już miał podwładnych, pętla nie wy-
kona się ani razu (fałszywy atrybut %FOUND) i powrócimy na mniej zagnieżdżony po-
ziom w hierarchii. Mamy tu do czynienia z zachłannym przeszukiwaniem grafu. Po prze-
szukaniu wszystkich rekordów danego poziomu następuje zamknięcie kursora oraz
przywrócenie wejściowej wartości zmiennej poziom.
Potrzebujemy teraz procedury, która zainicjuje proces. Możliwe jest bezpośrednie jej
wywołanie dla ustalonej wartości poziomu hierarchii (np. 1) oraz wskazanego identy-
fikatorem pracownika, od którego zaczynamy generowanie drzewa (również 1).
DECLARE
kto int;
poziom int;
BEGIN
poziom:=1;
kto:=1;
inner(kto, poziom);
END;
Skutek wykonania takiego skryptu w postaci kilku wybranych rekordów pokazuje ta-
bela 11.3.
Tabela 11.3. Rezultat wykonania bloku anonimowego wywołującego rekurencyjną procedurę inner
2 Nowak
3 Janik
4 Kowalski
4 Nowicki
4 Lew
3 Jasiński
3 Pawlak
3 Gawlik
2 Kow
3 Kowalczyk
4 Adamczyk
4 Zieliński
Dużo bardziej eleganckie oraz elastyczne jest jednak opracowanie procedury generu-
jącej dane początkowe dla wywoływanej w jej ciele procedury inner. Nazwijmy ją outer,
a w sekcji deklaracji zadeklarujmy typ kursora referencyjnego cur_t, zmienną cur typu
tego kursora, zmienną rekordową res o typie zgodnym z wierszem tabeli Osoby oraz
dwie zmienne pomocnicze poziom i kto, obie całkowite.
CREATE OR REPLACE PROCEDURE outer
IS
TYPE cur_t IS REF CURSOR;
cur cur_t;
res Osoby%ROWTYPE;
kto int;
poziom int;
Rozdział 11. ♦ Kursory 239
BEGIN
poziom:=1;
OPEN cur FOR SELECT * FROM Osoby;
FETCH cur INTO res;
WHILE cur%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(' #####################');
DBMS_OUTPUT.PUT_LINE(to_char(poziom)|| ' ' ||
res.Nazwisko );
kto:=res.IdOsoby;
inner(kto,poziom);
FETCH cur INTO res;
END LOOP;
CLOSE cur;
END;
W ciele procedury otwierany jest kursor referencyjny dla zestawu rekordów zawiera-
jącego wszystkie wiersze tabeli Osoby. W pętli WHILE następuje wywołanie procedury
inner dla poziomu ustawionego na 1 oraz zmiennej kto odpowiadającej identyfikato-
rowi każdego z przetworzonych kursorem pracowników. Aby oddzielić wywołania
procedury inner dla każdej z osób, wyprowadzony został statyczny napis zawierający
znaki # oraz poziom (zawsze 1), a także nazwisko przetwarzanej w procedurze outer
osoby. Wywołanie procedury outer odbywa się w prostym bloku anonimowym.
SET SERVEROUTPUT ON;
BEGIN
outer;
END;
Skutkiem wykonania tego bloku anonimowego (wywołania procedury outer) jest wy-
generowanie wszystkich poddrzew struktury hierarchicznej, poczynając od tych zawie-
rających główny korzeń (root), a kończąc na pojedynczych liściach. Część począt-
kowych i końcowych wierszy wygenerowanych takim kodem zawiera tabela 11.4.
Odpowiada to wykonaniu samozłączenia CONNECT BY bez użycia klauzuli START WITH.
res.Nazwisko);
kto:=res.IdOsoby;
inner(kto,poziom);
FETCH cur INTO res;
END LOOP;
CLOSE cur;
END;
Typ kursora referencyjnego może być zadeklarowany w pakiecie. Możliwe jest wtedy
zarówno zadeklarowanie zmiennych tego typu we wszystkich procedurach (funkcjach)
pakietu (jego ciała) — dotyczy to również procedur (funkcji) wewnętrznych — jak
Rozdział 11. ♦ Kursory 241
W ciele pakietu zdefiniowane zostały ciała obu procedur. Ciało pierwszej zawiera otwar-
cie kursora z klauzulą WHERE, ograniczającą zestaw rekordów tabeli Osoby do pracują-
cych w dziale o identyfikatorze danym parametrem Dzial. Ciało drugiej procedury
zawiera instrukcję FETCH, powodującą przejście do kolejnego rekordu w zestawie i przypi-
sanie pól do zmiennej rekordowej Osoby_row.
CREATE OR REPLACE PACKAGE BODY P_cur
AS
PROCEDURE Open_Osoby_c (Osoby_c IN OUT Osoby_T,
Dzial IN INTEGER) IS
BEGIN
OPEN Osoby_c FOR SELECT * FROM Osoby
WHERE IdDzialu = Dzial;
END Open_Osoby_c;
PROCEDURE Fetch_Osoby_c (Osoby_c IN Osoby_T,
Osoby_row OUT Osoby%ROWTYPE) IS
BEGIN
FETCH Osoby_c INTO Osoby_row;
END Fetch_Osoby_c;
END P_cur;
Kolejny przykład użycia kursora referencyjnego w pakiecie jest dość dziwny z prak-
tycznego punktu widzenia, ale ilustruje najważniejszą cechę tego typu — możliwość
definiowania dowolnego źródła rekordów.
Rozdział 11. ♦ Kursory 243
Zadeklarowano dwie zmienne rekordowe typu zgodnego z wierszami tabel Osoby oraz
Dzialy i zmienną typu kursora referencyjnego. W ciele bloku anonimowego na-
stępuje otwarcie kursora przy pomocy procedury pakietu dla tabeli Osoby (co => 1),
244 Część II ♦ ORACLE PL/SQL
W ciele pakietu zmiana źródła rekordów dla kursora jest związana ze zmianą zarówno
tabeli źródłowej, jak i funkcji (w rozważanym przypadku agregującej). Oba źródła re-
kordów będą zwracały zmienną numeryczną — przyjęcie typu real wydaje się zupełnie
satysfakcjonujące.
CREATE OR REPLACE PACKAGE BODY agregat
AS
PROCEDURE Open_Cur(Cur IN OUT Cur_type,
co IN POSITIVE) IS
BEGIN
IF co = 1 THEN
OPEN Cur FOR SELECT AVG(wzrost) FROM Osoby;
ELSIF co = 2 THEN
OPEN Cur FOR SELECT SUM(Brutto) FROM Zarobki;
ELSE
OPEN Cur FOR SELECT COUNT(IdDzialu) FROM Dzialy;
END IF;
END Open_Cur;
END Osoby_Dzialy_data;
agregat.open_cur(Cur, 1);
FETCH Cur wynik;
DBMS_OUTPUT.PUT(wynik);
EXCEPTION
WHEN ROWTYPE_MISMATCH THEN
DBMS_OUTPUT.PUT_LINE('Niezgodny typ rekordowy');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Inny błąd');
END;
Pozostaje do omówienia problem pojawiający się w przypadku, kiedy kursor nie zo-
staje zamknięty, a my ponownie chcemy go otworzyć. W dotychczasowych przykła-
dach kończyłoby się to komunikatem o błędzie i przerwaniem przetwarzania — doty-
czy to wszystkich rodzajów kursora, w tym kursorów referencyjnych. Rozwiązaniem
może być zastosowanie dyrektywy PRAGMA SERIALLY_REUSABLE w definicji pakietu za-
wierającego deklarację kursora statycznego. Należy zwrócić uwagę na to, że ponieważ
w pakiecie nie zadeklarowano żadnych procedur (funkcji), może on nie posiadać ciała.
Takie pakiety często są używane do deklarowania zmiennych dla innych pakietów
schematu.
CREATE OR REPLACE PACKAGE Sr_Pak
IS
PRAGMA SERIALLY_REUSABLE;
CURSOR Cur IS SELECT Nazwisko FROM Osoby;
END Sr_Pak;
Odwołanie do deklaracji kursora następuje w ciele procedury. Jeśli jest on już otwarty
(%ISOPEN), wyświetlany jest komunikat; w przeciwnym przypadku po wyświetleniu
komunikatu kursor jest otwierany. Następnym elementem jest dwukrotna nawigacja
kursorem po zestawie rekordów. Należy zwrócić uwagę, że na końcu procedury nie
jest on zamykany.
CREATE OR REPLACE PROCEDURE Fetch_from_cursor
IS
Nazwisko VARCHAR2(200);
BEGIN
IF (Sr_Pak.Cur%ISOPEN) THEN
DBMS_OUTPUT.PUT_LINE('Kursor już jest otwarty.');
ELSE
DBMS_OUTPUT.PUT_LINE('Kursor zamknięty; otwieram.');
OPEN Sr_Pak.Cur;
END IF;
FETCH Sr_Pak.Cur INTO Nazwisko;
DBMS_OUTPUT.PUT_LINE('Wybrano: ' || Nazwisko);
FETCH Sr_Pak.Cur INTO Nazwisko;
DBMS_OUTPUT.PUT_LINE('Wybrano: ' || Nazwisko);
END;
246 Część II ♦ ORACLE PL/SQL
Wywołanie procedury możliwe jest albo z bloku anonimowego, albo na skutek wy-
konania polecenia:
EXECUTE Fetch_from_cursor;
Tak jak zaznaczono, zapomniałem zamknąć kursor Sr_Pak.Cur, ale ponieważ obecna
jest dyrektywa SERIALLY REUSABLE, zostanie on zamknięty z chwilą zakończenia wy-
wołania. Zachęcam do przeprowadzenia eksperymentu z wielokrotnym wywoływaniem
procedury w przypadku zastosowania dyrektywy kompilatora i po jej usunięciu z pa-
kietu oraz do śledzenia pojawiających się komunikatów.
Na koniec tego rozdziału niezbędnych jest kilka słów komentarza związanego ze sto-
sowaniem kursorów w praktyce. Wielokrotnie obserwowałem podczas zajęć, jak cie-
szyło studentów pojawienie się kursorów. Dojście do tego etapu powodowało, że po-
jawiały się dobrze znane z programowania strukturalnego pętle i można było odetchnąć
od uciążliwych, złożonych, wielopoziomowych zapytań wybierających. Wreszcie na-
stępowało coś, co miało dobre oparcie w poprzednich doświadczeniach programistycz-
nych. Niestety kursory, chociaż są bardzo kuszącą konstrukcją, niosą ze sobą wiele przy-
krych niespodzianek. Przede wszystkim każdy z nich potrzebuje zasobów (pamięci). Można
powiedzieć, że jest to mało istotne, jeśli tylko zestaw rekordów nie zawiera dużej liczby
pól. To prawda, ale tylko połowiczna. Jeżeli kursor wywoływany będzie rekurencyjnie
(patrz przykłady z hierarchią), to potrzebne zasoby nie są związane tylko z liczbą pól,
ale również ze stopniem zagnieżdżenia rekurencji. Podobnie możemy rozważać zwięk-
szanie się zużycia zasobów, jeśli do kursora odwołuje się równolegle wiele sesji. Może
mniej spektakularne, bo przecież wykonywane równolegle, zapytania wybierające też
powodują większe zużycie zasobów (pamiętać jednak należy, że w tym przypadku część
danych pośrednich jest przechowywana w SGA i równoległe procesy mogą z nich wspól-
nie korzystać). Kolejnym problemem jest czas przetwarzania. Kursor możemy prze-
cież rozpatrywać jako n kolejno wykonywanych zapytań wybierających zwracających
po jednym wierszu, co daje zdecydowanie dłuższy czas przetwarzania. Dlatego też
kursory, chociaż bardzo atrakcyjne, powinny być zastępowane, gdzie tylko się da, za-
pytaniami wybierającymi. W bardzo wielu przypadkach jest to możliwe. W zasadzie je-
dynym uzasadnieniem dla ich stosowania jest konieczność wykonywania operacji na
danych pochodzących z różnych wierszy zestawu rekordów oraz, co zostanie omó-
wione później, konieczność stosowania dynamicznych zestawów danych (dynamicz-
nego SQL). Ostatnim kłopotem, który mogą nam sprawić kursory, jest możliwość blo-
kowania zasobów. Wyobraźmy sobie dwa kursory przetwarzane w dwóch równoległych
sesjach i operujące na tej samej tabeli — jeden przesuwający się od dołu do góry,
a drugi przeciwnie (różne klauzule ORDER BY) — dokonujące modyfikacji danych prze-
twarzanych kolejno rekordów. Grozi to „spotkaniem w połowie drogi” bez możliwości
„ominięcia się” (wzajemna blokada dostępu do rekordu), co wprowadzi obie sesje w stan
zakleszczenia LOCK. Jedynym wyjściem z tej sytuacji jest zabicie poleceniem KILL jednej
z blokujących się sesji. Na szczęście w praktyce taki stan ma miejsce stosunkowo
rzadko, zwłaszcza jeśli nie stosujemy zmieniającego się sortowania źródłowych rekor-
dów kursorów.
Rozdział 12.
Transakcje
Transakcje nie stanowią elementu PL/SQL. Trudno mówić również, że są one częścią
języka zapytań SQL. Transakcyjność stanowi raczej cechę, filozofię przetwarzania da-
nych. Czasami transakcja definiowana jest jako stan niestabilny między dwoma stanami
stabilnymi, jednak taka definicja niewiele tłumaczy. Przyjmijmy zatem, że pierwszym
stabilnym stanem wyjściowym jest ten, w którym wszystkie elementy są utrwalone na
nośniku (dysku). Dotyczy to danych zawartych w tabelach schematu. Jeżeli dokonujemy
jakichkolwiek ich modyfikacji, wprowadzamy schemat w stan niestabilny, podczas którego
nie wszystkie sesje mają dostęp do takiego samego zestawu informacji. Zmiany danych
są widoczne z poziomu sesji, w której zostały one wprowadzone — dopiero zatwierdzenie
transakcji poleceniem COMMIT powoduje utrwalenie danych na nośniku i daje możliwość
dostępu do nich z innych sesji. Rozważmy to na przykładzie zmiany danych w tabeli
Osoby, polegającej na pisowni nazwisk dużymi literami.
BEGIN
UPDATE Osoby SET Nazwisko = UPPER(Nazwisko);
...
COMMIT;
END;
W Oracle transakcje nie są otwierane w sposób jawny. Ich otwarcie powoduje każde
rozpoczęcie sesji. Pomiędzy zapytaniem UPDATE a zatwierdzeniem transakcji COMMIT może
zostać wykonanych wiele innych zapytań. Przez cały ten czas utrzymywany jest stan
niestabilny — otwarta transakcja. Jej zatwierdzenie powoduje zamknięcie jej z jedno-
czesnym otwarciem kolejnej. Nie zawsze transakcja musi kończyć się zatwierdzeniem
— czasami konieczne jest wycofanie wszystkich zmian wprowadzonych w trakcie jej
trwania. Dokonujemy tego poleceniem ROLLBACK, co ilustruje kolejny przykład.
BEGIN
UPDATE osoby SET Nazwisko = UPPER(Nazwisko);
...
ROLLBACK;
END;
Po wycofaniu transakcji stan bazy danych powinien być w całości zgodny ze stanem
występującym przed jej otwarciem. Nie dotyczy to kolejnych wartości generowanych
przez sekwencje. Zatwierdzenie i wycofanie transakcji zilustrowano na przykładzie bloku
248 Część II ♦ ORACLE PL/SQL
anonimowego, jednak w przypadku wykonywania zapytań SQL bez ujęcia ich w jego
ramy zasada transakcyjności pozostaje bez zmian. Stosowanie bloków anonimowych
ma tylko na celu określenie punktu początku przetwarzania. Nie zawsze wycofywanie trans-
akcji musi dotyczyć wszystkich instrukcji. Możemy wycofać tylko część z nich. W celu
zaznaczenia tego zakresu posługujemy się punktem wycofania transakcji SAVEPOINT.
W przykładzie mamy dwie instrukcje modyfikujące dane w tabeli Osoby — pierwsza
powoduje przepisanie nazwisk od dużej litery, a druga małymi literami. Obie instruk-
cje oddzielone są punktem wycofania transakcji o nazwie punkt.
BEGIN
UPDATE Osoby SET Nazwisko = INITCAP(Nazwisko);
SAVEPOINT punkt;
UPDATE Osoby SET Nazwisko = LOWER(Nazwisko);
ROLLBACK TO punkt;
COMMIT;
END;
Należy zwrócić uwagę na to, że punkt wycofania może być pierwszą instrukcją tego
kodu. Nie jest to prosty odpowiednik bezwarunkowego wycofania całej transakcji. Po-
zostaje do zatwierdzenia niejawna pusta instrukcja. Problemem jest nadal pytanie, co
ma stanowić warunek, jakie kryterium ma być sprawdzane. W praktyce wycofywanie
transakcji do punktu SAVEPOINT jest stosunkowo rzadkie, głównie ze względu na pro-
blem ze sformalizowaniem kryteriów.
Wszystkie dotychczasowe przykłady miały jedną podstawową wadę: nie było żadnej
przesłanki do zatwierdzenia lub wycofania zmian. Przede wszystkim ich wycofanie
powinno mieć swoje uzasadnienie. Najczęściej pojawiającą się przyczyną jest wystą-
pienie błędów przetwarzania. Wystąpienie wyjątku jest dobrym powodem — wtedy
wycofanie transakcji następuje w sekcji obsługi wyjątków, co ilustruje przykład.
BEGIN
UPDATE Osoby SET Nazwisko = INITCAP(Nazwisko);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END;
W ciele procedury, po sprawdzeniu, czy wstawiono właściwą nazwę funkcji, jeżeli od-
powiedź jest negatywna, generowany jest błąd użytkownika. Przy odpowiedzi pozy-
tywnej do zmiennej zap składane jest z fragmentów łańcuchów i zmiennych zapytanie
modyfikujące dane, które jest następnie wykonywane poleceniem EXECUTE IMMEDIATE.
250 Część II ♦ ORACLE PL/SQL
Utwórzmy teraz procedurę, która wstawi do naszej tabeli pojedynczy wiersz z warto-
ścią 1, a następnie dokona zatwierdzenia wpisanego rekordu. W definicji procedury
zastosowano dyrektywę kompilatora PRAGMA AUTONOMOUS_TRANSACTION.
CREATE OR REPLACE PROCEDURE autonomous_proc
IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO dummy VALUES (1);
COMMIT;
END;
W normalnej sytuacji kursor przetwarza wszystkie wpisane przed jego otwarciem re-
kordy, dlatego pominięcie deklaracji transakcji autonomicznych spowodowałoby przetwo-
rzenie tylko jednego rekordu oraz dopisanie tego, który wynika z działania procedury.
Zastosowanie AUTONOMOUS_TRANSACTION powoduje, że transakcja związana z otwartym
kursorem „widzi” dane wpisywane przez inną transakcję związaną z procedurą (trans-
akcja zagnieżdżona). Dzięki temu przetwarzane są kolejno wstawiane wiersze.
pisane w bazie. Wynika to z dwóch faktów. Jeżeli nie przełączaliśmy się do innej sesji,
to zmiany były widoczne, ponieważ pracowaliśmy cały czas w tej samej transakcji.
Jest to najczęściej spotykany przypadek podczas prowadzenia zajęć: transakcja trwa
od ich początku do końca — nawet wtedy, kiedy studenci poznają już polecenie COMMIT.
Drugim faktem działającym na naszą korzyść jest ten, że normalne zamknięcie sesji jest
związane z automatycznym zatwierdzeniem transakcji. Zamknięcie sesji może nastąpić
na skutek poprawnego zamknięcia aplikacji klienckiej, przelogowania się oraz rozłą-
czenia się z serwerem bazy danych. Również normalne zatrzymanie serwera spowoduje
zapisanie zmian. Wyłączenie komputera, na którym uruchomiona jest aplikacja kliencka,
przy pomocy wtyczki także nie spowoduje utraty danych, ponieważ sesja jest prze-
twarzana po stronie serwera. Niestety, takie samo zachowanie w odniesieniu do kom-
putera, na którym zainstalowano serwer, skończy się utratą danych z całej sesji (w przy-
padku większości studentów z całego przebiegu zajęć do chwili awarii). Również
zatrzymanie maszyny przez administratora SHUTDOWN z opcjami IMMEDIATE lub ABORT
powoduje w pierwszym wypadku wycofanie zmian, a w drugim nie jest wykonywana
żadna akcja i dane z całej transakcji są tracone.
Jak widać, stanem domyślnym jest w Oracle brak automatycznego zatwierdzania zmian
bezpośrednio po ich dokonaniu. Zmianę sposobu zatwierdzania modyfikacji na auto-
matyczny możemy uzyskać, wykonując polecenie:
SET AUTOCOMMIT ON;
Taki synonim jest jednak w praktyce mało użyteczny, ponieważ obowiązuje on w sche-
macie właściciela obiektu. O wiele bardziej praktyczne jest stosowanie synonimu pu-
blicznego.
CREATE PUBLIC SYNONYM Osoby_ps FOR Osoby
Wszyscy, którzy mają prawo do obiektu, widzą synonim publiczny i mogą się przez
niego odwoływać do obiektu. Pozostaje przy tym możliwość korzystania z nazwy ory-
ginalnej.
SELECT * FROM Osoby_ps
Taki sam skutek otrzymamy, kiedy łańcuch ten zostanie przypisany do zmiennej znako-
wej, a ta pojawi się jako parametr polecenia EXECUTE IMMEDIATE.
DECLARE
zap varchar2(222);
BEGIN
zap:= 'UPDATE Osoby SET Nazwisko=INITCAP(Nazwisko)';
EXECUTE IMMEDIATE zap;
END;
Oczywiście postać tego łańcucha może być dowolnie złożona, poskładana z fragmen-
tów i innych zmiennych znakowych, co można zobaczyć, wracając do rozdziałów 7.
oraz 11., gdzie pokazane zostały możliwości istniejące w przypadku różnych postaci
zapytania modyfikującego procedury exe_tekst. Jednak prawdziwym problemem jest
przypisanie do zmiennej wartości zwracanej przez zapytanie jednowierszowe SELECT,
czy też podstawienie wartości zmiennych do zapytania modyfikującego dane, np. wsta-
wiającego wiersz. Przeanalizujmy to na przykładzie bloku anonimowego, który ma za
zadanie najpierw wykryć maksymalny identyfikator w tabeli Dzialy, a następnie, zwięk-
szając tę wartość o jeden, wstawić nowy dział o nazwie Test.
DECLARE
ktory int;
BEGIN
EXECUTE IMMEDIATE 'SELECT MAX(IdDzialu)
FROM Dzialy' INTO ktory;
DBMS_OUTPUT.PUT_LINE(ktory);
254 Część II ♦ ORACLE PL/SQL
DECLARE
dzial int:=1;
stan int;
ok int := 0;
procedura varchar2(222) := 'test';
BEGIN
EXECUTE IMMEDIATE 'BEGIN ' || procedura ||
'(:1, :2, :3); END;' USING IN dzial, OUT stan, IN OUT ok;
IF ok != 0 THEN
DBMS_OUTPUT.PUT_LINE('Błąd');
ELSE
DBMS_OUTPUT.PUT_LINE('Liczba pracowników wynosi '
|| stan);
END IF;
END;
Tak jak w przypadku procedury, funkcja musi być wywoływana przynajmniej z ciała
bloku anonimowego. W jej przypadku konieczne jest jednak dodatkowe zadeklarowanie,
w sekcji deklaracji tego bloku, zmiennej przeznaczonej na wartość, którą funkcja zwraca
przez nazwę. Stąd początek łańcucha w poleceniu EXECUTE IMMEDIATE stanowi słowo
kluczowe DECLARE. Należy zauważyć, że o ile przy podstawianiu zmienna st jest zmienną
związaną (poprzedzający ją znak dwukropka), to przy jej deklarowaniu takiego zwią-
zania nie ma (brak dwukropka). Przekazanie zmiennych z całego bloku anonimowego
(zmienna st nie jest parametrem funkcji) odbywa się, tak samo jak dla procedury,
dzięki zastosowaniu operatora USING. Obowiązuje nas odpowiedniość pozycyjna zmien-
nych. Tym razem pierwszą, licząc od lewej strony, jest zmienna st, do której funkcja
przypisuje wartość, a dopiero w dalszej kolejności pojawiają się parametry formalne
funkcji. Ponieważ zmienna st jest przekazywana z bloku anonimowego na zewnątrz,
konieczne jest poprzedzenie pierwszej zmiennej po operatorze USING, czyli ok, deskrypto-
rem OUT. Reszta bloku anonimowego jest prostą analogią kodu zaprezentowanego przy
omawianiu dynamicznego wywoływania procedur.
Przy tej konwencji kolejność podstawiania zmiennej pod parametr jest dowolna. Na-
tomiast przypisanie zmiennych związanych do zmiennych występujących po operato-
rze USING jest w dalszym ciągu pozycyjne. Nazwy zmiennych związanych wybrano
w taki sposób, aby nie sugerowały, że istnieje jakakolwiek przyczyna stosowania w nich
kolejnych numerów. Nazwy zmiennych wiązania mogą być dowolne.
USING ktory;
DBMS_OUTPUT.PUT_LINE(nazwa);
END;
W ciele bloku został użyty niejawny kursor, który ma za zadanie przetwarzanie nazw
tabel w schemacie użytkownika. Zastosowano definicję zestawu rekordów kursora opartą
na perspektywie słownikowej USER_TABLES. W ciele pętli FOR, odpowiedzialnej za
nawigowanie po rekordach kursora w zmiennej v_sql, składany jest blok anonimowy
zawierający zapytanie zliczające liczbę wierszy o składni SELECT COUNT(*) INTO. Jego
wykorzystanie jest potrzebne właśnie ze względu na przyjęcie takiej postaci zapytania,
która może być stosowana tylko w ciele elementów programistycznych PL/SQL. Ze
względu na takie rozwiązanie zliczania rekordów w poleceniu EXECUTE IMMEDIATE użyty
został operator USING OUT. Następnie wyświetlany jest komunikat zawierający nazwę
tabeli oraz liczbę jej wierszy. Schemat taki powtarza się dla wszystkich nazw tabel
przetwarzanych przez kursor. Kod może zostać uproszczony, jeśli będziemy dynamicznie
przetwarzać jednowierszowe zapytanie wybierające (bez zastosowania INTO). Przykład
takiej realizacji zawiera kolejny kod.
SET SERVEROUTPUT ON;
DECLARE
v_sql VARCHAR2(1000);
v_cnt NUMBER(10);
BEGIN
FOR rec IN (SELECT TABLE_NAME FROM USER_TABLES)
LOOP
v_sql:= 'SELECT COUNT(*) FROM ' || rec.table_name;
EXECUTE IMMEDIATE v_sql INTO v_cnt;
DBMS_OUTPUT.PUT_LINE(rec.table_name || '-' ||v_cnt);
END LOOP;
END;
Ciało funkcji zawiera złożony z łańcuchów znakowych i zmiennych kod bloku anoni-
mowego PL/SQL o statycznej postaci, zapisanej w metajęzyku jak następuje:
DECLARE Licznik int;
BEGIN
SELECT COUNT (DISTINCT pKolumna) INTO Licznik FROM pTabela;
END;
Jednym z problemów, które wynikają ze stosowania Oracle, jest brak tabel mających
ograniczony czas życia. Jak pamiętamy, tabele tymczasowe w Oracle są trwałe, a tylko
ich zawartość jest nietrwała. Stąd może się pojawić potrzeba tworzenia tabel, które po
przetworzeniu pewnego fragmentu kodu i spełnieniu przez nie roli tymczasowego składu
danych zostaną wykasowane. Może być to wykonane z zastosowaniem dynamicznego
SQL. Wykorzystanie go w odniesieniu do operacji na tabeli (DDL) możemy prześle-
dzić na podstawie bloku anonimowego, w którym zadeklarowano trzy zmienne po-
Rozdział 13. ♦ Dynamiczny SQL 261
W ciele bloku inicjowana jest napisem testowa wartość zmiennej zawierającej nazwę
tabeli. Następnie zmienna zap jest zasilana łańcuchem zawierającym polecenie two-
rzące dwupolową tabelę o zadanej nazwie. Zapytanie to jest przetwarzane za pomocą
polecenia EXECUTE IMMEDIATE. Dalej do zmiennej zap przypisywany jest łańcuch za-
wierający zapytanie, które wstawia wiersz z dwoma zmiennymi związanymi. Przy jego
wykonywaniu użyto operatora USING, aby przekazać wartości do zmiennych związa-
nych — dokonać wstawienia wartości do tabeli. Po dwóch operacjach wstawiania da-
nych zmienna zap zostaje zasilona jednowierszowym zapytaniem wybierającym, obli-
czającym sumę wartości pierwszej z kolumn. Użyty operator INTO zapewnia przekazanie
obliczonej wartości do zmiennej bloku anonimowego suma. Kolejną zawartością zmien-
nej zap jest zapytanie usuwające tabelę, a następną zatwierdzające transakcję. Oba za-
pytania są kolejno wykonywane poleceniem EXECUTE IMMEDIATE. Ponieważ cała ope-
racja tworzenia, zasilania danymi i usuwania tabeli jest wykonywana w jednej sesji,
polecenie zatwierdzające transakcję jest nadmiarowe. W naszym przykładzie zostało
dodane, aby pokazać potencjalną możliwość jego zastosowania.
W ciele procedury zmienna zap jest zasilana zapytaniem wybierającym pole dane
pierwszym parametrem z tabeli danej parametrem drugim. Dla tak zdefiniowanego za-
pytania otwierany jest kursor referencyjny, którym nawigujemy za pomocą polecenia
FETCH, stosując pętlę WHILE. Kolejno odczytywane wartości są wstawiane do zmiennej
wart i wyświetlane. Nawet jeśli odczytana zmienna jest typu innego niż znakowy, to
zostanie ona do zmiennej znakowej przepisana, pod warunkiem że ta ostatnia ma za-
deklarowaną odpowiednio dużą liczbę znaków. Wywołanie tak utworzonej procedury
można wykonać w bloku anonimowym jak niżej, gdzie wyświetlone zostaną nazwy
działów zawarte w tabeli Dzialy.
SET SERVEROUTPUT ON;
DECLARE
pole varchar2(20);
tabela varchar2(20);
BEGIN
pole := 'Nazwa';
tabela := 'Dzialy';
dyn_cur(pole,tabela);
END;
END LOOP;
CLOSE cur;
END;
Ograniczenie NOT NULL jest opcjonalne. Typ ten ma jedną podstawową wadę: koniecz-
ność określenia rozmiaru definiującego maksymalną liczbę elementów. Indeksowanie
zmiennej tego typu rozpoczyna się od wartości 1. Drugą możliwością jest zastosowa-
nie typu TABLE OF.
TYPE NazwaTypu IS TABLE OF TypElementu NOT NULL
INDEX BY {PLS_INTEGER | BINARY_INTEGER | VARCHAR2(rozmiar)};
W typie tym nie podajemy rozmiaru maksymalnego, co jest ważne w przypadku, kiedy
nie jesteśmy w stanie określić nawet przybliżonej liczby elementów tabeli. Ogranicze-
nie NOT NULL jest opcjonalne. Tak samo opcjonalne jest stosowanie indeksowania, jed-
nak jego użycie znacznie przyspiesza operacje na zmiennych tego typu. Z reguły sto-
sujemy indeksowanie numeryczne PLS_INTEGER lub BINARY_INTEGER, a wtedy maksymalna
liczba elementów wynika z zakresu wartości indeksu, np. dla typu BINARY_INTEGER
(–2 147 483 647 – 2 147 483 647). W przypadku indeksowania znakowego maksymalny
zakres wynika z liczby znaków w definicji typu oraz z liczby symboli. Dla obu typów
tabelarycznych dostępne są metody:
EXISTS(n) — zwraca TRUE, gdy istnieje n-ty element tabeli;
w przeciwnym przypadku zwraca wartość FALSE.
COUNT — zwraca liczbę wierszy w tabeli.
FIRST — zwraca wartość najmniejszego indeksu tabeli;
kiedy jest ona pusta, zwraca NULL.
LAST — zwraca wartość największego indeksu tabeli; kiedy jest ona pusta,
zwraca NULL.
PRIOR(n) — zwraca indeks elementu poprzedzającego element o indeksie n;
kiedy taki nie istnieje, zwraca NULL.
NEXT — zwraca indeks elementu następującego po elemencie o indeksie n;
kiedy taki nie istnieje, zwraca NULL.
264 Część II ♦ ORACLE PL/SQL
Kolejny kod również ma swój początek w zadaniach zgłębiania danych, gdzie wskazane
jest wykrycie kolumny klucza głównego oraz wyeliminowanie z dalszej analizy ko-
lumn, na których zdefiniowano ograniczenia integralnościowe. Prześledźmy zastoso-
wanie typu tabelarycznego TABLE OF oraz dynamicznego kursora referencyjnego na
przykładzie bloku anonimowego, którego zadaniem jest wyświetlenie nazw kolumn
tabeli, dla których ustanowiono ograniczenia. Zadeklarowane zostały zmienne: rcur
— typ kursora referencyjnego, kol_r — typ rekordowy o dwóch polach znakowych
(pierwsze o zmiennej długości, a drugie zawierające jeden znak kodujący typ ograni-
czenia), kol_t — typ tabelaryczny o wierszach typu rekordowego kol_r. Ponadto za-
deklarowano instancje zdefiniowanych typów: kol dla typu tabelarycznego, temp dla
typu rekordowego oraz cur dla typu kursora referencyjnego. Zdefiniowana została też
zmienna numeryczna licz oraz dwie zmienne znakowe: tabela oraz zap.
DECLARE
TYPE rcur IS REF CURSOR;
TYPE kol_r IS RECORD
(NAZWA VARCHAR2(255), CONSTR CHAR(1));
TYPE kol_t IS TABLE OF KOL_R
INDEX BY BINARY_INTEGER;
kol kol_t;
temp kol_r;
cur rcur;
licz INT;
tabela varchar2(20);
zap varchar2(2000);
BEGIN
tabela := 'Dzialy';
licz := 1;
kol.DELETE;
zap := 'SELECT tc.column_name, constraint_type FROM
USER_TAB_COLUMNS TC LEFT JOIN
USER_CONS_COLUMNS CC
ON tc.table_name = cc.table_name AND
tc.column_name = cc.column_name
LEFT JOIN USER_CONSTRAINTS UC ON
uc.constraint_name = cc.constraint_name
Rozdział 13. ♦ Dynamiczny SQL 265
W ciele bloku anonimowego zmienną tabela zainicjowano nazwą tabeli Dzialy, a zmienną
licz wartością 1. Inicjacji zmiennej tabelarycznej dokonano za pomocą metody DELETE.
Pomimo że tabela jest pusta, metoda ta pozwala na przypisanie zmiennej zasobów. Źró-
dłem danych dla kursora referencyjnego jest zapytanie wybierające nazwę kolumny
i kod ograniczenia z połączonych operatorem LEFT JOIN perspektyw słownikowych:
USER_TAB_COLUMNS, zawierającej nazwy kolumn tabel użytkownika, USER_CONS_
COLUMNS, stanowiącej tabelę pośrednią, oraz USER_CONSTRAINTS, zawierającej
kody ograniczeń integralnościowych ustanowionych przez użytkownika. Zastosowanie
operatora LEFT JOIN powoduje, że kursor ma niepusty zestaw rekordów nawet wtedy,
kiedy w tabeli nie zdefiniowano żadnych więzów. Kursor jest otwierany dla tak zde-
finiowanego zapytania, a nawigacja odbywa się w pętli WHILE z wykorzystaniem pole-
cenia FETCH, które zwraca wartości pól przetwarzanego rekordu do pomocniczej zmien-
nej rekordowej temp. Jej wartość w kolejnych cyklach pętli jest przepisywana do
zmiennej tabelarycznej kol o indeksie wynikającym z wartości inkrementowanej w pętli
zmiennej licz. Dla sprawdzenia przetwarzania w ciele pętli generowany jest napis za-
wierający nazwę kolumny i kod ograniczenia. Aby pokazać sposób odczytywania da-
nych ze zmiennej tabelarycznej, zastosowano pętlę FOR, w której dolny indeks wyzna-
cza metoda FIRST, a górny metoda LAST zmiennej tabelarycznej kol. W ciele tej pętli
wyświetlany jest napis analogiczny do zastosowanego w pętli nawigującej kursorem.
Kolejnym przykładem, który może mieć zastosowanie praktyczne, jest zadanie wyszu-
kiwania drogi w grafie pomiędzy dowolnymi dwoma jego węzłami. Graf jest przecho-
wywany w schemacie relacyjnym za pomocą notacji krawędziowej (rysunek 13.1).
Wiersz zawierający wpis dwóch węzłów OD i DO oznacza, że istnieje droga od pierw-
szego węzła do drugiego. Przyjęto na potrzeby rozważań graf skierowany, aby więc
wskazać możliwość drogi powrotnej, konieczne jest umieszczenie kolejnego wiersza
z odwrotnym porządkiem węzłów OD i DO. Oznacza to, że pomiędzy niektórymi wę-
złami grafu może pojawić się tylko jeden kierunek przejścia (droga jednokierunkowa).
Trzecia kolumna określa wagę dla przejścia między węzłami. Może być to np. odle-
głość między miastami mierzona w kilometrach, ale również przepustowość łącza czy
wydajność przepływu, w zależności od tego, jaki konkretny obiekt fizyczny jest opi-
sywany grafem. Wagi dla przejścia od A do B oraz od B do A nie muszą być takie
266 Część II ♦ ORACLE PL/SQL
same (objazd dla jednego z kierunków ruchu, różne rodzaje łączy dla każdego z nich etc.).
Przykładowe dane do zadania wyszukiwania drogi między węzłami w grafie skierowa-
nym oraz ich reprezentację graficzną przedstawia rysunek 13.1.
W ciele funkcji do zmiennej war_old przepisywana jest wartość zmiennej war, czyli za-
pamiętywana jest wejściowa postać warunku. Następnie budowane jest zapytanie sta-
nowiące źródło danych dla kursora referencyjnego. Wybiera ono węzeł DO ze wszyst-
kich rekordów, w których węzeł OD jest równy parametrowi ODP funkcji, przy czym
nie są wybierane te z nich, dla których węzeł DO jest elementem listy już odwiedzo-
nych węzłów zapisanych w postaci listy zawartej w zmiennej war. Kursor jest otwierany,
a nawigacja w pętli WHILE jest wykonywana za pomocą polecenia FETCH, przekazują-
cego znalezione węzły do zmiennej ODB. Do zmiennej war dopisywany jest poprze-
dzony przecinkiem i ograniczony apostrofami wyszukany węzeł DO, co powoduje do-
pisanie kolejnego węzła do listy już przeszukanych. Jeśli wykryty w ten sposób węzeł
jest równy węzłowi końcowemu, zmienna war przekształcana jest w ten sposób, że
pomijanych jest pięć pierwszych znaków, a w pozostałym łańcuchu fraza ', ' jest za-
stępowana myślnikiem. Otrzymany tą drogą łańcuch wyświetlany jest na ekranie. Jeśli
nie osiągnięto węzła końcowego, rekurencyjnie wywoływana jest procedura find, w ten
sposób, że odnaleziony węzeł DO staje się węzłem początkowym ODP na kolejnym po-
ziomie rekurencji, natomiast zmienna war zawiera już kolejny węzeł, jak to opisano wy-
żej. W każdym cyklu pętli na jej końcu przywracana jest wejściowa wartość zmiennej
war przez podstawienie do niej zapamiętanej w zmiennej war_old wartości.
Przedstawiony algorytm wyszukiwania drogi w grafie nie jest wydajny i już w przy-
padku ok. 20 węzłów czas jego przetwarzania może być niedopuszczalnie długi,
szczególnie wtedy, gdy graf ma dużą liczbę krawędzi. Rozwiązaniem może być jed-
noczesne poszukiwanie drogi w obu kierunkach: od początku do końca i od końca do
początku. Nawet w przypadku grafu skierowanego będzie się to wiązało z zastosowa-
niem dwóch procedur o strukturze analogicznej do przedstawionej poprzednio. W prak-
tycznym rozwiązaniu zastosowany został o wiele bardziej wydajny algorytm Dijkstry,
który tworzy tzw. macierz sąsiedztwa i nie wymaga przejścia wszystkich potencjal-
nych dróg. W zamian za to znajduje on tylko najkrótszą drogę między dwoma węzłami,
podczas gdy prezentowany tutaj algorytm znajduje wszystkie możliwe.
Stosowanie dynamicznego SQL, poza dużą liczbą cech pozytywnych, ma jeden zde-
cydowany minus. Jak już pisano, zapytania podlegają optymalizacji przy użyciu we-
wnętrznych optymalizatorów. Generowane są też plany wykonania tych zapytań. W przy-
padku dynamicznego SQL taką optymalizację trudno sobie wyobrazić, gdyż ostateczna
postać zapytania powstaje dokładnie w chwili jego przetwarzania. Należy również za-
uważyć, że zapytania dynamiczne mogą być łatwiejszym celem ataków typu SQL
INJECTION, zwłaszcza jeśli postać przetwarzana dynamicznie jest kodem bloku ano-
nimowego. Między średnikami zawsze można ukryć dodatkowy, nadmiarowy kod za-
wierający niepożądane zapytanie.
Rozdział 14.
Zastosowanie
Javy do tworzenia
oprogramowania
po stronie serwera
Dość powszechnie znany jest fakt, że wszystkie produkty Oracle zostały napisane przy
użyciu języka Java. Również użytkownik może wykorzystać ten język do tworzenia wła-
snego oprogramowania po stronie serwera. Przy omawianiu tej części założyłem, że
czytelnik ma podstawowe umiejętności programistyczne. Ponieważ książka nie stanowi
podręcznika programowania obiektowego, ograniczyłem się do prostych przykładów
kodu, które z jednej strony nie powinny nastręczyć trudności mniej biegłym progra-
mistom, a z drugiej prezentują najbardziej użyteczne schematy tworzenia oprogramo-
wania dla serwera bazy danych. W tym miejscu należy zauważyć, że jeśli chcemy mieć
możliwość oglądania w drzewie obiektów tworzonych przez nas klas Javy, musimy za-
instalować najnowszą wersję SQL Developera zamiast albo oprócz tej, która jest au-
tomatycznie dołączana podczas instalacji Oracle. Oprogramowanie to jest również do-
stępne do pobrania ze strony domowej Oracle — rysunek 14.1.
Rysunek 14.1. Widok SQL Developera (nowa wersja) podczas tworzenia klasy Javy
Należy pamiętać, że środowisko Javy jest wrażliwe na wielkość znaków. Ta uwaga do-
tyczy nie tylko zmiennych łańcuchowych, ale również nazw zmiennych, funkcji etc.
Powyżej tworzony jest obiekt typu JAVA SOURCE o nazwie Hello, zwierający klasę pu-
bliczną o tej samej nazwie. W klasie tej została zdefiniowana pojedyncza publiczna
i statyczna klasa zwracająca zmienną typu String o nazwie hello (nazwy klasy i metody
są różne, bo użyto innej wielkości liter). W ciele metody zdefiniowano, że na zewnątrz
będzie przekazywany napis Witamy.
Usuwanie klasy Javy (JAVA SOURCE) na poziomie serwera Oracle wykonujemy polece-
niem:
DROP JAVA SOURCE Hello;
Aby można było wywołać metodę klasy Javy z poziomu PL/SQL, konieczna jest jej
enkapsulacja do obiektu proceduralnego — funkcji lub procedury. Dla klasy static
niezbędne jest utworzenie funkcji PL/SQL korzystającej z tej klasy. Funkcja ta musi
zwracać wartość typu, na który da się sensownie przekonwertować typ zwracany przez
klasę Javy (string → varchar2).
CREATE OR REPLACE FUNCTION hello
RETURN varchar2
AS
LANGUAGE JAVA
NAME 'Hello.hello () return string';
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 271
Ciało funkcji zawiera jedynie informację o enkapsulacji klasy Javy wyrażoną przez
dyrektywę LANGUAGE oraz kwalifikowaną nazwę metody (klasa.metoda). Dla klasy typu
static konieczne jest zdefiniowanie typu Javy, który jest przez tę metodę zwracany.
Bez względu na to, czy ciałem funkcji jest kod napisany w PL/SQL, czy też klasa
Javy, sposób jej wywołania jest dokładnie taki sam.
DECLARE com varchar2(333);
BEGIN
com := hello;
DBMS_OUTPUT.PUT_LINE(com);
END;
W ciele metody Javy zadeklarowana została pomocnicza zmienna prosta temp typu int,
do której podstawiono pierwszy (jedyny) element tablicy b, czyli ten o najniższym in-
deksie — 0. Możliwe byłoby zastosowanie zmiennej pomocniczej o typie tablicowym,
nie jest to jednak konieczne. Następnie przepisano pierwszy element tablicy a do ta-
blicy b oraz zmienną temp do pierwszego elementu tej ostatniej. Zmienna temp pełni
w metodzie rolę „pamięci”.
Kolejnym krokiem jest enkapsulacja tak sformułowanej klasy Javy do postaci proce-
dury w PL/SQL. Utworzona została procedura PL/SQL o nazwie Zamien z dwoma pa-
rametrami typu IN OUT. Zastosowanie zmiennych tego typu jest wymuszone koniecz-
nością przekazywania ich do procedury oraz odczytywania po jej wykonaniu.
272 Część II ♦ ORACLE PL/SQL
Ciało procedury PL/SQL zawiera, tak samo jak poprzednio, wskazanie na język Java
oraz kwalifikowaną nazwę metody Javy wraz z definicją nazw i typów zmiennych.
Wywołanie tak utworzonej procedury odbywa się na takich samych zasadach jak przy
zwykłej procedurze napisanej tylko z zastosowaniem PL/SQL.
DECLARE
x int;
y int;
BEGIN
x := 2;
y := 3;
Zamien(x, y);
DBMS_OUTPUT.PUT_LINE(x);
DBMS_OUTPUT.PUT_LINE(y);
END;
Kolejnym prostym przykładem jest klasa statyczna obliczająca iloraz dwóch parame-
trów wejściowych — obydwa typu double. Tym razem źródło Javy nazwano Dziel,
klasę Dzielenie, a metodę Dziele. Ponieważ parametry będą przekazywane tylko do
funkcji, nie było konieczności deklarowania ich jako zmiennych tablicowych, a wy-
starczyły typy proste.
CREATE OR REPLACE JAVA SOURCE NAMED Dziel
AS
public class Dzielenie
{
public static double Dziele (double a, double b)
{
return a/b;
}
};
return a/b;
}
catch (Exception e)
{
System.err.println(e.getMessage());
return 0;
}
}
};
x := binary_float_infinity;
DBMS_OUTPUT.PUT_LINE(x);
x := -binary_float_infinity;
DBMS_OUTPUT.PUT_LINE(x);
x := binary_double_infinity;
DBMS_OUTPUT.PUT_LINE(x);
x := utl_raw.cast_to_number('FF65');
DBMS_OUTPUT.PUT_LINE(x);
x := utl_raw.cast_to_number('00');
DBMS_OUTPUT.PUT_LINE(x);
END;
Jako rezultat otrzymano tym razem zamiast znaku ~ literał Infinity, który bardziej
precyzyjnie określa zwracaną wartość. Jak widać, predefiniowane wartości ±nieskoń-
czoność przenoszą się bez przeszkód na stronę kodu PL/SQL.
INFF
----------------------
Infinity
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 275
NINFF
----------------------
-Infinity
LNE(2)
----------------------
0,693147180559945
LNE(0)
----------------------
-Infinity
276 Część II ♦ ORACLE PL/SQL
Niestety, nie trzeba być mistrzem matematyki, aby odkryć, że to jednak nie to samo.
Za zgłoszenie wyjątku zapłaciliśmy zmianą wyniku obliczeń!
Instrukcje związane z przetwarzaniem danych muszą być zawarte w bloku try — w prze-
ciwnym wypadku generowany jest komunikat o błędzie składniowym klasy Javy. Po-
łączenie definiowane jest przez zainicjowanie zmiennej conn typu Connection z zasto-
sowaniem metody DriverManager.getConnection. W przypadku połączenia z bieżącą
instancją serwera Oracle parametrem tej metody jest łańcuch wskazujący na domyślne
połączenie o postaci jdbc:default:connection. Zmienna klasy String została zaini-
cjowana łańcuchem zawierającym zapytanie wybierające zliczające osoby o wzroście
wyższym niż wartość parametru param. Należy zauważyć, że nie ma żadnych ograni-
czeń dotyczących definicji zapytania wybierającego, ponieważ będzie ono przetwa-
rzane jak kursor, co zostanie opisane dalej. Następnie za pomocą metody obiektu
278 Część II ♦ ORACLE PL/SQL
Enkapsulacja do funkcji PL/SQL klasy operującej na danych w bazie nie różni się ni-
czym od każdej innej. W skrypcie przedstawiono dodatkowo przykład wywołania tej
funkcji.
CREATE OR REPLACE FUNCTION ilWzrf
(param IN REAL) RETURN real
AS
LANGUAGE JAVA
NAME 'LWzrost.ilosc(double) return double';
/
SET SERVEROUTPUT ON;
DECLARE
ok int;
BEGIN
ok := ilWzrf(1.7);
DBMS_OUTPUT.PUT_LINE(ok);
END;
Jak już pokazano, podobnie jak enkapsulacja do funkcji, może zostać wykonana także
enkapsulacja do procedury. Taka sama zasada obowiązuje dla klas Javy operujących
na danych z serwera danych. Wymaga to jedynie zmiany typu klasy Javy na static
void. Ponieważ tym razem wynik będzie przekazywany przez parametr występujący
na liście parametrów, czyli musi być przekazywany do miejsca wywołania, należy zmie-
nić typ tego parametru po stronie Javy na zmienną tabelaryczną, aby wymusić prze-
kazywanie zmiennej przez referencję, a nie przez wartość, jak to już pokazano we
wcześniejszych przykładach. Taka operacja jest konieczna dla wszystkich parametrów,
które po stronie PL/SQL są typu OUT lub IN OUT. Cała zawartość ciała klasy nie różni
się w sposób istotny od poprzednio pokazanego schematu, za wyjątkiem braku instruk-
cji return, co jest oczywiste dla metod void.
CREATE OR REPLACE AND COMPILE
JAVA SOURCE NAMED LWZROST AS
import java.sql.*;
import java.io.*;
import oracle.jdbc.*;
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 279
import java.io.*;
import oracle.jdbc.*;
public class wiersze
{
public static int licz (String str)
{
int ile = 0;
try
{
Connection conn =
DriverManager.getConnection ("jdbc:default:connection");
String sql = "SELECT COUNT(*) FROM " + str;
Statement stmt = conn.createStatement();
ResultSet rset = stmt.executeQuery(sql);
while (rset.next())
{
ile = rset.getInt(1);
}
rset.close();
stmt.close();
return ile;
}
catch (SQLException e)
{
System.err.println(e.getMessage());
return -1;
}
}
};
/
CREATE OR REPLACE FUNCTION iWierszy
(str varchar2) RETURN real
AS
LANGUAGE JAVA
NAME 'wiersze.licz (java.lang.String) return int';
/
DECLARE
ok int;
tab varchar2(22);
BEGIN
tab := 'inna';
ok := iWierszy(tab);
DBMS_OUTPUT.PUT_LINE('ok = ' || ok);
END;
Jeżeli tabeli o nazwie inna nie ma w schemacie, funkcja jest przetwarzana poprawnie,
natomiast zwracana wartość informuje nas o błędzie, który może zostać obsłużony
w miejscu wywołania.
anonymous block completed
ok = -1
Równie dobrze można byłoby użyć metody first() lub last() zestawu rekordów,
ponieważ każda z nich wskaże i tak ten sam pierwszy, ostatni i jedyny rekord. Nie-
stety, użycie jakiejkolwiek metody nawigacji różnej od next() wymaga zdefiniowania
przewijalnego obiektu Statement. W niektórych środowiskach baz danych podobną
strukturą jest kursor typu scroll, który nie występuje w PL/SQL. Stąd w przekształ-
conym przykładzie klasy Javy, obliczającej liczbę wierszy tabeli, której nazwa jest
dana parametrem, użyte zostały parametry ResultSet.TYPE_SCROLL_INSENSITIVE
i ResultSet.CONCUR_UPDATABLE definiujące atrybuty obiektu Statement. W skrypcie po-
kazano również enkapsulację oraz przykład wywołania o strukturze tożsamej z poka-
zanym poprzednio.
CREATE OR REPLACE AND COMPILE
JAVA SOURCE NAMED ilewierszy AS
import java.sql.*;
import java.io.*;
import oracle.jdbc.*;
public class wiersze
{
public static int licz (String str)
{
int ile = 0;
try
{
Connection conn =
DriverManager.getConnection ("jdbc:default:connection");
String sql = "SELECT COUNT(*) FROM " + str;
Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE);
ResultSet rset = stmt.executeQuery(sql);
boolean ok=rset.last();
ile = rset.getInt(1);
rset.close();
stmt.close();
return ile;
}
catch (SQLException e)
{
System.err.println(e.getMessage());
return -1;
}
catch (Exception ex)
{
System.err.println(ex.getMessage());
return -2;
}
}
};
/
CREATE OR REPLACE FUNCTION iWierszy
(str varchar2) RETURN real
AS
LANGUAGE JAVA
NAME 'wiersze.licz (java.lang.String) return int';
/
DECLARE
ok int;
282 Część II ♦ ORACLE PL/SQL
tab varchar2(22);
BEGIN
tab := 'Osoby';
ok := iWierszy(tab);
DBMS_OUTPUT.PUT_LINE('ok = ' || ok);
END;
Podobnie jak z bieżącą instancją, możliwe jest połączenie z innym serwerem Oracle.
Różnica polega głównie na zmianie łańcucha połączeniowego w definicji połączenia
z bazą zdefiniowanego w poniższym przykładzie wartością zmiennej dbURL. Wskazuje
ona na zastosowany sterownik, nazwę komputera, na którym zainstalowano serwer
Oracle, port, na którym prowadzony jest nasłuch, oraz nazwę instancji bazy danych.
Aby zrealizować połączenia, należy najpierw zarejestrować sterownik JDBC za pomocą
metody DriverManager.registerDriver, której parametrem jest nazwa klasy sterownika
zawarta w odpowiednim pliku biblioteki jar. Plik ten musi znajdować się w jednej z lo-
kalizacji wskazywanych zmienną systemową CLASSPATH. Dopiero wtedy możemy zre-
alizować połączenie, stosując metodę DriverManager.getConnection, której parametrami
są: łańcuch połączeniowy, nazwa użytkownika oraz jego hasło. Pozostałe elementy
klasy Javy nie różnią się niczym od przypadku przetwarzania zapytań w bieżącej in-
stancji Oracle. Przedstawiony poniżej przykład zawiera klasę zliczającą osoby o wzro-
ście wyższym niż wartość dana parametrem, enkapsulację tej klasy do procedury oraz
przykład wywołania.
CREATE OR REPLACE AND COMPILE
JAVA SOURCE NAMED LWZROST_INNY AS
import java.sql.*;
public class LWzrost_inny {
public static void ilosc_inny (double param, double[] ile)
{
Connection con;
String dbURL = "jdbc:oracle:thin:@host:1521:oracle";
String userid = "testowy";
String passwd = "kajak";
try {
DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
con = DriverManager.getConnection(dbURL, userid, passwd);
String sql ="SELECT COUNT(*) FROM Osoby WHERE Wzrost >" + param;
Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery(sql);
while (rset.next())
{
ile[0] = (double)rset.getInt(1);
}
rset.close();
con.close();
}
catch (SQLException e) {
e.printStackTrace();
ile[0] = -1;
}
}
};
/
CREATE OR REPLACE PROCEDURE ilWzr_inny
(param IN REAL,ile IN OUT NUMBER)
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 283
AS
LANGUAGE JAVA
NAME 'LWzrost_inny.ilosc_inny(double, double[])';
/
DECLARE
a real;
BEGIN
ilWzr_inny(1.7, a);
DBMS_OUTPUT.PUT_LINE(a);
END;
Niestety, aby wykonać taką procedurę, konieczne jest nadanie użytkownikowi praw
do połączenia oraz zastosowania symbolicznej nazwy hosta. Formalnie wystarczy
nadać tylko uprawnienie resolve. Do przypisania prawa użyto wewnętrznej procedury
dbms_java.grant_permission ze schematu sys.
CALL dbms_java.grant_permission
('AP', 'SYS:java.net.SocketPermission', 'host', 'connect,resolve');
W zasadzie przy użyciu mostu JDBC możliwe jest połączenie się z dowolnym serwerem
baz danych pod warunkiem zarejestrowania odpowiedniego sterownika, poprawnego
zdefiniowania łańcucha połączeniowego oraz posiadania odpowiedniej biblioteki jar.
Należy przy tym pamiętać, że plik sterownika musi być umieszczony w jednej z lo-
kalizacji wskazanych zmienną środowiskową CLASSPATH lub folder, w którym się ona
znajduje, zostanie do tej zmiennej dodany. Zaprezentowany poniżej przykład jest prze-
kształceniem poprzednich dwóch, tym razem jednak dla serwera danych MS SQL Se-
rver w wersji co najmniej 2000.
CREATE OR REPLACE AND COMPILE
JAVA SOURCE NAMED LWZROST_SQL AS
import java.util.*;
import java.sql.*;
public class LWzrost_sql {
public static String ilosc_sql (double param)
{
double ile = 0;
try
{
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
//DriverManager.registerDriver(new com.microsoft.sqlserver.jdbc.SQLServerDriver());
Connection con = DriverManager.getConnection("jdbc:microsoft:
sqlserver://localhost:1433", "sa", "kajak");
String sql = "SELECT COUNT(wzrost) FROM Osoby WHERE wzrost > " + param;
Statement stmt = con.createStatement();
ResultSet rset = stmt.executeQuery(sql);
while (rset.next())
{
284 Część II ♦ ORACLE PL/SQL
ile = (double)rset.getInt(1);
}
rset.close();
stmt.close();
return "dobrze "+ile;
}
catch (SQLException e) {
e.printStackTrace();
return "sql " + e.getErrorCode();
}
catch (Exception e2) {
return "inny blad " + e2.getMessage();
}
}
}
/
CREATE OR REPLACE FUNCTION iWierszy_sql
(param real) RETURN varchar2
AS
LANGUAGE JAVA
NAME 'LWzrost_sql.ilosc_sql (double) return java.lang.String';
/
DECLARE
ok varchar2(1000);
BEGIN
Ok := iWierszy_sql(1.7);
DBMS_OUTPUT.PUT_LINE('ok = ' || ok);
END;
W przykładzie pokazano tylko dwie metody obiektu File. Oferuje on w zasadzie wszyst-
kie możliwe operacje na folderach i plikach, takie jak kopiowanie, zmiana nazwy,
przeniesienie itp. Tym razem, zamiast enkapsulować metody klasy Javy do separowa-
nych funkcji (procedur) PL/SQL, wykorzystamy enkapsulację do postaci pakietu. Za-
stosowano ponadto kolejną, często wykorzystywaną w praktyce sztuczkę. Bezpośrednie
przekształcenie metody Javy do funkcji czy procedury nie pozwala na zastosowanie
sekcji obsługi wyjątków na poziomie enkapsulacji PL/SQL, dlatego zastosowana zo-
stała enkapsulacja dwuetapowa. Znana już metoda tworzy funkcje wewnętrzne pakietu,
które są wywoływane ze zwykłych funkcji. Na tym drugim etapie obsługiwane są we-
dług powszechnie obowiązujących zasad wyjątki. Funkcje wywołujące funkcje bezpo-
średnio enkapsulujące klasy Javy są zewnętrzne wobec pakietu. Ich deklaracje wystę-
pują w pakiecie, a ciała w jego ciele.
CREATE OR REPLACE PACKAGE XZBIOR
AS
FUNCTION delete (Zbior IN VARCHAR2) RETURN NUMBER;
286 Część II ♦ ORACLE PL/SQL
Enkapsulacja klasy Javy o wielu metodach do pakietu jest bardzo dobrym rozwiąza-
niem, zwłaszcza że wprowadza porządek w tworzony po stronie serwera kod, zapew-
nia możliwość obsługi wyjątków bez konieczności tworzenia dodatkowych bytów oraz
pozwala na przeciążanie. Ma natomiast jedną wadę. Należy pamiętać, że uprawnienia
przypisuje się zawsze do wszystkich elementów pakietu (do niego całego), w związku
z czym należy zachować szczególną ostrożność, nie mieszając w nim funkcji o różnym
stopniu zagrożenia. W przypadku klas Javy, które z łatwością mogą sięgać do operacji
niskopoziomowych na poziomie systemu operacyjnego, wymaga to szczególnej roz-
wagi. Poniżej przedstawiony został przykład wywołania funkcji utworzonego przed
chwilą pakietu.
SET SERVEROUTPUT ON;
DECLARE
ok number;
lista varchar2(2000);
BEGIN
ok := xfile.delete('x:\aeea.txt');
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 287
Utwórzmy jeszcze bardziej ogólną klasę Javy, która pozwoli na wykonanie dowolnego
polecenia systemu operacyjnego. W tym celu tworzymy klasę o nazwie SysOper, za-
wierającą statyczną metodę DoCommand, która zwraca wartość całkowitą, a której pa-
rametrem jest łańcuch command zawierający polecenie, które ma zostać przetworzone.
W klasie zaimportowano bibliotekę java.io.*, zawierającą metody związane z prze-
twarzaniem na poziomie systemu operacyjnego. W metodzie zadeklarowano obiekt
rt klasy Runtime pozwalający na przetwarzanie poleceń i zainicjowano go metodą
getRuntime(). Następnie zadeklarowano obiekt klasy Process, bezpośrednio odpowie-
dzialny za wykonanie polecenia metodą exec(command) obiektu Runtime. Zastosowanie
metody waitFor(), powodującej oczekiwanie na zakończenie przetwarzania polecenia
systemu, pozwala na wykrycie poprawności wykonania polecenia. W sekcji obsługi
wyjątków zwrócona zostaje zawartość stosu. Skrypt zawiera ponadto enkapsulację do
funkcji PL/SQL oraz przykład wywołania.
Jeszcze bardziej ogólny jest przykład kodu funkcji pozwalającej na wykonanie pole-
cenia systemu operacyjnego. Zaimportowano w tym celu bibliotekę java.io.* oraz za-
stosowano obiekt Runtime powodujący utworzenie instancji, w której wykonywane
mogą być polecenia, a także obiekt Process pozwalający na uruchomienie konkretnego
polecenia. Metoda waitFor() obiektu Process czeka na zakończenie przetwarzania
i zwraca TRUE, jeśli polecenie zakończyło się powodzeniem. W przykładzie pokazano
również metodę enkapsulacji oraz sposób wywołania.
CREATE OR REPLACE AND COMPILE
JAVA SOURCE NAMED SysOper AS
import java.io.*;
public class SysOper {
public static String DoCommand (String command)
{
Runtime rt = Runtime.getRuntime();
String xxx=" ";
int ok = -1;
try {
Process p = rt.exec(command);
ok = p.waitFor();
} catch (Exception e) {
xxx = e.getMessage();
ok = 7;
}
return xxx;
}
}
/
CREATE OR REPLACE FUNCTION DoCommand
(command varchar2)
RETURN varchar2 AS LANGUAGE JAVA
NAME 'SysOper.DoCommand(java.lang.String) return java.lang.String';
/
DECLARE
ok varchar2(1111);
288 Część II ♦ ORACLE PL/SQL
BEGIN
ok := DoCommand('ping host');
DBMS_OUTPUT.PUT_LINE('ok = ' || ok);
END;
Podczas tworzenia przestrzeni tabel możliwe jest użycie szerszego zestawu parametrów,
jak pokazano w przykładzie. Dyrektywa LOGGING oznacza, że będzie dla przestrzeni
generowany plik dziennika powtórzeń (plik loga; stan domyślny). Podanie parametru
SIZE określa wielkość pliku fizycznego (wartość domyślna jest zależna od wersji
Oracle). Poza tym w przykładzie użyto parametrów ustalających lokalne zarządzanie
ekstentami oraz automatyczne zarządzanie przestrzenią — automatyczne jej powięk-
szanie zgodnie z parametrami konfiguracyjnymi (oba mają ustawioną wartość do-
myślną).
290 Część II ♦ ORACLE PL/SQL
Niestety, jeśli w przestrzeni tabel istnieje chociaż jeden obiekt, polecenie to nie za-
kończy się sukcesem. Konieczne jest wtedy dodanie dyrektywy INCLUDING CONTENTS.
W takim przypadku usunięty zostanie logiczny wpis o przestrzeni tabel, jednak plik
fizyczny pozostanie nadal w systemie operacyjnym. Aby usunąć również ten plik,
należy polecenie wzbogacić o AND DATAFILES. Taka dyrektywa usuwa zawartość prze-
strzeni tabel, wpis logiczny oraz plik fizyczny, choć nie powoduje usunięcia użyt-
kowników, nawet jeśli jest to ich jedyna przestrzeń tabel. W związku z tym najczęściej
używana postać polecenia usuwającego przestrzeń tabel wygląda następująco:
DROP TABLESPACE P_Tabel
INCLUDING CONTENTS AND DATAFILES;
Jeśli jednak w systemie operacyjnym istnieje już plik fizyczny, który chcemy wyko-
rzystać na przestrzeń tabel, możemy użyć opcji REUSE.
CREATE TABLESPACE P_Tabel
DATAFILE 'c:\oracle\oradata\oracle\P_Tabel.ora' REUSE;
Użycie REUSE nie jest metodą włamywania się do starych przestrzeni tabel, a jedynie
pozwala na wykorzystanie przydzielonych plikowi zasobów fizycznych.
W poleceniu należy określić nazwę użytkownika (Nowy) oraz hasło (kajak). Przy tak
wykonanym poleceniu użytkownik ma przypisywany domyślny profil DEFAULT,
domyślną przestrzeń tabel, zwykle USERS, i nie może łączyć się z bazą. Podczas two-
rzenia go możliwe jest określenie dodatkowych parametrów takich jak profil, do-
myślna przestrzeń tabel oraz stan konta — odblokowane lub zablokowane. Ten drugi
stan ma istotne znaczenie, jeśli chcemy stworzyć dużą liczbę predefiniowanych kont,
które mogą być sukcesywnie odblokowywane.
CREATE USER Nowy PROFILE DEFAULT
IDENTIFIED BY "kajak" DEFAULT TABLESPACE P_Tabel
ACCOUNT UNLOCK;
Usuwanie użytkownika wykonywane jest przez zastosowanie polecenia DROP USER. Jeśli
ma on choć jeden obiekt, konieczne jest użycie opcji CASCADE. Przestrzeń tabel nie jest
usuwana nawet wtedy, kiedy usuwany jest ostatni użytkownik do niej przypisany.
DROP USER Nowy CASCADE;
Każdemu użytkownikowi może zostać przypisana QUOTA, czyli rozmiar fizyczny, jaki
może on przeznaczyć na swoje obiekty w ramach wskazanej przestrzeni tabel.
ALTER USER nowy QUOTA 5M ON P_tabel;
Przyznawanie konkretnej wartości QUOTA ma sens tylko dla użytkowników, dla których
nie ustawiono roli RESOURCE lub uprawnienia systemowego UNLIMITED_TABLESPACE (rola
RESOURCE zawiera to uprawnienie w sobie). Możliwe jest zdefiniowanie dwóch symbo-
licznych wartości parametru QUOTA.
ALTER USER nowy QUOTA 0 ON P_tabel;
ALTER USER nowy QUOTA UNLIMITED ON P_tabel;
Ustawienie QUOTA 0 powoduje, że nie zmienia się wartość QUOTA, ale użytkownik nie
może tworzyć nowych obiektów. Innymi słowy zasoby zostają ograniczone do takiego
rozmiaru, jaki jest obecnie wykorzystywany. QUOTA UNLIMITED oznacza natomiast moż-
liwość nieograniczonego powiększania obszaru w przestrzeni tabel. W rzeczywistości
zawsze istnieje górne ograniczenie, chociażby wynikające z rozmiaru (pojemności) fi-
zycznego urządzenia.
Jak widać, możliwe jest jednoczesne przyznanie wielu praw do jednego obiektu, choć,
oczywiście, można ograniczyć ten zakres do jednego, dwóch czy trzech dowolnie wy-
branych uprawnień. Ponowne wykonanie tego samego zapytania niczego nie zmienia;
292 Część II ♦ ORACLE PL/SQL
nie jest również sygnalizowany błąd. Innymi słowy, wielokrotne przyznanie tego sa-
mego uprawnienia jest równoważne jego jednokrotnemu przyznaniu. Można także
odebrać dowolny zestaw uprawnień na skutek wykonania polecenia REVOKE.
REVOKE INSERT, UPDATE ON Testowy.Osoby FROM Nowy;
Tak nadane uprawnienia nie mogą zostać przekazane innemu użytkownikowi. Dopiero
użycie klauzuli WITH GRANT OPTION pozwala na przekazanie mu przypisanych do użyt-
kownika praw do wykonywania operacji na obiektach.
GRANT SELECT, UPDATE(Nazwisko, Imie)
ON AP.Osoby TO Nowy WITH GRANT OPTION
Przekazanie tych uprawnień innemu użytkownikowi staje się możliwe dopiero po uży-
ciu klauzuli WITH ADMIN OPTION.
GRANT CREATE (ANY) PROCEDURE | TRIGGER | TYPE TO Nowy WITH ADMIN OPTION;
Uprawnienia mogą być zbierane w większe grupy. W tym celu konieczne jest stwo-
rzenie roli, o której możemy myśleć jak o zasobniku na nie. Rolę tę możemy utworzyć
za pomocą polecenia:
CREATE ROLE rola IDENTIFIED BY kajak;
Łańcuch po klauzuli IDENTIFIED BY jest hasłem. Możliwe jest również utworzenie roli,
która nie będzie identyfikowana za pomocą hasła.
CREATE ROLE rola NOTIDENTIFIED;
Hasło roli jest wykorzystywane, kiedy nie stanowi ona roli domyślnej, a jest jedynie
przyznawana czasowo, np. na okres trwania sesji. W takim przypadku przypisanie jej
użytkownikowi wymaga podania hasła. Nowo utworzona rola nie zawiera żadnych upraw-
nień — nadajemy je według takich samych zasad co prawa użytkownikom.
GRANT SELECT, UPDATE(Nazwisko, Imie)
ON AP.Osoby TO Rola;
Może ona też zostać przypisana do innej roli. Pozwala to na budowanie hierarchicznej
struktury uprawnień, na spodzie której znajdują się role zawierające uprawnienia ele-
mentarne. Wyżej leżą te łączące w sobie kilka ról, a nad nimi role łączące uprawnie-
nia z ról poziomu poprzedniego itd.
GRANT Rola TO Rola1;
Potencjalnie jest możliwe przypisanie roli, a następnie jawne odbieranie uprawnień w niej
zawartych, prowadzi to jednak do bałaganu i kłopotów z opanowaniem rzeczywistego
zakresu praw, jakie ma użytkownik. Dlatego postuluje się używanie tylko jednego ze
schematów przyznawania uprawnień — albo tylko przez jawne przypisywanie ich do
obiektów, albo tylko przez nadawanie ról. Należy zauważyć, że przypisane uprawnienia
wynikające z ról nie zawsze są poprawnie interpretowane. Dotyczy to przede wszyst-
kim wykorzystywania ich z poziomu końcówek klienta. W takim przypadku musimy
zdecydować się na jawne przypisanie praw do obiektów. Rola usuwana jest przez wy-
konanie polecenia:
DROP ROLE rola;
W każdej instancji zdefiniowane są role systemowe, które zawierają już zestaw upraw-
nień. Ważniejsze z tych ról zawarte zostały w tabeli 15.2.
Przełączenie się do innego schematu wymaga posiadania prawa do niego lub upraw-
nień DBA.
Oracle umożliwia dokonanie zmiany nazwy obiektu. Dotyczy to tabel, perspektyw, se-
kwencji oraz synonimów.
RENAME Osoby TO Pracownicy;
Rozdział 15. ♦ Elementy administracji — zarządzanie uprawnieniami z poziomu SQL 295
CONNECT_TIME DEFAULT
IDLE_TIME DEFAULT
SESSIONS_PER_USER DEFAULT
LOGICAL_READS_PER_SESSION DEFAULT
LOGICAL_READS_PER_CALL DEFAULT
PRIVATE_SGA DEFAULT
COMPOSITE_LIMIT DEFAULT
FAILED_LOGIN_ATTEMPTS DEFAULT
PASSWORD_LOCK_TIME DEFAULT
PASSWORD_GRACE_TIME DEFAULT
PASSWORD_LIFE_TIME DEFAULT
PASSWORD_REUSE_MAX DEFAULT
PASSWORD_REUSE_TIME DEFAULT
PASSWORD_VERIFY_FUNCTION DEFAULT;
Oczywiście nie jest zbyt sensowne tworzenie dwóch profili o takiej samej zawartości.
Przykład profilu z rozsądnym zestawem parametrów różnych niż domyślne przedsta-
wia kolejne polecenie.
CREATE PROFILE N_profil
LIMIT CPU_PER_SESSION 1000
CPU_PER_CALL 1000
CONNECT_TIME 30
IDLE_TIME 1
SESSIONS_PER_USER 2
LOGICAL_READS_PER_SESSION 1000
LOGICAL_READS_PER_CALL 1000
PRIVATE_SGA 4K
COMPOSITE_LIMIT 1000000
FAILED_LOGIN_ATTEMPTS 3
PASSWORD_LOCK_TIME 5
PASSWORD_GRACE_TIME 30
PASSWORD_LIFE_TIME 30
PASSWORD_REUSE_MAX UNLIMITED
PASSWORD_REUSE_TIME 30
PASSWORD_VERIFY_FUNCTION NULL;
profil ten może być definiowany wizualnie z poziomu konsoli Oracle Console (w wersji
11. tylko przy jej pomocy). Definiowanie domyślnego profilu za pomocą tego narzę-
dzia przedstawia rysunek 15.2.
Należy pamiętać, że każdy użytkownik musi mieć przypisany dokładnie jeden profil,
więc jeśli ten przypisany do niego jest usuwany, automatycznie przypisywany jest mu
profil domyślny. Domyślnego profilu nie można usunąć.
Aby precyzyjnie wyjaśnić rolę obiektu w schemacie relacyjnym, można dodać do niego
komentarz, opis. Można tego dokonać w przypadku tabel, perspektyw i migawek. Do-
dawanie komentarza do obiektu jest wykonywane dzięki zastosowaniu polecenia:
COMMENT ON TABLE Osoby IS 'Pracownicy firmy';
COMMENT ON TABLE [schema.]table IS 'tekst';
COMMENT ON TABLE [schema.]view IS 'tekst';
COMMENT ON TABLE [schema.]snapshot IS 'tekst';
Rysunek 15.2.
Profil domyślny
w Oracle Console
Aby usunąć komentarz, należy wpisać pusty łańcuch ' '. Informacje o komentarzach
przypisanych do obiektów oraz ich kolumn możemy uzyskać, odpytując perspektywy
systemowe.
SELECT * FROM USER_TAB_COMMENTS;
SELECT * FROM USER_COL_COMMENTS;
Rysunek 15.3.
Przykład zmiany
ustawień profilu
w Oracle Console
300 Część II ♦ ORACLE PL/SQL
Rozdział 16.
Obiektowość w Oracle
Jednym z kierunków rozwoju oprogramowania było wprowadzenie języków obiekto-
wych. Trend ten dotknął również baz danych. Obiektowe bazy danych pozostają do-
meną badań akademickich, również tych prowadzonych przez badaczy polskich. Po-
mimo że w praktyce komercyjnej w pełni obiektowe bazy danych praktycznie nie istnieją,
to część z serwerów ma zaimplementowane elementy obiektowości. Jednym z najbar-
dziej zaawansowanych na tym polu jest właśnie serwer Oracle. Podobnie jak w obiek-
towych językach programowania, definicja typu obiektowego składa się z dwóch czę-
ści: specyfikacji zawierającej opis interfejsu obiektu oraz ciała, w którym umieszczone
są implementacje metod zawartych w owej specyfikacji. Aby zdefiniować typ obiek-
towy, należy użyć polecenia:
CREATE TYPE nazwa AS OBJECT ...
Tak jak w przypadku wielu obiektów, możemy użyć składni OR REPLACE, aby mieć
możliwość nadpisania definicji danego typu, jeśli ta już istnieje:
CREATE OR REPLACE TYPE nazwa AS OBJECT ...
Jak widać, podstawowa definicja typu jest analogiczna do definicji tabeli. Zdefinio-
wany typ obiektowy usuwa się przy pomocy polecenia:
DROP TYPE t_adres_o;
Jak widać, do wartości pól zmiennej obiektowej odwołujemy się poprzez nazwę kwa-
lifikowaną składającą się z nazw typu i pola — przez analogię do kwalifikowanej na-
zwy pola w tabeli. Dane zawarte w zmiennej obiektowej mogą być modyfikowane.
Wpisanie nowej wartości do wybranego pola obiektu odbywa się przez przypisanie jej
do kwalifikowanej nazwy tego pola. W skrypcie pokazano wstawienie wartości do
obiektu, a następnie zmianę wartości jednego z pól.
SET SERVEROUTPUT ON;
DECLARE
adres t_adres_o;
BEGIN
adres:=t_adres_o('Lodz', 5, 'Polska');
DBMS_OUTPUT.PUT_LINE(adres.miejscowosc || ' ' || adres.numer_domu || ' ' || adres.kraj);
adres.numer_domu := 55;
DBMS_OUTPUT.PUT_LINE('po zmianie');
DBMS_OUTPUT.PUT_LINE(adres.miejscowosc || ' ' || adres.numer_domu || ' ' || adres.kraj);
end;
Inną metodą wpisania wartości do zmiennej obiektowej jest zastosowanie, zamiast pod-
stawienia pozycyjnego, podstawienia nazewniczego (analogicznie do przekazywania
zmiennych do procedury czy funkcji). W takim przypadku nie obowiązuje nas zacho-
wanie kolejności wymieniania pól zgodnej z definicją obiektu oraz nie jest konieczne
zasilenie wszystkich pól (o ile nie mają ograniczenia NOT NULL).
SET SERVEROUTPUT ON;
DECLARE
adres t_adres_o;
BEGIN
adres := t_adres_o(miejscowosc => 'Lodz', numer_domu => 5, kraj => 'Polska');
DBMS_OUTPUT.PUT_LINE(adres.miejscowosc || ' ' || adres.numer_domu || ' ' || adres.kraj);
END;
Jeżeli typ ma zadeklarowaną chociaż jedną metodę, konieczna jest jej implementacja.
Odbywa się ona poprzez utworzenie ciała obiektu, w którym muszą być zawarte ciała
wszystkich metod zadeklarowanych dla tego typu. Możemy tutaj mówić o analogii do
pakietu i jego ciała.
CREATE OR REPLACE TYPE BODY t_adres_o AS
MEMBER PROCEDURE ustaw_numer(n_numer number)
IS
BEGIN
numer_domu := n_numer;
END ustaw_numer;
MEMBER FUNCTION pokaz_adres RETURN varchar2
IS
BEGIN
RETURN (SELF.miejscowosc || ' ' || SELF.numer_domu || ' ' || SELF.kraj);
END pokaz_adres;
END;
Wszystkie utworzone do tej pory obiekty miały charakter nietrwały, ulotny. Nie pozo-
stał po nich żaden ślad w bazie danych. Istniały one tylko podczas wykonywania pro-
gramu PL/SQL. W celu utrwalenia obiektów w bazie możemy utworzyć tabelę, która
będzie je przechowywać. Dla szerszego zaprezentowania trwałych obiektów zmodyfi-
kujmy istniejący typ t_adres_o oraz utwórzmy nowe: t_dzialy_o i t_zarobki_o. Dwa
ostatnie są obiektowym analogiem znanych już tabel relacyjnych.
CREATE OR REPLACE TYPE t_adres_o AS OBJECT
(ulica varchar2(25),
numer_domu number),
numer_mieszkania varchar2(4),
miejscowosc varchar2(20),
kod varchar2(6),
kraj varchar(20));
CREATE OR REPLACE TYPE t_dzialy_o AS OBJECT
(iddzialu int,
opis varchar(20));
CREATE OR REPLACE TYPE t_zarobki_o AS OBJECT
(brutto number,
data_wyplaty date);
Ponieważ przewidujemy możliwość więcej niż jednej wypłaty, musimy stworzyć ko-
lekcję (typ tabelaryczny) k_zarobki, składającą się z obiektów typu t_zarobki_o, za
pomocą polecenia:
CREATE TYPE k_zarobki AS TABLE OF t_zarobki_o;
Teraz stwórzmy typ obiektowy t_osoba_o opisujący pracowników naszej firmy, w któ-
rego definicji wykorzystamy poprzednio utworzone typy t_adres_o i t_dzialy_o oraz
kolekcję k_zarobki.
CREATE OR REPLACE TYPE t_osoba_o AS OBJECT
(idosoby int,
imie varchar(15),
nazwisko varchar(15),
dataurodz date,
adres t_adres_o,
dzial REF t_dzialy_o,
zarobki k_zarobki);
Dla typu t_dzialy_o zastosowaliśmy referencję, ponieważ w przypadku dwóch osób pra-
cujących w tym samym dziale dla typów zagnieżdżonych muszą się pojawić dwa obiekty
odwzorowujące dany dział. Dla obiektów współdzielonych istnieją dwie odrębne ta-
bele: osoba i dzial. Każda z osób posiada odrębny wskaźnik do swojego działu, nic
nie stoi więc na przeszkodzie, aby dwa różne wskaźniki pokazywały ten sam dział. W ten
sposób unikniemy redundancji danych. Zagnieżdżony obiekt adres nie posiada wła-
snej tożsamości i jest częścią składową obiektu macierzystego t_osoba_o.
Rozdział 16. ♦ Obiektowość w Oracle 305
Podobnie jak poprzednio, dla typu obiektowego t_osoba_o dodajemy dwie metody w po-
staci funkcji odwzoruj i lata.
CREATE OR REPLACE TYPE t_osoba_o AS OBJECT
(idosoby int,
imie varchar(15),
nazwisko varchar(15),
dataurodz date,
adres t_adres_o,
dzial REF t_dzialy_o,
zarobki k_zarobki,
MAP MEMBER FUNCTION odwzoruj RETURN VARCHAR2,
MEMBER FUNCTION lata RETURN date);
Na tej samej zasadzie definicja typu t_osoba_o zostaje wzbogacona o kolejne metody:
razem — obliczającą sumę wszystkich zarobków dla osoby;
srednia — obliczającą średnią zarobków dla osoby;
najwiecej — wyznaczającą maksymalną wypłatę dla osoby;
najmniej — wyznaczającą minimalną wypłatę dla osoby;
rok — obliczającą, ile lat ma dana osoba;
adres_osoby — wypisującą adres danej osoby.
CREATE OR REPLACE TYPE t_osoba_o AS OBJECT
(idosoby int,
imie varchar(15),
nazwisko varchar(15),
dataurodz date,
adres t_adres_o,
dzial REF t_dzialy_o,
zarobki k_zarobki,
MAP MEMBER FUNCTION odwzoruj RETURN VARCHAR2,
MEMBER FUNCTION lata RETURN date,
MEMBER FUNCTION razem return number,
MEMBER FUNCTION srednia return number,
MEMBER FUNCTION najwiecej return number,
306 Część II ♦ ORACLE PL/SQL
r := (months_between(sysdate,self.dataurodz))/12;
RETURN r;
END;
MEMBER FUNCTION najmniej RETURN NUMBER IS
i INTEGER;
temp1 number(10, 2) := self.zarobki(1).brutto;
temp2 number(10, 2) := 0;
BEGIN
FOR i IN 2..self.zarobki.last() loop
temp2 := self.zarobki(i).brutto;
if (temp2 < temp1) then
temp1 := temp2;
ELSE
temp1 := temp1;
END IF;
END LOOP;
RETURN temp1;
END;
MEMBER FUNCTION adres_osoby RETURN VARCHAR2 IS
n varchar2(30);
BEGIN
n := SELF.adres.ulica || ' ' || SELF.adres.miejscowosc || ' ' || self.adres.kraj;
RETURN n;
END;
END;
DECLARE
dzial_ref REF t_dzialy_o;
BEGIN
INSERT INTO dzialy_o temp VALUES(1, 'Dyrekcja')
RETURNING REF(temp) INTO dzial_ref;
INSERT INTO osoba_o VALUES
(
t_osoba_o
(1, 'Jan', 'KOWALSKI',
to_date('1980-09-08', 'YYYY-MM-DD'),
t_adres_o('Ulica', '1', '2', 'Lodz', '99-400', 'Poland'),
dzial_ref,
k_zarobki
(t_zarobki_o(100, to_date('2008-11-11', 'YYYY-MM-DD')),
t_zarobki_o(200, to_date('2008-12-11', 'YYYY-MM-DD')))
)
);
END;
Dopisanie kolejnej wypłaty dla osoby o identyfikatorze 1 odbywa się poprzez okre-
ślenie źródła dla zapytania INSERT jako zapytania wybierającego wskazującego na ta-
belę zagnieżdżoną.
INSERT INTO
TABLE (SELECT Zarobki FROM osoba_o
WHERE idosoby=1)
VALUES(t_zarobki_o(1000, to_date('2008-10-11', 'YYYY-MM-DD')));
W analogiczny sposób mogą być dopisywane kolejne rekordy zarówno do tabeli osoba_o,
jak i do tabeli zagnieżdżonej. Możemy sobie jednak wyobrazić sytuację, kiedy do ta-
beli dzialy_o zostały już wpisane rekordy, np. na skutek wykonania zapytań o postaci:
INSERT INTO dzialy_o VALUES(2,'ksiegowosc');
INSERT INTO dzialy_o VALUES(3,'dziekanat');
Możliwa jest zmiana wartości dowolnego rekordu w tabeli obiektowej zarówno na po-
ziomie tabeli głównej, jak i zagnieżdżonej. Prześledźmy taką operację na przykładzie
zmiany wartości brutto osoby o identyfikatorze 3. Tak samo jak poprzednio, modyfi-
kacja jest dokonywana za pośrednictwem dynamicznej perspektywy — zapytania wy-
bierającego.
Rozdział 16. ♦ Obiektowość w Oracle 309
UPDATE TABLE
(SELECT zarobki FROM osoba_o
WHERE idosoby = 3) z
SET z.brutto = 225
WHERE z.brutto = 125;
Tak samo jak przy usuwaniu rekordów ze zwykłej tabeli, również w przypadku tabeli
zagnieżdżonej niezastosowanie klauzuli WHERE powoduje usunięcie z niej wszystkich
rekordów związanych z rekordem (rekordami) nadrzędnym. W prezentowanym przy-
kładzie usuwane są zarobki osoby o identyfikatorze 3.
DELETE TABLE
(SELECT zarobki FROM osoba_o
WHERE idosoby = 3);
Aby wyświetlić wybrane pola z pola zawartego w typie obiektowym, należy zastosować
nazwę kwalifikowaną. Tak więc aby wyświetlić tylko ulicę, która zawarta jest wewnątrz
typu t_adres_o, należy napisać adres.ulica.
SELECT
k.imie, k.nazwisko, DEREF(k.dzial),
k.adres.ulica AS miejsce_zam
FROM osoba_o k;
SELECT VALUE(k)
FROM osoba_o k
WHERE k.lata() = TO_DATE('1980-09-08','YYYY-MM-DD');
Funkcja rok wyświetli na podstawie daty urodzenia zapisanej w tabeli oraz daty sys-
temowej wiek pracownika.
SET SERVEROUTPUT ON
DECLARE
ile_lat NUMBER;
numer_osoby int := 1;
BEGIN
SELECT o.rok() INTO ile_lat FROM osoba_o o
WHERE o.idosoby=numer_osoby;
DBMS_OUTPUT.PUT_LINE('Osoba o numerze id ' || ' ' ||numer_osoby || ' ' || ' ma lat
' || ' ' || ile_lat);
END;
wynik := NULL;
END IF;
RETURN ODCIConst.Success;
END;
MEMBER FUNCTION ODCIAggregateMerge(self IN OUT GeomAVG, rown IN
GeomAVG) RETURN number is
BEGIN
IF (rown.GAVG IS NOT NULL) THEN
If(self.GAVG IS NULL) THEN
self.GAVG := rown.GAVG;
self.Licz := 1;
ELSE
self.GAVG := self.GAVG*rown.GAVG;
self.Licz := self.Licz+1;
END IF;
END IF;
RETURN ODCIConst.Success;
END;
END;
Stosowanie funkcji agregującej utworzonej przez użytkownika rządzi się takimi sa-
mymi prawami jak stosowanie wbudowanych funkcji agregujących.
SELECT IdDzialu, AVGG(Wzrost)
FROM Osoby
GROUP BY IdDzialu;
Autor uważa się, tak jak pewnie większość użytkowników „silnych” serwerów baz da-
nych, a w szczególności Oracle, za konserwatystę, stąd brak bezkrytycznego entuzja-
zmu do podejścia obiektowego w bazach danych. Przyjmując do wiadomości, że od
wdrożenia w coraz szerszym zakresie takiego podejścia nie ma raczej odwrotu, uważam
jednak, że zawsze podstawą dobrego programowania baz danych jest poznanie i wyczucie
przetwarzania relacyjnego. Czyli — wracamy do pierwszego rozdziału — przede wszyst-
kim należy bezbłędnie opanować składnię złożonych zapytań wybierających oraz za-
sady budowania tabel.
314 Część II ♦ ORACLE PL/SQL
Zakończenie
Drogi Czytelniku, jeśli czytasz ten rozdział, oznacza to, że udało Ci się przebrnąć przez
cały zgromadzony w tej książce materiał. Mam nadzieję, że udało mi się sprostać wy-
zwaniu ciekawego i rzetelnego przedstawienia w przystępnej formie złożonych proble-
mów związanych z programowaniem baz danych po stronie serwera Oracle. Jeśli jednak
odnosisz wrażenie, że wiesz już o tym środowisku wszystko, to muszę Cię niestety
pozbawić złudzeń. Książki i uniwersytety mają wspólną cechę: „Uniwersytety są skarb-
nicami wiedzy: studenci przychodzą ze szkół przekonani, że wiedzą już prawie wszystko;
po latach odchodzą pewni, że nie wiedzą praktycznie niczego. Gdzie się podziewa ta
wiedza? Zostaje na uniwersytecie, gdzie jest starannie suszona i składana w magazy-
nach” — Terry Pratchett (Nauka Świata Dysku). Niestety, nie jest możliwe ujęcie w jed-
nym podręczniku całej wiedzy związanej ze środowiskiem Oracle. Świadczy o tym fakt,
że dokumentacja firmowa zapisana w formacie pdf liczy ponad 700 MB, co można po-
równać z objętością porządnej encyklopedii. Czego należy jeszcze poszukiwać? Przede
wszystkim ogólnikowo została potraktowana tematyka związana z administracją, co zo-
stało uzasadnione w treści odpowiedniego rozdziału, ale nie stanowi ona głównej do-
meny autora oraz znacznie wykracza poza przyjęte ramy tej książki. Nieomówione po-
zostały podstawowe wbudowane funkcje poza szczegółowo przedstawionymi funkcjami
agregującymi i analitycznymi, zwłaszcza typu OVER(...). Wynika to z ich dużej ilości
oraz faktu, że nie niosą one ze sobą żadnych ważnych informacji, a także z tego, że
ich prosta postać i opis dostępne są w każdej dokumentacji oraz na szerokim spektrum
stron internetowych. Dodatkowo ich składnia jest w dużej części zgodna z tą, która
występuje w innych językach. Część z tych funkcji została przedstawiona i omówiona
przy okazji prezentowanych przykładów kodu. O wiele godniejsze uwagi jest to, że nie
zostały poruszone tematy związane z przetwarzaniem analitycznym OLAP. Hurtownie
danych zasługują jednak na powstanie oddzielnej książki, co, mam nadzieję, wkrótce
nastąpi przy udziale autora. Nie przedstawiono też żadnych informacji związanych ze
zgłębianiem danych i inteligencją obliczeniową, chociaż dostępne są zarówno pakiety
PL/SQL, jak i klasy Javy zintegrowane z serwerem i wspierające przetwarzanie zwią-
zane z tymi zagadnieniami (dostępny jest również Oracle Data Miner — narzędzie,
które to wbudowane oprogramowanie wykorzystuje). Możliwe jest też przetwarzanie
danych multimedialnych (grafika, audio, video) z wykorzystaniem biblioteki Oracle
InterMedia. Narzędzia w niej zawarte pozwalają np. na obsługę protokołu zapisu da-
nych medycznych DICOM. Znane z urządzeń typu GPS przetwarzanie danych graficz-
nych o postaci wektorowej (map) również jest obecne na serwerze Oracle.
316 Zakończenie
Jak widać, liczba tematów, które nie zostały w tej książce omówione, jest ogromna.
Niestety autor nie wyobraża sobie przygotowania materiału o tak szerokim zakresie
przy jednoczesnym utrzymaniu jego spójności. „Mówią, że niepełna wiedza jest rze-
czą niebezpieczną, ale jednak nie tak złą, jak całkowita ignorancja” — Terry Pratchett
(Równoumagicznienie). Na niekorzyść zarówno autora, jak i czytelników działa rów-
nież szybki rozwój środowiska. Tak czy inaczej, wszystkie przytoczone tu fakty świad-
czą o konieczności permanentnego uczenia się nie tylko ze strony studentów, ale przede
wszystkim wykładowców. Powyższe argumenty dają asumpt do opracowywania no-
wych, mam nadzieję interesujących, publikacji. Zachęcam również do lektury innych
opracowań autora dotyczących baz danych.
G stosowanie, 131
tworzenie, 132
generator pseudolosowy, 195 unikalność, 132
generowanie usuwanie, 132
błędy, 170 walidacja struktury, 135
kolejne numery, 204 INDEX_STATS, 135
pliki XML, 139 inicjacja wyjątku użytkownika
raporty, 87 wyjątkiem systemowym, 176
znaczniki XML, 137 INITCAP, 33, 170, 192
GLOBAL TEMPORARY, 116 INNER JOIN, 40, 41
GOTO, 162 INPUT BOX, 92
grafy, 94 INSERT, 99, 197
cykliczne, 94 INSERTING, 208
sprawdzenie acykliczności, 94 INSERTXMLBEFORE, 149
GRANT, 291, 292, 293 instalacja
GRANT CONNECT, 290 baza danych, 12
GROUP, 27 końcówka klienta, 12
GROUP BY, 37 instancja bazy danych, 12
GROUP BY CUBE, 51 INSTEAD OF, 212
GROUP BY GROUPING SETS, 52 INT, 192
GROUP BY ROLLUP, 51 integralność danych, 103
GROUP_ID, 53, 55 INTERSECT, 90
GROUPING, 55, 57 INTERVAL, 60
GROUPING SETS, 52, 53 INTO, 166, 230, 231, 257
GROUPING_ID, 57 INVALID, 112
grupowanie, 36, 49 INVALID_CURSOR, 173
CUBE, 52 INVALID_NUMBER, 173
GROUPING SETS, 52 IS, 163, 170, 203
ROLLUP, 51, 55 IS NULL, 32, 94, 95, 100
H J
hasła, 290, 291 Java, 269
HAVING, 27, 38, 39, 46, 47 działania na poziomie systemu
high water mark, 114 operacyjnego, 284
enkapsulacja, 271
File, 285
I JDBC, 276, 277
IDENTIFIED, 290 klasy, 269
IF, 157 nawigowanie po rekordach zestawu
IN, 31, 32, 43, 44, 160, 166, 183 rekordów, 280
IN OUT, 182, 237, 278 obsługa wyjątków, 272
INCLUDING CONTENTS, 290 operacje na plikach, 285
INCLUDING CONTENTS AND DATAFILES, 290 operacje na zbiorach, 284
INCREMENT BY, 119, 307 pliki, 285
indeksy, 130 połączenie z bazą, 276
B-drzewa, 130 przetwarzanie danych, 284
bitmapowe, 130, 133 SQLException, 278
monitorowanie, 135 wykonanie polecenia systemu
optymalizacja zajmowanych zasobów operacyjnego, 287
dyskowych, 134 wywołanie metod, 270
przebudowa, 134 JAVA SOURCE, 270
JDBC, 276, 277
322 Skorowidz
język LINESIZE, 92
Java, 269 LOCK, 246
PL/SQL, 155 LOGGING, 289
SQL, 9 LOGIN_DENIED, 173
JOIN, 40, 41, 42 LOGOFF, 219, 222
JUSTIFY, 89 logowanie do konsoli serwera, 13
LONG, 11
LONG RAW, 11
K LOOP, 160, 242
kierunek sortowania, 29 LOWER, 33, 170, 192
kierunek złączenia, 42
KILL, 246 Ł
klasy, 269
klucze łączenie
obcy, 106, 107 ekstenty, 135
podstawowy, 104, 111 pola podczas generowania pliku XML, 141
komentarze, 155, 297 pola znakowe, 29
komunikat błędu, 175
konkatenacja łańcuchów, 29
konwersja liczby na napis, 184
M
kowariancja, 68 Math.log(), 275
kursory, 229 MAX, 37, 47, 204
atrybuty, 230 MAXEXTENTS, 115
CURRENT OF, 232 MAXVALUE, 119
deklaracja, 229 mediana, 67
FOR UPDATE, 232 MEMBER, 303
nawigacja między rekordami, 231 metody klasy Javy, 270
niejawne kursory, 235 miejsce wiersza w rankingu, 69
otwieranie, 229 MIN, 37, 48
pakiety, 241 MINEXTENTS, 115
parametry, 232 MINUS, 90
przejście do pierwszego rekordu, 230 MINVALUE, 119
REF CURSOR, 234 moda, 67
referencyjne, 234, 235, 237 MODIFY, 111
wartości domyślne, 232 modulo, 28
wywołanie nazewnicze, 234 modyfikacja
zagnieżdżone kursory, 237 dane, 99, 100
zmiana źródła rekordów, 244 sekwencje, 120
źródło rekordów, 234 tabele, 110
kwantyle, 75 użytkownicy, 291
MONITORING USAGE, 135
L MONTHS, 60
most JDBC, 17
LAG, 78 MOVE TABLESPACE, 116
LAST, 263
last(), 281
LAST_VALUE, 66
N
LEAD, 78 NATURAL JOIN, 42, 43, 45, 50
LEFT JOIN, 41, 45, 265 nazwy
LEVEL, 95 pola, 10
LIKE, 31, 33, 34, 92, 160 tabele, 10
znaki specjalne, 33 użytkownicy, 290
LIMIT, 264 zmienne, 155
Skorowidz 323
PRECEDING, 60 ranking, 69
PRESERVE, 116 RATIO_TO_REPORT, 62
PRIMARY KEY, 104, 105, 111 RAW, 11
PRIOR, 94, 95, 96, 263 REBUILD, 134
procedury anonimowe, 156 REF CURSOR, 234
procedury składowane, 163 REFERENCES, 106
parametry, 165 REFERENCING, 207
plany wykonania, 163 REGR_AVGX, 83, 84
tworzenie, 163 REGR_AVGY, 83
usuwanie, 163 REGR_COUNT, 83, 84
wykonywanie zapytań, 169 REGR_INTERCEPT, 82, 84
wywołanie, 170 REGR_R2, 83
procedury wyzwalane, 197 REGR_SLOPE, 82, 84
blokowanie, 206 REGR_SYY, 84
modyfikacja, 197 regresja liniowa, 82
perspektywy, 212 regresja sumy kwadratów, 84
trigger, 197 RENAME, 114, 252, 294
tworzenie, 197 RENAME COLUMN, 114
zdarzenia, 219 REPLACE, 93
Process, 287 residuum sumy kwadratów, 84
profile, 295 RESOURCE, 14, 15, 291
przypisanie do użytkownika, 297 Results, 18
tworzenie, 295 ResultSet.CONCUR_UPDATABLE, 281
PROGRAM_ERROR, 173 ResultSet.TYPE_SCROLL_INSENSITIVE, 281
przeciążanie, 189 RETURN, 179, 180, 183, 190, 234
funkcje, 189 RETURNING, 258
procedury, 189 REUSE STORAGE, 102
przełączanie więzów, 119 REVERSE, 231
przestrzeń tabel, 12, 13, 289 REVOKE, 292, 293, 294
przetwarzanie równoległe, 313 RIGHT JOIN, 41
przetwarzanie zapytania z parametrem, 91 role, 293
przypisanie systemowe role, 294
profil do użytkownika, 297 tworzenie, 293
wartość do zmiennej obiektowej, 302 usuwanie, 294
przywracanie klucza podstawowego, 118 ROLLBACK, 247
przyznawanie praw do obiektów, 293 ROLLBACK TO, 248
przyznawanie uprawnień, 291 ROLLUP, 51, 53, 54, 55
pseudoautomatyczna inkrementacja klucza, 307 ROW_NUMBER, 63
punkt wycofania transakcji, 248 ROWID, 11, 114
PUT_LINE, 157, 191, 205, 213 ROWNUM, 35, 36, 48, 63
ROWS, 60
ROWTYPE_MISMATCH, 244
Q rozkład normalny, 66
QUOTA, 291 RPAD, 95
QUOTA UNLIMITED, 291 Runtime, 287
R S
RAISE, 175, 190 samozłączenia, 239
RAISE_APPLICATION_ERROR, 170, 171, 176 SAVEPOINT, 248
RANDOM, 195 schemat bazy danych, 9, 10
RANGE, 60 Script Output, 18
RANK, 70, 71 SEED, 194
326 Skorowidz
TABLE, 151
TABLE OF, 264
U
TABLESPACE, 12 UNBOUNDED FOLLOWING, 60
TEMP, 12 UNBOUNDED PRECEDING, 60
THEN, 157, 172 UNDEFINE, 91, 92
TIMEOUT_ON_RESOURCE, 173 UNION, 89, 90
TITLE, 89 UNION ALL, 89
tnsnames.ora, 16, 17, 24 UNIQUE, 104, 105
TO_CHAR, 44, 184 UNLIMITED_TABLESPACE, 291
TO_DATE, 184 UPDATE, 100, 110, 164, 197, 198, 247, 292
TOO_MANY_ROWS, 173 SET, 100
transakcje, 247 WHERE, 100
otwieranie, 247 UPDATEXML, 146
procedury, 249 UPDATING, 208, 212
punkt wycofania, 248 UPPER, 101, 170, 171, 192, 206
wycofanie, 247 uprawnienia, 289
zatwierdzanie, 247 odebranie, 292
trigger, 197 przypisanie, 291
TRIM, 264 role, 293
TRUE, 27, 159 UROWID, 11
TRUNCATE TABLE, 101 USER_ALL_TABLES, 125
DROP STORAGE, 102 USER_CATALOG, 125
REUSE STORAGE, 102 USER_CLU_COLUMNS, 125
try, 272 USER_CLUSTER_HASH_EXPRESSIONS, 125
tryb logowania, 13 USER_CLUSTERS, 125
tworzenie USER_COL_COMMENTS, 125
funkcje, 179 USER_COL_PRIVS, 126
indeks, 132 USER_COL_PRIVS_MADE, 126
klasy Javy, 270 USER_COL_PRIVS_RECD, 126
klucz obcy, 106 USER_COL_TYPES, 125
klucz podstawowy, 104 USER_CONS_COLUMNS, 126
oprogramowanie po stronie serwera, 269 USER_CONSTRAINTS, 126
pakiety, 187 USER_DB_LINKS, 126
perspektywy, 121 USER_DEPENDENCIES, 126
procedury składowane, 163 USER_ERRORS, 126
procedury wyzwalane, 197 USER_FREE_SPACE, 126
profile, 295 USER_IND_EXPRESSIONS, 126
przestrzeń tabel, 289 USER_IND_PARTITIONS, 126
role, 293 USER_IND_SUBPARTITIONS, 126
sekwencje, 119 USER_INDEXES, 126, 132
synonimy, 252 USER_INDEXTYPE_COMMENTS, 126
tabele, 103 USER_INDEXTYPES, 126
tabele tymczasowe, 116 USER_INTERNAL_TRIGGERS, 126
trigger, 197 USER_JOIN_IND_COLUMNS, 126
typ obiektowy, 301 USER_LIBRARIES, 126
użytkownicy, 14, 18, 290 USER_LOBS, 126
zapytania SQL, 16 USER_NESTED_TABLES, 126
tymczasowa przestrzeń tabel, 12 USER_OBJ_COLATTRS, 126
typ obiektowy, 301 USER_OBJECT_SIZE, 126
TYPE, 225 USER_OBJECT_TABLES, 126
typy danych, 11 USER_OBJECTS, 123, 126
USER_PASSWORD_LIMITS, 126
USER_PENDING_CONV_TABLES, 126
328 Skorowidz