You are on page 1of 328

Spis treści

Od autora . ....................................................................................... 5

Część I Oracle SQL . ................................................................... 7


Rozdział 1. Wstęp . ............................................................................................ 9
Organizacja serwera . ....................................................................................................... 10
Instalacja bazy i końcówki klienta ................................................................................... 12
Rozdział 2. Zapytania wybierające . ................................................................. 27
Podstawowe elementy składni . ........................................................................................ 27
Grupowanie i funkcje agregujące .................................................................................... 36
Zapytania do wielu tabel — złączenia ............................................................................. 40
Grupowanie i funkcje analityczne ................................................................................... 49
Funkcje analityczne i rankingowe . .................................................................................. 63
Pozostałe elementy składniowe stosowane w SQL . ....................................................... 87
Obsługa grafów w SQL . .................................................................................................. 94
Rozdział 3. Zapytania modyfikujące dane . ....................................................... 99
Rozdział 4. Zapytania tworzące tabele . ......................................................... 103
Zapytania modyfikujące tabelę . ..................................................................................... 110
Dodatkowe informacje . ................................................................................................. 114
Sekwencja . ..................................................................................................................... 119
Perspektywy . ................................................................................................................. 121
Indeksy ........................................................................................................................... 130
Rozdział 5. Dodatkowe funkcjonalności SQL . ................................................. 137
Zapytania dla struktur XML . ......................................................................................... 137

Część II ORACLE PL/SQL . ...................................................... 153


Rozdział 6. PL/SQL . ...................................................................................... 155
Podstawy składni . .......................................................................................................... 155
Rozdział 7. Procedury składowane . ................................................................ 163
Rozdział 8. Funkcje w PL/SQL . ..................................................................... 179
4 Spis treści

Rozdział 9. Pakiety . ...................................................................................... 187


Rozdział 10. Procedury wyzwalane . ................................................................. 197
Rozdział 11. Kursory . ...................................................................................... 229
Rozdział 12. Transakcje . ................................................................................. 247
Rozdział 13. Dynamiczny SQL . ......................................................................... 253
Rozdział 14. Zastosowanie Javy do tworzenia oprogramowania
po stronie serwera . ..................................................................... 269
Rozdział 15. Elementy administracji
— zarządzanie uprawnieniami z poziomu SQL . ............................... 289
Rozdział 16. Obiektowość w Oracle . ................................................................ 301
Zakończenie . .............................................................................. 315
Skorowidz . .................................................................................. 317
Od autora
Drogi Czytelniku, jeśli sięgnąłeś po tę książkę, zapewne masz już pierwsze doświad-
czenia z bazami danych. Masz za sobą „pierwsze starcie” z tą tematyką i jesteś zainte-
resowany informacjami na temat nowego środowiska. Być może zetknąłeś się już z serwe-
rem Oracle i tylko oczekujesz szerszych informacji o nim. W każdym z tych przypadków
jest to książka dla Ciebie. Jeśli jednak jesteś nowicjuszem, masz do wyboru dwie drogi:
albo sięgniesz po poprzednią książkę autora (Bazy danych. Pierwsze starcie), przygoto-
wując się od podstaw, albo, licząc się jednak z większym wysiłkiem przy opanowywaniu
materiału, rozpoczniesz potyczkę z tym pięknym i potężnym, ale trudnym serwerem od
tej pozycji.

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).

Zakres poruszanej tu tematyki obejmuje zagadnienia związane z programowaniem


w Oracle. Całość została podzielona na dwie części, z których pierwsza poświęcona
jest językowi zapytań SQL, a druga rozszerzeniu proceduralnemu PL/SQL. Poza pod-
stawami składni SQL pierwsza część zawiera przykłady i analizę złożonych zapytań
z wielopoziomowymi podzapytaniami, które są rzadko omawiane w innych książkach.
Pokazuje ona najważniejsze z praktycznego punktu widzenia konstrukcje. Omówione
zostały również funkcje analityczne stanowiące wstęp do przetwarzania OLAP (ang. On-
line Analytical Processing), hurtowni danych oraz systemów raportujących. Druga część,
6 Od autora

poświęcona PL/SQL, omawia ponadto zastosowanie Javy do tworzenia oprogramowania


po stronie serwera, co w znacznej mierze zwiększa zakres możliwości przetwarzania.
Omówione zostały tu elementy administracji konieczne przy budowaniu oprogramowa-
nia oraz poruszone zostały kwestie dotyczące rozszerzenia obiektowego Oracle wraz
z przykładami ich praktycznego zastosowania, np. do tworzenia elementów definiowa-
nych przez użytkownika.

Zakres informacji przedstawionych w tym opracowaniu obejmuje rozszerzony materiał


realizowany podczas semestru zajęć prowadzonych na wyższych latach na kierunku
informatyka na studiach jednolitych magisterskich, a obecnie na studiach drugiego stop-
nia. Sposób prowadzenia wywodu wynika z wieloletnich doświadczeń związanych z pro-
wadzeniem wykładów, ale również ćwiczeń laboratoryjnych na Wydziale Elektrotech-
niki, Elektroniki, Informatyki i Automatyki Politechniki Łódzkiej oraz w Wyższej Szkole
Informatyki w Łodzi, a także z realizowania szkół w ramach działalności Stowarzysze-
nia PLOUG (ang. Polish Oracle User Group). Doświadczenia wynikające z wielolet-
niej działalności dydaktycznej poświęconej serwerowi Oracle oraz wydanie publikacji
naukowych o tej tematyce ośmieliły mnie do przedstawienia Państwu tego materiału
w uporządkowanej postaci. Kolejnym argumentem na rzecz powstania tej książki
było wieloletnie doświadczenie w prowadzeniu Studenckiego Koła Naukowego Baz
Danych. Pozytywne opinie absolwentów i jednocześnie „emerytów” tego koła, obec-
nie profesjonalnie zajmujących się programowaniem serwerów baz danych, również
były impulsem mobilizującym mnie do wysiłku związanego z uporządkowaniem do-
tychczas zebranych materiałów i opracowania ich w zwartej formie. Piszę o swoich do-
świadczeniach zawodowych, ponieważ uważam, że nie ma lepszego sposobu sprawdzenia
własnej wiedzy, jak zmierzenie się z przekazaniem tej wiedzy innym. Podzielam po-
gląd Einsteina, że „byłoby lepiej, gdybyś zabrał się do uczenia innych dopiero wtedy,
gdy sam się czegoś nauczysz”. Myślę, że praktyczne doświadczenia przemawiają na
moją korzyść. Jestem silnie związany z moimi dyplomantami i staram się utrzymywać z
nimi kontakt, dlatego dziękuję im za współpracę, której efektem jest niewątpliwie to
opracowanie. Jednak NAJWIĘKSZE podziękowania należą się mojej rodzinie, żonie
i córce, które dość cierpliwie znosiły moje zaangażowanie w pisanie tej książki i którym
ją dedykuję.
Część I
Oracle SQL
8 Część I ♦ Oracle SQL

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.

Przedstawiony schemat jest podzbiorem bardziej rozbudowanego schematu bazy danych


(rysunek 1.2) i opisuje przechowywanie informacji w firmie handlowej (mała hur­
townia). Stanowi on pomoc do prowadzonego przeze mnie kursu baz danych, obejmu­
jącego zakres od podstaw, aż do najbardziej zaawansowanych z hurtowni i zgłębiania
danych. W tej książce odwołania do niego praktycznie nie występują, ale można go
traktować jak dobry materiał ćwiczeniowy.
Rysunek 1.2. TRANSAKCJE TOWAR
KATEGORIE
Pelny schemat P N IDTRANSAKCJI P N IDTOWARU
F A IDFAKTURY F A IDKATEGORII
bazy danych F A IDTOWARU t>--- F A IDPRODUCENTA
A SZT A NAZWATOWARU

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

P N IDOSOSY F A l DWOJEWO DZTWA

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

Prezentowane schematy relacyjne zostały wykonane za pomocą Data Modelera. W kilku


przypadkach będą wprowadzane dodatkowe tabele, których struktura i znaczenie w sche­
macie relacyjnym zostaną omówione w miejscu odwoływania się do nich. Typami wszyst­
kich danych są typy proste (podstawowe) - numeryczne, znakowe oraz daty. Pełny
wykaz dostępnych w środowisku Oracle typów danych przedstawia tabela 1.1.

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

Tabela 1.1. Wykaz typów danych dla środowiska Dracle

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.

NVARCHAR2(rozmiar) Ciąg znaków o zmiennej długości. Minimalna długość jest reprezentowana


przez liczbę bajtów niezbędnych do reprezentacji pojedynczego znaku.
Maksymalna długość: 4000 znaków. Specyfikacja maksymalnej długości
jest niezbędna.

NUMBER(p,s) Liczba mająca p miejsc całkowitych i s miejsc po przecinku

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.

NCLOB Obiekt zawierający duże ilości tekstu (do 4 �' go>


� eden znak jest
reprezentowany przez kilka bajtów.

BLOB Duży binarny plik o maksymalnym r �


'�
�m ")-GB.

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.

ROWID Szesnastkowy ciąg��zentujący logiczny adres krotki zorganizowanej


w indeks. Mi�'\ al�ozmiar: l bajt.

UROWID Szesnastko � reprezentujący logiczny adres krotki zorganizowanej


w indek . aksymalny oraz domyślny rozmiar: 4000 bajtów.

Rysunek 1.3.
Organizacja
� Organizacja bazy danych
serwera Dracle LOGICZNA FIZYCZNA
12 Część l + Oracle SQL

Najmniejszym elementem po stronie fizycznej jest blok systemu operacyjnego, czyli


najmniejsza wartość, o jaką można powiększyć plik na dysku. Faktycznie, z punktu
widzenia hardware'u, najmniejszą jednostką podziałową nośnika, np. dysku twardego,
jest segment (cluster), ale nie jest on dostępny z poziomu programistycznego. Odpowied­
nikiem bloku systemu operacyjnego po stronie logicznej jest blok danych, na któty składa
się kilka bloków systemu operacyjnego (różna ich liczba w zależności od systemu ope­
racyjnego- dla Windowsa 2). Kilka bloków tworzy ekstent Podobnie jak w syste­
mie operacyjnym, blok to najmniejsza wartość, o jaką można powiększyć strukturę lo­
giczną w bazie danych (np. tabelę). Idąc dalej, kilka ekstentów może stanowić jeden
segment, a te z kolei składają się na przestrzeń tabel (TABLESPACE). Pojęcie prze­
strzeni tabel jest jednym z najważniejszych terminów związanych z organizacją bazy
danych Oracle. Na każdą przestrzeń tabel składa się przynajmniej jeden plik systemu
operacyjnego (zwyczajowo o rozszerzeniu GRA), domyślnie jest ona jednak skojarzona
z dokładnie jednym plikiem. Każdy użytkownik musi mieć przydzieloną przestrzeń tabel;
domyślnie przydzielana jest przestrzeń USERS, ale możliwe jest wskazanie dowolnej
innej istniejącej przestrzeni. Dodatkowo użytkownik ma p�dzielaną tymczasową
przestrzeń tabel (domyślnie TEMP) przeznaczoną do prze �1Xvania np. pośrednich
stanów sortowań, stanów kursorów etc. W tym przy i ku ��ły stan domyślny jest
zachowywany. Tak więc w obrębie przestrzeni tabel ��\półistnieć wielu użytkow­
ników, ale również jeden użytkownik może mieć r ieloną więcej niż jedną prze­
strze� tabel, � kt� tych �okł�?nie jedn� jest � ego � � enią domyślną. Jeż�li nie ':sk�­
zano Jawme _ InneJ lokalizacJI, wszystkie ob�Ł \6)órzone przez użytkowmka znaJdują
się w domyślnej przestrzeni tabel. W jej o� �orzą one schemat (struktura logiczna
nieuwzględniona na tysunku ), co pozwal� rozróżnienie obiektów stworzonych przez
różnych użytkowników w obrębie tej s �j przestrz�ni tabel na�et wtedy, kiedy mają
one taką samą nazwę. Schemat �wmka powstaJe wraz z pierwszym stworzonym
przez niego obie�tem. Zesta': � ").1ich przes_trze� �a�el składa się n� instancję bazy
danych. W obrębie bazy m �du�ię zawsze WięCeJ mż jedna przestrzen tabel, gdyż do
jej poprawnego funkcjo i� niezbędne są tworzone podczas instalacji przestrzenie:
SYS dla "superadm · i a
� ", SYSTEM dla administratora oraz wspomniane wcześniej
USERS i TEMP. strzeniach SYS i SYSTEM zawarte są obiekty niezbędne do
administrowani� �ą anych, tabele i perspektywy systemowe oraz bardzo bogaty ze­
staw narzędzi �amistycznych. Formalnie w obrębie jednego silnika bazy danych
- instancji serwera - może funkcjonować wiele instancji baz danych, jednak ze
względu na fakt, że Oracle jest przeznaczony do przetwarzania bardzo dużych zbio­
rów informacji, w praktyce takie rozwiązania spotyka się niezwykle rzadko (czasami
w obrębie jednej instancji serwera tworzy się instancję bazy produkcyjnej i testowej).

Instalacja bazy i końcówki klienta


Aby przećwiczyć przedstawione w podręczniku przykłady, wskazane jest zainstalo­
wanie serwera bazy danych Oracle. W pełni funkcjonalne programy instalujące go można,
po uprzednim zarejestrowaniu się (sprowadzającym się do podania podstawowych da­
nych personalnych) oraz zaakceptowaniu warunków licencjonowania, pobrać z oficjal­
nej witcyny firmy Oracle o adresie: http:/ /www. oracle. com/technology/softwarelproducts/
database/index. html.
Rozdział 1. + Wstęp 13

W chwili obecnej dostępna jest najnowsza wersja Oracle Database l lg Release l


(11.1.0.6.0) Standard Edition, Standard Edition One oraz Enterprise Edition. Możliwe
jest również zainstalowanie wersji wcześniejszych- zachęcam, aby instalować co
najmniej realizację 9., gdyż wcześniejsze różnią się znacznie zarówno funkcjonalno­
ściami, jak i sposobem instalacji. Ponieważ serwery mogą pracować na wielu platfor­
mach, podczas pobierania należy zwrócić uwagę, dla jakiego systemu operacyjnego dany
serwer jest przeznaczony. Pomimo że zainstalowanie samego serwera daje pełną funk­
cjonalność, między innymi pozwalającą na wykonanie ćwiczeń zawartych w tej książce,
zachęcam do ściągnięcia i zainstalowania zarówno serwera, jak i klienta bazy. Należy
rozpocząć od zainstalowania serwera- co we wcześniejszych realizacjach nie było
wcale takie oczywiste. Proces ten jest intuicyjny i nie powinien nastręczać większych
problemów. Jedynym krokiem różniącym się od stanu domyślnego proponowanego
w kolejnych okienkach dialogowych jest zmiana folderu, w którym nastąpi instalacja
(Gracle Home). Przyjęte przez użytkownika rozwiązanie powoduje, że ścieżka do ele­
mentów bazy jest długa, stąd propozycja jej skrócenia np. do C: \Oracle\dbl. Po wy­
konaniu tego kroku dostępna będzie konsola http (rysunek l . �która powinna urucho­
mić się automatycznie bezpośrednio po zakończeniu instal Y �
ORACLE" Enterprise Manager 11 g
Database Control � • ------------,

§User Name lsystem


§Password l*****
�o
Connect As l Normai v l

Copyright© 1996, 2007, Oracle. Ali right< re<e,...ed.
(/ 1

Oracle, JD Edward<, PeopleSoft, a�d Retek are registere d rade arb � e Corporatio� a�dlor it< affiliate<, Other �ame< may be trademath of their re<pective ow�er<,
u�authorized acce" i< <trictly prohibited.
•______________________j
---=-

Rysunek 1.4. �
Widok ekranu lo a · do konsoli serwera

Możliwe jest zał._1&�cie się na konta dwóch wbudowanych, aktywnych użytkowni­


ków- syste� trybie logowania (Connect As) Norma! oraz sys przy trybie logo­
wania SysDBA lub SysOper. Na tym etapie właściwym wydaje się załogowanie się
jako system z domyślnym hasłem administratorów ustalonym w początkowej fazie in­
stalacji lub też z hasłem, jakie ustalono w menedżerze haseł w jej końcowej fazie.

Pierwsza zakładka konsoli (rysunek 1.5) przedstawia informacje związane z konfigu­


racją serwera oraz podstawowe dane dotyczące monitorowania wydajności.

Narzędzi bardziej istotnych w początkowej fazie pracy, tuż po instalacji, dostarcza


zakładka Server (rysunek 1.6) pozwalająca bezpośrednio na stworzenie użytkownika
(grupa Security, pozycja Users)- jeśli będziemy chcieli przypisać mu istniejącą prze­
strzeń tabel (zwyczajowo Users). Możemy również najpierw stworzyć dodatkową,
własną przestrzeń tabel (grupa Storage, pozycja Tablespaces), którą w kolejnym kroku
przydzielimy nowo tworzonemu użytkownikowi. Podczas tworzenia przestrzeni tabel
konieczne jest utworzenie na dysku fizycznego pliku (również z poziomu konsoli ser­
wera), który będzie jej fizyczną reprezentacją
14 Część l + Oracle SQL

o
Lo gged inAsSYSTEM
Databa5e ln5tance: oradell

� H me [ Performance � Schema SoftwareandSupport

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

Instarx:e Name oradell


11.1.0.6.0 .oraclell
0.5 -� 0.5 (seconds)
Referenceconection
1/ersion .
Host pc21431.LR.WSINF.EDU.PL 25 .<:Eli (seconds)
Listener LISTENER pc21431.LR.WSINF.E ...
o OD OD

Load L.QQ. PaginJ 3.13 SQL Response Time ('%) Z1.....ZZ.


( EditReference Co l lection )

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

5everity Category paet Mes age �ered


'i'Aierts

Cateoory � Critical O Warnino O

lm
(No alerts)

�Related Alerts

��·� -
Policy__'llolations

_________..
Rysunek 1.5. Widok konsoli serw era Gracle po za!ogowani

ORACLE" Enterprise Manager 11 g �


Database Control
tO- ______________-,

LCQJedinAs:SYSTEM

Database I nstance: oracle 11

Storage Dracle Scheduler


i
Cont ro Files Jobs
Tables:paces Ch""ains:
TemporarylablespaceGroups Schedules

G
Datafiles �
RollbackSeqments Job Cl�sses
� �

�-
Archive o s lq Win odw roups
MiaratetoASM Global Attributes
MakelablespacelocallyManaqed �enancelas:ks:

Statistics Management Resource Manager Security


AutomaticWorkJoadRepository GettingStarted Users:
AWRBas:elines Roi es:
�Mapoms
Plans
Profiles
AuditSettinqs
� TransparentData Encryption
Stat1stlcs Virtual Private DatabasePolicies
AppkationContext:s

Query_Q�timizer Change Database


ManaqeOptimizerStatistics Addlnstance
SO i
L P anControl Deleteln:stance

Enterr;:!rise Manager Administration

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

Rysunek 1.6. Widok zakładki Server konsoli serwera Dracle

Podczas tworzenia użytkownika automatycznie jest mu przypisywana rola Connect po­


zwalająca na połączenie z serwerem - zalogowanie. Jeżeli nie chcemy nadawać na­
szemu użytkownikowi silnych uprawnień administratora (DBA), to aby mógł on wy­
konywać zapytania związane z tworzeniem obiektów, powinniśmy mu nadać rolę
RESOURCE (uniemożliwienie pełnej administracji serwerem). Powoduje to automa­
tyczne przypisanie uprawnień nieograniczonego rozmiaru przestrzeni tabel ( Unlimited
Rozdział 1. ♦ Wstęp 15

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.

Po wykonaniu wszystkich czynności możemy wylogować się z konta administratora


i ponownie zalogować na konto nowo utworzonego użytkownika. Po przejściu do za-
kładki Server i wybraniu z grupy Related links pozycji SQL Worksheet zostaniemy
przekierowani (nowe okienko przeglądarki) do narzędzia pozwalającego na wykony-
wanie zapytań SQL (rysunek 1.7) i ćwiczenie z przykładami zawartymi w książce. Two-
rzenie przykładowej struktury danych i zasilanie jej danymi pokazane zostanie podczas
omawiania końcówek klienta. Plik ddl.sql zawierający skrypt, który generuje dane, do-
stępny jest pod adresem ftp://ftp.helion.pl/przyklady/prseor.zip.

Rysunek 1.7. Konsola serwera w widoku SQL Worksheet w fazie po wykonaniu zapytania wybierającego

Zapytania zapisywane są w okienku SQL Command i wykonywane po naciśnięciu


przycisku Execute (rysunek 1.7). Rezultat działania zapytań wybierających jest wy-
świetlany na tej samej stronie. Niestety, małe okienko na polecenia (brak możliwości
powiększenia) oraz mała ilość miejsca na prezentowanie wyników nie zachęcają do
korzystania z tego narzędzia. Należy wspomnieć, że wykonywanie zapytań innych niż
wybierające wymaga usunięcia zaznaczenia opcji Allow only SELECT statements.
16 Część I ♦ Oracle SQL

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

Pomimo możliwości tworzenia zapytań SQL w konsoli serwera, polecam, ze względu


na jego spore ograniczenia i dość małą funkcjonalność, zainstalowanie końcówki klienta
nawet wtedy, kiedy pracujemy na jednym komputerze. Również w tym przypadku pro-
ces instalacji jest dość intuicyjny. Wygodne jest instalowanie oprogramowania w ka-
talogu domowym serwera, w „sąsiednim” w stosunku do bazy danych podkatalogu (pro-
ponuję, tak jak w przypadku serwera, skrócić ścieżkę np. do C:\Oracle\client). Aby
uzyskać pełną funkcjonalność, należy wybrać typ instalacji z opcją administratora. Ko-
lejne etapy procesu nie powinny nastręczyć większych kłopotów.

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

Rysunek 1.9. Oracle SQL Developer w momencie logowania do serwera

zwłaszcza dla początkujących, skopiować plik tnsnames.ora konfigurujący połączenie


serwera (przy proponowanej lokalizacji znajduje się on w katalogu C:\Oracle\db1\
network\admin) i zastąpić nim plik konfigurujący połączenie klienta (znajdujący się
w katalogu C:\Oracle\client\network\admin). Przykładowa zawartość pliku konfigu-
rującego proces nasłuchu pokazana została na listingu 1.1.

Listing 1.1. Przykładowa zawartość pliku konfiguracyjnego tnsnames.ora


# tnsnames.ora Network Configuration File:
# c:\oracle\db1\network\admin\tnsnames.ora
# Generated by Oracle configuration tools.

ORACLE11 =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = komputer)(PORT = 1521))
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = oracle11)
)
)

Rzadko zdarzają się problemy z uzyskaniem połączenia (zarówno z poziomu serwera,


jak i klienta) spowodowane dynamicznym przydziałem adresów IP. Dotyczy to nie-
licznej grupy dostawców usług sieciowych, u których proces odczytywania adresów
IP z nazw symbolicznych jest niedopracowany. W takim przypadku pozostaje (poza
zmianą dostawcy usług) zastosowanie połączenia wewnętrznego (LookUp) — posłu-
giwanie się nazwą symboliczną localhost z IP 127.0.0.1 lub tylko statycznym IP — co
powinno zawsze dać dobry efekt. Sam SQL Developer wykorzystuje natomiast defi-
nicję połączenia przy pomocy mostu JDBC i nie korzysta, w przeciwieństwie do np.
SQL*Plus, z definicji zawartej w pliku konfiguracyjnym tnsnames.ora.
18 Część I ♦ Oracle SQL

Po nawiązaniu połączenia w stanie domyślnym po lewej stronie wyświetlane są w po-


staci drzewa elementy schematu użytkownika (rysunek 1.10). Części drzewa mogą być
rozwijane poprzez przestawianie konkretnych obiektów oraz, w przypadku tabel i per-
spektyw, ich struktury. Zwykle z poziomu drzewa jesteśmy w stanie stworzyć przy
użyciu kreatorów nowe obiekty wybranej kategorii. Niestety, w kilku przypadkach taka
możliwość nie istnieje (Directories, XML Schemas) i możliwe jest tylko wyświetlenie
listy uprzednio stworzonych obiektów danej klasy, natomiast tworzenie nowych musi
dokonywać się albo za pomocą zapytań SQL, albo wizualnie przy użyciu konsoli.
Najniższym elementem drzewa jest pozycja Other Users, z poziomu której jesteśmy
w stanie, posiadając odpowiednie uprawnienia, oglądać (a także tworzyć lub modyfi-
kować) obiekty zawarte w schematach innych użytkowników. Możliwe jest również
administrowanie ich uprawnieniami, w tym zmiana hasła. Z poziomu grupy istnieje
możliwość stworzenia nowego użytkownika. Niestety nie ma możliwości tworzenia
nowych przestrzeni tabel, tak więc schemat użytkownika musi zostać umieszczony
w istniejącej przestrzeni (USERS) albo może zostać dla niego utworzona poleceniem
SQL lub z poziomu konsoli nowa przestrzeń.

Rysunek 1.10. Oracle SQL Developer po wykonaniu zapytania wybierającego do zakładki Results

Najważniejszą funkcjonalnością oferowaną przez Oracle SQL Developer jest możli-


wość wykonywania zapytań, skryptów SQL (rysunki 1.10 i 1.11). Widocznymi udo-
godnieniami są skalowanie okien (istotne przy złożonych skryptach), kolorowanie
elementów składni, wykrywanie par nawiasów etc. Możliwe jest również śledzenie
wykonywania skryptów, co ułatwia ich testowanie i poprawianie.

Wyniki zapytań wybierających mogą być wyświetlane zarówno na zakładce Results


(kontrolka typu Data Grid; rysunek 1.10), jak i na zakładce Script Output (postać tek-
stowa; rysunek 1.11). Ta druga forma prezentacji, pomimo iż wydaje się mniej atrak-
cyjna wizualnie, jest bardzo użyteczna w przypadku przetwarzania procedur, ale również
wtedy, gdy odpytujemy perspektywy słownika danych, w których zawartość poje-
dynczego pola może być bardzo duża (np. definicja obiektu bazy danych).
Rozdział 1. ♦ Wstęp 19

Rysunek 1.11. Oracle SQL Developer po wykonaniu zapytania wybierającego do zakładki Script Output

Podobnie jak w przypadku konsoli, możliwe jest wygenerowanie i prześledzenie planu


wykonania zapytania (rysunek 1.12), który Oracle SQL Developer wyświetla w postaci
czytelnego, rozwijalnego drzewa, co znacznie ułatwia jego analizę.

Rysunek 1.12. Oracle SQL Developer po wygenerowaniu planu wykonania zapytania wybierającego

Możliwe jest również wygenerowanie podobnego w założeniu schematu automatycz-


nego śledzenia wykonania zapytania — rysunek 1.13.

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.13. Oracle SQL Developer po wygenerowaniu automatycznego śledzenia zapytania


wybierającego

Podstawowym narzędziem jest konsola w postaci aplikacji systemu operacyjnego — Oracle


Enterprise Manager; rysunek 1.14. Po wybraniu zarejestrowanego serwera — węzeł
drzewa — następuje proces logowania, który jest trywialny. Jeśli chcemy zarejestrować
nowy serwer lub dołączyć zdefiniowany w pliku tnsnames.ora, korzystamy z pozycji
Navigator górnego menu, wybierając Add Database. W przypadku gdy końcówka klienta
jest instalowana po serwerze i właściwie wskazano serwer podczas tego procesu, to
powinien być on już dostępny w czasie pierwszego uruchomienia — rysunek 1.15.

Rysunek 1.14. Oracle Enterprise Manager w stanie logowania

W przeciwieństwie do Oracle Developera, Enterprise Manager (rysunek 1.15) umoż-


liwia pełne administrowanie bazą danych zarówno na poziomie logicznym, jak i fi-
zycznym — tworzenie plików, przestrzeni tabel etc. Każdy węzeł drzewa pozwala na
uruchomienie kreatora bądź okna dialogowego odpowiadającego za tworzenie odpo-
wiednich elementów bazy czy schematu — z tego punktu widzenia Enterprise Mana-
ger jest bardziej wszechstronny. Niestety nie zawiera on narzędzia do pisania skryptów,
zapytań SQL (rysunek 1.16) — w takim przypadku należy skorzystać z kolejnego na-
rzędzia. Podobnie jest z monitorowaniem bazy.
Rozdział 1. ♦ Wstęp 21

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

Narzędziem pozwalającym na stosowanie SQL jest SQL Worksheet (rysunek 1.16),


który uruchamia się z poziomu systemowego menu Programy — z pozycji Oracle
Client — Integrated and Management Tools. Różnica podczas logowania polega na
jawnym podaniu nazwy serwisu, z którym będziemy starali się połączyć. Nazwę tę
trzeba podać nawet wtedy, kiedy zdefiniowany jest tylko jeden serwer z jedną instan-
cją bazy. W przeciwnym przypadku pojawi się komunikat o błędzie: TNS nie może
rozstrzygnąć nazwy. Jest to uzasadnione tym, że z tej końcówki jesteśmy w stanie łą-
czyć się z dowolnym zdefiniowanym w pliku konfiguracyjnym serwerem w sieci.
22 Część I ♦ Oracle SQL

Uwaga — jeśli po uruchomieniu SQL Worksheet, a przed logowaniem, przełączymy


się lub włączy się inna aplikacja, będziemy musieli poszukać właściwego okienka
dialogowego, które zwykle wyświetlane jest na samym końcu stosu okien (informacja
o jego wyświetleniu nie pojawia się na dolnym pasku) — brak opcji Always on Top.
Dodatkowo myląca jest nazwa okna dialogowego, sugerująca próbę połączenia z En-
terprise Managerem. Rozróżnienia nazwy serwisu można dokonać po polu tekstowym,
które jest aktywne (rysunek 1.16).

Po zalogowaniu możliwe jest wykonywanie dowolnych zapytań SQL (rysunek 1.17).


Pomimo niepozornego wyglądu jest to w pełni funkcjonalne narzędzie. Ważną jego
zaletą jest fakt przechowywania wszystkich zapytań, które zostały wykonane podczas
jednego uruchomienia SQL Worksheet. Sesja ta może obejmować wiele sesji bazy
danych — wielokrotne przelogowywanie się w obrębie aplikacji.

Rysunek 1.17. SQL Worksheet po wykonaniu zapytania wybierającego

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

Rysunek 1.18. SQL*Plus w stanie logowania

Rysunek 1.19. SQL*Plus w stanie po wykonaniu zapytania wybierającego


24 Część I ♦ Oracle SQL

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

Jeżeli logujemy się do serwera o wielu instancjach, na komputerze zainstalowanych jest


więcej serwerów lub jest więcej niż jedna definicja połączenia w pliku tnsnames.ora,
to podczas podawania loginu użytkownika po znaku at (@) należy jawnie podać serwis,
z którym chcemy się połączyć — user@service (rysunek 1.21). Takie połączenie od-
bywa się zawsze w trybie Normal. W związku z tym, jeśli będziemy chcieli połączyć się
z bazą jako użytkownik sys, który może się łączyć w dwóch trybach: SysDBA i SysOper,
będziemy musieli dodatkowo wskazać wybrany tryb, np. sys@service as sysdba. Uwaga
ta dotyczy wszystkich użytkowników o uprawnieniach SysDBA lub SysOper.

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)
)
)

Po pomyślnym zalogowaniu możliwe jest wykonywanie zapytań SQL (rysunek 1.22)


według zasad przedstawionych dla SQL*Plus. Sposób wyświetlania wyników tu także
jest mało atrakcyjny, należy jednak pamiętać, że jest to narzędzie przeznaczone głównie
do wykonywania zadań administracyjnych z poziomu użytkowników z uprawnieniami
SysDBA. To właśnie z jego użyciem najczęściej wykonuje się operacje zatrzymania czy
uruchomienia instancji (istnieje możliwość zalogowania się do instancji zatrzymanej,
nieaktywnej), odzyskiwania danych czy naprawy plików. Z tej przyczyny jest ono bar-
dzo istotnym elementem Oracle. Poza omawianymi tutaj narzędziami zintegrowanymi
z Oracle i instalowanymi podczas instalacji czy to serwera, czy klienta bazy, na stronie
domowej dostępna jest też najnowsza wersja SQL Developera — posiadająca nieco
szerszą funkcjonalność, zwłaszcza w odniesieniu do prezentacji graficznej niektórych
obiektów — oraz wspomniany podczas prezentacji schematu bazy Data Modeler.

Rysunek 1.22.
SQL Plus w stanie
logowania po
wykonaniu zapytania
wybierającego

Ponieważ książka ta jest nastawiona głównie na elementy związane z budowaniem za-


pytań oraz programowaniem na poziomie serwera, ten z konieczności ograniczony zasób
informacji dotyczących jego organizacji, konfiguracji i administracji powinien być wy-
starczający dla zrozumienia dalszego wywodu.
26 Część I ♦ Oracle SQL
Rozdział 2.
Zapytania wybierające

Podstawowe elementy składni


Podstawową operacją, jaką najczęściej wykonujemy na danych zgromadzonych w ba-
zie, jest ich wybieranie (selekcja). Na tym etapie abstrahujemy od tego, w jaki sposób
zostały one do bazy danych wpisane — ta tematyka będzie przedmiotem szczegóło-
wych rozważań w kolejnych rozdziałach. Podstawowa składnia zapytania wybierającego
ma postać.
SELECT pola FROM relacja
WHERE warunek
GROUP BY pola
HAVING warunek
ORDER BY pola

W prezentowanym szablonie zapytania:


 pola reprezentują listę pól tabel (relacji) lub ich aliasów separowaną przecinkami;
 relacja stanowi definicję źródła danych, tabelę lub połączenie kilku tabel,
również dynamicznych;
 warunek jest wyrażeniem zwracającym zmienną logiczną (TRUE, FALSE, NULL).

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.

Prostą realizacją przedstawionego tu ogólnego schematu zapytania wybierającego jest


zapytanie wyświetlające trzy wybrane pola z tabeli Osoby. Wyświetli ono wszystkie
wiersze tabeli.
SELECT Nazwisko, Imie, RokUrodz
FROM Osoby;

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.

Tabela 2.1. Wykaz operatorów dla środowiska Oracle


Algebraiczne Bitowe
+ &
dodawanie iloczyn bitowy (AND)
- |
odejmowanie suma bitowa (OR)
* ^
mnożenie symetryczna różnica bitowa (XOR)
/ ~
dzielenie przeczenie bitowe (NOT)
%
modulo
||
konkatenacja łańcuchów
Rozdział 2. ♦ Zapytania wybierające 29

SELECT RokUrodz*wzrost AS Iloczyn,


UPPER(Nazwisko) AS Nazwiska
FROM Osoby;

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;

Prezentowane przykłady powodują wyświetlenie wszystkich rekordów tabeli w kolej-


ności ich wpisywania (precyzyjnie według unikatowego indeksu zbudowanego na ba-
zie klucza głównego). Jeśli chcemy uzyskać w wyniku posortowane rekordy, musimy
skorzystać z klauzuli ORDER BY.
SELECT Nazwisko, Imie FROM Osoby
ORDER BY RokUrodz;

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;

W przypadku gdy w zapytaniu wybierającym sformułowano wyrażenie, również może


ono zostać użyte do sortowania. Należy jednak zauważyć, że zarówno pole, jak i wy-
rażenie może posłużyć do sortowania nawet wtedy, kiedy nie zostanie wyprowadzone
na ekran — nie występuje po instrukcji SELECT, a tylko w klauzuli ORDER BY.
SELECT RokUrodz*wzrost AS Iloczyn FROM Osoby
ORDER BY RokUrodz*wzrost;

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;

Istnieje również instrukcja o składni:


DESC NazwaTabeli

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.

W prezentowanych do tej pory przykładach wyprowadzane były zawsze wszystkie


rekordy zawarte w tabeli. Jeśli chcemy ograniczyć liczbę wyświetlanych rekordów,
możemy użyć filtrowania. Jego sposób jest określany po klauzuli WHERE.
SELECT Nazwisko, Imie FROM Osoby
WHERE RokUrodz >1960;

Wyrażenie po klauzuli filtrującej musi zwracać wartość logiczną, a wyświetlane są


tylko te rekordy, dla których będzie ono miało wartość TRUE.
SELECT Nazwisko, Imie FROM Osoby
WHERE RokUrodz >1960 AND RokUrodz <1988;

Do budowania wyrażeń można użyć dowolnej kombinacji operatorów algebraicznych


i logicznych (tabela 2.2), które mogą działać zarówno na pola, jak i na wyrażenia na
nich oparte czy też dotyczyć funkcji.

W poprzednich przykładach zakładano, że zawsze wszystkie pola tabeli są wypełnione.


Jednak może się zdarzyć, że posiadamy informację niekompletną albo że informacja,
która miałaby być zawarta w którymś polu, nie dotyczy tego rekordu czy też nie może
Rozdział 2. ♦ Zapytania wybierające 31

Tabela 2.2. Operatory dostępne w Oracle


Operatory algebraiczne Operatory logiczne Operatory specjalne
= równe NOT przeczenie BETWEEN przedział
< mniejsze niż OR suma logiczna IN lista
<= mniejsze niż lub równe AND iloczyn logiczny LIKE podobieństwo
> większe niż SOME lista z operatorem
>= większe niż lub równe ANY lista z operatorem
<> różne ALL lista z operatorem
!= nierówne EXISTS istnieje

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.

Tabela 2.3. Wartości operatora AND w logice trójwartościowej


AND TRUE FALSE NULL
TRUE TRUE FALSE NULL
FALSE FALSE FALSE FALSE
NULL NULL FALSE NULL

Tabela 2.4. Wartości operatora OR w logice trójwartościowej


OR TRUE FALSE NULL
TRUE TRUE TRUE TRUE
FALSE TRUE FALSE NULL
NULL TRUE NULL NULL

Tabela 2.5. Wartości operatora NOT w logice trójwartościowej


A NOT A
TRUE FALSE
FALSE TRUE
NULL NULL

Jednym z najważniejszych wniosków płynących z tych tabel jest to, że ponieważ


NULL AND NULL = NULL

to
(NULL = NULL) ⇒ NULL
32 Część I ♦ Oracle SQL

Aby wykryć wartości niezdefiniowane (nieznane) występujące w jakimś polu, należy


więc użyć specjalnego operatora IS NULL. Stąd zapytanie wyświetlające osoby, dla
których nie podano roku urodzenia (rok urodzenia nie jest znany), ma postać:
SELECT Nazwisko, Imie, RokUrodz FROM Osoby
WHERE RokUrodz IS NULL;

Kolejnym przykładem jest zastosowanie operatora BETWEEN do wyznaczenia przedziału.


Przedstawione zapytanie wyświetla osoby, które urodziły się pomiędzy rokiem 1960
a 1980. Należy zwrócić uwagę na to, iż pole, względem którego dokonujemy filtrowania,
nie musi zostać użyte do wyświetlenia. Zwykle jest ono jednak pokazywane, chociażby
w celu weryfikacji poprawności filtrowania.
SELECT Nazwisko, Imie FROM Osoby
WHERE RokUrodz BETWEEN 1960 AND 1980;

Ponieważ operator BETWEEN zwraca zawsze przedział obustronnie zamknięty, to równo-


ważne jest mu wyrażenie opierające się na nierównościach nieostrych. Pomimo takiej
równoważności operator ten jest chętnie stosowany, zwłaszcza dla pól typu data/czas.
SELECT Nazwisko, Imie FROM Osoby
WHERE RokUrodz >=1960 AND RokUrodz <=1980;

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);

Zastosowanie operatora listy jest równoważne wielokrotnemu porównaniu z wartościami


połączonymi operatorem OR. Jak widać, przy dużej liczbie elementów listy operator IN
daje dużo większą zwartość zapisu.
SELECT Nazwisko, Imie FROM Osoby
WHERE RokUrodz = 1960 OR RokUrodz = 1970
OR RokUrodz = 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

rzystamy z funkcji konwertującej pole na postać pisaną jednolicie (zwykle dużymi


literami, UPPER, chociaż równie dobrze można zastosować konwersję LOWER — małe
litery, czy, znacznie rzadziej, INITCAP — od dużej litery) z odpowiednio do tej kon-
wersji zapisaną listą wartości.
SELECT Nazwisko, Imie FROM Osoby
WHERE UPPER(Nazwisko) IN ('KOWALSKI', 'NOWAK');

Zdecydowanie częściej w przypadku pól znakowych stosowany jest operator podo-


bieństwa LIKE.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko LIKE 'KOW';

W podstawowej postaci jest on po prostu równoważny operatorowi równości, z taką


samą uwagą dotyczącą rozpoznawania wielkości liter jak w przypadku omawianym
poprzednio.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko = 'KOW';

Dla tego operatora wprowadzone zostały znaki specjalne, które zawiera tabela 2.6.

Tabela 2.6. Operator LIKE — znaki specjalne


% dowolny ciąg znaków (w tym ciąg pusty)
_ dokładnie jeden znak
ESCAPE definiuje znak, po którym znaki specjalne są traktowane dosłownie

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%';

W analogiczny sposób możemy wykrywać nazwiska kończące się określoną frazą.


W tym przypadku wyświetlone zostaną nazwiska typu JAKOW, BUŁHAKOW, POLA-
KOW. Tak samo jak poprzednio, ze względu na reprezentowanie przez symbol % pu-
stego ciągu wyświetlone zostanie nazwisko KOW. Podobnie należy uwzględnić uwagę
dotyczącą zgodności wielkości znaków poza podaną jawnie frazą.
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

w środku (ZIÓŁKOWSKI, JANKOWSKI, PANKOWSKI itp.), ale także te rozpo-


czynające się tą frazą (KOWALSKI, KOWALCZYK, KOWALEWSKI itp.) i koń-
czące się nią (JAKOW, BUŁHAKOW, POLAKOW itp.). Uwzględnione zostanie
również nazwisko KOW.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko LIKE '%KOW%';

Kolejnym przykładem zastosowania tego znaku specjalnego jest poszukiwanie nazwisk


rozpoczynających się od pewnego znaku, a kończących się innym. W pokazanym frag-
mencie kodu będą to nazwiska, które rozpoczynają się literą K, a kończą I — KO-
WALSKI, KWIATKOWSKI, KAMIŃSKI. Tak samo jak w przykładach analizowa-
nych poprzednio, gdyby istniało w bazie dwuliterowe nazwisko KI (co w Polsce jest
mało prawdopodobne, ale nie jest niemożliwe), również zostałoby ono wypisane. Aktu-
alna pozostaje uwaga dotycząca wielkości liter niewchodzących w skład poszukiwanej
frazy.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko LIKE 'K%I';

Kolejnym symbolem specjalnym jest podkreślnik _, który zastępuje dokładnie jeden


znak. W przykładzie pokazano wyszukiwanie nazwisk, które na trzeciej pozycji mają
literę W. Pominięcie znaku procenta (%) na końcu filtra spowodowałoby ograniczenie
obszaru poszukiwań do nazwisk trójliterowych (LEW, PAW, KOW), natomiast jego
zastosowanie spowoduje, że poza takimi nazwiskami zostaną również wyświetlone te,
które mają inną długość (NOWAK, KOWALSKI, PAWLAK etc.).
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko LIKE '__W%;

Na platformie Oracle nie został zdefiniowany symbol pozwalający na traktowanie do-


słownie znaków, które pełnią rolę symboli specjalnych, tzn. % czy _. W zamian za to
użytkownik może po definicji filtru, po literale ESCAPE zdefiniować symbol, po którym
znaki specjalne analizowane są jak zwykłe. Z reguły tym symbolem jest slash (\; tak
jak pokazano w przykładzie) lub backslash (/), ale możliwe jest użycie dowolnego
innego. Należy jedynie zwrócić uwagę, aby nie był to znak, którego wystąpienia możemy
spodziewać się w obrębie filtrowanej kolumny (pola). Przykład pokazuje wyszukiwa-
nie nazwisk rozpoczynających się od symbolu %. Ponieważ znakiem, po którym znaki
specjalne traktowane są dosłownie, jest slash, pierwszy znak % jest traktowany tak jak
napisano, natomiast drugi (nie jest poprzedzony przez \) jako symbol specjalny — do-
wolny ciąg znaków.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko LIKE '\%%' ESCAPE'\';

Analogicznie możemy skonstruować filtr wykrywający nazwiska, w których na do-


wolnym miejscu pojawia się symbol %.
SELECT Nazwisko, Imie FROM Osoby
WHERE Nazwisko LIKE '%\%%' ESCAPE'\';

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 …;

Dotychczas filtrowanie powodowało wyświetlenie zestawu rekordów, które spełniały


określone kryterium, ale nie sposób było przed wykonaniem zapytania precyzyjnie okre-
ślić ich liczby. Aby to zrobić, możemy zastosować funkcję ROWNUM, która numeruje
każdy wyprowadzony wiersz kolejnymi liczbami naturalnymi, począwszy od liczby 1.
Stąd przedstawiony przykład powoduje wyświetlenie dokładnie sześciu wierszy.
SELECT ROWNUM AS Numer, Nazwisko FROM Osoby
WHERE ROWNUM <= 6

Ponieważ numer nadawany jest wierszowi w momencie jego wyprowadzania na ekran,


poprawne jest jedynie stosowanie operatorów mniejszy < albo mniejszy lub równy <=.
Warunek ROWNUM = n może zostać użyty tylko dla wartości n = 1; dla wyższych n
zawsze zostanie wyświetlony pusty zestaw rekordów. Bezpośrednie użycie operatorów
większy > albo większy lub równy >= również spowoduje wyświetlenie pustego zestawu
rekordów. Aby wyświetlić n-ty wiersz, trzeba posłużyć się pewnym wybiegiem. Naj-
pierw należy wyświetlić pierwszych n rekordów, posortować je według numeru wier-
sza malejąco, a z takiego dynamicznego źródła wybrać pierwszy rekord. Zastosowanie
funkcji ROWNUM do wyboru szóstego wiersza z tabeli Osoby będzie mogło zostać zreali-
zowane następująco:
SELECT Nazwisko FROM
(
SELECT Nazwisko FROM Osoby
WHERE ROWNUM <=6
ORDER BY ROWNUM DESC
) WHERE ROWNUM =1;

Pamiętać jednak należy, że odwoływanie się do wierszy o bardzo wysokim numerze


będzie bardziej czasochłonne, ponieważ w pierwszym, przygotowawczym zapytaniu
należy przetworzyć dokładnie n wierszy. Podobnie możemy zrealizować zapytanie
wybierające przedział od n-tego do m-tego wiersza — najpierw wybierając m wierszy
(jeśli m > n), a po odwróceniu kolejności pierwszych m–n+1. Dla przykładu zapytanie
wyświetlające wiersze od 5. do 7. z tabeli Osoby może mieć postać:
SELECT Nazwisko FROM
(
SELECT Nazwisko FROM Osoby
WHERE ROWNUM <= 7
ORDER BY ROWNUM DESC
) WHERE ROWNUM =< 3;

Niezwiązane bezpośrednio z filtrowaniem jest zastosowanie w zapytaniu funkcji wa-


runkowej CASE, która na podstawie analizy warunków może wyprowadzić w postaci
kolumny wyrażenie. W prezentowanym przykładzie na podstawie analizy kolumny
RokUrodz wyprowadzany jest statyczny napis. Sekcja ELSE nie jest obligatoryjna, a w przy-
kładzie, ponieważ warunki pokrywają wszystkie wartości liczb, zadziała ona tylko dla
36 Część I ♦ Oracle SQL

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 instrukcji CASE dopuszczalne są wszystkie operatory i funkcje, jakie zdefiniowano


w środowisku Oracle, np. BETWEEN, LIKE, IN, ..., UPPER, COS etc.

Grupowanie i funkcje agregujące


Obliczenia wykonywane do tej pory dotyczyły danych zawartych w obrębie pojedynczego
wyprowadzanego rekordu. Możemy jednak dokonywać operacji algebraicznych doty-
czących wszystkich rekordów lub też ustalonych ich grup. Przykładowo wyznaczmy
sumę wszystkich zarobków (dla całej tabeli — wszystkich osób).
SELECT SUM(Brutto) AS Razem FROM Zarobki

Funkcję występującą w części głównej zapytania (w naszym przypadku SUM) będziemy


nazywać funkcją agregującą lub agregatem. Jej wyznaczenie jest możliwe również dla
grupy (w tym przypadku dla każdej osoby).
SELECT SUM(Brutto) AS Razem FROM Zarobki
GROUP BY IdOsoby
Rozdział 2. ♦ Zapytania wybierające 37

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.

Tabela 2.7. Funkcje agregujące w Oracle


Nazwa funkcji Opis Wyrażenie

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

SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki


GROUP BY IdOsoby

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

Zastanówmy się nad skutkiem działania następującego zapytania:


SELECT COUNT(*), COUNT(IdOsoby), COUNT(RokUrodz)
FROM Osoby

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

postanowiono inaczej, wprowadzając dodatkowe ograniczenia, może się w nim pojawić


wartość NULL. Zliczane są wtedy tylko te rekordy, które są określone (RokUrodz jest
określony; nie ma wartości NULL). Rozważmy działanie:
NULL ϑ A ≡ NULL

gdzie A to dowolna wartość, a ϑ jest dowolnym operatorem.

Ta właściwość powoduje, że jeśli w którymkolwiek z rekordów pola, na którym wy-


znaczamy funkcję agregującą, byłoby NULL, funkcja ta także wynosiłaby NULL. Aby tego
uniknąć, do wyznaczania funkcji agregujących używane są tylko wartości określone
(niebędące NULL). Reguła ta dotyczy ich wszystkich — również zliczania (COUNT).

W podanych przykładach występował tylko pojedynczy poziom grupowania. Istnieje


możliwość grupowania na wielu poziomach, ale ponieważ wymaga to operacji na
więcej niż jednej tabeli, zostanie pokazane po wprowadzeniu złączeń.

W przypadku zapytań agregujących możliwe jest również filtrowanie. W takim typie


zapytań możliwe jest ono na dwóch poziomach. Pierwszy to poziom rekordów oparty
na znanej nam już klauzuli WHERE.
SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki
WHERE Brutto >100
GROUP BY IdOsoby

Wykonanie takiego zapytania powoduje wyprowadzenie sum brutto składających się


z liczb większych niż 100; przykładowe działanie ilustruje tabela 2.8.

Tabela 2.8. Skutek wykonania zapytania agregującego z filtrowaniem na poziomie rekordu


Kowalski Nowak Janik
100 100 300
100 200 200
100 100
100
RAZEM bez filtrowania 400 400 500
RAZEM z filtrowaniem Niewyświetlane 200 500

Jeżeli chcemy zastosować filtrowanie wobec funkcji agregujących, używamy klauzuli


HAVING. Ponieważ dotyczy ona funkcji wyznaczanych przy zmianie grupy, musi zawsze
występować po klauzuli GROUP BY, np.:
SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki
GROUP BY IdOsoby
HAVING SUM(Brutto) > 400

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

Należy zauważyć, że funkcja, względem której odbywa się filtrowanie w klauzuli


HAVING, nie musi być tą samą, której rezultat jest wyświetlany (która jest podana w części
głównej zapytania). Oczywiście możliwe jest stosowanie obu rodzajów filtrowania
(na poziomie rekordu oraz na poziomie grupy — funkcji agregującej) jednocześnie,
w jednym zapytaniu.
SELECT IdOsoby, SUM(Brutto) AS Razem FROM Zarobki
WHERE Brutto > 100
GROUP BY IdOsoby
HAVING SUM(Brutto) > 400
ORDER BY SUM(Brutto)

W takim przypadku przykładowe wyniki będą wyglądały tak jak pokazano w tabeli 2.10.

Tabela 2.10. Skutek wykonania zapytania agregującego z filtrowaniem na poziomie rekordu


i grupy rekordów
Kowalski Nowak Janik
100 100 300
100 200 200
100 100
100
RAZEM bez filtrowania 400 400 500
RAZEM z filtrowaniem NULL 200 500
Niewyświetlane Niewyświetlane Wyświetlane

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

Zapytania do wielu tabel — złączenia


Dotychczas prezentowane zapytania wyświetlały dane z pojedynczej tabeli, jednakże
często interesują nas dane zgromadzone w kilku tabelach — taka ich organizacja jest
przecież charakterystyczna dla modelu relacyjnego. W przypadku danych przecho-
wywanych w dwóch tabelach (Osoby i Zarobki) możemy wykonać zapytanie:
SELECT Nazwisko, Brutto FROM Osoby, Zarobki

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

Operator JOIN jest stosunkowo „młodą” konstrukcją na platformie Oracle. Poprzednio


(do 8. realizacji) programiści musieli radzić sobie z uwzględnianiem kierunku złącze-
nia, stosując jedynie klauzulę WHERE — przy polach łączących wprowadzono dodat-
kowy znak (+) wskazujący kierunek. Złączenie prawostronne w oparciu o klauzulę
WHERE miało w takim ujęciu następującą postać:
SELECT Nazwisko, Brutto FROM
Osoby, Zarobki
WHERE Osoby.IdOsoby (+)= Zarobki.IdOsoby

Natomiast aby zrealizować złączenie lewostronne z klauzulą WHERE, należało przestawić


tylko znak (+) za pole leżące po prawej stronie znaku równości.
SELECT Nazwisko, Brutto FROM
Osoby , Zarobki
WHERE Osoby.IdOsoby = Zarobki.IdOsoby (+)

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.

Po okresie nieufności do realizowania złączeń w oparciu o operator JOIN twórcy


Oracle do tego stopnia się do niego przekonali, że wdrożyli na tej platformie wszyst-
kie zakładane przez standard jego warianty. Jeżeli tabele zawierają pola o tej samej
nazwie (nie muszą być one kluczami), a złączenie ma być realizowane za pomocą
operatora równości, to zapis można uprościć, stosując wyrażenie USING zawierające nazwę
tych pól.
SELECT Nazwisko, Brutto
FROM Osoby JOIN Zarobki
USING (IdOsoby);

Najczęściej spotykanym w praktyce przypadkiem złączenia jest takie, do którego re-


alizacji wykorzystujemy pole klucza głównego (tabela nadrzędna) i pole klucza obcego
(tabela podrzędna) połączone równością. Należy zauważyć, że jeżeli złączenie zawiera
wyrażenie ON, to konieczne jest stosowanie nazw kwalifikowanych zarówno po SELECT,
jak i w każdym z wyrażeń. Użycie wyrażenia USING jest równoznaczne z zakazem
używania nazw kwalifikowanych — jest to zakaz bezwzględny, którego złamanie kończy
się komunikatem o błędzie składniowym.

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

SELECT Nazwa, Nazwisko, Brutto


FROM Dzialy NATURAL JOIN Osoby
NATURAL JOIN Zarobki;

W prezentowanym przykładzie parser ma kłopoty z interpretacją (wykryciem) pola


łączącego połączone tabele Dzialy i Osoby z trzecim elementem — tabelą Zarobki.
Z tego powodu zapytanie takie zwraca krotkę (wszystkie rekordy obiektu nadrzędnego
połączone ze wszystkimi rekordami obiektu podrzędnego) pomiędzy połączonymi pierw-
szym złączeniem tabelami i ostatnim z elementów. W takim przypadku konieczne jest
jawne wyświetlenie pola łączącego te elementy — IdOsoby.
SELECT Nazwa, IdOsoby, Nazwisko, Brutto
FROM Dzialy NATURAL JOIN Osoby
NATURAL JOIN Zarobki;

Innym skutecznym rozwiązaniem jest zastosowanie podzapytania, które łączy opera-


torem NATURAL JOIN dwie spośród trzech tabel (w przykładzie Osoby i Zarobki), a na-
stępnie połączenie go z nadrzędnym zapytaniem operującym na tabeli nadrzędnej i zde-
finiowanym podzapytaniu. Pole łączące te dwa elementy musi wtedy zostać wyświetlone
jawnie.
SELECT Nazwa, Nazwisko, Brutto
FROM Dzialy
NATURAL JOIN
(SELECT IdDzialu, Nazwisko, Brutto
FROM Osoby
NATURAL JOIN Zarobki) xxx;

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;

Jednym z dość istotnych elementów składniowych jest zastosowanie dynamicznych


list — podzapytań. W przedstawianym przykładzie pokazywane są dane tych osób,
których nazwisko znajduje się na liście w słownikowej tabeli TTT (rysunek 2.1).

Rysunek 2.1.
Graficzna prezentacja
działania operatora
listy

SELECT Nazwisko, Imie, RokUrodz FROM Osoby


WHERE Nazwisko IN
(SELECT Nazwisko FROM ttt);
44 Część I ♦ Oracle SQL

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

SELECT Nazwisko, Imie, RokUrodz FROM Osoby


WHERE Nazwisko IN (SELECT Nazwisko FROM ttt)
AND imie IN (SELECT Imie FROM ttt);

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);

Najbardziej ogólnym, a jednocześnie najwydajniejszym sposobem sprawdzania zgod-


ności z listą jest stosowanie wielokrotnego złączenia na porównywanych polach. W przed-
stawionym przykładzie porównywane są jednocześnie nazwisko, imię oraz rok uro-
dzenia (rysunek 2.3). Należy zauważyć, że w tym przypadku do realizacji złączenia
wykorzystano pola niebędące kluczami — tabela ttt nie ma w ogóle zdefiniowanego
klucza. Z punktu widzenia rachunku zbiorów teorii mnogości wyznaczany jest iloczyn
zbiorów (Osoby ∩ ttt) z atrybutami opisanymi porównywanymi cechami. Można po-
wiedzieć, że iloczyn ten jest wyznaczany z dokładnością do tych trzech właściwości.

Rysunek 2.3.
Graficzna
prezentacja działania
wielokrotnego
złączenia
Rozdział 2. ♦ Zapytania wybierające 45

SELECT Osoby.Nazwisko, Osoby.Imie, Osoby.RokUrodz,


ttt.Nazwisko, ttt.Imie, ttt.RokUrodz
FROM Osoby JOIN ttt
ON Osoby.Nazwisko = ttt.Nazwisko
AND Osoby.Imie = ttt.Imie
AND Osoby.RokUrodz = ttt.RokUrodz;

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

WHERE ttt.Nazwisko IS NULL


AND ttt.Imie IS NULL
AND ttt.RokUrodz IS NULL;

Aby wyznaczyć różnicę (ttt–Osoby), wystarczy zmienić kierunek złączenia oraz spraw-
dzić, czy pole z tabeli Osoby ma wartość NULL.

W wielu zastosowaniach praktycznych szczególne miejsce mają zapytania skalarne, tzn.


takie, które zwracają jedno pole w jednym wierszu. Z reguły wynika to z użycia funkcji
agregujących, ale w ogólnym przypadku nie jest to konieczne. Podzapytania skalarne
mogą być wykorzystywane bezpośrednio do tworzenia warunków np. w klauzulach
WHERE lub HAVING. Ściśle rzecz biorąc, aby zapytanie takie było traktowane jako wartość,
np. liczba, musi być ujęte w nawiasy. W pokazanym przykładzie wyświetlany jest identy-
fikator osoby oraz wartość brutto dla tych wypłat, które są większe niż średnia pensja
w firmie.
SELECT IdOsoby, Brutto FROM Zarobki
WHERE Brutto > (SELECT AVG(Brutto) FROM Zarobki);

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);

O ile operator listy IN uniemożliwiał stosowanie operatora arytmetycznego (domyślnie


oznaczał równość), to kolejne operatory pozwalają na ich użycie. Operator listy ANY
zwraca prawdę, jeśli dla jakiegokolwiek elementu listy wyrażenie jest prawdziwe. W ana-
lizowanym przykładzie wyświetlony zostanie identyfikator osoby oraz wartość brutto
dla tych rekordów, w których Brutto jest wyższe od średniej wypłaty dla dowolnego
działu. Synonimem operatora ANY jest operator SOME.
SELECT IdOsoby, Brutto FROM Zarobki
WHERE Brutto > ANY
(SELECT AVG(Brutto) FROM Zarobki JOIN Osoby
ON Osoby.IdOsoby = Zarobki.IdOsoby GROUP BY IdDzialu);

W powyższym przykładzie wystarczy, aby wypłata była większa od najmniejszej śred-


niej dla działów. Podobnie operator ALL zwraca prawdę, jeśli dla wszystkich elemen-
tów listy wyrażenie jest prawdziwe. Przedstawiony został przykład, gdzie wyświetlane
są te rekordy, dla których wartość Brutto jest wyższa niż średnia wypłata dla wszystkich
działów. W tym przypadku wypłata Brutto musi być wyższa od najwyższej średniej
pensji wyznaczonej dla każdego z działów.
SELECT IdOsoby, Brutto FROM Zarobki
WHERE Brutto > ALL
(SELECT AVG(Brutto) FROM Zarobki JOIN Osoby
ON Osoby.IdOsoby = Zarobki.IdOsoby GROUP BY IdDzialu);

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

Rozważmy kilka przykładów bardziej złożonych zapytań wybierających wykorzystują-


cych poznane dotąd mechanizmy. Spróbujmy zbudować zapytanie wybierające najczę-
ściej pojawiającą się w tabeli Osoby wartość pola RokUrodz, czyli medianę (wartość
najczęściej występująca). W przypadku takich zapytań zarówno ich konstrukcję, jak
i analizę najwygodniej jest rozpoczynać od najniższego poziomu — najbardziej za-
gnieżdżonego podzapytania. Za pomocą funkcji agregującej COUNT uzyskiwana jest
liczba wystąpień dla wszystkich wartości pola RokUrodz. Wyliczanemu polu zostaje
nadany alias — rok. Jeśli podzapytanie to zostanie ujęte w nawias i obdarzone aliasem,
będzie mogło być traktowane jak tabela — dokładnie tabela dynamiczna, perspektywa.
Następnie wyznaczamy przy pomocy funkcji agregującej MAX wartość maksymalną
z obliczonego w podzapytaniu pola — alias był niezbędny, aby się do niego można
było odwołać w zapytaniu nadrzędnym. To z kolei jest zapytaniem skalarnym i może
zostać użyte do filtrowania. W głównym zapytaniu wystarczy wyświetlić interesującą
nas wielkość (zastosowany alias nie jest konieczny) i sprawdzić w klauzuli HAVING,
dla jakiego roku urodzenia liczebność wystąpień jest równa policzonemu w podza-
pytaniu maksimum. Przedstawione rozwiązanie jest słuszne również wtedy, kiedy
kilka wartości pola RokUrodz jest równie licznych i stanowi maksimum.
SELECT RokUrodz AS rok FROM Osoby
GROUP BY RokUrodz
HAVING COUNT(RokUrodz) =
(SELECT MAX(rok) FROM
(SELECT COUNT(RokUrodz) as rok FROM Osoby
GROUP BY RokUrodz) xxx );

Kolejny przykład jest zdecydowanie trudniejszy: mamy wyznaczyć wartość środkową,


czyli modę. Moda statystyczna (czasami nazywana modą matematyczną) jest w przy-
padku nieparzystej liczby rekordów wartością występującą w rekordzie środkowym,
jeżeli natomiast ich liczba jest parzysta, wyciągana jest średnia arytmetyczna z dwóch
sąsiednich, środkowych rekordów. Można powiedzieć, że aby wyznaczyć modę, należy
znaleźć pierwszy rekord po pominięciu 50% ich liczebności, patrząc na zestaw rekor-
dów uporządkowanych malejąco i rosnąco, po czym z tych dwóch wielkości wyznaczyć
średnią. Takie ujęcie problemu zapewnia jednolite rozwiązanie zarówno w przypadku
parzystej, jak i nieparzystej liczby rekordów. Najbardziej elementarnym zadaniem jest
obliczenie 50% z liczebności rekordów. Następnie musimy wskazać na ten rekord, który
daje liczebność większą lub równą liczebności 50% z tych rekordów, dla których rok
urodzenia jest większy od wartości zawartej w aktualnie przetwarzanym wierszu, a jed-
nocześnie (stąd zastosowanie operatora AND) jego wartość jest większa od 50% z tych,
dla których rok urodzenia jest mniejszy od wartości aktualnej. Zarówno w przypadku
nieparzystej, jak i parzystej liczby rekordów otrzymamy dwa rekordy (dla nieparzystej
liczby dwukrotnie rekord centralny, a dla parzystej dwa sąsiednie), z których funkcją
agregującą AVG należy wyznaczyć średnią.
SELECT 0.5 AS Wspolczynnik,
AVG(RokUrodz) AS Moda
FROM Osoby a
WHERE
(SELECT COUNT(*) FROM Osoby aa
WHERE aa.RokUrodz <= a.RokUrodz) >=
(SELECT 0.5*COUNT(*) FROM Osoby)
AND
48 Część I ♦ Oracle SQL

(SELECT COUNT(*) FROM Osoby bb


WHERE bb.RokUrodz >= a.RokUrodz) >=
(SELECT 0.5*COUNT(*) FROM Osoby);

Ponieważ w rozwiązaniu zastosowano zapytania skorelowane — odwołanie się w podza-


pytaniu poprzez alias do pola w zapytaniu nadrzędnym — wydajność takiego przetwa-
rzania jest słaba.

Efektywniejszym wariantem jest zastosowanie na dwóch kopiach tej samej tabeli


krotki realizowanej przy użyciu operatora CROSS JOIN (równie dobrze można zastosować
klauzulę WHERE) i pogrupowanej po roku urodzenia z pierwszej tworzącej ją tabeli oraz
funkcji CASE. Funkcja ta przypisuje wartość 1 rekordom, w których pierwsze pole nie
jest ani mniejsze, ani większe od drugiego przy liczebności mniejszej lub równej 50%
całości, a jednocześnie pierwsze pole rekordu jest nie większe od drugiego pola przy
liczebności mniejszej lub równej 50% całości. Dla tak wyznaczonych rekordów wy-
starczy zsumować pomnożony przez wyliczony współczynnik rok urodzenia, a następnie
podzielić wynik przez sumę współczynników. Zastosowano tutaj definicyjne wyzna-
czanie średniej arytmetycznej jako sumy wartości podzielonej przez ich liczbę.
SELECT 0.5 AS Wspolczynnik,
SUM(RokUrodz*wsp)/sum(wsp) AS Moda FROM (
SELECT a.RokUrodz, COUNT(*) AS wsp
FROM Osoby a
CROSS JOIN Osoby b
GROUP BY a.RokUrodz
HAVING
SUM(CASE WHEN b.RokUrodz <= a.RokUrodz
THEN 1 ELSE 0 END) >= 0.5*COUNT(*)
AND
SUM(CASE WHEN b.RokUrodz >= a.RokUrodz
THEN 1 ELSE 0 END) >= 0.5*COUNT(*)
) xxx;

Należy zwrócić uwagę, że jeżeli we wszystkich miejscach zastąpimy współczynnik 0.5


innym ułamkiem, gdzie x∈<0, 0.5), to wyznaczona zostanie średnia arytmetyczna po
pominięciu (1–x)*100% skrajnych rekordów, co może mieć zastosowanie praktyczne,
jeśli przypuszczamy, że nazbyt odległe od centralnych wartości wynikają z błędów czy
zakłóceń. Przyjęcie x > 0.5 spowoduje, że podzapytanie nie wybierze żadnych rekordów.

Kolejnym dość trudnym i jednocześnie przydatnym przykładem jest realizacja nume-


rowania rekordów w grupie (tabela 2.11). Przede wszystkim musimy posortować je
względem pola wyznaczającego grupę IdDzialu oraz interesującego nas pola porządko-
wego IdOsoby, a następnie pogrupować rekordy pola wyznaczającego grupę IdDzialu.
Z tak otrzymanego zestawu rekordów wybieramy pole grupujące oraz minimalną wartość
numeru wiersza w obrębie tej grupy — MIN(ROWNUM). To zapytanie łączone jest z ko-
pią najbardziej wewnętrznego podzapytania przy użyciu pola grupującego IdDzialu.
Połączone zapytania są źródłem danych dla zapytania najbardziej zewnętrznego, nad-
rzędnego. Sprawdza ono, jaka jest różnica między bieżącym numerem wiersza a obli-
czonym minimalnym numerem wiersza w grupie. Wykorzystujemy tu funkcję DECODE,
której pierwszym argumentem jest testowane wyrażenie, drugim wartość wyświetlana,
gdy jest ono ujemne, trzecim, gdy jest równe zero, a czwartym, gdy wyrażenie jest
dodatnie. Druga linia zapytania, testując to wyrażenie, przypisuje wartości zerowej
Rozdział 2. ♦ Zapytania wybierające 49

Tabela 2.11. Skutek wykonania zapytania numerującego pozycje w dziale


IDDZIALU NR IDOSOBY NAZWISKO
1 1 1 Kowalski
2 2 Nowak
3 10 Nowik
2 1 4 Nowacki
2 6 Kow
3 7 Jakow
3 1 3 Kowalik
2 8 Adamiak
3 9 Kowalski
4 11 Xxx
4 1 5 Wilk
2 12 yy

wartość 1, a wartościom dodatnim wartość tej różnicy powiększoną o jeden. Wartość


parametru dla ujemnej różnicy jest ustawiana na zero, ale ponieważ wyznaczona przez nas
różnica nigdy wartości ujemnej nie osiągnie (numer rekordu w grupie jest zawsze co
najmniej równy najniższemu numerowi jej wiersza), może ona być ustawiona na do-
wolną wartość. Tak samo, zamiast dla wartości równych zero podawać jawnie wartość
jeden, moglibyśmy powielić wyrażenie będące czwartym parametrem. Pierwsza linia kodu
ma za zadanie zastąpić powtarzające się w kolejnych wierszach wartości IdDzialu pu-
stymi polami. Jeżeli godzimy się na występowanie powtórzeń, zamiast używać funkcji
DECODE, wystarczy tylko podać nazwę pola wyznaczającego grupę IdDzialu.
SELECT DECODE(ROWNUM-min_nr,0,a.IdDzialu,NULL) IdDzialu,
DECODE(ROWNUM-min_nr,0,1,ROWNUM+1-min_nr) nr,
a.Idosoby, Nazwisko FROM
(SELECT * FROM Osoby ORDER BY IdDzialu, IdOsoby) a,
(
SELECT IdDzialu, MIN(ROWNUM) min_nr FROM
(
SELECT * FROM Osoby ORDER BY IdDzialu, IdOsoby
)
GROUP BY IdDzialu
) b
WHERE a.IdDzialu = b.IdDzialu;

Grupowanie i funkcje analityczne


Sporo problemów możemy napotkać, próbując otrzymać podsumowania na kilku po-
ziomach grupowania. Jeśli dokonamy go na poziomie nazwy działu, to podsumowania
będą dotyczyły tylko tego poziomu. Tu wyznaczyliśmy sumy zarobków wypłaconych
w każdym dziale.
50 Część I ♦ Oracle SQL

SELECT Nazwa, SUM(Brutto) AS Razem


FROM Dzialy JOIN Osoby
ON Dzialy.IdDzialu = Osoby.IdDzialu
JOIN Zarobki
ON Osoby.IdOsoby = Zarobki.IdOsoby
GROUP BY Nazwa;

Wprowadzając kolejny poziom grupowania, nazwisko pracownika, wyznaczymy sumy


dla każdego z pracowników. Co prawda, nazwisko każdego z nich jest poprzedzone
nazwą działu, w którym pracuje, ale straciliśmy informację o sumie zarobków w dziale.
SELECT Nazwa, Nazwisko, SUM(Brutto) AS Razem
FROM Dzialy JOIN Osoby
ON Dzialy.IdDzialu = Osoby.IdDzialu
JOIN Zarobki
ON Osoby.IdOsoby = Zarobki.IdOsoby
GROUP BY Nazwa, Nazwisko;

Możemy powiedzieć, że pojedyncze zapytanie może wyznaczyć funkcje agregujące


tylko na jednym poziomie grupowania, agregacji. Oczywiście to samo możemy uzyskać,
stosując operator USING lub NATURAL JOIN.
SELECT Nazwa, Nazwisko, SUM(Brutto) AS Razem
FROM Dzialy JOIN Osoby
USING(IdDzialu)
JOIN Zarobki
USING(IdOsoby)
GROUP BY Nazwa, Nazwisko;

Pierwszym rozwiązaniem takiego problemu jest zastosowanie podzapytania wyznacza-


jącego podsumowanie na zewnętrznym poziomie grupowania — IdDzialu. Podzapy-
tanie to jest łączone z zapytaniem wyznaczającym sumę na niższym poziomie agrega-
cji — dla każdego pracownika — do którego przekazuje ono wartość obliczonego
pola sumy dla działu. Połączenie zapytania nadrzędnego z podrzędnym odbywa się za
pomocą pola IdDzialu. Należy zwrócić uwagę, że lista grupowania wynikowego za-
pytania zawiera poza polami Nazwa i Nazwisko również pole SumaDzial, gdyż w za-
pytaniu nadrzędnym nie działa już na nie żadna funkcja agregująca. Ponieważ suma ta
dla każdego działu jest jednakowa, nie zmienia to faktycznego sposobu grupowania,
a wynika tylko z wymogów składniowych. W prezentowanym rozwiązaniu nazwa działu
oraz suma dla niego będą powtarzane tyle razy, ilu pracowników danego działu otrzy-
mało jakąś wypłatę brutto.
SELECT Nazwa, Nazwisko, SUM(Brutto) AS Razem, SumaDzial
FROM Osoby JOIN Zarobki
ON Osoby.IdOsoby=Zarobki.IdOsoby
JOIN
(SELECT IdDzialu, SUM(Brutto) AS SumaDzial
FROM Osoby JOIN Zarobki
USING(IdOsoby)
GROUP BY IdDzialu) xxx
USING(IdDzialu)
GROUP BY Nazwa, Nazwisko, SumaDzial;
Rozdział 2. ♦ Zapytania wybierające 51

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.

Tabela 2.12. Skutek wykonania zapytania z opcją grupowania ROLLUP


Nazwa Nazwisko Razem
Dyrekcja Kowalski 2109
Dyrekcja 2109
Handlowy Nowicki 2109
Handlowy 2109
Techniczny Kow 222
Techniczny Kowalczyk 777
Techniczny 999
Administracja Janik 555
Administracja Nowak 1332
Administracja 1887
7104

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 ROLLUP (Nazwa, Nazwisko);

Jeśli zastosujemy klauzulę GROUP BY CUBE, skutek poprzedniego zapytania zostanie


wzbogacony o dodatkowe podsumowania dla pracowników (tabela 2.13). Sumy dla
działów z podziałem na pracowników pozostaną bez zmian.
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 CUBE (Nazwa, Nazwisko);

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 GROUPING SETS (Nazwa, Nazwisko);
52 Część I ♦ Oracle SQL

Tabela 2.13. Skutek wykonania zapytania z opcją grupowania CUBE


Nazwa Nazwisko Razem
7104
Kow 222
Janik 555
Nowak 1332
Nowicki 2109
Kowalski 2109
Kowalczyk 777
Dyrekcja 2109
Dyrekcja Kowalski 2109
Handlowy 2109
Handlowy Nowicki 2109
Techniczny 999
Techniczny Kow 222
Techniczny Kowalczyk 777
Administracja 1887
Administracja Janik 555
Administracja Nowak 1332

Najbardziej ubogim wariantem grupowania wielopoziomowego jest użycie klauzuli


GROUP BY GROUPING SETS, która co prawda wykonuje podsumowania wielopoziomowe
(tabela 2.14), ale rozdziela je tak, że nie ma możliwości przypisania sumom nadrzęd-
nym ich składników. Na każdym poziomie stosowane jest porządkowanie alfabetyczne.

Tabela 2.14. Skutek wykonania zapytania z opcją grupowania GROUPING SETS


Nazwa Nazwisko Razem
Administracja 1887
Dyrekcja 2109
Handlowy 2109
Techniczny 999
Janik 555
Kow 222
Kowalczyk 777
Kowalski 2109
Nowak 1332
Nowicki 2109
Rozdział 2. ♦ Zapytania wybierające 53

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);

Stosowanie dowolnego sposobu grupowania nie zmienia ogólnego postulatu, że jeśli


na liście po poleceniu SELECT występuje jakiekolwiek pole (wyrażenie), na które nie
działa funkcja agregująca, to musi się ono znaleźć w definicji opcji grupowania. Nie-
stety, każde takie pole daje swoje podsumowania. W poprzednim przykładzie dodana
w opcji grupowania pozycja IdOsoby — zastosowana, aby rozróżnić pracowników o tym
samym imieniu — da kolejne, nie zawsze potrzebne rekordy. Dlatego warto do defi-
niowania grup używać pól kluczowych, a dopiero w zapytaniu nadrzędnym wyprowa-
dzać bardziej czytelne pola opisowe (Nazwisko, Imie etc.).
SELECT Nazwa, Nazwisko || || Imie AS Pracownik, Razem
FROM Dzialy JOIN Osoby USING(IdDzialu)
JOIN
(SELECT IdDzialu, IdOsoby, SUM(Brutto) AS Razem
FROM Dzialy JOIN Osoby USING(IdDzialu)
JOIN Zarobki USING(IdOsoby)
GROUP BY GROUPING SETS (IdDzialu, IdOsoby)) xxx
USING(IdDzialu, 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);

Jak widać, zdublowanie pola w opcjach grupowania prowadzi również do zdublowa-


nia rekordów z podsumowaniami dla powielanego poziomu grupowania. Eliminowana
jest też pozycja podsumowania dla wszystkich rekordów (całej firmy), ponieważ war-
tość NULL nie jest poziomem grupowania klauzuli GROUP BY. Wykrycie stanu, w któ-
rym dublowane są rekordy, jest możliwe dzięki zastosowaniu funkcji GROUP_ID(), która
dla pierwszego wystąpienia rekordu zwraca wartość zero, a dla kolejnych wartości 1
i wyższe (tabela 2.16).
SELECT Nazwa, Nazwisko, SUM(Brutto) AS Razem,
GROUP_ID() AS GR
FROM Dzialy JOIN Osoby
ON Dzialy.IdDzialu = Osoby.IdDzialu
JOIN Zarobki
ON Osoby.IdOsoby = Zarobki.IdOsoby
GROUP BY Nazwa, ROLLUP (Nazwa, Nazwisko);
54 Część I ♦ Oracle SQL

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

Takie kodowanie może być stosowane do eliminowania powtórzeń. Ponieważ funkcja


GROUP_ID jest traktowana jak każda funkcja agregująca, do filtrowania musimy użyć
klauzuli HAVING.
SELECT Nazwa, Nazwisko, SUM(Brutto) AS Razem,
GROUP_ID() AS GR
FROM Dzialy JOIN Osoby
ON Dzialy.IdDzialu = Osoby.IdDzialu
JOIN Zarobki ON Osoby.IdOsoby = Zarobki.IdOsoby
GROUP BY Nazwa, ROLLUP (Nazwa, Nazwisko)
HAVING GROUP_ID<1;

Analogiczny rezultat, związany z duplikowaniem rekordów, eliminowaniem globalnego


podsumowania oraz kodowaniem powtórzeń, otrzymujemy, stosując klauzulę CUBE (ta-
bela 2.17).
SELECT Nazwa, Nazwisko, SUM(Brutto) AS Razem,
GROUP_ID() AS GR
FROM Dzialy JOIN Osoby
ON Dzialy.IdDzialu = Osoby.IdDzialu
JOIN Zarobki
ON Osoby.IdOsoby = Zarobki.IdOsoby
GROUP BY Nazwa, CUBE (Nazwa, Nazwisko);
Rozdział 2. ♦ Zapytania wybierające 55

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

Kolejnym przykładem kodowania pustych i niepustych kolumn jest funkcja


GROUPING_ID(ListaPol). Kodowanie ma postać binarną ustalającą dla każdej pozycji
— będącej miejscem pola na liście — wartość odpowiadającego jej bitu na zero lub
jeden (tabela 2.19). Całość jest konwertowana na liczbę całkowitą.
Rozdział 2. ♦ Zapytania wybierające 57

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);

Aby nie powielać podzapytań, w złożonych zapytaniach wybierających (najczęściej, ale


niekoniecznie, zawierających agregację) można użyć klauzuli WITH, w której definiu-
jemy podzapytanie, do którego później możemy się odwołać za pomocą jego aliasu.
W prezentowanym przykładzie zdefiniowano podzapytanie obliczające sumy brutto na
poziomie każdego działu. Zostało ono wykorzystane do wyświetlenia nazw i sum brutto
tych działów, w których łączne wypłaty były wyższe do przeciętnej łącznej pensji we
wszystkich działach.
WITH
Podsumowanie AS (
SELECT Nazwa, Sum(Brutto) AS Razem
FROM Dzialy JOIN Osoby USING(IdDzialu)
JOIN Zarobki USING(IdOsoby)
GROUP BY Nazwa)
SELECT Nazwa, Razem
FROM Podsumowanie
WHERE Razem >
(SELECT AVG(Suma) FROM
(SELECT SUM(Razem) AS Suma FROM Podsumowanie) xxx)
ORDER BY Razem;

Prezentowany stan funkcji podsumowujących, agregujących, utrzymywał się jeszcze


na poziomie 9. realizacji Oracle. Od wersji 10. obserwujemy pojawianie się nowych
funkcji tego rodzaju (tabela 2.14) oraz wprowadzenie nowego sposobu określania grup
i zasięgu ich działania dzięki zastosowaniu konstrukcji OVER (... PARTITION ...).

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

Tabela 2.20. Skutek wykonania zapytania wyświetlającego podsumowanie z zastosowaniem definicji


partycji
IdOsoby Brutto Razem
1 111 2664
1 555 2664
1 333 2664
1 666 2664
1 999 2664
2 444 1998
2 666 1998
2 888 1998
3 222 1443
3 888 1443
3 333 1443
... ... ...

Użyteczność takiej definicji partycjonowania będzie szczególnie widoczna podczas de-


finiowania więcej niż jednego poziomu sumowania (tabela 2.21), eliminując podza-
pytanie.

Tabela 2.21. Skutek wykonania zapytania wyświetlającego podsumowanie z zastosowaniem definicji


partycji dla wielu poziomów grupowania
IdDzialu IdOsoby Brutto RazemO RazemD
1 1 333 2664 2664
1 1 555 2664 2664
1 1 111 2664 2664
1 1 666 2664 2664
1 1 999 2664 2664
2 2 888 1998 4773
2 2 666 1998 4773
2 2 444 1998 4773
2 4 222 1776 4773
2 9 999 999 4773
2 4 555 1776 4773
2 4 999 1776 4773
... ... ... ... ...

SELECT IdDzialu, IdOsoby, Brutto,


SUM(Brutto) OVER (PARTITION BY IdOsoby) RazemO,
SUM(Brutto) OVER (PARTITION BY IdDzialu) RazemD
FROM Osoby JOIN Zarobki
USING(IdOsoby);
60 Część I ♦ Oracle SQL

Oprócz statycznego okienka definiowanego przez partycję możliwe jest zdefiniowanie


ruchomego okna obliczeniowego, w którym granice (dolna i [lub] górna) grupy rekor-
dów przesuwają się wraz z rekordem bieżącym. Takie okna mogą być tworzone przy
pomocy wyrażeń ROWS lub RANGE. Wyrażenie ROWS definiuje tzw. okno fizyczne, którego
rozmiar określony jest liczbą rekordów, natomiast klauzula RANGE definiuje tzw. okno
logiczne o rozmiarze określonym warunkiem ich selekcji. Składnia ruchomego okna
logicznego jest następująca:
FUNKCJA() OVER
([PARTITION BY Kolumna1]) ORDER BY Kolumna2 [DESC]
RANGE BETWEEN Wyrażenie1 AND Wyrażenie2)

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.

Wyrażenie1 może przyjąć jedną z kilku postaci:


 UNBOUNDED PRECEDING — okno rozpoczyna się od pierwszego rekordu grupy.
 CURRENT ROW — okno rozpoczyna się od wartości stanowiącej bieżący rekord.
 n PRECEDING — okno rozpoczyna się od rekordu posiadającego w kolumnie
Kolumna2 wartość o n mniejszą (dla DESC większą) niż rekord bieżący.
 INTERVAL 'n' DAYS | MONTHS | YEARS PRECEDING — okno rozpoczyna się
od rekordu posiadającego w kolumnie Kolumna2 wartość o n dni/miesięcy/lat
mniejszą (dla DESC większą) niż rekord bieżący.

Wyrażenie2 przyjmuje następującą postać:


 UNBOUNDED FOLLOWING — okno kończy się na ostatnim rekordzie grupy.
 CURRENT ROW — okno kończy się na wartości stanowiącej bieżący rekord.
 n FOLLOWING — okno kończy się na rekordzie posiadającym w kolumnie
Kolumna2 wartość o n większą (dla DESC mniejszą) niż rekord bieżący.
 INTERVAL 'n' DAYS | MONTHS | YEARS FOLLOWING — okno kończy się na
rekordzie posiadającym w kolumnie Kolumna2 wartość o n dni/miesięcy/lat
większą (dla DESC mniejszą) niż rekord bieżący.

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

kich elementów pomiędzy pierwszym wierszem a rekordem bieżącym (tabela 2.22).


Skutek tego zapytania jest ściśle uwarunkowany sposobem sortowania (w zastosowa-
niach praktycznych bardzo często wykorzystujemy porządkowanie względem pola daty,
uzyskując np. przyrost sprzedaży czy zysk w czasie).

Tabela 2.22. Skutek wykonania zapytania wyświetlającego sumę bieżącą


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 3219
2 03/02/14 888 4107
2 03/05/05 666 4773
3 02/02/07 222 4995
3 02/07/03 999 5994
3 05/10/11 888 6882
4 03/03/08 222 7104
4 04/01/03 555 7659

SELECT IdOsoby, DataWyp, Brutto, SUM(Brutto)


OVER (ORDER BY IdOsoby ROWS BETWEEN
UNBOUNDED PRECEDING AND CURRENT ROW) Razem
FROM Zarobki;

Zmiana sposobu sortowania zmienia wynik uzyskiwany w zapytaniu (tabela 2.23).


W prezentowanym przykładzie ograniczono się do sortowania według daty wypłaty
DataWyp.
SELECT DataWyp, Brutto, SUM(Brutto)
OVER (ORDER BY IdOsoby ROWS BETWEEN
UNBOUNDED PRECEDING AND CURRENT ROW) Razem
FROM Zarobki;

Dokonując podziału na partycje za pomocą klauzuli PARTITION BY, możemy ograni-


czyć zakres obliczanej sumy bieżącej do obrębu tych grup (tabela 2.24). W prezento-
wanym przykładzie podział powoduje, że sumy te wyznaczane są dla każdego z pra-
cowników.
SELECT IdOsoby, Brutto, SUM(Brutto)
OVER (PARTITION BY IdOsoby
ORDER BY IdOsoby ROWS BETWEEN
UNBOUNDED PRECEDING AND CURRENT ROW) Razem
FROM Zarobki;
62 Część I ♦ Oracle SQL

Tabela 2.23. Skutek wykonania zapytania wyświetlającego sumę bieżącą


IdOsoby DataWyp Brutto Razem
1 02/01/03 999 999
3 02/02/07 222 1221
11 02/03/17 222 1443
6 02/07/01 333 1776
3 02/07/03 999 2775
11 02/09/23 111 2886
2 02/11/22 444 3330
2 03/02/14 888 4218
6 03/02/19 333 4551
4 03/03/08 222 4773
6 03/03/11 666 5439
2 03/05/05 666 6105
17 03/06/17 333 6438
12 03/07/14 888 7326

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

Podobnie możliwe jest wyznaczenie wartości wszystkich klasycznych funkcji agregujących


dla zdefiniowanej partycji, a także wartości złożenia funkcji agregujących (np. średniej
z sum wypłat). Przydatną w przeprowadzaniu analiz jest funkcja RATIO_TO_REPORT, po-
zwalająca na wyznaczenie udziału pola (również takiego, które jest skutkiem oblicze-
nia funkcji agregującej) jako części sumy wartości w obszarze zdefiniowanej partycji
(tabela 2.25).
Rozdział 2. ♦ Zapytania wybierające 63

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

SELECT IdDzialu, Nazwisko, SUM(Brutto),


AVG(SUM(Brutto)) OVER () "Średnia wszystkich",
AVG(SUM(Brutto))
OVER (PARTITION BY IdDzialu) "średnia sumy dla działu",
RATIO_TO_REPORT(SUM(Brutto))
OVER () "Procent z całości",
RATIO_TO_REPORT(SUM(Brutto))
OVER (PARTITION BY IdDzialu) "Procent w dziale"
FROM Osoby JOIN Zarobki USING (IdOsoby)
GROUP BY idDzialu, Nazwisko, IdOsoby;

Funkcje analityczne i rankingowe


Zmiany i uzupełnienia w obszarze zdefiniowanych na platformie funkcji analitycznych
są w ostatnich realizacjach Oracle bardzo znaczące, a zakres ich funkcjonalności ma
duże znaczenie praktyczne, stąd wydaje się uzasadnione ich bardziej dokładne omó-
wienie. Pierwszą z nich jest funkcja ROW_NUMBER(), stanowiąca klon wcześniej wpro-
wadzonej i omówionej funkcji ROWNUM, a dedykowana grupowaniu z zastosowaniem
klauzuli PARTITION BY. Rozważmy przykład zastosowania tej funkcji dla różnych za-
kresów partycjonowania: dla całej tabeli (brak partycjonowania), obrębu każdego działu
oraz dla każdego z pracowników. Numerację stworzono w ten sposób, aby na każdym
z zakresów generowany był ciąg kolejnych wartości.
SELECT IdDzialu, Nazwisko, Brutto,
ROW_NUMBER() OVER (
ORDER BY IdDzialu, IdOsoby) AS Numer,
ROW_NUMBER() OVER (PARTITION BY IdDzialu
ORDER BY IdDzialu, IdOsoby) AS Numer_w_dziale,
ROW_NUMBER() OVER (PARTITION BY IdOsoby
ORDER BY IdOsoby) AS Numer_wyplaty
FROM Osoby JOIN Zarobki
USING(IdOsoby)
ORDER BY IdDzialu, IdOsoby;
64 Część I ♦ Oracle SQL

Należy zwrócić uwagę na to, że w przypadku definicji pola Numer_w_dziale zastoso-


wano partycjonowanie tylko w obrębie IdDzialu, natomiast dla pola Numer_wyplaty
tylko na poziomie IdOsoby, z takim samym sortowaniem w tej partycji (tabela 2.26).
Spowodowało to, że pomimo poprawnego wygenerowania numerów, zostały one przy-
pisane zgodnie z kolejnością indeksów — z punktu widzenia osoby nieznającej struktury
tabel bez żadnego widocznego porządku.
Tabela 2.26. Skutek wykonania zapytania numerującego rekordy w obrębie grupy (partycji)
IdDzialu Nazwisko Brutto Numer Numer_w_dziale Numer_wyplaty
1 Kowalski 666 34 6 4
1 Kowalski 111 36 7 6
1 Kowalski 555 35 2 5
1 Kowalski 111 31 3 1
1 Kowalski 999 33 5 3
1 Kowalski 333 32 4 2
1 Polakow 999 1 1 1
2 Nowak 444 30 8 3
2 Nowak 888 28 9 1
2 Nowak 666 29 7 2
2 Janik 222 24 6 2
2 Janik 555 23 5 1
2 Jasiński 888 8 3 2
2 Jasiński 666 7 4 1
2 Baranowski 333 3 2 1
2 Baranowski 444 4 1 2
3 Kow 999 25 3 1
3 Kow 222 26 5 2
3 Kow 888 27 4 3
3 Kowalczyk 777 21 1 1
3 Kowalczyk 777 22 2 2

Jeżeli chcemy utrzymać porządek w numeracji, musimy zadbać o ujednolicenie spo-


sobu porządkowania rekordów w obrębie każdej zastosowanej partycji (tabela 2.27).
Dodatkowo, tam gdzie dokonujemy partycjonowania na poziomie nadrzędnym (pole
Numer_w_dziale), należy zastosować definicję partycji zawierającą oba pola.
SELECT IdDzialu, Nazwisko, Brutto,
ROW_NUMBER() OVER (
ORDER BY IdDzialu, IdOsoby) AS Numer,
ROW_NUMBER() OVER (PARTITION BY IdDzialu, IdOsoby
ORDER BY IdDzialu, IdOsoby) AS Numer_w_dziale,
ROW_NUMBER() OVER (PARTITION BY IdOsoby
ORDER BY IdDzialu, IdOsoby) AS Numer_wyplaty
FROM Osoby JOIN Zarobki
USING(IdOsoby)
ORDER BY IdDzialu, IdOsoby;
Rozdział 2. ♦ Zapytania wybierające 65

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.28. Wykaz nazw dodatkowych funkcji analitycznych


FIRST_VALUE REGR_SLOPE
LAST_VALUE REGR_INTERCEPT
VAR_SAMP REGR_R2
VAR_POP REGR_AVGX
STDDEV_SAMP REGR_AVGY
STDDEV_POP REGR_COUNT
COVAR_SAMP REGR_SXX
COVAR_POP REGR_SXY
REGR_SYY
66 Część I ♦ Oracle SQL

Jednymi z prostszych w tej kategorii są funkcje FIRST_VALUE() i LAST_VALUE(), wy-


znaczające pierwszy i ostatni rekord w grupie określonej oknem. W przykładzie wy-
korzystano funkcję FIRST_VALUE() w celu wyszukania najstarszej osoby w dziale (za-
kres okna — partycji), stosując sortowanie rekordów w jego obrębie rosnąco względem
roku urodzenia.
SELECT IdDzialu, Nazwisko, RokUrodz,
FIRST_VALUE(Nazwisko) OVER
(PARTITION BY IdDzialu
ORDER BY RokUrodz ASC) AS Najstarszy
FROM Osoby ORDER BY IdDzialu, RokUrodz;

Zmiana kierunku sortowania z rosnącego ASC na malejący DESC spowoduje oczywiście


wyznaczenie najmłodszej osoby w każdym z działów.
SELECT IdDzialu, Nazwisko, RokUrodz,
FIRST_VALUE(Nazwisko) OVER
(PARTITION BY IdDzialu
ORDER BY RokUrodz DESC) AS Najmlodszy
FROM Osoby ORDER BY IdDzialu, RokUrodz;

Dokładnie taki sam efekt uzyskamy, zamieniając funkcję FIRST_VALUE() na LAST_VALUE(),


przy zachowaniu pierwotnego kierunku sortowania — w prezentowanym przykładzie
danego w sposób niejawny (pominięto operator ASC).
SELECT IdDzialu, Nazwisko, RokUrodz,
LAST_VALUE(Nazwisko) OVER
(PARTITION BY IdDzialu) AS Najmlodszy
FROM Osoby ORDER BY IdDzialu, RokUrodz;

Należy zwrócić uwagę, że w przypadku istnienia na pierwszej lub ostatniej pozycji


partycji rekordów o tej samej wartości pola, po którym wykonujemy sortowanie, wy-
brany zostanie tylko jeden z nich. O tym, który będzie to rekord, zdecyduje sposób
indeksowania lub, w razie braku indeksów, kolejność wpisywania danych do tabeli.

Jednymi z prostszych funkcji analitycznych są mediana (wartość środkowa) i wartość


modalna (najczęściej występująca), które poprzednio wyznaczaliśmy przy pomocy kla-
sycznych funkcji SQL (tabela 2.29). Prowadziło to do bardzo złożonej struktury wy-
nikowych zapytań. W poniższym przykładzie zrezygnowano z definicji okna, ale funkcje
te mogą być również definiowane z opcjami grupowania.
SELECT IdOsoby, MEDIAN(Brutto) AS Median,
STATS_MODE(Brutto) AS Stats_Mode
FROM Zarobki
GROUP BY IdOsoby;

Podobnie możemy stosować funkcje wyznaczające parametry statystyczne rozkładu


danych wykorzystujące rozkład normalny. Wprowadzone zostały funkcje analityczne
VAR_SAMP(), VAR_POP(), STDDEV_POP() oraz STDDEV_SAMP(), których opis w postaci wy-
rażeń algebraicznych zawiera tabela 2.30.

Zastosujmy te funkcje do wyznaczenia dystrybucji roku urodzenia (tego, jak jest on


skupiony wokół wartości średniej) w obrębie każdego z działów (tabela 2.31).
Rozdział 2. ♦ Zapytania wybierające 67

Tabela 2.29. Wyznaczanie mediany i mody (z przykładem nieoznaczonego działania)


IdOsoby Median Stats_Mode IdOsoby Brutto
1 555 111 1 111
2 666 444 1 333
3 333 222 1 999
4 555 222 1 555
5 777 777 1 666
6 499,5 111 5 777
7 333 111 5 777
8 610,5 444 5 666
9 999 999 6 666
11 111 111 6 111
6 888
6 333

Tabela 2.30. Wyrażenia algebraiczne opisujące wybrane funkcje analityczne


Nazwa funkcji Opis Wyrażenie
VAR_POP () Wariancja populacji σ pop
2

VAR_SAMP() Wariancja próbki σ


STDDEV_POP() Odchylenie standardowe 2
n
⎛ 1 n ⎞
populacji
∑⎜ x
i =1 ⎝
i − ∑ xi ⎟
n i =1 ⎠
σ pop =
(n − 1)
STDDEV_SAMP() Odchylenie standardowe 2
n
⎛ 1 n ⎞
próbki
∑⎜ x
i =1 ⎝
i
− ∑ xi ⎟
n i =1 ⎠
σ=
n

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

SELECT DISTINCT IdDzialu,


VAR_SAMP(RokUrodz) OVER
(PARTITION BY IdDzialu ) AS Wariancja,
68 Część I ♦ Oracle SQL

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;

Podobną rolę pełnią funkcje COVAR_SAMP() i COVAR_POP(), wyznaczające kowariancję


dla próbki i populacji (tabela 2.33).

Tabela 2.33. Wyrażenia algebraiczne opisujące wybrane funkcje analityczne


Nazwa Wyrażenie
COVAR_POP() 1 l k ⎛ 1 k ⎞⎛ 1 l ⎞
cxy = ∑∑ ⎜ xi − ∑ xi ⎟⎜ y j − ∑ y j ⎟
kl j =1 i=1 ⎝ k i =1 ⎠⎝ l j =1 ⎠
COVAR_SAMP() 1 l k
(xi y j ) − 1 ∑ xi ∑ y j
k l
cxy = ∑∑
kl j =1 i=1 kl i=1 j =1

W prezentowanym zapytaniu wyznaczono kowariancję między polami Wzrost i RokUrodz


dla całej tabeli (tabela 2.34). Oczywiście, tak samo jak wszystkie prezentowane do tej
pory, powyższe funkcje analityczne mogą działać w obrębie zdefiniowanych klauzulą
PARTITION BY okien.

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;

Kolejnym elementem są funkcje, które wyznaczają miejsce wiersza w rankingu, po-


równując wartości w nim zawarte z wartościami wierszy znajdujących się w tej samej
partycji. Definicja partycji pozwala na wyznaczenie wielu oddzielnych rankingów,
a porządek wierszy w partycji jest podstawowym elementem definiującym ranking.
Podstawowa składnia funkcji rankingu ma postać:
FUNKCJA() OVER (PARTITION BY Kolumna1
ORDER BY Kolumna2 [DESC] [NULLS FIRST | LAST])

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

Tabela 2.35. Skutek działania funkcji rankingowej RANK()


IdDzialu Nazwisko Brutto Rank
1 Polakow 999 1
1 Kowalski 999 1
1 Kowalski 666 3
1 Kowalski 555 4
1 Kowalski 333 5
1 Kowalski 111 6
1 Kowalski 111 6
2 Nowak 888 1
2 Jasiński 888 1
2 Jasiński 666 3
2 Nowak 666 3
2 Janik 555 5
2 Nowak 444 6
2 Baranowski 444 6
2 Baranowski 333 8
2 Janik 222 9
3 Kow 999 1
3 Kow 888 2
3 Kowalczyk 777 3
3 Kowalczyk 777 3
3 Kow 222 5
4 Nowicki 999 1

Funkcja rankingowa z parametrem może zostać użyta do wyznaczenia liczby elemen-


tów znajdujących się powyżej lub poniżej określonej parametrem wartości progowej
(tabela 2.36). Dla malejącego kierunku sortowania wyznaczana jest liczba elementów
większych niż wartość.

Tabela 2.36. Skutek działania funkcji rankingowej RANK(wartość)


Powyżej 250
30

SELECT RANK(250) WITHIN GROUP


(ORDER BY Brutto DESC) "Powyżej 250"
FROM Zarobki;

Zmiana kierunku sortowania na rosnący powoduje, że zliczane są elementy mniejsze


od danej wartości.
SELECT RANK(250) WITHIN GROUP
(ORDER BY Brutto ASC) "Poniżej 250"
FROM Zarobki;
Rozdział 2. ♦ Zapytania wybierające 71

Można powiedzieć, że parametr funkcji wyznacza granicę, do której zliczane są rekordy


z uwzględnieniem kierunku sortowania oraz, dodatkowo, parametru definiującego na
liście pozycje nieokreślone (NULL). Podobnie działa funkcja DENSE_RANK(), która również
oblicza miejsce w rankingu każdego wiersza zwracanego przez zapytanie zgodnie
z klauzulą ORDER BY (tabela 2.37). Jeżeli jednak dwie pozycje mają taką samą wartość,
następna wartość rankingu jest o 1 większa (dla n-krotnego powtórzenia kolejna po-
zycja w rankingu jest zwiększana o n) — pozycje równe traktowane są tak samo.

Tabela 2.37. Skutek działania funkcji rankingowej DENSE_RANK()


IdDzialu Nazwisko Brutto Rank
1 Polakow 999 1
1 Kowalski 999 1
1 Kowalski 666 2
1 Kowalski 555 3
1 Kowalski 333 4
1 Kowalski 111 5
1 Kowalski 111 5
2 Nowak 888 1
2 Jasiński 888 1
2 Jasiński 666 2
2 Nowak 666 2
2 Janik 555 3
2 Nowak 444 4
2 Baranowski 444 4
2 Baranowski 333 5
2 Janik 222 6
3 Kow 999 1
3 Kow 888 2
3 Kowalczyk 777 3
3 Kowalczyk 777 3

SELECT IdDzialu, Nazwisko, Brutto,


DENSE_RANK() OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC) "Rank"
FROM Osoby JOIN Zarobki USING(IdOsoby);

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

Tabela 2.38. Skutek działania funkcji rankingowej DENSE_RANK(wartość)


Powyżej 250
9

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.

Tabela 2.39. Skutek działania funkcji rankingowej PERCENT_RANK()


IdDzialu Nazwisko Brutto Rank
1 Polakow 999 0
1 Kowalski 999 0
1 Kowalski 666 ,333333333
1 Kowalski 555 ,5
1 Kowalski 333 ,666666667
1 Kowalski 111 ,833333333
1 Kowalski 111 ,833333333
2 Nowak 888 0
2 Jasiński 888 0
2 Jasiński 666 ,25
2 Nowak 666 ,25
2 Janik 555 ,5
2 Nowak 444 ,625
2 Baranowski 444 ,625
2 Baranowski 333 ,875
2 Janik 222 1
3 Kow 999 0
3 Kow 888 ,25
3 Kowalczyk 777 ,5
3 Kowalczyk 777 ,5
3 Kow 222 1

SELECT IdDzialu, Nazwisko, Brutto,


PERCENT_RANK() OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC) "Rank"
FROM Osoby JOIN Zarobki ON Osoby.IdOsoby = Zarobki.IdOsoby;
Rozdział 2. ♦ Zapytania wybierające 73

Przy malejącym kierunku sortowania kodowanie jest takie, że Max = 0, a Min → 1.


Funkcję tę można wyznaczyć za pomocą wyrażenia PERCENT_RANK() = (RANK()-1)/
(liczba_wierszy-1).

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.40. Skutek działania funkcji analitycznej CUME_DIST()


IdDzialu Nazwisko Brutto Ułamkowy rozkład
1 Polakow 999 ,285714286
1 Kowalski 999 ,285714286
1 Kowalski 666 ,428571429
1 Kowalski 555 ,571428571
1 Kowalski 333 ,714285714
1 Kowalski 111 1
1 Kowalski 111 1
2 Nowak 888 ,222222222
2 Jasiński 888 ,222222222
2 Jasiński 666 ,444444444
2 Nowak 666 ,444444444
2 Janik 555 ,555555556
2 Nowak 444 ,777777778
2 Baranowski 444 ,777777778
2 Baranowski 333 ,888888889
2 Janik 222 1
3 Kow 999 ,2
3 Kow 888 ,4
3 Kowalczyk 777 ,8
3 Kowalczyk 777 ,8
3 Kow 222 1

SELECT IdDzialu, Nazwisko, Brutto,


CUME_DIST() OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC) "Ułamkowy rozkład"
FROM Osoby JOIN Zarobki USING(IdOsoby);

W zasadzie funkcje PERCENT_RANK() i CUME_DIST() różnią się tylko przedziałem kodo-


wania. Pierwsza z nich koduje do przedziału lewostronnie domkniętego, natomiast druga
do prawostronnie domkniętego (tabela 2.41). Funkcja CUME_DIST() może zostać wy-
znaczona przy użyciu zależności: liczba wierszy w partycji posiadających wartość
poprzedzającą lub równą x/liczba wszystkich wierszy w partycji.
SELECT IdDzialu, Nazwisko, Brutto,
CUME_DIST() OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC) "Ułamkowy rozkład",
74 Część I ♦ Oracle SQL

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

COUNT (1) OVER (PARTITION BY IdDzialu


ORDER BY Brutto DESC
RANGE UNBOUNDED PRECEDING) /
COUNT (*) OVER (PARTITION BY IdDzialu) Obliczone
FROM Osoby JOIN Zarobki USING(IdOsoby);

Różnice wartości rankingu ułamkowego dla obu sposobów jego tworzenia przedstawia
rysunek 2.4.

Rysunek 2.4. 1.0


Porównanie
działania funkcji
PERCENT_RANK()
oraz CUME_DIST() 0.8

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.42. Skutek działania funkcji analitycznej CUME_DIST(wartość)


Jaki ułamek powyżej 250
,769230769

W dotychczasowych rozważaniach zakres kodowania dla funkcji rankingowych był


określony przez twórców Oracle. Funkcja NTILE(liczba) pozwala zdecydować, przy
użyciu jakiego zakresu liczb naturalnych będzie się odbywało kodowanie. Parametr
funkcji jest maksymalną liczbą naturalną, do jakiej można nadawać pozycje w ran-
kingu. W przypadku gdy liczba rekordów jest nie większa niż ten parametr, NTILE za-
chowuje się dokładnie tak samo jak funkcja RANK(). Jeśli liczba rekordów jest większa,
kilka kolejnych wartości będzie miało w rankingu tę samą pozycję (tabela 2.43). Za-
sada przypisywania jest taka, że jeśli liczba rekordów jest podzielna przez zakres NTILE,
to każdej wartości rankingu zostaje przyporządkowana taka sama liczba rekordów.
W przypadku gdy jest to dzielenie z resztą r, to pierwszych r wartości rankingu będzie
miało liczebność powiększoną o jeden.
SELECT IdDzialu, Nazwisko, Brutto,
ROW_NUMBER() OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC) "Numer",
NTILE(4) OVER (PARTITION BY IdDzialu
ORDER BY Brutto DESC) "Zakresowy rozkład"
FROM Osoby JOIN Zarobki USING(IdOsoby);

Funkcja CUME_DIST() służy do wyznaczania tzw. percentyli (kwantyli), czyli określania,


na jakim miejscu w uporządkowanym zbiorze, wyrażonym procentowo, znajduje się war-
tość dana parametrem tej funkcji. Funkcje PERCENTILE_DISC() i PERCENTILE_CONT()
służą do przeprowadzania operacji odwrotnej. Wyliczają one, jaka wartość znajduje
się na określonej pozycji w ich uporządkowanym zbiorze (zwracają wartość rekordu,
który w rankingu reprezentowany jest przez zadany wynik funkcji CUME_DIST() ).
PERCENTILE_DISC() wyznaczane jest przez przeglądanie wyników uzyskanych w rezul-
tacie zastosowania funkcji CUME_DIST. Pierwszy wiersz, w którym jej wynik jest więk-
szy od argumentu funkcji PERCENTILE_DISC(), wyznacza poszukiwaną wartość.

Wynik funkcji PERCENTILE_CONT() jest uzyskiwany przez liniową interpolację wierszy


otaczających wskazaną pozycję. Wzór do wyznaczania wartości PERCENTILE_CONT(x)
jest realizowany w ten sposób, że najpierw obliczana jest wartość pomocnicza:
RN = (1+x*(n-1))
gdzie n jest liczbą wierszy w grupie. Następnie wyznaczamy:
 CRN = CEIL(RN),
 FRN = FLOOR(RN).
gdzie:
 CEIL — funkcja zwracająca najmniejszą wartość, która jest większa lub równa
liczbie przekazanej jako parametr.
 FLOOR — funkcja zwracająca największą wartość całkowitą, która jest mniejsza
lub równa liczbie przekazanej jako parametr.
76 Część I ♦ Oracle SQL

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)

Składnia funkcji PERCENTILE_DISC() i PERCENTILE_CONT() ma postać ogólną:


FUNKCJA() (stała1) WITHIN GROUP (ORDER BY wyrażenie1)
[OVER (PARTITION BY wyrażenie2)

gdzie:
 FUNKCJA() — nazwa funkcji raportującej,
Rozdział 2. ♦ Zapytania wybierające 77

 wyrażenie1 — kolumna (lub wyrażenie) grupująca rekordy pojawiające się


w raporcie,
 wyrażenie2 — kolumna (lub wyrażenie) porządkująca rekordy wewnątrz grupy,
 stała1 — wartość argumentu, jaki może pobierać funkcja.

Poniższe zapytanie pokazuje działanie funkcji PERCENTILE_DISC() w odniesieniu do


przedstawionej poprzednio funkcji CUME_DIST() (tabela 2.44).

Tabela 2.44. Skutek wykonania zapytania wyświetlającego funkcje agregujące PERCENTILE_DISC()


i CUME_DIST() w obrębie grupy (partycji)
IdOsoby Brutto Per_Disc Cume_Dist
1 111 555 ,2
1 333 555 ,4
1 555 555 ,6
1 666 555 ,8
1 999 555 1
2 444 666 ,333333333
2 666 666 ,666666667
2 888 666 1
3 222 333 ,333333333
3 333 333 ,666666667
3 888 333 1
… … … …

SELECT IdOsoby, Brutto,


PERCENTILE_DISC(0.5) WITHIN GROUP
(ORDER BY Brutto) OVER
(PARTITION BY IdOsoby) AS Per_Disc,
CUME_DIST() OVER
(PARTITION BY IdOsoby ORDER BY Brutto) AS Cume_Dist
FROM Zarobki;

Dla argumentu funkcji PERCENTILE_DISC() równego 0.5 otrzymano wartości środ-


kowe zbioru rekordów w obrębie partycji. W celu zaprezentowania działania funkcji
PERCENTILE_CONT() posłużono się analogicznym do poprzedniego zapytaniem (tabela 2.45).
SELECT IdOsoby, Brutto,
PERCENTILE_DISC(0.5) WITHIN GROUP
(ORDER BY Brutto)
OVER (PARTITION BY IdOsoby) AS Per_Disc,
CUME_DIST() OVER
(PARTITION BY IdOsoby ORDER BY Brutto) AS Cume_Dist,
PERCENTILE_CONT(0.5) WITHIN GROUP
(ORDER BY BRUTTO)
OVER (PARTITION BY IdOsoby) AS Per_Cont
FROM Zarobki;
78 Część I ♦ Oracle SQL

Tabela 2.45. Skutek wykonania zapytania wyświetlającego wyniki funkcji agregujących


PERCENTILE_DISC() i CUME_DIST() w obrębie grupy (partycji)
IdOsoby Brutto Per_Disc Cume_Dist Per_Cont
1 111 555 ,2 555
1 333 555 ,4 555
1 555 555 ,6 555
1 666 555 ,8 555
1 999 555 1 555
… … … … …
6 111 333 ,25 499,5
6 333 333 ,5 499,5
6 666 333 ,75 499,5
6 888 333 1 499,5
7 111 111 ,5 333
7 555 111 1 333
… … … … …

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.

Kolejne funkcje powodują wyświetlenie: LAG() wartości pola z poprzedniego rekordu


okna, a LEAD() wartości pola z następnego względem bieżącego rekordu (tabela 2.46).
Parametrem może być zarówno pole tabeli, jak i dowolne wyrażenie, również oparte
na funkcjach (w tym agregujących).

Tabela 2.46. Skutek działania funkcji analitycznych LAG(wyrażenie) i LEAD(wyrażenie)


DataWyp SUM(Brutto) Poprzedni_rekord_wg_DataWyp Następny_rekord_wg_DataWyp
02/01/03 999 222
02/02/07 222 999 222
02/03/17 222 222 333
02/07/01 333 222 999
02/07/03 999 333 111
02/09/23 111 999 444
02/11/22 444 111 888

SELECT DataWyp, SUM(Brutto),


LAG(SUM(Brutto))
OVER (ORDER BY DataWyp) Poprzedni_rekord_wg_DataWyp,
LEAD(SUM(Brutto))
Rozdział 2. ♦ Zapytania wybierające 79

OVER (ORDER BY DataWyp) Następny_rekord_wg_DataWyp


FROM Zarobki
GROUP BY DataWyp
ORDER BY DataWyp;

Funkcja WIDTH_BUCKET pozwala na zdefiniowanie zakresu i liczby przedziałów oraz


przypisanie numeru przedziału wartości bieżącego wiersza. W przeciwieństwie do
funkcji NTILE(), która dzieli grupę wierszy (o ile to możliwe) na równoliczne prze-
działy, WIDTH_BUCKET() dzieli zbiór wierszy na przedziały o równej szerokości i o ta-
kiej samej głębokości — jednakowym zakresie zmian partycjonowanego pola. Składnię
tej funkcji można zapisać następująco:
WIDTH_BUCKET() (wyrażenie1, Dgranica, Ggranica, LPrzedz)

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.

Zastosowanie funkcji WIDTH_BUCKET() pokazane zostało na przykładzie zapytania, w któ-


rym, opierając się na sumach zarobków brutto, podzielono pracowników na trzy grupy
(tabela 2.47). Statycznie zadeklarowano, że maksymalny próg wynosi 3000, a mi-
nimalny to 100.

Tabela 2.47. Skutek działania funkcji analitycznej WIDTH_BUCKET()


IdOsoby Razem Podzial
5 2220 1
1 2664 1
8 1221 2
3 1443 2
4 1776 2
2 1998 2
6 1998 2
11 111 3
7 666 3
9 999 3

Stosowanie dynamicznie wyznaczanych parametrów określających granice przedziału


jest dość trudnym zadaniem. Nie jest możliwe bezpośrednie wyznaczenie wartości
minimalnej i maksymalnej zakresu zmian w obrębie parametrów funkcji WIDTH_BUCKET()
— wymaga to zastosowania opcji grupowania, co powoduje, że wartości te są obliczane
na poziomie każdego z pól. Stąd konieczność użycia dodatkowego podzapytania, które
wyznacza te granice za pomocą funkcji agregujących MAX i MIN. Ponieważ zapytanie to
jest jednowierszowe, możliwe jest użycie złączenia typu CROSS JOIN, które nie wymaga
80 Część I ♦ Oracle SQL

podania warunku. Do każdego rekordu dołączona zostanie więc wartość odpowiednio


aliasowanego pola reprezentującego obliczone w podzapytaniu granice. W prezento-
wanym przykładzie schemat ten zastosowano do podzielenia na cztery przedziały o rów-
nej głębokości względem pełnego zakresu zmian pola RokUrodz (tabela 2.48). Należy
zwrócić uwagę, że dolny zakres musi zostać zmniejszony (w przykładzie o jeden), po
to by najmniejsze wartości nie zostały przypisane do zerowego przedziału (wtedy mie-
libyśmy podział na pięć zakresów zamiast żądanych czterech). Jeśli jako trzeci para-
metr zostanie podane górne ograniczenie (zamiana miejscami parametrów opisujących
granice zmienności) musi on zostać, z tych samych przyczyn, nieznacznie zwiększony.

Tabela 2.48. Skutek działania funkcji analitycznej WIDTH_BUCKET() z dynamicznym określeniem


granic wyznaczania przedziałów
Nazwisko RokUrodz Podzial
Polakow 1959 4
Jasiński 1961 4
Gawlik 1966 3
Kow 1967 3
... ... ...
Nowicki 1972 3
Kowal 1973 3
Pawlak 1974 2
Baranowski 1975 2
... ... ...
Raczyński 1980 2
Ziółkowski 1980 2
Kowalczyk 1982 1
... .... ...
Bury 1986 1
Janicki 1989 1
Ktoś
... .... ...

SELECT Nazwisko, RokUrodz,


WIDTH_BUCKET
(RokUrodz, Mk, Mi-1, 4) AS Podzial
FROM Osoby
CROSS JOIN
(SELECT MAX(RokUrodz) AS Mk, MIN(RokUrodz) AS Mi
FROM Osoby) xxx
ORDER BY RokUrodz ASC;

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.

Tabela 2.49. Skutek działania funkcji analitycznej WIDTH_BUCKET() z dynamicznym określeniem


granic wyznaczania przedziałów
Nazwisko Razem Podzial
Nowicki 3108 1
Kowalski 2775 1
Kow 2109 2
Nowak 1998 2
Jasiński 1554 3
Kowalczyk 1554 3
Polakow 999 4
Adamczyk 777 4
Kowalski 777 4
Baranowski 777 4
Janik 777 4
Ktoś 666 4
Kowalik 555 4
Procent 444 4
Kowalik 333 4
Jakow 333 4

SELECT Nazwisko, SUM(BRUTTO) AS Razem,


WIDTH_BUCKET
(SUM(Brutto), Mk, Mi-1, 4) AS Podzial
FROM Osoby JOIN Zarobki
ON Osoby.IdOsoby = Zarobki.IdOsoby
CROSS JOIN
(SELECT MAX(Razem) AS Mk, MIN(Razem) AS Mi FROM
(SELECT SUM(Brutto) AS Razem FROM Zarobki
GROUP BY IdOsoby) xxx) yyy
GROUP BY Osoby.IdOsoby, Nazwisko, Mk, Mi
ORDER BY Podzial, Razem DESC;

Kolejną grupą funkcji analitycznych są funkcje wyznaczające regresję liniową w opar-


ciu o dwa pola tabeli. Aproksymacja liniowa jest wyznaczana zgodnie z zależnością
y = ax + b , gdzie współczynniki obliczane są według wzorów podanych w tabeli 2.50.

Obie zależności opisujące współczynnik kierunkowy prostej są równoważne. W celu


zilustrowania działania funkcji obliczających współczynniki aproksymacji zbadajmy
regresję wartości pola RokUrodz względem pola Wzrost.
SELECT REGR_SLOPE (RokUrodz, Wzrost) AS a,
REGR_INTERCEPT (RokUrodz, Wzrost) AS b
FROM Osoby;
82 Część I ♦ Oracle SQL

Tabela 2.50. Wyrażenia opisujące współczynniki aproksymacji (regresji) liniowej


n
⎛ 1 n
⎞⎛ 1 n ⎞ n n n

∑ ⎜ 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 ⎠

Funkcja REGR_SLOPE wyznacza współczynnik kierunkowy prostej, natomiast funkcja


REGR_INTERCEPT wyznacza wyraz wolny (tabela 2.51, rysunek 2.5).

Tabela 2.51. Obliczone współczynniki aproksymacji (regresji) liniowej


A B
28,137931 1924,05951

Rysunek 2.5. 1988


Przykład wizualizacji
regresji liniowej
1984

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.

Tabela 2.52. Obliczone współczynniki aproksymacji (regresji) liniowej


A B
,005786062 –9,6383387

SELECT REGR_SLOPE (Wzrost, RokUrodz) AS a,


REGR_INTERCEPT (Wzrost, RokUrodz) AS b
FROM Osoby;
Rozdział 2. ♦ Zapytania wybierające 83

Rysunek 2.6. 2
Przykład wizualizacji
regresji liniowej

1.9

1.8

1.7

1.6
1964 1968 1972 1976 1980 1984 1988

Co oczywiste, wartości otrzymanych w ten sposób współczynników są zupełnie od-


mienne, natomiast charakter zmian pozostał taki sam, co ilustruje wykres z naniesioną
prostą regresji.

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.

Tabela 2.53. Obliczone parametry aproksymacji (regresji) liniowej


A B Regr_R2 Regr_Avgx Regr_Avgy Regr_Count
,005786062 –9,6383387 ,162807802 1974,28571 1,785 14

SELECT REGR_SLOPE (Wzrost, RokUrodz) AS a,


REGR_INTERCEPT (Wzrost, RokUrodz) AS b
REGR_R2(Wzrost, RokUrodz) AS Regr_R2,
REGR_AVGX(Wzrost, RokUrodz) AS Regr_Avgx,
REGR_AVGY(Wzrost, RokUrodz) AS Regr_Avgy,
REGR_COUNT(Wzrost, RokUrodz) AS Regr_Count
FROM Osoby;
84 Część I ♦ Oracle SQL

Oceny jakości regresji możemy dokonać, używając kolejnych współczynników wy-


znaczanych za pomocą funkcji analitycznych, których postać wyrażoną przy użyciu
już poznanych funkcji pokazuje tabela 2.54, a skutek działania tabela 2.55.

Tabela 2.54. Zależności między parametrami aproksymacji (regresji) liniowej


REGR_SXX: REGR_COUNT(e1, e2) * VAR_POP(e2)
REGR_SYY: REGR_COUNT(e1,e2) * VAR_POP(e1)
REGR_SXY: REGR_COUNT(e1, e2) * COVAR_POP(e1, e2)

Tabela 2.55. Obliczone parametry aproksymacji (regresji) liniowej


A B Regr_Sxx Regr_Syy Regr_Sxy
,005786062 –9,6383387 528,857143 ,10875 3,06

SELECT REGR_SLOPE (Wzrost, RokUrodz) AS a,


REGR_INTERCEPT (Wzrost, RokUrodz) AS b,
REGR_SXX(Wzrost, RokUrodz) AS Regr_Sxx,
REGR_SYY(Wzrost, RokUrodz) AS Regr_Syy,
REGR_SXY(Wzrost, RokUrodz) AS Regr_Sxy
FROM Osoby;

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.

Wprowadzone na platformie Oracle funkcje obliczające parametry aproksymacji linio-


wej dla danego zbioru punktów pozwalają na zdefiniowanie kolejnych jej cech dzięki
zastosowaniu wyrażeń przedstawionych w tabeli 2.56.

Tabela 2.56. Zastosowanie parametrów aproksymacji (regresji) liniowej do wyznaczania innych


wielkości analitycznych
Rodzaj statystyki Wyrażenie
Standaryzowana R2 1-((1-REGR_R2)*((REGR_COUNT-1)/(REGR_COUNT-2)))
Błąd standardowy SQRT((REGR_SYY-(POWER(REGR_SXY,2)/REGR_SXX))/(REGR_COUNT-2))
Suma kwadratów REGR_SYY
Regresja sumy kwadratów POWER(REGR_SXY,2)/REGR_SXX
Residuum sumy kwadratów REGR_SYY-(POWER(REGR_SXY,2)/REGR_SXX)
Statystyka t dla współczynnika REGR_SLOPE*SQRT(REGR_SXX)/(Błąd standardowy)
kierunkowego
Statystyka t dla REGR_INTERCEPT/((Błąd standardowy)*
wyrazu wolnego SQRT((1/REGR_COUNT)+(POWER(REGR_AVGX,2)/REGR_SXX))
Rozdział 2. ♦ Zapytania wybierające 85

Ważnym parametrem oceniającym dane jest współczynnik korelacji liniowej Pearsona.


Przyjmuje on wartości z przedziału <–1, 1>. Wartość 0 tego współczynnika wskazuje,
że badane zmienne są niezależne, co pociąga za sobą fakt, iż reprezentują one idealny
dwuwymiarowy rozkład normalny. Innymi słowy możemy powiedzieć, że zmiany warto-
ści pierwszego parametru nie mają żadnego wpływu na zmienność drugiego. Współ-
czynnik ten dla zmiennych x i y może zostać opisany zależnością:

∑ (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:

 x i y są średnimi wartościami zmiennych;


 C(X,Y) jest kowariancją populacji tych zmiennych;
 Sx i Sy są odchyleniami standardowymi.

Jeśli współczynnik korelacji ma wartość ±1, oznacza to, że zmienność parametrów


może zostać opisana równaniem prostej y = ax + b . Dla wartości 1 współczynnik kie-
runkowy prostej jest dodatni, a dla wartości współczynnika korelacji –1 jest on ujemy.
Ze względu na możliwość wyznaczenia korelacji w oparciu o inne parametry rozkładu,
w prezentowanym przykładzie, oprócz określenia korelacji między wzrostem a rokiem
urodzenia za pomocą wbudowanej funkcji Oracle, wyznaczono ją, opierając się na
kowariancji. Obliczono również kowariancję oraz odchylenia standardowe obydwu
zmiennych (tabela 2.57).

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

SELECT CORR(Wzrost, RokUrodz) as Korelacja,


COVAR_POP(Wzrost, RokUrodz) /
(STDDEV_POP(Wzrost) * STDDEV_POP(RokUrodz)) AS Oblicz,
COVAR_POP(Wzrost, RokUrodz) AS Kowariancja,
STDDEV_POP(Wzrost) AS Stdev_W,
STDDEV_POP(RokUrodz) AS Stdev_R
FROM Osoby;

Ilustracją dość niskiej wartości korelacji analizowanych pól są rysunki prezentowane


podczas wyznaczania współczynników regresji liniowej (rysunki 2.5 i 2.6). Współ-
czynnik korelacji Pearsona nie wykrywa zależności innych niż liniowe, np. wielomia-
nowej wyższego rzędu. W celu zidentyfikowania zależności między zmiennymi opisanymi
za pomocą funkcji monotonicznych bardziej właściwym jest stosowanie współczynnika
rang Spearmana:
86 Część I ♦ Oracle SQL

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).

Współczynnik korelacji Spearmana może zostać wyznaczony za pomocą funkcji CORR_S,


która pozwala dodatkowo określić jedno- i dwustronny współczynnik istotności (ta-
bela 2.58).

Tabela 2.58. Skutek wykonania zapytania wyświetlającego współczynnik korelacji Spearmana


(rho Spearmana) pola RokUrodz względem pola Wzrost oraz jednostronna i dwustronna istotność korelacji
Korelacja_s ONE_SIDED_SIG TWO_SIDED_SIG
,318663186 ,049192111 ,098384221

SELECT CORR_S(Wzrost, RokUrodz) as Korelacja_S,


CORR_S(Wzrost, RokUrodz,'ONE_SIDED_SIG') AS
ONE_SIDED_SIG,
CORR_S(Wzrost, RokUrodz,'TWO_SIDED_SIG') AS
TWO_SIDED_SIG
FROM Osoby;

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

Podobnie jak współczynnik korelacji, zależności ściśle monotoniczne opisuje współ-


czynnik tau Kendalla (tabela 2.59). W celu jego wyznaczenia należy zestawić obser-
wacje we wszystkie możliwe pary, a następnie pary te podzielić na trzy możliwe ka-
tegorie:
 Pary zgodne — zmienne w ich obrębie zmieniają się zgodnie, czyli albo
w pierwszej obserwacji obydwie są większe niż w drugiej, albo obie
są mniejsze. Liczba takich par będzie oznaczana przez P.
 Pary niezgodne — zmienne zmieniają się przeciwnie, tzn. gdy jedna z nich
jest większa, druga jest mniejsza. Liczba takich par będzie oznaczana przez Q.
 Pary wiązane — jedna ze zmiennych ma równe wartości w obydwu
obserwacjach. Liczba takich par w próbie będzie oznaczana przez T.

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

Współczynnik korelacji Kendalla możemy ująć w następujący sposób:


P−Q
rK = 2
n(n − 1)

Może on zostać wyznaczony za pomocą funkcji CORR_K, która pozwala dodatkowo


określić jedno- i dwustronny współczynnik istotności.

W tym wypadku otrzymaliśmy najniższą z wartości korelacji. Również współczynniki


istotności są mniejsze niż przy współczynniku Spearmana.

Pozostałe elementy składniowe


stosowane w SQL
Ważną częścią pracy z bazą danych jest możliwość generowania raportów. Zanim zo-
stały wprowadzone w środowisku Oracle funkcje analityczne klasy OVER (PARTITION ...),
jedynym sposobem wielopoziomowego wyznaczania funkcji agregujących było
88 Część I ♦ Oracle SQL

stosowanie polecenia COMPUTE. Pomimo iż jest to rozwiązanie znacznie starsze, daje


ono wiele możliwości, które powodują, że jest chętnie wykorzystywane i dzisiaj. Głów-
nym elementem generowania raportu w oparciu o polecenie COMPUTE jest stworzenie
zapytania wybierającego (bez funkcji agregujących), które wyświetla zarówno pola
podsumowywane (te, na które ma działać funkcja agregująca), jak i te, które posłużą
do wyznaczenia poziomów podsumowania. Takie zapytanie poprzedzane jest przy-
najmniej jednym poleceniem COMPUTE, w którym definiujemy, jakie funkcje agregujące
mają zostać użyte i którego pola mają dotyczyć, oraz wskazujemy przez podanie na-
zwy pola poziom, na którym mają one zostać obliczone. Na każdym poziomie zarówno
lista funkcji, jak i analizowana kolumna mogą być różne. W naszym przypadku dla
każdego pracownika wyznaczana będzie suma i średnia z wypłat, przy zmianie działu
suma, średnia i maksymalna wypłata w danym dziale oraz, na koniec, dla całego ra-
portu tylko całkowita suma wynagrodzeń. Gdybyśmy chcieli wyznaczać jednocześnie
podsumowania (funkcje) dla innych kolumn, musimy powtórzyć polecenie COMPUTE
dla tego poziomu ze zmienioną listą funkcji i kolejnym polem. Należy pamiętać, aby
poziomy podsumowań były ułożone logicznie, począwszy od najniższego poziomu
grupowania.
SET PAGES 33
TTITLE 'Tytuł|Druga Linia'
BTITLE 'STOPKA|DRUGA LINIA'
BREAK ON Osoba SKIP 1 ON Nazwa SKIP 2 ON REPORT PAGE
COMPUTE SUM AVG OF Brutto ON Osoba
COMPUTE SUM MAX AVG OF Brutto ON Nazwa
COMPUTE SUM OF Brutto ON REPORT

COLUMN Nazwa FORMAT A15 HEADING 'Nazwisko|Imię'


JUSTIFY left
COLUMN Osoba FORMAT A25 HEADING 'Nazwisko|Imię'
JUSTIFY left
COLUMN Wzrost FORMAT 90.99 HEADING 'Wysokość'
JUSTIFY right
COLUMN BRUTTO FORMAT 999999.00 HEADING 'Zarobki'
JUSTIFY center
SELECT Nazwa, Nazwisko || ' ' || Imie AS Osoba, Wzrost, Brutto
FROM Dzialy JOIN Osoby USING(IdDzialu)
JOIN Zarobki USING(IdOsoby);

CLEAR BREAKS
CLEAR COMPUTES
CLEAR COLUMNS

W tak zdefiniowanym zapytaniu (ale również w dowolnym innym zapytaniu wybie-


rającym) możliwe jest stosowanie kilku sposobów formatowania. Pierwszym, bezpo-
średnio wynikającym z typu zapytania, jest dokonanie łamania tekstu dzięki poleceniu
BREAK, w którym wskazuje się albo liczbę wstawianych pustych linii SKIP N, albo
przejście do końca strony. Jej długość mierzona w liniach może zostać zdefiniowana
poleceniem SET PAGE (miało to istotne znaczenie przy wyprowadzaniu raportu na dru-
karkę). Innym ważnym elementem, tym razem mającym praktyczne zastosowanie rów-
nież przy wykonywaniu klasycznych zapytań wybierających, jest możliwość formatowa-
nia sposobu wyświetlania zawartości kolumn dzięki poleceniu COLUMN. Pozwala ono
sformatować pole poleceniem FORMAT (A15 — pole tekstowe o 15 znakach; dla forma-
towania liczb: 9 — cyfra nieobowiązkowa, 0 cyfra obowiązkowa. Np. 90,00 to liczba
Rozdział 2. ♦ Zapytania wybierające 89

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.

Zdecydowanie ważniejszymi są zdefiniowane w Oracle polecenia umożliwiające ope-


racje na zbiorach — skutkach wykonania dwóch zapytań. Pierwszym z nich jest ope-
rator UNION, powodujący wyznaczenie sumy rekordów pochodzących z dwóch zapytań.
Dla podstawowej składni rekordy powtarzające się (bez względu na to, czy powtórzenie
dotyczy tego samego zapytania, czy też występuje między zapytaniami) reprezento-
wane są przez jeden rekord. Można powiedzieć, że operator UNION wyznacza sumę
dwóch zbiorów z eliminacją powtórzeń. Dodatkowym elementem tej eliminacji jest
posortowanie wynikowych rekordów, tak jakby umieszczona została dodatkowa klau-
zula ORDER BY z pełną listą pól. Podstawą sortowania jest więc pierwsze z pól w za-
pytaniu.
SELECT Nazwisko, Imie FROM Osoby
UNION
SELECT Nazwisko, Imie FROM ttt;

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

SELECT Nazwisko, Imie FROM Osoby


UNION ALL
SELECT Nazwisko, Imie FROM ttt
UNION ALL
SELECT Nazwiska, Imiona FROM Inna;

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ń.

Według analogicznych zasad możemy wyznaczyć różnicę dwóch zbiorów (rekordów


zwracanych przez dwa zapytania). W naszym przykładzie jest to różnica między za-
wartością tabel Osoby i ttt (Osoby–ttt) określona z dokładnością do dwóch atrybutów:
Nazwisko i Imie.
SELECT Nazwisko, Imie FROM Osoby
MINUS
SELECT Nazwisko, Imie FROM ttt;

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;

Jedyną metodą zmiany podstawionej przy pierwszym wywołaniu wartości w ramach


tej samej sesji jest użycie polecenia:
UNDEFINE 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;

Skutek uruchomienia takiego zapytania w środowisku SQL Developera przedstawia


rysunek 2.7.

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

Tabela 2.60. Zawartość pliku generowanego dzięki zastosowaniu polecenia SPOOL

SELECT * FROM Dzialy;


1 Dyrekcja
2 Administracja
3 Techniczny
4 Handlowy
6 Pomocniczy
SPOOL OFF;

SPO[OL] [file_name[.ext]
[CRE[ATE] | REP[LACE] | APP[END]] | OFF];

Możliwe jest tu określenie sposobu tworzenia pliku:


 CREATE — tworzony jest nowy plik; jeśli plik już istnieje, wyświetlany jest
komunikat o błędzie.
 REPLACE — jeśli plik nie istnieje, tworzony jest nowy; w przeciwnym
przypadku jego zawartość jest nadpisywana nową — stan domyślny.
 APPEND — skutek zapytania jest dopisywany na końcu istniejącego pliku;
gdy plik nie istnieje, wyświetlany jest komunikat o błędzie.

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

Wykonywanie skryptów zawartych w plikach jest bardzo wygodną metodą, zwłaszcza


w przypadku złożonych operacji administracyjnych, szczególnie kiedy wykonywane są
one za pośrednictwem schedulera.

Obsługa grafów w SQL


Dość trudnym zadaniem w relacyjnych bazach danych jest obsługa grafów. W żadnym
komercyjnym produkcie nie istnieje ogólne, przeznaczone do tego narzędzie. Dla gra-
fów skierowanych, jednokierunkowych (drzew) narzędzie takie oferuje Oracle i jest
chyba jedynym środowiskiem dającym takie możliwości. O wadze grafów w praktyce
baz danych świadczą liczne opracowania poświęcone tematowi zastosowania SQL do
ich analizy i przeprowadzania na nich operacji. W tym miejscu można przywołać cho-
ciażby prace J. Celko. Aby można było wykonywać jakiekolwiek operacje na grafie,
musi być on zapisany w tzw. notacji krawędziowej, tak jak to pokazano w tabeli Osoby,
opisując relację podwładności, czyli hierarchię w firmie (rysunek 2.8). Dla każdego re-
kordu zdefiniowane jest pole IdSzefa wskazujące na klucz podstawowy IdOsoby z tej
samej tabeli, bo każdy przełożony jest również pracownikiem firmy. Aby wyróżnić na-
czelnego dyrektora (szefa szefów), w reprezentującym go rekordzie identyfikator zawiera
wartość NULL. Tak właśnie wygląda krawędziowa reprezentacja drzewa. Konkurencyjną
dla niej jest tzw. notacja odwiedzinowa, która, pomimo wielu zalet, nie doczekała się
żadnych narzędzi wbudowanych. Aby graf mógł być analizowany na platformie Oracle,
nie może być grafem cyklicznym (brak pętli) — z żadnego jego węzła nie może się dać,
przechodząc w jednym kierunku przez inne węzły, dotrzeć do niego ponownie. Nie-
stety sama notacja krawędziowa nie prowadzi do takiego ograniczenia i sprawdzenie
acykliczności grafu należy do programisty.

Rysunek 2.8. Osoby


Idea krawędziowej IdOsoby
reprezentacji grafów ...
w strukturze relacyjnej IdSzefa

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;

Przedstawione powyżej zapytanie spowoduje wyświetlenie pełnego drzewa i wszystkich


możliwych poddrzew (aż do pojedynczych liści — węzłów, od których nie ma już przej-
ścia do poziomu podległego), ponieważ nie wskazaliśmy węzła, od którego generowanie
hierarchii ma się rozpocząć. Wskazanie węzła startowego odbywa się dzięki wyraże-
niu zdefiniowanemu w klauzuli START WITH. Aby wyświetlić pełne drzewo, wskazujemy
na rekord, dla którego nie podano szefa: IdSzefa IS NULL. Zastosowana dla listy pól
oraz do sortowania funkcja LEVEL wyświetla poziom w hierarchii.
SELECT LEVEL, IdOsoby, Stanowisko, IdSzefa
FROM Osoby CONNECT BY PRIOR IdOsoby = IdSzefa
START WITH IdSzefa IS NULL ORDER BY LEVEL;
Rozdział 2. ♦ Zapytania wybierające 95

Ustanowienie innego punktu startowego jest możliwe poprzez wskazanie w klauzuli


START WITH odpowiedniej wartości klucza IdOsoby lub odpowiedniego identyfikatora
szefa. Te dwa rozwiązania różnią się tylko miejscem rozpoczęcia wyświetlania grafu.
SELECT LEVEL, IdOsoby, Nazwisko, IdSzefa
FROM Osoby CONNECT BY PRIOR IdOsoby = IdSzefa
START WITH IdOsoby = 5 ORDER BY LEVEL;

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;

Zdefiniowanie połączenia CONNECT BY przez podanie warunków PRIOR IdSzefa = IdOsoby


oraz IdSzefa = PRIOR IdOsoby ma taki sam skutek — zapytania te są równoważne.
W celu poprawienia wizualnej reprezentacji drzewa można zastosować różne sposoby
formatowania. Najprostszym elementem jest użycie funkcji SUBSTR, wycinającej ze
zdefiniowanego łańcucha znaków (myślniki) podłańcuch rozciągający się od pierwszego
znaku do zmieniającej się wraz ze zmianą poziomu w hierarchii wartości — w pre-
zentowanym przykładzie 2*LEVEL. Wadą takiego rozwiązania jest fakt, że łańcuch pier-
wotny musi być dostatecznie długi, tak aby nie została przekroczona jego długość dla
najniższego poziomu w hierarchii (najwyższa wartość LEVEL). Kolejnym zabiegiem jest
połączenie operatorami konkatenacji do postaci jednego pola wszystkich pól (za wyjąt-
kiem LEVEL), które chcemy wyświetlić w postaci drzewa; w innym przypadku automa-
tyczne wyrównanie kolejnych kolumn wyświetlanych rekordów zniweczy nasze wysiłki.
SELECT LEVEL,
'|' || SUBSTR('----------------',1,2* LEVEL) || Nazwisko AS Kto
FROM Osoby
START WITH IdSzefa IS NULL
CONNECT BY PRIOR IdOsoby = IdSzefa;

Lepszym rozwiązaniem wydaje się zastosowanie do formatowania funkcji RPAD('ciag',


ilosc, 'znak'), która powoduje uzupełnienie zmiennej ciag do długości ilosc znakami
znak. Pozwala to na skuteczne formatowanie drzew o dowolnej głębokości (poziomie
zagnieżdżenia).
SELECT LEVEL,
RPAD('|', 2*LEVEL, '-') || Nazwisko AS Kto ,IdSzefa
FROM Osoby
START WITH IdSzefa IS NULL
CONNECT BY PRIOR IdOsoby = IdSzefa;

Zastosowanie operatora CONNECT BY umożliwia używanie wszystkich dotychczas przed-


stawionych elementów składniowych, takich jak operatory złączenia, funkcje agregu-
jące itp. Przykładem sumowania w obrębie hierarchii może być zapytanie wyświetla-
jące sumę zarobków wszystkich pracowników będących na tym samym poziomie
(tabela 2.61), stąd wprowadzenie opcji grupowania z zastosowaniem funkcji LEVEL oraz
złączenia między tabelami Osoby i Zarobki. Możliwa jest realizacja złączenia zarówno
przez operator ON, jak i poprzez USING().
96 Część I ♦ Oracle SQL

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

SELECT LEVEL, SUM(Brutto)


FROM Osoby JOIN Zarobki
ON
Zarobki.IdOsoby = Osoby.IdOsoby
START WITH IdSzefa IS NULL
CONNECT BY PRIOR Osoby.IdOsoby = IdSzefa
GROUP BY LEVEL;

Dotychczasowe zapytania ograniczały się do wyświetlania informacji tylko z jednego


poziomu, ponieważ zarówno IdOsoby, jak i IdSzefa znajdują się w tym samym rekor-
dzie. Jeśli zamiast identyfikatorów będziemy chcieli wyświetlić nazwiska pracowników
oraz nazwiska ich szefów, musimy pobrać dane i z poziomu podstawowego (nazwisko
pracownika), i z poziomu nadrzędnego (nazwisko szefa) (tabela 2.62). W wersji pełnej
wymaga to zastosowania podzapytania zwracającego nazwisko pracownika (aliasowane
Prac) oraz identyfikator szefa (dane znajdują się w tym samym rekordzie), a następ-
nie połączenia go z zapytaniem nadrzędnym wyświetlającym dane z podzapytania oraz
dane szefa. Złączenie musi być realizowane przez operator ON, ponieważ zapytania
składowe łączymy przy pomocy pól o różnych nazwach oraz nazw kwalifikowanych
Osoby.IdOsoby = xxx.IdSzefa, gdyż zapytania składowe zawierają oba z tych pól.
SELECT Poziom, Prac, Osoby.IdSzefa, Osoby.Nazwisko AS Szef
FROM
Osoby RIGHT JOIN
(SELECT LEVEL AS Poziom, Nazwisko AS Prac, IdSzefa
FROM Osoby
START WITH IdSzefa IS NULL
CONNECT BY PRIOR IdOsoby = IdSzefa) xxx
ON Osoby.IdOsoby = xxx.IdSzefa;

Osoby o mniejszej biegłości w tworzeniu wielopoziomowych zapytań wybierających


mogą skorzystać z właściwości operatora PRIOR, który może wyświetlać dowolne pole
z rekordu nadrzędnego (w prezentowanym przykładzie nazwisko szefa), i zastosować
wersję uproszczoną (uproszczenie dotyczy składni; skutek wykonania zapytania jest
taki sam jak poprzednio; tabela 2.62).
SELECT LEVEL, Nazwisko, IdSzefa, PRIOR Nazwisko AS Szef
FROM Osoby
START WITH IdSzefa IS NULL
CONNECT BY PRIOR IdOsoby = IdSzefa;

Zapytania wykorzystujące obsługę drzew należą do najbardziej złożonych zapytań wy-


bierających, a ich opanowanie świadczy o dużej biegłości w zakresie języka SQL.
Rozdział 2. ♦ Zapytania wybierające 97

Tabela 2.62. Skutek wykonania zapytania wyświetlającego szefa każdego pracownika


LEVEL NAZWISKO IDSZEFA SZEF
1 Kowalski
2 Nowak 1 Kowalski
3 Janik 2 Nowak
4 Kowalski 4 Janik
4 Kowal 4 Janik
4 Jasiński 4 Janik
4 Bury 4 Janik
4 Wilk 4 Janik
4 Raczyński 4 Janik
4 Lew 4 Janik
3 Pawlak 2 Nowak
3 Gawlik 2 Nowak
2 Kow 1 Kowalski
3 Kowalczyk 3 Kow
4 Zieliński 5 Kowalczyk
4 Zięba 5 Kowalczyk
3 Nowicki 3 Kow
3 Adamiak 3 Kow
2 Polakow 1 Kowalski
1 Procent
98 Część I ♦ Oracle SQL
Rozdział 3.
Zapytania
modyfikujące dane
Do tej pory ograniczaliśmy się do tworzenia zapytań, które wyświetlają informację wcze-
śniej zapisaną w tabelach modelu relacyjnego. Drugą ważną grupę stanowią te, które
pozwalają na zmianę danych. Do grupy tej należą zapytania wstawiające nowe rekordy,
modyfikujące ich zawartość oraz usuwające rekordy zbędne.

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);

Pojedyncze wykonanie takiego zapytania powoduje wstawienie zawsze dokładnie jed-


nego wiersza. Jeśli nie znamy wartości pola, możemy w sposób jawny umieścić na li-
ście wartość NULL — bez względu na typ zasilanej w ten sposób kolumny. Takie po-
dejście jest charakterystyczne dla platformy Oracle. Drugim rozwiązaniem (bardziej
ogólnym) — stosowanym, gdy nie chcemy, nie możemy lub nie potrafimy podać danych
dla wszystkich kolumn — jest użycie po nazwie tabeli ujętej w nawiasy listy kolumn,
dla których podawane będą wartości. Również w tym przypadku obowiązuje nas zgod-
ność pozycyjna, tym razem jednak względem listy kolumn zdefiniowanej w zapytaniu.
Kolejność pól na tej liście nie musi się zgadzać z porządkiem w definicji tabeli, nato-
miast wiąże nas zasada, że nazwa pola może się tam pojawić co najwyżej raz. Do po-
mijanych kolumn niejawnie zostaną wstawione wartości NULL. Obowiązuje nas również
zgodność typów wstawianych danych z typami zadeklarowanymi podczas tworzenia
tabeli. Od wersji 9. ograniczenie to nie jest tak ostre — wymagana jest tylko możli-
wość wykonania automatycznej konwersji.
INSERT INTO Nowa(Nazwisko, Imie)
VALUES ('NOWAK','KAROL');
100 Część I ♦ Oracle SQL

Drugą grupę stanowią zapytania, w których źródłem danych wstawianych do tabeli są


wartości zapisane w innej tabeli Oracle. W takim przypadku po jej nazwie, zamiast słowa
kluczowego VALUES, stosujemy zapytanie wybierające. Podczas tworzenia go możemy
wykorzystywać wszystkie elementy składniowe dostępne w zapytaniach wybierających:
filtrowanie, sortowanie, grupowanie, stosowanie złączeń, tworzenie wyrażeń etc. Obo-
wiązuje nas tylko ogólna zasada, że liczba kolumn zapytania ma być równa liczbie
kolumn w tabeli oraz są one odpowiednich typów. Nazwy pól zapytania nie muszą się
zgadzać z nazwami pól w tabeli docelowej, ponieważ stosowane jest podstawienie
pozycyjne, a nie nazewnicze.
INSERT INTO Nowa
SELECT Nazwisko, Imie, RokUrodz FROM Osoby
WHERE RokUrodz >1960
ORDER BY Nazwisko;

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.

Modyfikacje danych zawartych w tabeli wprowadzamy, wykonując zapytanie aktuali-


zujące, w którym po poleceniu UPDATE wymieniamy nazwę tej tabeli, a po słowie klu-
czowym SET umieszczamy wyrażenie zmieniające dane. Zwykle w zapytaniu takim
stosowana jest klauzula WHERE, ograniczająca zakres modyfikacji do tych wierszy, dla
których wyrażenie w niej zawarte daje wartość TRUE (prawda). Pominięcie tej klauzuli
powoduje, że zmieniane są wszystkie rekordy tabeli. W prezentowanym przykładzie
w pole RokUrodz, w rekordach, gdzie miało ono wartość nieokreśloną NULL, wstawiana
jest wartość zero.
UPDATE Nowa
SET RokUrodz = 0
WHERE RokUrodz IS NULL;
Rozdział 3. ♦ Zapytania modyfikujące dane 101

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);

Ponieważ modyfikacja zawartości pól w obrębie jednego wiersza następuje w tym


samym czasie, możliwe jest wykonanie poprzedniego zapytania z jednoczesną zamianą
miejsc zawartości pól. Dowodzi to, że kolejność wyrażeń modyfikujących dane nie ma
wpływu na sposób ich wykonania.
UPDATE Nowa
SET Nazwisko = UPPER(Imie),
Imie = UPPER(Nazwisko);

Niedopuszczalne jest występowanie na liście dwóch wyrażeń modyfikujących zawar-


tość tego samego pola.

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;

Pominięcie klauzuli WHERE powoduje, że usuwane są wszystkie rekordy wymienionej


tabeli.
DELETE FROM Nowa;

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;

Ze względu na sposób wykonywania w poleceniu TRUNCATE niemożliwe jest stosowa-


nie klauzuli WHERE, zawsze usuwane są więc wszystkie wiersze. Dostępne są natomiast
dwa parametry, które określają sposób oddziaływania polecenia na przydzielone ta-
beli zasoby dyskowe (tabela 3.1).
TRUNCATE TABLE nazwa_tabeli
[DROP STORAGE|REUSE STORAGE];
102 Część I ♦ Oracle SQL

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

Mechanizmy te uniezależniają utrzymanie integralności od narzędzi wykorzystywanych


do edycji danych oraz od rodzaju i sposobu opracowania końcówki klienta bazy.

Podstawowymi rodzajami więzów są ograniczenia:


 PRIMARY KEY,
 UNIQUE,
 NOT NULL,
 FOREIGN KEY,
 CHECK.

Głównym ograniczeniem, występującym w praktycznie każdej tabeli (zgodnie z jej


pierwszą postacią normalną), jest ograniczenie klucza podstawowego. Zapewnia ono
niepowtarzalność wartości pola, którego dotyczy, oraz zapobiega występowaniu w nim
wartości NULL (pole klucza głównego musi być jednoznacznie określone). Przykładem
jest polecenie tworzące tabelę Dzialy, opisującą strukturę organizacyjną firmy.
CREATE TABLE Dzialy
(
IdDzialu NUMBER(10) PRIMARY KEY,
Nazwa VARCHAR2(20)
);

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.

Rolę podobną do ograniczenia klucza podstawowego pełni ograniczenie UNIQUE. Za-


pewnia ono niepowtarzalność wartości pola lub listy pól — w prezentowanym przy-
kładzie dotyczy to ich pary: Nazwisko i Imie. W odróżnieniu od klucza podstawowego
dopuszczalne jest, aby pola wchodzące w skład tego ograniczenia przyjmowały war-
tość NULL. Ponieważ, jak wiemy, (NULL = NULL) ⇒ NULL, to w polach UNIQUE wartość
nieokreślona może pojawiać się wielokrotnie. Dotyczy to zarówno pojedynczego pola,
jak i dowolnej kombinacji pól ograniczenie to tworzących.
DROP TABLE Nowa;
CREATE TABLE Nowa
(Nazwisko varchar2(15),
Imie varchar2(15),
CONSTRAINT spr UNIQUE (Nazwisko, Imie)
);

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

Nazwisko varchar2(15) NOT NULL,


Imie varchar2(15) DEFAULT 'Brak',
m_v number(3) CHECK (m_v > 10)
);

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)
);

Dla takiej definicji ograniczenia, nawet w przypadku zastosowania bardziej złożonych


wyrażeń typu CHECK(m_v > 10 AND m_v < 300), nie możemy zapewnić — o ile zakresy
wartości pól m_v i mm_v nie są rozłączne — że ograniczenie dolne nie będzie większe
niż górne, co przeczyłoby roli tych pól. Użycie w ograniczeniu CHECK wyrażenia od-
wołującego się do więcej niż jednego pola jest możliwe dopiero wtedy, kiedy wystę-
puje ono w klauzuli CONSTRAINT. Próba skonstruowania takiego wyrażenia bezpośred-
nio przy definicji kolumny skończy się błędem podczas wykonywania zapytania.
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) ,
CONSTRAINT spr CHECK(m_v < mm_v)
);

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

Znacznie bezpieczniejsze jest pozostawienie stanu domyślnego, któremu na poziomie


jawnym odpowiada opcja SET NULL. Powoduje ona, że przy usuwaniu pola z tabeli
nadrzędnej w odpowiednie pola tabeli połączonej z nią kluczem obcym wpisywane są
wartości NULL. Jest to możliwe tylko wtedy, kiedy nie zablokowano takiej opcji przez
jawne wstawienie ograniczenia NOT NULL dla kolumny klucza obcego.
DROP TABLE Osoby;
CREATE TABLE Osoby
(nr number(3) PRIMARY KEY,
IdDzialu number(3),
CONSTRAINT fk FOREIGN KEY(IdDzialu)
REFERENCES Dzialy(IdDzialu) ON DELETE SET NULL,
Nazwisko varchar2(15) NOT NULL,
Imie varchar2(15) DEFAULT 'Brak'
);

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)
);

W przypadku łączenia wielu ograniczeń w definicji jednego pola należy pamiętać, że


zawsze pierwszym musi być ograniczenie DEFAULT (jeśli jest), natomiast kolejność po-
zostałych jest już dowolna. Przypadek ten pokazano w zapytaniu tworzącym tabelę Osoby,
w którym do zdefiniowania pola Imie zastosowano określenie wartości domyślnej,
blokadę wpisywania wartości NULL, wymóg unikalności oraz sprawdzenie wartości.
Można zauważyć, że wyrażenie użyte w ograniczeniu CHECK nie powoduje żadnych
dodatkowych konsekwencji, a jest jedynie zabiegiem formalnym ilustrującym omawiany
problem.
DROP TABLE Osoby;
CREATE TABLE Osoby
(nr number(3) PRIMARY KEY,
IdDzialu number(3) NOT NULL,
Rozdział 4. ♦ Zapytania tworzące tabele 109

CONSTRAINT fk FOREIGN KEY(IdDzialu)


REFERENCES Dzialy(IdDzialu) ON DELETE CASCADE,
Nazwisko varchar2(15) NOT NULL,
Imie varchar2(15) DEFAULT 'Brak' NOT NULL UNIQUE CHECK(Imie LIKE '%')
);

Jednym z ciekawych i użytecznych rozwiązań jest możliwość odwołania się w defini-


cji wartości domyślnej do wbudowanych funkcji serwera bazy danych. Przykładem jest
nieudokumentowana możliwość zastosowania dla DEFAULT funkcji daty systemowej.
DROP TABLE Nowa;
CREATE TABLE Nowa
(nr number(3) PRIMARY KEY,
Data date DEFAULT Sysdate
);

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)
);

W definicji ograniczenia sprawdzającego CHECK nie można również odwoływać się do


funkcji użytkownika oraz stosować podzapytań.

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.

Zapytania modyfikujące tabelę


W poprzednim podrozdziale tworzyliśmy tabele od podstaw, jednak dość często pod-
czas eksploatacji bazy pojawia się konieczność modyfikacji struktury istniejących już
tabel. Z punktu widzenia tego, co już wiemy, możliwe byłoby stworzenie nowej tabeli
o pożądanej strukturze, a następnie przepisanie do niej istniejących już danych. Ko-
lejnym krokiem musiałoby być usunięcie starej tabeli oraz zmiana nazwy nowej. Ta-
kie działanie już na pierwszy rzut oka wydaje się mało ekonomiczne (choć niekiedy jest
wykorzystywane) i może zostać zastąpione zapytaniem modyfikującym tabelę ALTER
TABLE.

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

Innym zastosowaniem opcji MODIFY jest możliwość przypisania do istniejącej kolumny


ograniczenia zezwalającego lub blokującego wpisywanie do niej wartości NULL. W tym
drugim przypadku, ponieważ następuje walidacja danych (sprawdzenie ich zgodności
z ograniczeniem docelowym), jeśli którakolwiek z pozycji w tej kolumnie zawiera
wartość NULL, wygenerowany zostanie komunikat o błędzie i zmiana ograniczenia nie
zostanie wykonana.
ALTER TABLE Dzialy
MODIFY kod NOT NULL;

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;

Jeśli stworzyliśmy tabelę, w której zdefiniowano ograniczenie, dajmy na to klucz pod-


stawowy, tak jak w przykładzie:
DROP TABLE Nowa;
CREATE table Nowa
(nr number(3),
Nazwisko varchar2(15) NULL,
CONSTRAINT kl PRIMARY KEY(nr));

to wykonanie polecenia usuwającego kolumnę z ograniczeniem:


ALTER TABLE Nowa
DROP COLUMN nr;

spowoduje jej usunięcie pomimo zdefiniowanego ograniczenia. Wyjątek stanowi sy-


tuacja, kiedy do usuwanej kolumny odwołują się w definicjach kluczy obcych inne
tabele, co w przypadku kolumn klucza podstawowego jest częste. Aby kolumnę taką
skasować, należy albo usunąć wszystkie ograniczenia kluczy obcych ze wszystkich tabel,
które się do niej odwołują, a następnie ponowić próbę jej usunięcia, albo zastosować
opcję CASCADE CONSTRAINTS, która wykona to samo automatycznie.
112 Część I ♦ Oracle SQL

ALTER TABLE Dzialy


DROP COLUMN IdDzialu
CASCADE CONSTRAINTS;

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);

Dopiero wykonanie polecenia:


ALTER TABLE Dzialy
DROP UNUSED COLUMNS;

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.

W przypadku usuwania kolumn z bardzo dużych wolumenów danych wygodnym jest


używanie wymuszenia punktu kontrolnego poprzez dyrektywę CHECKPOINT. Zastoso-
wane w przykładzie rozwiązanie CHECKPOINT 1000 wymusza wystąpienie punktu kon-
trolnego co 1000 przetworzonych wierszy.
ALTER TABLE test
DROP COLUMN idTest
CASCADE CONSTRAINTS
CHECKPOINT 1000;

Jest to wskazane, kiedy liczymy się z możliwością wystąpienia awarii. W momencie


usuwania kolumn status tabeli jest ustawiony na INVALID i podczas awarii instancji zo-
staje on zachowany. Po jej usunięciu operacja może zostać dokończona poleceniem:
ALTER TABLE test
DROP COLUMNS CONTINUE;

Kolumna jest wtedy usuwana od stanu wynikającego z ostatnio zapisanego punktu


kontrolnego. Podobną metodę można zastosować podczas blokowania kolumn prze-
znaczonych do usunięcia. Oczywiście wymuszenie punktów kontrolnych nastąpi wtedy
dopiero w momencie ich fizycznego usuwania.
ALTER TABLE test
SET UNUSED COLUMN IdTest
CASCADE CONSTRAINTS
CHECKPOINT 1000;

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

Znalezienie tabel posiadających kolumny zaznaczone do usunięcia jest natomiast moż-


liwe dzięki wykonaniu zapytania:
SELECT * FROM USER_UNUSED_COL_TABS.

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.

W stosunku do usuwanych kolumn istnieją również innego rodzaju obostrzenia, a mia-


nowicie nie można usunąć:
 kolumny z tabeli zagnieżdżonej;
 kolumny, która wchodzi w skład złożonego klucza obcego;
 kolumny stanowiącej wskaźnik partycjonowania;
 wszystkich kolumn z tabeli.

Nie można również usunąć kolumny z tabeli słownika danych.

Ważnym elementem modyfikacji istniejących tabel jest dodawanie do nich ograniczeń


przy użyciu opcji ADD CONSTRAINT, po której następuje definicja ograniczenia, np.:
ALTER Table Nowa
ADD CONSTRAINT kl PRIMARY KEY (nr);

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;

Zdecydowanie najczęściej spotykamy się z sytuacją, kiedy dodawanym lub usuwanym


ograniczeniem jest klucz obcy.
ALTER TABLE Zarobki
ADD CONSTRAINT Fk1 FOREIGN KEY(IdOsoby)
REFERENCES Osoby(IdOsoby) ON DELETE CASCADE;

Dopuszczalne są oczywiście wszystkie opcje dostępne w definicji FOREIGN KEY.


ALTER TABLE Zarobki
ADD CONSTRAINT Fk1 FOREIGN KEY(IdOsoby)
REFERENCES Osoby(IdOsoby) ON DELETE SET NULL;

Dodawanie ograniczeń po utworzeniu tabeli może być spowodowane wzajemnym od-


woływaniem się do pól dwóch lub większej liczby tabel, co prowadzi do komplikacji
w ustalaniu kolejności ich tworzenia. Na szczęście ten przypadek w praktyce występuje
stosunkowo rzadko. Może być to spowodowane chęcią zachowania czystości kodu,
114 Część I ♦ Oracle SQL

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.

Ciekawym zapytaniem jest polecenie pozwalające na zmianę nazwy obiektu, w tym


również tabeli.
RENAME Nowa TO Inna

W przypadku modyfikacji nazwy tabeli zmieniane są również na nową nazwę odwo-


łania do niej w definicjach kluczy obcych tabel podrzędnych. Inne odwołania do ta-
beli o zmienionej w ten sposób nazwie, w szczególności te w procedurach i funkcjach
PL/SQL, muszą zostać poprawione ręcznie. Podobnie jak nazwa całej tabeli, zmieniane
mogą być nazwy jej dowolnych kolumn.
ALTER TABLE Dzialy
RENAME COLUMN Nazwa TO Dzial;

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

Tabela 4.2. Sposób kodowania informacji w identyfikatorze wiersza


AAAMVh AAK AAAAAy AAA
Identyfikator obiektu (tabeli) Względny numer pliku Numer bloku Numer wiersza
w ramach pliku w tabeli

ALTER TABLE ttt


PCTFREE 10
PCTUSED 40
STORAGE (
NEXT 100K
MINEXTENTS 1
MAXEXTENTS 5
PCTINCREASE 0);

W zapytaniu zmieniającym alokację poszczególne opcje oznaczają:


 PCTFREE — procent utrzymywanej wolnej powierzchni bloku;
 PCTUSED — procentowy stopień zajętości bloku, od którego jest on już
uznawany za zajęty;
 NEXT — rozmiar następnego przydzielanego ekstentu;
 MINEXTENTS — minimalna liczba ekstentów przydzielonych tabeli;
 MAXEXTENTS — maksymalna liczba ekstentów, które można przypisać tabeli
podczas jej rozrastania się;
 PCTINCREASE — procent, o jaki jest powiększany każdy nowy ekstent,
począwszy od trzeciego.

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;

Najczęściej przeniesienie odbywa się z domyślnej przestrzeni tabel do innej należącej


do użytkownika.

Tabela tymczasowa jest tworzona według ogólnych zasad obowiązujących podczas


tworzenia zwykłych tabel — jedynie jej nazwa jest poprzedzona słowami kluczowymi
GLOBAL TEMPORARY. Ponadto dostępne są dwie opcje określające sposób zachowania pod-
czas zatwierdzania transakcji.
DROP TABLE temp;
CREATE GLOBAL TEMPORARY TABLE temp
(nr NUMBER,
opis VARCHAR2(11)
)
ON COMMIT PRESERVE ROWS;

Wbrew temu, co sądzimy o obiektach tymczasowych, na platformie Oracel tabela tym-


czasowa jest „trwała” (istnieje od chwili jej utworzenia, aż do momentu jawnego usu-
nięcia jej poleceniem DROP TABLE), natomiast „czas życia” jej zawartości jest zależny
od dyrektywy ON COMMIT — wartością domyślną jest ON COMMIT DELETE ROWS. W sta-
nie tym po każdym zatwierdzeniu transakcji poleceniem COMMIT dane z tabeli są usu-
wane. Przy zastosowaniu opcji PRESERVE ROWS są one natomiast przechowywane dalej.
Bez względu na tę dyrektywę, zawartość tabeli jest jednak zawsze usuwana z chwilą
zamknięcia sesji. W czasie jej trwania użytkownik „widzi” tylko dane zapisane w jej
trakcie. Nie ma możliwości przekazania zawartości tabeli do innych sesji nawet wtedy,
kiedy zostały one otwarte przez tego samego użytkownika. Tabela tymczasowa, jak
każda inna, powstaje w schemacie użytkownika, który ją stworzył.

Dotychczas, mówiąc o ograniczeniach, więzach integralności, milcząco zakładaliśmy,


że ich sprawdzenie, walidacja, następuje natychmiast po wprowadzeniu zmiany w da-
nych. Rzeczywiście, w stanie domyślnym więzy CONSTRAINTS są typu nieodraczalnego
NON DEFERRABLE, czyli Oracle sprawdza zgodność od razu, a kiedy naruszone zostaną
ograniczenia, zgłasza błąd przetwarzania CONSTRAINT VIOLATION. Jednakże możliwe
jest przestawienie tych więzów w stan sprawdzania odroczonego DEFERRABLE, a wtedy
Oracle weryfikuje zgodność z ograniczeniami w momencie zatwierdzania transakcji
(po wykonaniu polecenia COMMIT), tzn. na jej zakończenie. Jeśli stworzymy prostą ta-
belę z polem klucza podstawowego, a następnie spróbujemy do niej wpisać dwa rekordy
o takiej samej jego wartości:
CREATE TABLE Osoba
(Id number, imie varchar2(10),
CONSTRAINT pk PRIMARY KEY (Id));
INSERT INTO Osoba VALUES(1,'Jan');
INSERT INTO Osoba VALUES(1,'Karol');

to tuż po wykonaniu zapytania wstawiającego drugi z rekordów otrzymamy komunikat


o błędzie: ORA-00001: naruszono więzy unikatowe (TESTOWY.PK). Informacja w na-
wiasie zawiera kwalifikowaną nazwę więzów, składającą się z nazw schematu i ogra-
Rozdział 4. ♦ Zapytania tworzące tabele 117

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;

Niestety, również ta próba kończy się niepowodzeniem i serwer zwraca komunikat


o błędzie: ORA-02447: nie można odroczyć więzów, które nie dają możliwości odra-
czania. Dzieje się tak dlatego, że w stanie domyślnym więzy, w tym i nasz klucz pod-
stawowy, nie są odraczalne (DEFERRABLE), a dla tego typu ograniczeń nie możemy od-
roczyć ich walidacji. Aby było to możliwe, podczas tworzenia tabeli ograniczenie musi
zostać stworzone jako odraczalne (DEFERRABLE), tak jak to pokazano w przykładzie
poniżej.
CREATE TABLE Osoba
(Id number, imie varchar2(10),
CONSTRAINT pk PRIMARY KEY (Id) DEFERRABLE);
INSERT INTO Osoba VALUES(1,'Jan');
INSERT INTO Osoba VALUES(1,'Karol');

Pomimo to w przypadku wstawiania powtarzających się wartości klucza Oracle w dal-


szym ciągu natychmiast zwraca błąd: ORA-00001: naruszono więzy unikatowe (TE-
STOWY.PK), co świadczy o tym, że także dla więzów odraczalnych stanem domyślnym
jest walidacja natychmiastowa. Teraz możemy jednak z powodzeniem zmienić ją 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.

Walidację więzów powoduje dopiero wykonanie (również po długim czasie) polecenia


zatwierdzającego transakcję:
COMMIT;

Dlatego dopiero teraz Oracle wyświetla komunikat o błędzie:


 ORA-02091: transakcja została wycofana;
 ORA-00001: naruszono więzy unikatowe (TESTOWY.PK1).

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;

Zmienione zostaną tylko te więzy, dla których ustawiono możliwość odraczania


DEFERRABLE. Jeśli istnieje ograniczenie z nieustawioną opcją odraczania DEFERRABLE,
nie są generowane komunikaty o błędach. Teraz każdy ze wstawianych wierszy, który
nie spełnia ograniczeń, generuje błąd i tylko ten wiersz jest wycofywany. Więzy od-
roczone DEFERRABLE są użyteczne wtedy, gdy chcemy mieć pewność, że na skutek błędu
walidacji ograniczeń ma zostać wycofana cała transakcja, a nie tylko wiersze, które nie
spełniają ograniczenia. Najczęściej ma to miejsce podczas masowego kopiowania da-
nych. Dodatkowo walidacja wszystkich rekordów jednocześnie, a nie każdego z osobna,
powoduje poprawę wydajności takich operacji. Najczęściej również w przypadku prze-
pisywania dużych wolumenów danych, na przykład związanego z migracją, mamy praw-
dopodobieństwo bliskie pewności, że wprowadzane dane są poprawne, a zależy nam
na dużej wydajności procesu. Nieco gorzej sprawa wygląda podczas procesu integra-
cji baz danych, gdzie ryzyko błędu, np. wynikającego z dublowania się rekordów, może
być większe. W takim przypadku wskazane jest wcześniejsze filtrowanie łączonych da-
nych. Przełączanie trybu pracy więzów (ograniczeń) może być również wykonywane
nie na poziomie definicji tabeli, ale tylko w trakcie trwania pojedynczej sesji, w której
wskazane jest wykonanie odroczenia walidacji.
ALTER SESSION
SET CONSTRAINTS = {IMMEDIATE |DEFERRED|DEFAULT};

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) ;

Podobnie można zastosować opcje dla ograniczenia ze zdefiniowaną nazwą użytkownika.


ALTER TABLE Dzialy
ADD CONSTRAINT Uni UNIQUE(Nazwa) DISABLE VALIDATE;

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

ALTER TABLE Dzialy


CONSTRAINT Uni ENABLE VALIDATE;
ALTER TABLE Osoby DISABLE PRIMARY KEY CASCADE;

Zbiorcze podsumowanie wpływu przełączania więzów w różne kombinacje stanów


blokady i walidacji zawiera tabela 4.3.

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

Parametr CACHE informuje o liczbie przechowywanych w pamięci kolejnych wartości


generowanych przez sekwencję. Ma to znaczenie przy dużym zrównolegleniu opera-
cji sięgających do tej sekwencji. Zwiększenie wartości CACHE przyspiesza w takim
przypadku przetwarzanie — oczywiście kosztem zasobów pamięci. Wartość CACHE
(dla parametru NOCYCLE) nie może być większa od liczby wszystkich możliwych do
wygenerowania w obrębie jednego cyklu wartości. W przypadku zastosowania opcji
NOORDER generowanie ściśle kolejnych wartości dla następujących po sobie wywołań nie
jest zagwarantowane, chociaż z reguły tak się dzieje. Odstępstwo od tej zasady może
wynikać z generowania wartości w kilku równoległych sesjach.

Prześledźmy użycie sekwencji do zasilania w tabeli pola numerycznego. W zapytaniu


wstawiającym dane zastosowana została metoda sekwencji NEXVAL, która powoduje
wygenerowanie kolejnej liczby zgodnie z definicją użytej sekwencji.
DROP TABLE Nowa;
CREATE TABLE Nowa
(nr number(3),
Nazwisko varchar2(15));
INSERT INTO Nowa VALUES(Licznik.NEXTVAL, 'NOWAK');
INSERT INTO Nowa Values(Licznik.Nextval, 'KOWAL');

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;

Metoda dynamicznego generowania wartości pól, bez konieczności odwoływania się


do sekwencji w zapytaniu wstawiającym wiersze, zostanie pokazana w rozdziale po-
święconym procedurom wyzwalanym — triggerom.
Rozdział 4. ♦ Zapytania tworzące tabele 121

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;

Zamiast wykonywać dwa polecenia — najpierw usuwające, a następnie tworzące ta-


belę — możemy skorzystać z konstrukcji CREATE OR REPLACE. W przypadku kiedy
obiekt docelowy nie istnieje, jest on tworzony, natomiast jeśli istnieje perspektywa
o tej nazwie, nowa definicja jest nadpisywana na starą. Z punktu widzenia operatora
żaden z tych stanów nie jest wyróżniany specjalnym komunikatem; nie są one rozróż-
nialne.
CREATE OR REPLACE VIEW mlodzi
AS
SELECT Idosoby, RokUrodz FROM Osoby
WHERE RokUrodz > 1970;
/
INSERT INTO mlodzi VALUES(11, 1980);
INSERT INTO mlodzi VALUES(12, 1960);

SELECT * FROM mlodzi;


SELECT * FROM Osoby;

UPDATE mlodzi SET RokUrodz = 1977 WHERE Idosoby = 11;

SELECT * FROM mlodzi;


SELECT * FROM Osoby;

Ponieważ perspektywa jest dynamicznym obrazem danych zgromadzonych w tabeli,


można się do niej odwoływać tak jak do każdej zwykłej tabeli, wykorzystując wszelkie
elementy składniowe zapytania wybierającego. W niektórych przypadkach, kiedy mamy
do czynienia z prostą perspektywą, możliwe jest również wykonywanie zapytań mo-
dyfikujących dane: wstawiających wiersze, aktualizujących pola czy usuwających re-
kordy (INSERT, UPDATE, DELETE). Prosta perspektywa to taka, w której definicji wystę-
puje zapytanie wybierające korzystające z danych zawartych w pojedynczej tabeli. Nie
może ono zawierać żadnych wyrażeń czy funkcji, włączając w to oczywiście funkcje
agregujące, natomiast dopuszczalne jest filtrowanie rekordów przy użyciu klauzuli
WHERE. Prostym widokiem będzie również taki, który odwołuje się do innego prostego
122 Część I ♦ Oracle SQL

widoku. Przykład perspektywy spełniającej powyższe kryteria przedstawiono w po-


staci kodu. Należy zwrócić uwagę, że za pomocą zapytania INSERT wstawiono dwa re-
kordy: jeden, dla którego wyrażenie w klauzuli WHERE definicji perspektywy jest praw-
dziwe, i drugi, dla którego jest ono fałszywe. Przy zastosowanej uprzednio definicji
perspektywy oba rekordy zostaną jednak wstawione do tabeli źródłowej. Oczywiście
zapytanie wybierające opierające się na perspektywie nie wyświetli drugiego z nich,
ale zapytanie do tabeli źródłowej Osoby potwierdzi fakt wstawienia rekordu. Taki sam
efekt uzyskamy, kiedy, wykonując zapytanie modyfikujące, zmienimy pola znajdujące
się w klauzuli WHERE w taki sposób, że zawarte w niej wyrażenie będzie nieprawdziwe.
Dopiero zastosowanie podczas definiowania perspektywy dodatkowej opcji WITH CHECK
OPTION spowoduje, że drugi spośród rekordów nie zostanie wpisany do tabeli, a silnik
bazy danych wyświetli komunikat o naruszeniu opcji CHECK. Tak samo stałoby się w przy-
padku modyfikacji danych, kiedy wartość zmodyfikowanego rekordu nie byłaby zgodna
z warunkiem zawartym w klauzuli WHERE.
CREATE OR REPLACE VIEW mlodzi
AS
SELECT Idosoby, RokUrodz FROM Osoby
WHERE RokUrodz > 1970
WITH CHECK OPTION;
/
INSERT INTO mlodzi VALUES(11, 1980);
INSERT INTO mlodzi VALUES(12, 1960);

SELECT * FROM mlodzi;


SELECT * FROM Osoby;

UPDATE mlodzi SET RokUrodz = 1977 WHERE Idosoby = 11;

SELECT * FROM mlodzi;


SELECT * FROM Osoby;

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;

Po wykonaniu takiego polecenia zgodność danych z klauzulą WHERE przestanie być


sprawdzana. Z praktycznego punktu widzenia taka modyfikacja perspektywy nie wy-
daje się szczególnie przydatna. Kolejną opcją, z którą może zostać stworzona perspek-
tywa, jest WITH READ ONLY. Powoduje ona zablokowanie możliwości wykonywania ja-
Rozdział 4. ♦ Zapytania tworzące tabele 123

kichkolwiek zapytań modyfikujących zawartość tabeli. Wykonanie zapytania INSERT,


UPDATE lub DELETE odnoszącego się do tej perspektywy, zakończy się więc komunikatem
o błędzie.
CREATE OR REPLACE VIEW mlodzi
AS
SELECT Idosoby, RokUrodz FROM Osoby
WHERE RokUrodz > 1970
WITH READ ONLY;
/
INSERT INTO mlodzi VALUES(11, 1980);
INSERT INTO mlodzi VALUES(12, 1960);

SELECT * FROM mlodzi;


SELECT * FROM Osoby;

Bardzo ważnym problemem wynikającym z wykonywania zapytania wstawiającego


wiersze za pośrednictwem perspektywy są pomijane pola tabeli źródłowej w jej defi-
nicji. Podobnie jak przy wykonywaniu zapytania wstawiającego rekordy bezpośrednio
do tabeli, ale definiującego tylko wybrane kolumny, pola te muszą spełniać takie same
kryteria, czyli:
 pozwalać na przyjęcie wartości NULL;
 posiadać zdefiniowaną wartość domyślną;
 mieć zdefiniowaną metodę automatycznej inkrementacji.

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

Tabela 4.4. Zakres działania perspektyw na przykładzie perspektyw systemowych *_OBJECTS


DBA_objects (wszystkie obiekty)

ALL_objects (obiekty dostępne dla użytkownika)

USER_objects (obiekty będące własnością użytkownika)

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.

Podobna struktura trójstopniowego zasięgu perspektyw dotyczy przeważającej ich liczby.


Aby zapoznać się z dostępnymi perspektywami systemowymi, możemy sprawdzić za-
wartość słownika.
SELECT * FROM DICTIONARY;

Są w nim nazwy perspektyw, a komentarze im towarzyszące opisują w sposób skrócony


ich zawartość (tabela 4.5).

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
••• •••

Informacje o kolumnach składających się na definicje perspektyw systemowych za-


warte są w kolejnym słowniku (tabela 4.6):
SELECT * FROM DICT_COLUMNS;

Tabela 4.6. Pierwsze rekordy słownika zawierającego informacje o kolumnach perspektyw


systemowych — komentarze pozostawiono w pisowni oryginalnej
TABLE_NAME COLUMN_NAME COMMENTS
ALL_ALL_TABLES TABLESPACE_NAME Name of the tablespace containing the table
ALL_ALL_TABLES CLUSTER_NAME Name of the cluster, if any, to which the table belongs
ALL_ALL_TABLES IOT_NAME Name of the index-only table, if any, to which the overflow
or mapping table entry belongs
••• ••• •••
Rozdział 4. ♦ Zapytania tworzące tabele 125

Opierając się na danych zawartych w słowniku, możemy poprzez wykonanie przed-


stawionych zapytań uzyskać informacje o ważniejszych elementach stworzonych przez
użytkownika.
SELECT * FROM USER_TABLES;
SELECT * FROM USER_INDEXES;
SELECT * FROM USER_TAB_COLUMNS;
SELECT * FROM USER_CONSTRAINTS;
SELECT * FROM USER_VIEWS;
SELECT * FROM USER_PROCEDURES;
SELECT * FROM USER_TRIGGERS;

Wykonanie skryptu spowoduje wyświetlenie informacji o stworzonych przez użyt-


kownika tabelach, występujących w nich indeksach, kolumnach, z których się skła-
dają, ograniczeniach zdefiniowanych dla każdej z tabel, stworzonych perspektywach,
procedurach składowanych (ale również funkcjach i pakietach, które w perspektywie
USER_PROCEDURES są traktowane równoważnie) oraz procedurach wyzwalanych. Jak łatwo
zaobserwować, nazewnictwo perspektyw jest intuicyjne, zwłaszcza tam, gdzie nie są
stosowane skróty. Podobnie jak w przypadku perspektyw *_OBJECTS, stosowany jest trój-
poziomowy zakres wyświetlanych informacji (tabela 4.7).

Tabela 4.7. Zakres działania perspektyw — xxx symbolizuje element nazwy wskazujący na przeznaczenie
perspektywy
DBA_xxx (wszystkie obiekty)

ALL_xxx (obiekty dostępne dla użytkownika)

USER_xxx (obiekty będące własnością użytkownika)

Nie wszystkie perspektywy mają swoje odpowiedniki na wszystkich poziomach. Przy-


kładem może być perspektywa DBA_DATA_FILES, która nie ma swoich odpowiedników
na poziomach ALL_ oraz USER_ (tabela 4.8). Zawiera ona bowiem informację o nazwach
plików bazy danych, która nie powinna być dostępna dla każdego z użytkowników.
SELECT * FROM DBA_DATA_FILES;

Tabela 4.8. Wybrane perspektywy systemowe — wykaz dla poziomu użytkownika


— obiekty, których dany użytkownik jest właścicielem (twórcą)
TABLE_NAME COMMENTS
USER_ALL_TABLES Opis obiektów oraz tabel
USER_CATALOG Informacje o tabelach, perspektywach, synonimach i sekwencjach
USER_CLUSTERS Opis klastrów (partycji)
USER_CLUSTER_HASH_EXPRESSIONS Funkcje haszujące dla klastrów (partycji)
USER_CLU_COLUMNS Mapowanie kolumn tabeli na kolumny klastra (partycji)
USER_COL_TYPES Opis typów kolekcji
USER_COL_COMMENTS Komentarze stworzone dla kolumn tabel i perspektyw
126 Część I ♦ Oracle SQL

Tabela 4.8. Wybrane perspektywy systemowe — wykaz dla poziomu użytkownika


— obiekty, których dany użytkownik jest właścicielem (twórcą) — ciąg dalszy
TABLE_NAME COMMENTS
USER_COL_PRIVS Prawa do kolumn nadane przez użytkownika lub mu przyznane
(grantor lub grantee)
USER_COL_PRIVS_MADE Wszystkie prawa do kolumn obiektów użytkownika
USER_COL_PRIVS_RECD Prawa do kolumn nadane użytkownikowi (grantee)
USER_CONSTRAINTS Ograniczenia (więzy) stworzone na tabelach
USER_CONS_COLUMNS Wykaz kolumn wykorzystanych w ograniczeniach (więzach)
USER_DB_LINKS Połączenia do baz danych (przetwarzanie rozproszone)
USER_DEPENDENCIES Zależności między obiektami użytkownika
USER_ERRORS Bieżące błędy przetwarzania procedur, funkcji składowanych
USER_FREE_SPACE Wolne ekstenty w przestrzeni tabel dostępne dla użytkownika
USER_INDEXES Informacja o indeksach
USER_INDEXTYPES Typy indeksów użytkownika
USER_INDEXTYPE_COMMENTS Komentarze stworzone do indeksów
USER_IND_EXPRESSIONS Indeksy oparte na wyrażeniach
USER_IND_PARTITIONS Indeksy partycjonowane
USER_IND_SUBPARTITIONS Subpartycje indeksów partycjonowanych
USER_INTERNAL_TRIGGERS Procedury wyzwalane dla tabel i perspektyw użytkownika
USER_JOIN_IND_COLUMNS Indeksy wykorzystywane w realizacji złączeń
USER_LIBRARIES Biblioteki użytkownika
USER_LOBS Opis zastosowanych typów binarnych (LOB) w tabelach
USER_NESTED_TABLES Opis tabel zagnieżdżonych w tabelach użytkownika
USER_OBJECTS Obiekty użytkownika
USER_OBJECT_SIZE Rozmiar w bajtach obiektów programowalnych PL/SQL
USER_OBJECT_TABLES Opis tabel obiektowych
USER_OBJ_COLATTRS Opis kolumn zawartych w tabelach obiektowych
USER_PASSWORD_LIMITS Ograniczenia dla hasła użytkownika
USER_PENDING_CONV_TABLES Tabele, w których występują typy danych ze starszych wersji Oracle
(wskazanie na konieczność konwersji)
USER_PROCEDURES Procedury składowane, funkcje i pakiety użytkownika
USER_ROLE_PRIVS Role przypisane do użytkownika
USER_RULES Role, których użytkownik jest właścicielem i które ma prawo przypisać
USER_SEGMENTS Pamięć przydzielona na wszystkie segmenty bazy
USER_SEQUENCES Opis sekwencji użytkownika
USER_SOURCE Źródła przechowywanych procedur, funkcji i pakietów
USER_STORED_SETTINGS Ustawienia parametrów dla obiektów użytkownika
USER_SUMMARIES Opis podsumowań stworzonych przez użytkownika
Rozdział 4. ♦ Zapytania tworzące tabele 127

Tabela 4.8. Wybrane perspektywy systemowe — wykaz dla poziomu użytkownika


— obiekty, których dany użytkownik jest właścicielem (twórcą) — ciąg dalszy
TABLE_NAME COMMENTS
USER_SYNONYMS Prywatne synonimy użytkownika
USER_SYS_PRIVS Przywileje systemowe przypisane do użytkownika
USER_TABLES Opis tabel
USER_TABLESPACES Opis dostępnych przestrzeni tabel
USER_TAB_COLS Kolumny w tabelach, perspektywach i klastrach użytkownika
USER_TAB_COLUMNS Kolumny w tabelach, perspektywach i klastrach użytkownika
USER_TAB_COL_STATISTICS Statystyka dla kolumn w tabelach, perspektywach i klastrach
użytkownika
USER_TAB_COMMENTS Komentarze przypisane do tabel i perspektyw
USER_TAB_HISTOGRAMS Histogramy dla kolumn w tabelach
USER_TAB_PARTITIONS Opis partycji tabel
USER_TAB_PRIVS Prawa do obiektów nadanych użytkownikowi lub nadanych przez niego
USER_TAB_PRIVS_MADE Wszystkie prawa do obiektów posiadane przez użytkownika
USER_TAB_PRIVS_RECD Prawa do obiektów przypisane do użytkownika (grantee)
USER_TAB_SUBPARTITIONS Opis subpartycji tabel
USER_TRIGGERS Procedury wyzwalane
USER_TRIGGER_COLS Kolumny wykorzystywane przez procedury wyzwalane
USER_TS_QUOTAS Kwoty na przestrzeniach tabel
USER_TYPES Opis typów użytkownika
USER_TYPE_ATTRS Opis atrybutów typów użytkownika
USER_TYPE_METHODS Opis metod typów użytkownika
USER_TYPE_VERSIONS Opis wersji typów użytkownika
USER_UNUSED_COL_TABS Tabele z kolumnami zaznaczonymi do usunięcia (unused columns)
USER_UPDATABLE_COLUMNS Opis modyfikowalnych kolumn
USER_USERS Informacje o bieżącym użytkowniku
USER_USTATS Statystyki użycia tabel i indeksów
USER_VARRAYS Opis zmiennych typu varrays użytych w definicjach tabel
USER_VIEWS Opis perspektyw użytkownika
USER_WORKSPACES Opis przestrzeni roboczych użytkownika

Część spośród perspektyw systemowych wyłamuje się z ogólnego trójpoziomowego


zasięgu i schematu nazw. Należą do nich tzw. dynamiczne perspektywy wydajności
(dynamic performance views). Większość z nich posiada nazwy zaczynające się od V$.
Domyślnie dynamiczne perspektywy wydajności dostępne są dla użytkownika SYS
oraz dla wszystkich użytkowników o uprawnieniu SYSDBA lub mających uprawnienie
SELECT ANY TABLE. Reprezentatywnym przykładem może być perspektywa o nazwie
V$FIXED_TABLE, która wyświetla nazwy wszystkich dynamicznych perspektyw wydaj-
ności, pełni zatem rolę słownika dla perspektyw tej klasy. Innym przykładem może
128 Część I ♦ Oracle SQL

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.

Tabela 4.9. Nazwy początkowych kolumn w perspektywie V$INSTANCE


INSTANCE_NUMBER
INSTANCE_NAME
HOST_NAME
VERSION
STARTUP_TIME
STATUS
...

DESC V$INSTANCE;

Kolejną przykładową perspektywą wydajności jest V$VERSION, prezentująca w postaci


pojedynczej komórki o nazwie BANNER opis wersji zainstalowanego serwera Oracle
(tabela 4.10).

Tabela 4.10. Przykład skutku odwołania się do perspektywy V$VERSION


BANNER
Oracle Database 11g Enterprise Edition Release 11.1.0.6.0 - Production
PL/SQL Release 11.1.0.6.0 – Production
CORE11.1.0.6.0Production
TNS for 32-bit Windows: Version 11.1.0.6.0 – Production
NLSRTL Version 11.1.0.6.0 - Production

SELECT * FROM V$VERSION;

Kolejny przykład to perspektywa przechowująca informacje o sposobie realizacji po-


łączenia sesji z serwerem Oracle (tabela 4.11).

Tabela 4.11. Nazwy początkowych kolumn w perspektywie V$SESSION_CONNECT_INFO


SID
AUTHENTICATION_TYPE
OSUSER
NETWORK_SERVICE_BANNER
....

DESC V$SESSION_CONNECT_INFO;

Z punktu widzenia administracji serwerem istotne są perspektywy opisujące alokację


plików składających się na jego instancje: plików kontrolnych (tabela 4.12), plików
dziennika powtórzeń (logi), plików śladu etc. Przykładowo przedstawiono dwie spo-
śród nich.
Rozdział 4. ♦ Zapytania tworzące tabele 129

Tabela 4.12. Przykład rezultatu odwołania się do perspektywy V$CONTROLFILE


NAME
C:\ORACLE\ORADATA\ORACLE\CONTROL01.CTL
C:\ORACLE\ORADATA\ORACLE\CONTROL02.CTL
C:\ORACLE\ORADATA\ORACLE\CONTROL03.CTL

SELECT * FROM V$CONTROLFILE;


SELECT * FROM V$LOGFILE;

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).

Tabela 4.13. Nazwy kolumn w perspektywie DBA_FREE_SPACE


TABLESPACE_NAME
FILE_ID
BLOCK_ID
BYTES
BLOCKS
RELATIVE_FNO

DESC DBA_FREE_SPACE;

Istotne informacje niesie również perspektywa DBA_SEGMENTS, której odpytanie pozwala


uzyskać dane dotyczące sposobu alokacji obiektów (segmentów) bazy. Ze względu na
ogrom informacji dostępnych w tej perspektywie, w prezentowanym zapytaniu ogra-
niczono się do obiektów jednego użytkownika o nazwie TESTOWY, i to takich, których
jest on właścicielem (twórcą) — tabela 4.14.

Tabela 4.14. Przykład rezultatu odwołania się do perspektywy DBA_SEGMENTS


(przedstawione zostały początkowe kolumny i przykładowe wiersze)
OWNER SEGMENT_NAME PARTITION_NAME SEGMENT_TYPE TABLESPACE_NAME …
TESTOWY DZIALY TABLE TESTOWY ...
TESTOWY SYS_C007159 INDEX TESTOWY …
TESTOWY OSOBY TABLE TESTOWY …
... ... ... ... ... ...

SELECT * FROM DBA_SEGMENTS


WHERE TABLESPACE_NAME = 'TESTOWY'
AND OWNER = 'TESTOWY';
130 Część I ♦ Oracle SQL

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.

Tabela 4.15. Przykład skutku odwołania się do perspektywy DBA_USERS


(przedstawione zostały przykładowe wiersze)
USERNAME PASSWORD ACCOUNT_STATUS DEFAULT_TABLESPACE CREATED
SYS C662D4B00A46720B OPEN SYSTEM 05/08/30
SYSTEM 60428D68C19A5E81 OPEN SYSTEM 05/08/30
TESTOWY 7B968F892F827157 OPEN TESTOWY 08/03/10
FLOWS_FILES 428ECAC9CFE30D5 LOCKED TESTOWY 08/03/10
ANONYMOUS anonymous EXPIRED & LOCKED SYSAUX 05/08/30
SCOTT F894844C34402B67 EXPIRED & LOCKED USERS 05/08/30

SELECT USERNAME, PASSW,ORD,


ACCOUNT_STATUS, DEFAULT_TABLESPACE, CREATED
FROM DBA_USERS;

Jak widać z przedstawionych przykładów, perspektywy pełnią w bazie danych bardzo


ważną rolę w programowaniu, ale przede wszystkim są bardzo ważnym, można po-
wiedzieć elementarnym, narzędziem administracyjnym.

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

Prostą konsekwencją stosowania indeksów jest znaczne przyspieszenie sortowania


w zapytaniach wybierających, kiedy klauzula ORDER BY odwołuje się do pól definicji
indeksu, ponieważ wystarczy tylko odczytać rekordy zgodnie z ich występowaniem
na poziomie liści. Zwiększenie szybkości filtrowania jest związane z tym, że wystarczy
znalezienie granic zakresów, natomiast wyboru wierszy z ich wnętrza można dokonać
bez sprawdzania warunku, posługując się jedynie zakresami indeksów. Jednak najwięk-
szy jest zysk podczas realizowania złączeń JOIN, ponieważ indeks przyspiesza znaj-
dowanie odpowiednich rekordów — tych, dla których poprawne jest wyrażenie okre-
ślające sposób złączenia (zazwyczaj równość pól dwóch tabel). Dlatego na polach kluczy
podstawowych i obcych oraz na polach unikalnych indeksy tworzone są automatycznie.

Podsumowując, możemy powiedzieć, że:


 Indeksy typu B-drzewo mogą być tworzone zarówno na jednej kolumnie tabeli,
jak i na kilku jej kolumnach.
 Pola typu long i long raw nie mogą być indeksowane.
 Jedna tabela może posiadać dowolną liczbę indeksów.
 Indeksy typu B-drzewo tworzone są również niejawnie przez system Oracle
każdorazowo podczas włączania ograniczeń integralnościowych typu unique
oraz primary key.

Podstawowa instrukcja tworząca indeks ma postać:


CREATE INDEX dzial ON Dzialy(IdDzialu);

Niestety, próba wykonania takiego zapytania kończy się komunikatem o błędzie


— ORA-01408: taka lista kolumn jest już zaindeksowana — ponieważ IdDzialu jest
kluczem podstawowym, a takie pole jest indeksowane automatycznie. Dowodzi to tego,
że na jednym polu może zostać utworzony tylko jeden indeks. W ten sposób Oracle
chroni użytkownika przed nadmiarowym tworzeniem indeksów — niepotrzebne re-
zerwowanie dodatkowego miejsca na dokładną kopię tej samej informacji oraz większa
132 Część I ♦ Oracle SQL

ilość czasu wynikająca z konieczności jej dwukrotnego przetworzenia. Kolumna, na


której stworzony został indeks, może jednak zostać wykorzystana do budowy indeksu
opartego na liście kolumn.
CREATE INDEX dzial ON Dzialy (IdDzialu, Nazwa);

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;

Dodatkowym parametrem przy tworzeniu indeksów jest możliwość określenia ich


unikalności.
CREATE UNIQUE INDEX dzial ON Dzialy (kod);

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 to zostało stwierdzone, na polach kluczy tworzone są automatycznie indeksy


dla kluczy głównych — są to indeksy unikalne. Jeśli ograniczenie zostało zdefiniowane
przy definicji kolumny, jego nazwę ustala silnik bazy. Możemy ją odczytać, sięgając
do perspektywy systemowej USER_INDEXES. Następnie możemy, odwołując się przez nazwę,
próbować usunąć ten indeks.
DROP INDEX SYS_C003341;

Próba wykonania takiego zapytania kończy się niepowodzeniem i powoduje


wyświetlenie komunikatu: ORA-02429: nie można usunąć indeksu odpowiedzialnego
za klucz unikatowy/główny. Taka ochrona indeksów jest uzasadniona tym, że pola
kluczy najczęściej biorą udział w realizacji złączeń i są wykorzystywane do identyfikacji
rekordów, stąd pozbawienie ich indeksów znacznie spowolniłoby przetwarzanie.

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

Możliwe jest również tworzenie indeksu z odwróconym kluczem (kierunkiem porząd-


kowania). Taka jego organizacja jest korzystna w przypadku, gdy jego modyfikacje
skoncentrowane są na małym zbiorze liści. Odwrócony kierunek porządkowania warto-
ści indeksu lepiej rozłoży jego wiersze na przypisane do niego bloki, co spowoduje
zmniejszenie rywalizacji o nie. Tworzenie indeksu z odwróconym kluczem zawarte
zostało w kolejnym przykładzie:
CREATE INDEX nazwa_indeksu
ON nazwa_tabeli (pole1, pole2, ...)
REVERSE
[PCTFREE liczba] [PCTUSED liczba]
[INITRANS liczba] [MAXTRANS liczba]
[TABLESPACE nazwa_przestrzeni]
STORAGE (parametry_składowania);

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);

Konkurencyjnym dla indeksu o postaci B-drzewa rozwiązaniem jest indeks bitmapowy.


Można go sobie wyobrazić w postaci przypisania do każdego pola kodu na zasadach
analogicznych do wybrania koloru z palety barw. Ze względu na swoją budowę indeks
ten nie może kodować bardzo dużej liczby różnych wartości, a dla takich samych
wartości pola przypisuje ten sam kod (kolor). W związku z tym, w przeciwieństwie
do poprzednika, nie może on być zadeklarowany jako unikalny. Wynika stąd fakt, że
indeks bitmapowy jest użyteczny dla kolumn o małej liczbie różnych wartości. Lepiej
od indeksu B-drzewo sprawdza się on w przypadku stosowania w wyrażeniach filtru-
jących zawierających indeksowaną kolumnę dużej liczby operatorów logicznych (szcze-
gólnie OR).
CREATE BITMAP INDEX ix_dzial
ON Osoby(IdDzialu DESC)
STORAGE (...)
TABLESPACE nazwa

Po ich utworzeniu możliwe jest wykonywanie na indeksach szeregu operacji. Wykony-


wane są one w celu poprawienia wydajności działania indeksów, a co za tym idzie,
zwiększenia wydajności zapytań wybierających. Przykładowo możliwa jest zmiana alo-
kacji i przeniesienie indeksu do innego pliku danych (z reguły do innej lokalizacji fi-
zycznej, innego urządzenia, w celu zrównoleglenia i przyspieszenia z zasady wolnych
operacji odczytu z dysku):
134 Część I ♦ Oracle SQL

ALTER INDEX ix_dzial ALLOCATE EXTENT


(SIZE 500K DATAFILE 'E:\ORA\ORADATA\ORCL\AP.ORA');

Optymalizacja od strony zajmowanych zasobów dyskowych może polegać również na


zwolnieniu niewykorzystanej przez indeks przestrzeni, a pozostawieniu tylko nieznacz-
nego naddatku ponad przestrzeń zajętą.
ALTER INDEX ix_dzial DEALLOCATE UNUSED
[KEEP rozmiar K/M] ;

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.

Istnieją dwie konkurencyjne metody przebudowy indeksu:


 usunięcie go i zbudowanie od nowa;
 zmodyfikowanie go opcją REBUILD (bez usuwania).

Zdecydowanie częściej wykonywana jest przebudowa z opcją REBUILD. W prezento-


wanym przykładzie dodano opcjonalne przeniesienie do wskazanej przestrzeni tabel
(opcja TABLESPACE nazwa).
ALTER INDEX ix_dzial
REBUILD TABLESPACE Testowy;

Podczas takiej przebudowy tabele oraz przebudowywane indeksy z nimi związane są


dostępne dla zapytań, choć tworzone są czasowe blokady. Aby je ograniczyć, możemy
zastosować przebudowę z opcją ONLINE, która gwarantuje zminimalizowanie ich liczby.
Proces przebudowy będzie jednak wtedy bardziej czasochłonny. Opcja ta nie może być
stosowana podczas przebudowywania indeksu bitmapowego.
ALTER INDEX ix_dzial REBUILD ONLINE;

Jednocześnie z usuwaniem rekordów informacje z nimi związane są również usuwane


z indeksów, jednak puste bloki pozostałe po takich operacjach nie są scalane. Podob-
nie operacja modyfikacji rekordu może prowadzić do powstania pustych bloków, po-
nieważ możemy ją rozłożyć na usuwanie rekordu ze starymi wartościami pól i wstawianie
zamiast niego rekordu z nowymi wartościami. Aby dokonać połączenia niewykorzysta-
nych fragmentów ekstentów w obszarze zajmowanym przez indeks (rysunek 4.2), mo-
żemy wykonać polecenie:
ALTER INDEX ix_dzial COALESCE;
Rozdział 4. ♦ Zapytania tworzące tabele 135

COALESCE

Rysunek 4.2. Ilustracja łączenia ekstentów za pomocą polecenia 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;

Podobnie możemy dokonać walidacji struktury, oceniając sposób wykorzystania zasobów.


ANALYZE INDEX ix_dzial
VALIDATE STRUCTURE;

Przełączenie indeksu w stan, w którym jego wykorzystanie jest monitorowane, uzysku-


jemy na skutek wykonania polecenia:
ALTER INDEX ix_dzial
MONITORING USAGE;

Pamiętać należy, że monitorowanie indeksów może poważnie obciążać silnik bazy.


Informacja o czasie monitorowania (nazwa indeksu, początek i koniec śledzenia) zawarta
jest w perspektywie INDEX_STATS.
136 Część I ♦ Oracle SQL

SELECT * FROM V$OBJECT_USAGE;

Przełączenie do domyślnego stanu bez monitorowania użycia indeksów uzyskamy na


skutek wykonania polecenia:
ALTER INDEX ix_dzial
NOMONITORING USAGE;

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

Zapytania dla struktur XML


Jedną z ostatnio dodanych funkcjonalności Oracle (w wersji 9.; ciągle rozbudowywana)
jest obsługa danych strukturalnych zapisanych w formacie XML. Podstawową moż-
liwością jest wyświetlenie zawartości tabeli (pojedynczych kolumn i wierszy) w for-
macie pól ujętych w znaczniki odpowiadające nazwom kolumn. Aby to zrobić, możemy
w zapytaniu wybierającym zastosować funkcję SYS_XMLGEN. Do każdego z wyświetlanych
pól automatycznie dodawany jest znacznik z informacją o standardzie, wersji pliku XML
(tabela 5.1). W przykładzie liczbę pokazanych pól ograniczono klauzulą WHERE.

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>

COLUMN Nazwiska FORMAT A22


COLUMN Imiona FORMAT A22
SELECT SYS_XMLGEN(Nazwisko) AS Nazwiska, SYS_XMLGEN(Imie) AS Imiona
FROM Osoby
WHERE IdOsoby<3;

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

Tabela 5.2. Podstawowa postać pliku XML zawierającego jedną kolumnę


Rekordy
<?xml version="1.0"?>
<ROWSET>
<NAZWISKO>Kowalski</NAZWISKO>
<NAZWISKO>Nowak</NAZWISKO>
</ROWSET>

ż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 SYS_XMLAGG(SYS_XMLGEN(Nazwisko)) AS Rekordy


FROM Osoby
WHERE IdOsoby<3;

O wiele bardziej interesujące jest zastosowanie funkcji XMLELEMENT, która również


może być zagnieżdżana, ale, w przeciwieństwie do poprzednich, może zawierać do-
wolnie długą listę argumentów. Pierwszym z nich jest zawsze nazwa przypisywana
znacznikowi na danym poziomie, a kolejnym albo wartość w znaczniku zawarta, albo
lista funkcji XMLELEMENT, w najprostszym przypadku jednoelementowa. W tym przy-
kładzie generowany jest oddzielnie dla każdego rekordu zdefiniowany parametrami
ciąg (tabela 5.3).

Tabela 5.3. Skutek zastosowania zagnieżdżonych funkcji XMLELEMENT


Rekordy
<Pracownik><Nazwisko>Kowalski</Nazwisko><Imie>Jan</Imie></Pracownik>
<Pracownik><Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie></Pracownik>

SELECT XMLELEMENT("Pracownik",
XMLELEMENT("Nazwisko", Nazwisko),
XMLELEMENT("Imie", Imie)) AS Rekordy
FROM Osoby
WHERE IdOsoby<3;

Zamieniając XMLELEMENT na podobną w działaniu funkcję XMLATTRIBUTES, możemy uzy-


skać plik XML ze zdefiniowanymi dla wybranego znacznika atrybutami (tabela 5.4).
Parametry wywołania to lista kolumn z ewentualnie dodanymi aliasami.

Tabela 5.4. Skutek zastosowania funkcji XMLELEMENT oraz XMLATTRIBUTES do wygenerowania


pliku XML z atrybutami
Rekordy
<Pracownik ID="1" NAZWISKO="Kowalski"><Imie>Jan</Imie></Pracownik>
<Pracownik ID="2" NAZWISKO="Nowak"><Imie>Karol</Imie></Pracownik>
Rozdział 5. ♦ Dodatkowe funkcjonalności SQL 139

SELECT XMLELEMENT("Pracownik",
XMLATTRIBUTES(IdOsoby AS ID, Nazwisko),
XMLELEMENT("Imie", Imie)) AS Rekordy
FROM Osoby
WHERE IdOsoby<3;

Jako parametru wywołania funkcji XMLATTRIBUTES możemy użyć zapytania skorelowa-


nego. Spowoduje to, że w naszym przykładzie dla każdej osoby zostanie dodany znacz-
nik zawierający jako atrybuty identyfikator działu IdDzialu oraz jego nazwę. Możliwe
jest odwołanie się do dowolnej liczby tabel nadrzędnych, zarówno przez proste, sko-
relowane zapytanie (tabela 5.5), jak również przez skorelowane zapytania do kilku ta-
bel połączonych np. poprzez zastosowanie operatora JOIN. Nie jest możliwe odwoły-
wanie się w ten sposób do tabeli podrzędnej Zarobki, ponieważ dla każdego z wierszy
tabeli nadrzędnej Osoby zapytanie skorelowane może zwrócić więcej niż jeden rekord.

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;

Łatwiejszą drogą uzyskania struktury XML, bez konieczności odwoływania się do


każdego z elementów, jest użycie polecenia XMLForest (tabela 5.6). Parametrem tej
funkcji jest lista kolumn, która automatycznie jest konwertowana do zapisu znaczni-
kowego.

Tabela 5.6. Skutek zastosowania funkcji XMLForest do wygenerowania pliku XML


Rekordy
<Pracownik><NAZWISKO>Kowalski</NAZWISKO><IMIE>Jan</IMIE><WZROST>
1,67</WZROST></Pracownik>
<Pracownik><NAZWISKO>Nowak</NAZWISKO><IMIE>Karol</IMIE><WZROST>
1,92</WZROST></Pracownik>

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.

Tabela 5.7. Skutek zastosowania funkcji XMLCOLATTVAL do wygenerowania pliku XML


Rekordy
<Pracownik><column name = "NAZWISKO">Kowalski</column><column name =
"IMIE">Jan</column><column name = "WZROST">1,67</column></Pracownik>
<Pracownik><column name = "NAZWISKO">Nowak</column><column name =
"IMIE">Karol</column><column name = "WZROST">1,92</column></Pracownik>

SELECT XMLELEMENT("Pracownik",
XMLCOLATTVAL(Nazwisko, Imie, Wzrost))AS Rekordy
FROM Osoby
WHERE IdOsoby<3;

W przypadku użycia funkcji agregujących w obrębie zapytania wybierającego możliwe


jest zastosowanie funkcji XMLAGG (tabela 5.8). Powoduje to, że wewnątrz nadrzędnego
znacznika uzyskanego dzięki funkcji XMLELEMENT wygenerowane zostają wszystkie ele-
menty wchodzące w skład tej grupy.

Tabela 5.8. Skutek zastosowania funkcji XMLAGG do wygenerowania pliku XML


Dzialy
<Dzial><Pracownik>Kowalski</Pracownik></Dzial>
<Dzial><Pracownik>Nowak</Pracownik><Pracownik>Janik</Pracownik></Dzial>
<Dzial><Pracownik>Kow</Pracownik><Pracownik>Kowalczyk</Pracownik></Dzial>

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.

SELECT XMLCONCAT(XMLELEMENT("Nazwisko", Nazwisko),


XMLELEMENT("Imie", Imie))
AS Pracownik
FROM Osoby
WHERE IdOsoby<6;
Rozdział 5. ♦ Dodatkowe funkcjonalności SQL 141

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>

Jak widać, zestaw procedur przydatnych do eksportu danych relacyjnych w formacie


XML jest bogaty. Pomimo to nie zaspokaja on wszystkich potrzeb wynikających z wy-
magań odnośnie pliku wynikowego, co powoduje konieczność tworzenia własnych pro-
cedur i pakietów na potrzeby eksportu do tego formatu. Dotyczy to przede wszystkim
możliwości eksportowania nie danych zawartych w pojedynczej tabeli, ale w wielota-
belowej strukturze relacyjnej. Jest to szczególnie widoczne, kiedy chcemy zachować
wielopoziomową hierarchię bez konieczności powielania znaczników na każdym z po-
ziomów. Poza podstawową funkcją eksportowania do formatu XML Oracle oferuje moż-
liwość przechowywania struktur XML po stronie serwera bazy danych, w postaci pól
tabeli. Stwórzmy prostą tabelę, która będzie przechowywała informacje o pracowni-
kach naszej firmy w kolumnie typu XMLTYPE. Aby zachować czystość postaci relacyj-
nej i ułatwić wskazywanie odpowiednich wierszy, dodane zostało pole klucza podsta-
wowego.
CREATE TABLE Pracownicy
(IDPracownika NUMBER(3),
Dane XMLTYPE);

Wprowadzanie danych do tabeli, w której zdefiniowane zostało pole zawierające dane


w postaci XML, wykonywane jest za pomocą standardowego zapytania INSERT INTO,
gdzie dla wartości pola XML zastosowano funkcję XMLTYPE o argumencie w postaci
łańcucha zawierającego poprawny składniowo fragment pliku w tym formacie. W na-
szym przypadku argumentem jest ciąg znaków zawierający dane pojedynczego pra-
cownika w formacie znacznikowym.
INSERT INTO Pracownicy (IDPracownika, Dane)
VALUES (1,
XMLTYPE('
<PRecord>
<Nazwisko>Kowalski</Nazwisko>
<Imie>Jan</Imie>
<RokUrodz>1980</RokUrodz>
<Wzrost>1.77</Wzrost>
<DataZatr>2001/02/10</DataZatr>
</PRecord>')
);

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

COLUMN Dane FORMAT a50


SELECT * FROM Pracownicy;

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.

COLUMN Dane FORMAT A50


SET LONG 100000
SELECT * FROM Pracownicy;

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>

COLUMN NazwiskoXML FORMAT A50


SELECT IdPracownika,
EXTRACT(dane, '/PRecord/Nazwisko') AS NazwiskoXML
FROM Pracownicy;

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

COLUMN NazwiskoXML FORMAT A50


SELECT IdPracownika,
EXTRACTVALUE(dane, '/PRecord/Nazwisko') AS NazwiskoXML
FROM Pracownicy;

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.

Tabela 5.14. Zastosowanie funkcji XMLQuery do filtrowania pól XML


IdPracownika NazwiskoXML
2
3 <PRecord><Nazwisko>Kowalski</Nazwisko>
<Imie>Karol</Imie><RokUrodz>1967</RokUrodz>
<Wzrost>1.67</Wzrost>
<DataZatr>1991/05/15</DataZatr></PRecord>
1 <PRecord><Nazwisko>Kowalski</Nazwisko>
<Imie>Jan</Imie><RokUrodz>1980</RokUrodz>
<Wzrost>1.77</Wzrost>
<DataZatr>2001/02/10</DataZatr></PRecord>

COLUMN NazwiskoXML FORMAT A50


SELECT IdPracownika,
XMLQuery(
'FOR $i IN /PRecord
WHERE $i /Nazwisko = "Kowalski"
ORDER BY $i/Imie
RETURN $i'
Rozdział 5. ♦ Dodatkowe funkcjonalności SQL 145

PASING BY VALUE Dane


RETURNING CONTENT) NazwiskoXML
FROM Pracownicy;

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>

COLUMN NazwiskoXML FORMAT A50


SELECT IdPracownika,
XMLQuery(
'FOR $i IN /PRecord
WHERE $i /Nazwisko = "Kowalski"
ORDER BY $i/Imie
RETURN $i/Nazwisko | $i/Imie'
PASSING BY VALUE Dane
RETURNING CONTENT) NazwiskoXML
FROM Pracownicy;

Aktualnym problemem pozostaje eliminacja rekordów z pustą (NULL) zawartością pola


NazwiskoXML. Ich usunięcia możemy dokonać zewnętrzną klauzulą WHERE, tak jak to
pokazuje kolejny przykład.
SELECT * FROM
(SELECT IdPracownika,
XMLQuery(
'FOR $i IN /PRecord
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;

Oprócz operatorów arytmetycznych zapisywanych zwyczajowo (symbolicznie) do-


puszczalne jest stosowanie równoważnych literałów zawartych w tabeli 5.16.
146 Część I ♦ Oracle SQL

Tabela 5.16. Równoważny symboliczny i literałowy zapis operatorów dla funkcji XMLQuery
= eq
> gt
>= ge
< lt
<= le
!= ne

Ważnym zagadnieniem jest konieczność poprawnego wskazania poziomu znaczników,


po których przechodzi kursor. Jeśli wskażemy na poziom /PRecord/Nazwisko, a bę-
dziemy próbowali odwołać się do równoważnego poziomu $i/Imie, to zapytanie zo-
stanie przetworzone, ale zamiast zawartości znacznika Imie otrzymamy wartość pustą,
co ilustruje kolejny przykład (tabela 5.17). Zawartość znacznika <Imie> zostałaby wy-
świetlona, gdyby był on zagnieżdżony w znaczniku <Nazwisko> (gdyby znajdował się
na niższym poziomie hierarchii).

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).

COLUMN NazwiskoXML FORMAT a20;


COLUMN ImieXML FORMAT a20
SELECT IdPracownika,
Rozdział 5. ♦ Dodatkowe funkcjonalności SQL 147

Tabela 5.18. Modyfikowany rekord przed zmianą


IdPracownika NazwiskoXML ImieXML
1 Kowalski Jan

Tabela 5.19. Modyfikowany rekord po zmianie


IdPracownika NazwiskoXML ImieXML
1 Kowalski Jerzy

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;

Kolejnym rodzajem modyfikacji jest możliwość dopisywania znaczników wraz


z wartością do istniejącej struktury XML. Możemy zastosować w tym celu funkcję
INSERTCHILDXML, której parametrami są: nazwa kolumny zawierającej typ XML, wskaź-
nik do poziomu, na którym ma zostać dopisany znacznik, symboliczna nazwa znacz-
nika oraz zawarta w funkcji XMLType rzeczywista jego postać wraz z wartością (ta-
bele 5.20 i 5.21). Wskazanie rekordu, dla którego wykonywane jest dodanie potomnego
znacznika może zostać wykonane w klauzuli WHERE zewnętrznego zapytania modyfi-
kującego.

Tabela 5.20. Modyfikowany rekord przed dopisaniem znacznika


IdPracownika Dane
2 <PRecord>
<Nazwisko>Nowak</Nazwisko>
<Imie>Karol</Imie>
<RokUrodz>1977</RokUrodz>
<Wzrost>1.81</Wzrost>
<DataZatr>1998/03/05</DataZatr>
</PRecord>

Tabela 5.21. Modyfikowany rekord po dopisaniu znacznika


IdPracownika Dane
2 <PRecord><Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie><RokUrodz>
1977</RokUrodz><Wzrost>1.81</Wzrost><DataZatr>1998/03/05</DataZatr>
<IdSzefa>1</IdSzefa>
</PRecord>
148 Część I ♦ Oracle SQL

COLUMN Dane FORMAT a50


SET LONG 100000
SELECT * FROM Pracownicy
WHERE IdPracownika = 2;

UPDATE Pracownicy
SET dane = INSERTCHILDXML(dane,
'/PRecord', 'IdSzefa',
XMLType('<IdSzefa>1</IdSzefa>'))
WHERE IdPracownika = 2;

SELECT * FROM Pracownicy WHERE IdPracownika = 2;

Można zauważyć, że dopisanie znacznika spłaszcza sposób wyświetlania danych XML


w prostym zapytaniu XML. Jest to charakterystyczne dla każdej modyfikacji tego
typu danych. Podobnie wstawienia danych XML możemy dokonać, stosując funkcję
APPENDCHILDXML. Tym razem nie jest podawany parametr określający symboliczną na-
zwę znacznika (inne parametry pozostają bez zmian), jednak wstawiana struktura może
zawierać znaczniki wewnętrzne znaczniki (poprzednio nie istniała taka możliwość).
W prezentowanym przykładzie do wskazania modyfikowanego wiersza (wierszy) za-
stosowano w klauzuli WHERE funkcję EXTRACTVALUE, tak aby dopisanie dotyczyło wszyst-
kich pracowników o znaczniku <Imie> zawierającym wartość Jerzy (tabele 5.22 i 5.23).
Dla danych testowych był tylko jeden taki pracownik.

Tabela 5.22. Modyfikowany rekord przed dopisaniem znacznika


IdPracownika Dane
1 <PRecord><Nazwisko>Kowalski</Nazwisko><Imie>Jerzy</Imie><RokUrodz>
1980</RokUrodz><Wzrost>1.77</Wzrost><DataZatr>2001/02/10</DataZatr>
</PRecord>

Tabela 5.23. Modyfikowany rekord po dopisaniu znacznika


IdPracownika Dane
1 <PRecord><Nazwisko>Kowalski</Nazwisko><Imie>Jerzy</Imie><RokUrodz>
1980</RokUrodz><Wzrost>1.77</Wzrost><DataZatr>2001/02/10</DataZatr>
<IdSzefa>1</IdSzefa></PRecord>

COLUMN Dane FORMAT a50


SET LONG 100000

SELECT * FROM Pracownicy WHERE


EXTRACTVALUE(dane, '/PRecord/Imie')='Jerzy';

UPDATE Pracownicy
SET dane = APPENDCHILDXML(dane,
'/PRecord',
XMLType('<IdSzefa>1</IdSzefa>'))
WHERE EXTRACTVALUE(dane,
'/PRecord/Imie')='Jerzy';

SELECT * FROM Pracownicy WHERE


EXTRACTVALUE(dane, '/PRecord/Imie')='Jerzy';
Rozdział 5. ♦ Dodatkowe funkcjonalności SQL 149

Sprawdzenia wykonania zapytań możemy dokonać, nie tylko wyświetlając zawartość


lub jej fragment z pola XMLType, ale również odwołując się do funkcji EXISTSNODE (ta-
bela 5.24), której parametrami są nazwa pola oraz wskaźnik do odpowiedniego po-
ziomu hierarchii, a która zwraca wartość 1 dla pól XMLType zawierających wskazany
węzeł na danym poziomie oraz 0 w przypadku przeciwnym.

Tabela 5.24. Skutek sprawdzenia istnienia węzła /PRecord/IdSzefa dla danych testowych
IdPracownika Wezel
2 1
3 0
1 1

SELECT IdPracownika, EXISTSNODE(dane,


'/PRecord/IdSzefa') AS Wezel
FROM Pracownicy;

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.

Tabela 5.25. Modyfikowany rekord po dopisaniu znacznika zagnieżdżonego


IdPracownika Dane
2 <PRecord><Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie><RokUrodz>
1977</RokUrodz><Wzrost>1.81</Wzrost><DataZatr>1998/03/05</DataZatr>
<IdSzefa>1</IdSzefa><Wyplaty><Brutto>1111</Brutto></Wyplaty></PRecord>

SELECT * FROM Pracownicy


WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';

UPDATE Pracownicy
SET dane = APPENDCHILDXML(dane,
'/PRecord',
XMLType('<Wyplaty><Brutto>1111</Brutto></Wyplaty>'))
WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';

SELECT * FROM Pracownicy


WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';

Dodawanie kolejnych wartości brutto możliwe jest do przeprowadzenia za pomocą


funkcji APPENDCHILDXML, jednakże pełną kontrolę nad miejscem wpisywania kolejnych
danych oferuje funkcja INSERTXMLBEFORE, pozwalająca na określenie poziomu oraz po-
zycji wskaźnika, przed którym nowa wartość zostanie dopisana. Numeracja wskaźników
rozpoczyna się od wartości 1 (tabele 5.26 i 5.27). Podobnie jak poprzednio, skrypt przy-
kładowy zawiera zapytania weryfikujące działanie zapytania modyfikującego.
150 Część I ♦ Oracle SQL

Tabela 5.26. Modyfikowany rekord przed dopisaniem znacznika


IdPracownika Dane
2 <PRecord><Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie><RokUrodz>
1977</RokUrodz><Wzrost>1.81</Wzrost><DataZatr>1998/03/05</DataZatr>
<IdSzefa>1</IdSzefa><Wyplaty><Brutto>1111</Brutto></Wyplaty></PRecord>

Tabela 5.27. Modyfikowany rekord po dopisaniu znacznika przed wskazaną pozycją


IdPracownika Dane
2 <PRecord><Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie><RokUrodz>
1977</RokUrodz><Wzrost>1.81</Wzrost><DataZatr>1998/03/05</DataZatr>
<IdSzefa>1</IdSzefa><Wyplaty><Brutto>999</Brutto><Brutto>1111</Brutto>
</Wyplaty></PRecord>

SELECT * FROM Pracownicy


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';

SELECT * FROM Pracownicy


WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';

Ostatnią a jednocześnie najprostszą operacją modyfikującą dane XML jest usuwanie


znacznika (znaczników) na określonym poziomie. W tym celu wystarczy zastosować
funkcję DELETEXML (tabele 5.28 i 5.29), której parametrami są nazwa pola ze zmienną
XMLType oraz wskaźnik lub poziom, z którego usuwamy wpisy. Podobnie jak poprzednio,
wskazanie na rekord, z którego usuwamy wpisy, jest wykonywane za pomocą klau-
zuli WHERE, gdzie do porównania wartości wskaźnika ze wzorcem zastosowano funkcję
EXTRACTVALUE.

Tabela 5.28. Modyfikowany rekord przed usunięciem znaczników


IdPracownika Dane
2 <PRecord><Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie><RokUrodz>
1977</RokUrodz><Wzrost>1.81</Wzrost><DataZatr>1998/03/05</DataZatr>
<IdSzefa>1</IdSzefa><Wyplaty><Brutto>999</Brutto><Brutto>1111</Brutto>
</Wyplaty></PRecord>

Tabela 5.29. Modyfikowany rekord po usunięciu znaczników


IdPracownika Dane
2 <PRecord><Nazwisko>Nowak</Nazwisko><Imie>Karol</Imie><RokUrodz>
1977</RokUrodz><Wzrost>1.81</Wzrost><DataZatr>1998/03/05</DataZatr>
<IdSzefa>1</IdSzefa><Wyplaty/></PRecord>
Rozdział 5. ♦ Dodatkowe funkcjonalności SQL 151

SELECT * FROM Pracownicy


WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';

UPDATE Pracownicy
SET dane =
DELETEXML(Dane,
'/PRecord/Wyplaty/Brutto')
WHERE EXTRACTVALUE(dane,
'/PRecord/Nazwisko')='Nowak';

SELECT * FROM Pracownicy


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.

Tabela 5.30. Efekt zastosowania funkcji TABLE


Value(Pracownik)
<Nazwisko>Nowak</Nazwisko>
<Imie>Karol</Imie>
<RokUrodz>1977</RokUrodz>
<Wzrost>1.81</Wzrost>
<DataZatr>1998/03/05</DataZatr>
<IdSzefa>1</IdSzefa>
<Wyplaty/>
<Nazwisko>Kowalski</Nazwisko>
<Imie>Karol</Imie>
<RokUrodz>1967</RokUrodz>
<Wzrost>1.67</Wzrost>
<DataZatr>1991/05/15</DataZatr>
<Nazwisko>Kowalski</Nazwisko>
<Imie>Jerzy</Imie>
<RokUrodz>1980</RokUrodz>
<Wzrost>1.77</Wzrost>
<DataZatr>2001/02/10</DataZatr>
<IdSzefa>1</IdSzefa>

SELECT Value(Pracownik) FROM Pracownicy,


TABLE(XMLSEQUENCE(EXTRACT(Dane, 'PRecord/*'))) Pracownik;

Od wersji 11g dostępna jest funkcja XMLDIFF, pozwalająca na porównanie zawartości


dwóch zmiennych typu XML (pól tabeli lub stałych), które stanowią jej dwa parametry
(tabela 5.31). Niestety, opis różnic zwracany przez tę funkcję jest niezbyt czytelny
nawet dla mało złożonych danych XML, co pokazuje kolejny z przykładów.
152 Część I ♦ Oracle SQL

Tabela 5.31. Skutek zastosowania funkcji XMLDIFF


xxx
<xd:xdiff xsi:schemaLocation="http://xmlns.oracle.com/xdb/xdiff.xsd http://xmlns.oracle.com/
xdb/xdiff.xsd" xmlns:xd="http://xmlns.oracle.com/xdb/xdiff.xsd" xmlns:xsi="http://www.w3.org/
2001/XMLSchema-instance"><?oracle-xmldiff operations-in-docorder="true" output-model=
"snapshot" diff-algorithm="global"?>
<xd:update-node xd:node-type="text" xd:xpath="/osoba[1]/nazwisko[1]/text()[1]"><xd:content>
Kowalski</xd:content></xd:update-node><xd:delete-node xd:node-type="element" xd:xpath=
"/osoba[1]/imie[1]"/></xd:xdiff>

SELECT XMLDIFF(
XMLTYPE('
<Osoba><Nazwisko>Kowalski</Nazwisko><Imie>Jan</Imie>
</Osoba>
'),
XMLTYPE('
<Osoba><Nazwisko>Kowalski</Nazwisko></Osoba>
')
) xxx
FROM DUAL;

Pomimo że Oracle dostarcza wielu narzędzi do obsługi typów znacznikowych, to wy-


generowanie pliku XML za ich pomocą jest dość kłopotliwe. Równie kłopotliwa jest
konwersja źródłowego pliku XML na postać relacyjną. Dlatego też wielce prawdopo-
dobny jest rozwój tych narzędzi — na chwilę obecną programiście pozostaje w wielu
przypadkach opracowanie własnych algorytmów na poziomie rozszerzenia procedu-
ralnego PL/SQL lub końcówki klienckiej.
Część II
ORACLE PL/SQL
154 Część II ♦ ORACLE PL/SQL
Rozdział 6. ♦ PL/SQL 155

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
/* ... */.

Tabela 6.1. Wykaz symboli specjalnych dla środowiska PL/SQL


Symbol Znaczenie Przykład
. separator w nazwach Tabela.pole
kwalifikowanych
' lub " ogranicznik łańcucha 'to jest napis'
:= przypisanie i:=1;
|| konkatenacja 'Jan' || ' ' || 'Kowalski'
-- Komentarz jednoliniowy --komentarz
/* Komentarz wieloliniowy /* komentarz1
*/ Komentarz2*/

Wszystkie występujące w kodzie PL/SQL zmienne muszą być zadeklarowane. Nazwa


zmiennej musi zaczynać się od litery, po której może wystąpić dowolnie wiele liter, cyfr
lub znaków specjalnych $, # lub _. Jej długość nie może przekraczać 30 znaków i nie
może ona zawierać spacji. Ogólna postać deklaracji zmiennej wygląda następująco:
156 Część II ♦ ORACLE PL/SQL

<nazwa_zmiennej> <typ_danych> [NOT NULL] [:= <wartość_pocz.>];

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 >];

Można to zilustrować przykładem odwołującym się do elementów definicji tabeli Osoby.


Nazwiska Osoby.Nazwisko%TYPE;
rekord Osoby%ROWTYPE;

Podstawowym elementem programistycznym jest blok anonimowy. De facto ma on


postać definicji procedury bez nagłówka, a co za tym idzie, możliwości przekazywa-
nia danych. Struktura bloku anonimowego (procedury anonimowej) PL/SQL wygląda
następująco:
DECLARE
-- Deklaracje zmiennych i stałych
BEGIN
-- Ciało procedury (część wykonawcza)
EXCEPTION
-- Sekcja wyjątków
END;

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

sekcja obsługi wyjątków rozpoczynająca się słowem kluczowym EXCEPTION, a kończąca


się wraz z końcem bloku. Jak widać, sekcja obsługi wyjątków, o ile występuje, zawsze
stanowi końcową część ciała procedury. Przytoczmy prosty przykład bloku anonimo-
wego, którego zadaniem jest wypisanie na standardowym urządzeniu wyjściowym sta-
tycznego tekstu.
SET SERVEROUTPUT ON;
DECLARE
BEGIN
DBMS_OUTPUT.PUT_LINE(‘tekst napisu’);
END;

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).

W SQL Developerze wymagane jest przełączenie urządzenia na zakładce DBMS Output


w stan Enable DBMS Output (pierwsza ikona od lewej strony — rysunek 6.2). Po-
prawne wykonanie bloku anonimowego pokazano na rysunkach 6.3 i 6.4. Pomimo tego,
tak jak zaznaczono wcześniej, polecenie SET SERVEROUTPUT będzie używane, a osoby
trenujące procedury w nowszym środowisku są proszone o pomijanie tej linii kodu.
Uwaga: do silnika serwera 11g można również podłączyć się, używając końcówki
klienta z wersji wcześniejszej, pamiętając tylko o właściwym dla tego klienta wpisie
w pliku tnsnames.ora.

Jednym z podstawowych elementów składni PL/SQL jest instrukcja warunkowa IF ...


THEN o składni zgodnej ze spotykaną w większości języków programowania. Instrukcje
zawarte w jej wnętrzu wykonają się tylko w przypadku, gdy warunek w niej występujący,
a będący wyrażeniem zwracającym zmienną logiczną, będzie miał wartość prawdziwą
— TRUE.
IF licz > 18 THEN
licz := licz + 1;
END IF ;
158 Część II ♦ ORACLE PL/SQL

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;

Warunki występujące w zagnieżdżonych instrukcjach warunkowych nie muszą w żaden


sposób być ze sobą powiązane. Mogą dotyczyć zupełnie niezależnych od siebie zmien-
nych. W prezentowanych przykładach, w przypadku kiedy warunki przyjmowały wartość
Rozdział 6. ♦ PL/SQL 159

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

IF var1 > 10 THEN


var2 : = var1 + 20;
ELSEIF var1 BETWEEN 7 AND 8 THEN
var2 : = 2 * var1;
ELSE
var2 : = var1 * var1;
END IF;

Jak widać w prezentowanym przykładzie, do tworzenia warunków można wykorzy-


stać dowolne operatory zdefiniowane na platformie Oracle, w tym operatory specjalne
(BETWEEN, IN, LIKE, ...). Można również wykorzystać wbudowane funkcje lub funkcję
użytkownika, pamiętając jedynie o zasadzie, że całe wyrażenie musi zwracać wartość
typu logicznego.

Kolejnym elementem składni są instrukcje organizujące pętle. Najbardziej podstawową


jest pętla LOOP ... END LOOP, w ciele której można umieścić dowolne instrukcje. Przy
takiej organizacji wykonywałaby się ona bez końca (zapętlenie). W celu przerwania
jej wykonywania w jej ciele, najczęściej w instrukcji warunkowej, umieszczane jest
słowo kluczowe EXIT, które powoduje bezwzględne zakończenie przetwarzania pętli
(wyjście z niej).
licz : = 1; -- inicjalizacja licznika
LOOP -- LOOP — słowo kluczowe (bez średnika)
licz : = licz + 1; -- inkrementacja licznika pętli
IF licz > 100 THEN -- sprawdzenie warunku zakończenia

EXIT; -- wyjście z pętli
END IF;

END LOOP; -- END LOOP — słowo kluczowe

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

FOR licz IN 1..13


LOOP
...
END LOOP;

W prezentowanym przykładzie pętla wykona się dokładnie 13 razy, a zmienna inkre-


mentowana będzie przybierała wartości, począwszy od 1. Ważne jest, że zmienna, która
jest używana jako licznik pętli, nie może być zadeklarowana. Można przyjąć, że uży-
cie jej w składni pętli narzuca jej typ. Należy również pamiętać, że·niedozwolona jest
zmiana licznika we wnętrzu pętli (nie można użyć w niej jego „ręcznej” inkrementacji).
Oprócz pętli o inkrementacji narastającej możliwe jest skonstruowanie pętli o inkre-
mentacji odwróconej, czyli takiej, gdzie zmienna przybiera wartości, począwszy od
górnego, aż do dolnego ograniczenia, a wartość licznika zmienia się o –1 przy każdym
przetworzeniu pętli.
SET SERVEROUTPUT ON;
DECLARE
liczba VARCHAR2(5) := '12345';
dlugosc NUMBER(2);
odwrocona VARCHAR2(5);
BEGIN
dlugosc:= LENGTH(liczba);
FOR licz IN REVERSE 1.. dlugosc
LOOP
odwrocona:= odwrocona || SUBSTR(liczba, licz, 1);
END LOOP;
DBMS_OUTPUT.PUT_LINE('Liczba = ' || liczba);
DBMS_OUTPUT.PUT_LINE('Odwrocona = ' || odwrocona);
END;

W przykładzie zastosowano konstrukcję pętli odwróconej do przepisania wejściowego


łańcucha znaków ‘12345’ w kolejności odwrotnej ‘54321’. Pierwszym krokiem było wy-
znaczenie długości wejściowego łańcucha, który stanowi górne ograniczenie pętli, przy
użyciu funkcji LENGTH. W ciele pętli zastosowano funkcję SUBSTR(liczba, licz, 1), która
wycina z wejściowego łańcucha liczba jeden znak, począwszy od pozycji wyznaczanej
przez zmienną licz. Nową funkcjonalnością dodaną w Oracle 11g jest możliwość po-
minięcia końcowej części pętli i powrotu do początku, aby wykonać kolejny przebieg.
W tym celu stosujemy polecenie CONTINUE.
BEGIN
FOR i IN 1..4
LOOP
DBMS_OUTPUT.PUT_LINE ('Przed CONTINUE i= ' || i);
CONTINUE;
DBMS_OUTPUT.PUT_LINE ('Po CONTINUE i= ' || i);
END LOOP;
END;

Użycie bezwarunkowej instrukcji CONTINUE spowodowało, że występujące po niej pole-


cenia nie wykonały się ani razu, co ilustruje przedstawiony wynik wykonania poprzed-
niego skryptu.
Przed CONTINUE i= 1
Przed CONTINUE i= 2
Przed CONTINUE i= 3
Przed CONTINUE i= 4
162 Część II ♦ ORACLE PL/SQL

Użycie bezwarunkowej instrukcji CONTINUE raczej nie ma więc sensu praktycznego,


ponieważ można by po prostu nie pisać występujących po niej instrukcji. Praktyczne
zastosowanie może znaleźć natomiast jej warunkowa postać CONTINUE WHEN.
BEGIN
FOR i IN 1..4
LOOP
DBMS_OUTPUT.PUT_LINE ('Przed CONTINUE i = ' || i);
CONTINUE WHEN i > 2;
DBMS_OUTPUT.PUT_LINE ('Po CONTINUE i = ' || i);
END LOOP;
END;

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>>

Powoduje ona przeniesienie punktu przetwarzania do miejsca oznaczonego etykietą,


którą jest dowolny literał ujęty w znaki << i >>. Instrukcja skoku bezwarunkowego nie
powinna być wykorzystywana w programowaniu — traktować ją należy jako relikt
przeszłości.
Rozdział 7.
Procedury składowane
Zamiast tworzyć bloki anonimowe, wygodniej jest organizować kod w nazwane pro-
cedury. W środowisku baz danych noszą one miano procedur składowanych. W przy-
padku języków wyższego rzędu taka organizacja kodu podyktowana jest chęcią utrzy-
mania jego przejrzystości — jeśli jego fragment ma być wykonywany wielokrotnie,
warto go umieścić w procedurze i odwoływać się do niego tylko przez jej wywoływanie.
Także różne funkcjonalności, zadania kodu są argumentem za jego podziałem. Pod-
stawowa składnia polecenia tworzącego procedurę ma postać:
CREATE PROCEDURE nazwa
IS
BEGIN
-- ciało procedury
END;

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

DROP PROCEDURE nazwa;

Wielokrotnie możemy chcieć modyfikować kod procedury. Ponowne wykonanie po-


lecenia CREATE PROCEDURE spowoduje wykrycie obiektu o tej samej nazwie i wyświe-
tlenie komunikatu o błędzie. W związku z tym musimy najpierw usunąć definicję pro-
cedury, a następnie utworzyć ją ponownie. Zamiast tego możemy posłużyć się składnią
CREATE OR REPLACE PROCEDURE, która, w zależności od tego, czy procedura o danej na-
zwie już istnieje, czy też nie, stworzy ją od podstaw lub nadpisze na istniejącej nową
definicję. Z punktu widzenia programisty nie da się rozróżnić, która z tych akcji została
wykonana (taki sam komunikat — Procedura została pomyślnie utworzona), dlatego,
korzystając z tej składni, należy się upewnić, czy nadpisujemy kod właściwej proce-
dury! Przykładem może być utworzenie procedury, której zadaniem będzie zamiana
wszystkich nazwisk w tabeli Osoby na pisane dużymi literami.
CREATE OR REPLACE PROCEDURE up
IS
BEGIN
UPDATE Osoby SET Nazwisko=UPPER(Nazwisko);
END;

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;

Załóżmy, że w ramach tego samego skryptu chcielibyśmy sprawdzić poprawność wy-


konania procedury zapytaniem wybierającym SELECT. Zgodnie z wcześniejszą uwagą,
zamieszczenie go w ciele bloku anonimowego jest niedozwolone. Również umiesz-
czenie zapytania wybierającego bezpośrednio po słowie kluczowym END; zakończy
się komunikatem o błędzie. Stąd konieczność podzielenia skryptu na dwie części, które
z punktu widzenia serwera stanowić będą osobne fragmenty. Znakiem odpowiedzial-
nym za taki podział jest slash (/), przy czym części będą traktowane jako fragmenty
kodu PL/SQL lub zapytania SQL, w zależności od zawartości. Skrypt może zostać
podzielony w ten sposób na dowolną liczbę niezależnych fragmentów.
BEGIN
up;
END;
/
SELECT Nazwisko FROM Osoby;

Innym przykładem realizacji procedury składowanej jest zastosowanie jej do przepi-


sywania nazwisk i wzrostu osób do tabeli wys_tab, przy czym rekordy posortowane
będą malejąco według wzrostu. Należy zauważyć, że tabela wys_tab o odpowiedniej
strukturze musi już istnieć w schemacie użytkownika.
CREATE OR REPLACE PROCEDURE wysocy
IS
BEGIN
INSERT INTO wys_tab(Nazwisko,Wzrost)
SELECT Nazwisko, Wzrost FROM Osoby
ORDER BY Wzrost DESC;
END wysocy;
Rozdział 7. ♦ Procedury składowane 165

W przedstawionym przykładzie definicja tworzonej procedury jest kończona polece-


niem END nazwa;, gdzie nazwa jest jej nazwą. Pominięcie jej w tym poleceniu nie po-
ciąga za sobą żadnych zmian. Kończenie definicji procedury w prezentowany sposób
jest wskazane ze względów organizacyjnych, porządkowych, zwłaszcza wtedy, kiedy
skrypt zawiera większą liczbę złożonych procedur, co utrudnia znalezienie końców de-
finicji przy jego poprawianiu.

Dotychczas prezentowane przykłady były procedurami niepobierającymi żadnych da-


nych z wywołującego je skryptu — były bezparametrowe. Brak parametrów wywoła-
nia jest szczególnie widoczny w drugim przypadku, gdzie do tabeli wys_tab trafiają
wszyscy pracownicy — wszyscy są traktowani jako wysocy. Zmodyfikujmy tę proce-
durę tak, aby do tabeli docelowej przepisywane były tylko osoby wyższe od pewnego
progu danego w postaci parametru.
CREATE OR REPLACE PROCEDURE wysocy
(mm number)
IS
BEGIN
INSERT INTO wys_tab(Nazwisko,wzrost)
SELECT Nazwisko, wzrost FROM Osoby
WHERE wzrost > mm
ORDER BY wzrost DESC;
END wysocy;

Jak pokazano, parametry procedury definiowane są w nawiasie po jej nazwie, a na mi-


nimalną definicję składa się nazwa i typ parametru, przy czym typ podawany jest bez
definiowania rozmiaru, czyli number, a nie number(10). Podanie rozmiaru w tym miej-
scu powoduje wyświetlenie komunikatu o błędzie. Zdefiniowanie parametru procedury
sprawia, że jest on traktowany tak jak zadeklarowana zmienna i nie może zostać zade-
klarowany ponownie w jej ciele. Może on zostać użyty w dowolnym miejscu definicji
procedury; w przedstawionym przykładzie został wykorzystany do sformułowania wa-
runku w klauzuli WHERE zapytania wstawiającego wiersze.

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;

Jak przedstawiono to w dotychczasowych przykładach, zawartość procedury mogą sta-


nowić wszystkie zapytania modyfikujące dane, ale również zapytania tworzące lub mo-
dyfikujące strukturę bazy. Czasami jednak, zamiast wykonywać jakieś operacje na da-
nych, chcemy dokonać operacji zwracającej wynik w postaci zmiennej, np. policzyć czy
166 Część II ♦ ORACLE PL/SQL

podsumować jakąś wielkość zapisaną w bazie. W takim przypadku w ciele procedury


wykorzystujemy bardzo często składnię SELECT ... INTO zmienna. Powoduje ona, że
wyznaczona zapytaniem wartość skalarna jest przypisywana do zmiennej występują-
cej po słowie kluczowym INTO. Zapytanie takie nie skutkuje wyprowadzeniem danych
na standardowe wyjście. Nie jest ono elementem SQL, a co za tym idzie, może być
używane tylko we wnętrzu procedur i funkcji albo w blokach anonimowych PL/SQL.
Jeżeli dokonujemy takiego przypisania do zmiennej, która jest parametrem procedury,
to parametr taki musi mieć sufiks OUT wskazujący, że jest on przekazywany z proce-
dury do miejsca jej wywołania. Jeśli nie zdefiniujemy parametru jako „wychodzącego”,
to próba przypisania mu wartości spowoduje pojawienie się komunikatu o błędzie.
Wszystkie parametry, które nie mają jawnie określonego kierunku przekazywania da-
nych, są domyślnie typu IN, czyli przekazują dane tylko do procedury. Dopuszczalny
jest jeszcze trzeci przypadek, w którym zarówno zmienna przekazywana jest do pro-
cedury, jak i obliczona wewnątrz procedury wartość przekazywana jest na zewnątrz.
W takiej sytuacji zmienna jest opisywana parą IN OUT. Prezentowana procedura zlicza
osoby wyższe, niż to określa wartość progowa dana pierwszym parametrem.
CREATE OR REPLACE PROCEDURE wysocy
(mm number, ile OUT number)
IS
BEGIN
SELECT COUNT(wzrost) INTO ile FROM Osoby
WHERE wzrost > mm;
END wysocy;

Wywołanie tak zdefiniowanej procedury z bloku anonimowego wymaga zadeklaro-


wania zmiennej, do której zostanie przypisany parametr typu OUT. Musi ona mieć typ
zgodny z parametrem formalnym, ale wymagane jest też podanie jego rozmiaru, o ile
typ nie oferuje rozmiaru domyślnego. W prezentowanym przykładzie zadeklarowano
zmienną ile jako number (bo domyślnie number ≡ number(10)). W przypadku zmien-
nych znakowych rozmiar pola musi być dany jawnie (varchar(11)). Wywołania pro-
cedury dokonujemy przez podanie jej nazwy oraz zdefiniowanie wartości parametrów
— czyli podanie listy parametrów aktualnych. Do parametrów typu IN możemy przy-
pisywać zarówno zmienne, jak i stałe, natomiast do parametrów typu OUT musimy
przypisać zmienne. W prezentowanym przykładzie nazwa zmiennej oraz parametru
w definicji procedury jest taka sama, co jest powszechnie stosowaną notacją, choć ze
względów składniowych zgodność nazw nie jest wymagana.
SET SERVEROUTPUT ON;
DECLARE
ile number;
BEGIN
wysocy(1.8, ile);
DBMS_OUTPUT.PUT_LINE(ile);
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

CREATE OR REPLACE PROCEDURE wysocy


(mm number, ile OUT number, sr OUT real)
IS
BEGIN
SELECT COUNT(wzrost), AVG(wzrost) INTO ile, sr FROM Osoby
WHERE wzrost > mm;
END wysocy;

Dla każdego parametru może zostać zdefiniowana wartość domyślna. Wykonujemy to


przez podanie po typie zmiennej słowa kluczowego DEFAULT, po którym ustanawiana
jest wartość. W prezentowanym przykładzie przyjęto, że domyślną wartością progu,
od którego zliczamy osoby wysokie, jest 1.7.
CREATE OR REPLACE PROCEDURE wysocy
(mm NUMBER DEFAULT 1.7, ile OUT NUMBER)
IS
BEGIN
SELECT COUNT(wzrost) INTO ile FROM Osoby
WHERE wzrost > mm;
END wysocy;

Dla procedury ze zdefiniowaną wartością domyślną poprawne jest wywołanie, w któ-


rym podajemy wartość posiadającego ją parametru. Wówczas wartość podana przy
wywołaniu „nadpisuje” się na domyślną.
SET SERVEROUTPUT ON;
DECLARE
ile number;
BEGIN
wysocy(1.8,ile);
DBMS_OUTPUT.PUT_LINE(ile);
END;

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;

W definicji procedury zmieniamy kolejność parametrów tak, że drugi z nich ma przy-


pisaną wartość domyślną. Reszta procedury pozostaje bez zmian.
168 Część II ♦ ORACLE PL/SQL

CREATE OR REPLACE PROCEDURE wysocy


(ile OUT NUMBER, mm NUMBER DEFAULT 1.7)
IS
BEGIN
SELECT COUNT(wzrost) INTO ile FROM Osoby
WHERE wzrost > mm;
END wysocy;

W takim przypadku, ponieważ przy odwołaniu do wartości domyślnej pomijamy ostatni


parametr, dopuszczalne jest zastosowanie wywołania pozycyjnego. Jest ono dozwolone
wtedy, gdy pominięciu podlega n ostatnich parametrów posiadających wartości domyślne.
SET SERVEROUTPUT ON;
DECLARE
ile number;
BEGIN
wysocy(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

W przykładowym skrypcie pokazane zostały poprawne wywołania tej procedury:


w pełni pozycyjne, z dwoma pierwszymi parametrami danymi pozycyjnie oraz z danym
pozycyjnie pierwszym parametrem.
DECLARE
res real;
BEGIN
dziel(10, 9, res);
DBMS_OUTPUT.PUT_LINE('Wynik ' || res);
dziel(10, 9, c => res);
DBMS_OUTPUT.PUT_LINE('Wynik ' || res);
dziel(10, b => 9, c => res);
DBMS_OUTPUT.PUT_LINE('Wynik ' || res);
dziel(10, c => res, b => 9);
DBMS_OUTPUT.PUT_LINE('Wynik ' || res);
END;

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;

W przypadku próby wykonania takiego bloku anonimowego wygenerowany zostanie


komunikat o błędzie w następującej postaci:
Error report:
ORA-06550: linia 12, kolumna 15:
PLS-00312: skojarzenie parametrów przez nazwę może nie implikować skojarzenia przez pozycję
ORA-06550: linia 12, kolumna 1:
PL/SQL: Statement ignored
06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
*Action:

Ciekawą możliwością zastosowania procedury jest wykonywanie zapytań składanych


z fragmentów, np. danych statycznymi napisami czy też zawartych w zmiennej. Zbu-
dujmy procedurę, która w zależności od wartości parametru zmieni sposób zapisu pola
Nazwisko w tabeli Osoby. Wykonanie takiego zapytania, składającego się z fragmentów
napisów, wymaga użycia polecenia EXECUTE IMMEDIATE.
CREATE OR REPLACE PROCEDURE exe_tekst
(typ varchar2)
IS
BEGIN
EXECUTE IMMEDIATE
'UPDATE osoby SET Nazwisko=' || typ || '(Nazwisko)';
END exe_tekst;
170 Część II ♦ ORACLE PL/SQL

Bardziej eleganckim rozwiązaniem jest zastosowanie zmiennej pomocniczej zap, co


oczywiście nie wpływa na sposób wykonania. Przykład ten pokazuje natomiast, że miej-
scem deklaracji zmiennych lokalnych procedury (takich, które są widoczne tylko w jej
ciele) jest obszar między słowami kluczowymi IS oraz BEGIN. Przy deklarowaniu zmien-
nych w tym miejscu nie wolno stosować słowa kluczowego DECLARE.
CREATE OR REPLACE PROCEDURE exe_tekst
(typ varchar2)
IS
zap varchar2(111);
BEGIN
zap:= 'UPDATE osoby SET Nazwisko=' || typ || '(Nazwisko)';
EXECUTE IMMEDIATE zap
END exe_tekst;

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;

W większości sytuacji będziemy jednak stosować klasyczne wywołanie wewnątrz bloku


anonimowego. W takim przypadku zastosowanie w nim zapytania wybierającego zgod-
nie z wymogami składni nie jest dozwolone, jeśli jednak chcemy w ramach tego sa-
mego skryptu wykonać blok anonimowy i zapytanie wybierające, musimy rozdzielić je
znakiem /. Faktycznie powoduje to, że pomimo tego, iż oba elementy zapisane są w tym
samym miejscu, stanowią one dwa przetwarzane po sobie skrypty — jeden PL/SQL,
drugi SQL. Każdy dłuższy skrypt może zostać podzielony znakami / na mniejsze frag-
menty, które będą przetwarzane szeregowo, oczywiście pod warunkiem, że każdy z nich
jest składniowo poprawny.
BEGIN
exe_tekst('INITCAP');
END;
/
SELECT * FROM osoby;

Kolejny przykład przedstawia zastosowanie w definicji procedury wywołania wbudo-


wanej procedury RAISE_APPLICATION_ERROR, która powoduje wygenerowanie (ustano-
wienie) błędu użytkownika o numerze danym pierwszym parametrem oraz komuni-
kacie stanowiącym drugi z parametrów. Należy zauważyć, że numeracja błędów w Oracle
przebiega przez wartości ujemne, a dla błędów użytkownika zarezerwowano przedział
<–29999, –20001>. Wywołanie powyższej procedury powoduje przerwanie działania
programu i wyświetlenie komunikatu o błędzie.
Rozdział 7. ♦ Procedury składowane 171

CREATE OR REPLACE PROCEDURE Blad


IS
BEGIN
RAISE_APPLICATION_ERROR (-20205, 'Błąd programu');
END blad;

Bardziej złożonym przykładem zastosowania RAISE_APPLICATION_ERROR jest wykorzy-


stanie jej podczas wykonywania procedury o ograniczonym zestawie danych wej-
ściowych, co ma miejsce np. w opracowanej poprzednio procedurze exe_tekst. Jeżeli
parametr wejściowy nie jest jednym z dopuszczalnych elementów wymienionych na
liście, generowany jest błąd z odpowiednim komentarzem. W przeciwnym przypadku
wykonywane jest za pomocą polecenia EXECUTE IMMEDIATE zapytanie. Należy zwrócić
uwagę na fakt, że do porównania z elementami listy użyto zmiennej przetworzonej do
postaci pisanej dużymi literami UPPER(typ), gdyż przy porównywaniu łańcuchów
Oracle rozróżnia ich wielkość. Pozwala to na podanie w wywołaniu procedury nazwy
funkcji modyfikującej łańcuch pisanej w dowolny sposób (literami małymi, dużymi oraz
różnej wielkości).
CREATE OR REPLACE PROCEDURE exe_tekst
(typ varchar2)
IS
BEGIN
IF UPPER(typ) NOT IN('UPPER', 'LOWER', 'INITCAP') THEN
RAISE_APPLICATION_ERROR (-20205, 'Zła funkcja');
ELSE
EXECUTE IMMEDIATE
'UPDATE osoby SET Nazwisko=' || typ || '(Nazwisko)';
END IF;
END exe_tekst;

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;

W środowisku Oracle zdefiniowano wiele wyjątków, których nazwy symboliczne i przy-


czyny wystąpienia zawiera tabela 7.1. Jeżeli gdziekolwiek w ciele procedury występuje
sytuacja wyjątkowa, przetwarzanie przenoszone jest do sekcji obsługi wyjątków. Naj-
pierw sprawdzane jest to, czy wyjątek, który przerwał przetwarzanie, jest zgodny z nazwą
symboliczną występującą w sekcji EXCEPTION. Następnie wykonywane są instrukcje znaj-
dujące się po słowie kluczowym THEN jego obsługi, po czym przerywane jest przetwa-
rzanie procedury i następuje powrót do miejsca, z którego została ona wywołana. Jeśli
w trakcie przetwarzania procedury pojawią się jednocześnie dokładnie dwa wyjątki,
co jest możliwe, chociaż mało prawdopodobne, to obsłużony zostanie tylko ten, który
występuje jako pierwszy na liście w sekcji obsługi wyjątków. Na szczególną uwagę
zasługuje wyjątek o nazwie OTHERS, który obsługuje wszystkie inne, dotąd nieobsłu-
żone. Gdyby znalazł się on na pierwszym miejscu listy obsługi wyjątków, to bez względu
na to, jakie zdarzenie powodowałoby wystąpienie sytuacji wyjątkowej, wykonywana
byłaby zawsze ta sama sekcja znajdująca się po OTHERS. Od wersji 9. Oracle umiesz-
czanie obsługi wyjątku OTHERS przed obsługą jakiegokolwiek innego jest zabronione
składniowo. Innymi słowy, jego obsługa musi być ostatnim elementem sekcji obsługi
wyjątków. W prezentowanym przykładzie zastosowano minimalną obsługę wyjątku
(sekcja nie może być pusta) — NULL, nie rób nic. Bez względu na to, czy wyjątki są w pro-
cedurze obsługiwane, czy też nie, zasady jej wywołania pozostają bez zmian.
Rozdział 7. ♦ Procedury składowane 173

Tabela 7.1. Wykaz najczęściej występujących wyjątków serwera


Nazwa wyjątku Numer błędu Opis
NO_DATA_FOUND ORA-01403 Jednowierszowe zapytanie wybierające SELECT nie zwróciło
danych.
TOO_MANY_ROWS ORA-01422 Zapytanie wybierające SELECT zwróciło więcej niż jeden wiersz.
INVALID_CURSOR ORA-01001 Niedopuszczalna operacja na kursorze.
ZERO_DIVIDE ORA-01476 Próba dzielenia przez zero.
DUP_VAL_ON_INDEX ORA-00001 Próba wstawienia powtarzającej się wartości w pole,
dla którego ustanowiono indeks unikatowy.
INVALID_NUMBER ORA-01722 Konwersja łańcucha na liczbę zakończyła się niepowodzeniem.
CURSOR_ALREADY_OPEN ORA-06511 Próba otwarcia kursora, który już został otwarty.
LOGIN_DENIED ORA-01017 Próba zalogowania się z nieodpowiednim hasłem lub loginem.
NOT_LOGGED_ON ORA-01012 Próba wykonania polecenia operującego na bazie danych
bez uprzedniego zalogowania się.
PROGRAM_ERROR ORA-06501 PL/SQL napotkał wewnętrzny problem podczas przetwarzania.
STORAGE_ERROR ORA-06500 PL/SQL dysponuje zbyt małymi zasobami pamięci
lub pamięć została uszkodzona.
TIMEOUT_ON_RESOURCE ORA-00051 Został przekroczony czas oczekiwania na odpowiedź
bazy danych.
ACCESS_INTO_NULL ORA-06530 Próba przypisania do zmiennej wartości niezainicjowanej
(NULL).
CASE_NOT_FOUND ORA-06592 Żadna z wartości określonych warunkami WHEN w poleceniu
CASE nie jest prawdziwa, a nie występuje sekcja ELSE.

Wywołanie pozycyjne procedury może mieć postać:


SET SERVEROUTPUT ON;
DECLARE
kto NUMBER;
status NUMBER;
BEGIN
kto:=1;
czy_jest (kto, status);
DBMS_OUTPUT.PUT_LINE(status);
END;

Wywołanie nazewnicze można zrealizować według schematu:


SET SERVEROUTPUT ON;
DECLARE
kto NUMBER;
status NUMBER;
BEGIN
kto := 1;
czy_jest (status => status, num => kto);
DBMS_OUTPUT.PUT_LINE(status);
END;
174 Część II ♦ ORACLE PL/SQL

Przedstawiony poprzednio przykład jest niewątpliwie akademicki, ponieważ wyko-


rzystuje sekcję obsługi wyjątków do wyznaczania parametru OUT. W praktyce, jeśli
w tym miejscu jest wyznaczany jakiś parametr wyjściowy, to jest on odpowiedzialny
za kodowanie sposobu zakończenia przetwarzania, np. 0 — przetwarzanie zakończone
sukcesem, <>0 — przetwarzanie zakończone niepowodzeniem; konkretna wartość ko-
duje przyczynę. Naszą procedurę moglibyśmy więc doprowadzić do postaci, w której
wykorzystywalibyśmy funkcję agregującą COUNT.
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 OTHERS THEN
ok := 99;
DBMS_OUTPUT.PUT_LINE('Błąd przetwarzania');
END;

Zamiana zapytania wybierającego na takie, które zawiera funkcję zliczającą rekordy,


spowoduje, że jeśli pracownik o danym numerze istnieje, to policzony zostanie 1 re-
kord, a jeśli nie istnieje — 0 rekordów. Jak widać, zmiana taka sprawia, że przypisanie
może od razu dotyczyć zmiennej wychodzącej oraz że wyjątek NO_DATA_FOUND nie po-
jawia się. Zawsze jednak możliwe jest wystąpienie innych błędów przetwarzania, stąd
obsługa wyjątku OTHERS, w której ustawiono zmienną ok na wartość różną od zera, w tym
przypadku 99, co koduje stan pojawienia się błędu. Zwyczajowo pierwszą linijką ciała
procedury jest ustawienie zmiennej kodującej sposób wykonania na 0 — przetwarza-
nie zakończone poprawnie.

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.

Bardzo często wykorzystujemy przy obsłudze wyjątków dwie wbudowane funkcje


PL/SQL: SQLCODE — zwracającą numer wyjątku (błędu), oraz SQLERRM — wyświetla-
jącą związany z tym błędem (wyjątkiem) komunikat. Sposób ich zastosowania ilustruje
następny przykład.
176 Część II ♦ ORACLE PL/SQL

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
DBMS_OUTPUT.PUT_LINE('Nie ma takich');
DBMS_OUTPUT.PUT_LINE('kod - ' || SQLCODE);
DBMS_OUTPUT.PUT_LINE('opis - ' || SQLERRM);
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;

Niestety, w tym miejscu czeka na programistę niemiła niespodzianka. Wszystkie wyjątki


zdefiniowane przez użytkownika mają taki sam numer (–1) oraz komunikat (Wyjątek
użytkownika). Mogą przez to stać się nierozróżnialne, jeśli w procedurze będzie ich
więcej niż jeden. W takim przypadku możemy zainicjować wyjątek użytkownika wyjąt-
kiem systemowym, stosując dyrektywę PRAGMA EXCEPTION_INIT. Posiada ona dwa pa-
rametry: pierwszym jest nazwa inicjowanego błędu użytkownika, a drugim reprezen-
tujący go numer błędu systemowego. Od tej chwili błąd użytkownika będzie przejmował
po błędzie systemowym jego atrybuty: numer i komunikat.
CREATE OR REPLACE PROCEDURE licz
(mini NUMBER, ile out INT)
IS
brakuje EXCEPTION;
PRAGMA EXCEPTION_INIT(brakuje,-13467);
BEGIN
SELECT COUNT(IdOsoby) INTO ile FROM Osoby
WHERE Wzrost > mini;
IF (ile = 0) THEN
RAISE brakuje;
END IF;
EXCEPTION
WHEN brakuje THEN
DBMS_OUTPUT.PUT_LINE('Nie ma takich');
DBMS_OUTPUT.PUT_LINE('kod - ' || SQLCODE);
DBMS_OUTPUT.PUT_LINE('opis - ' || SQLERRM);
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;

Takie rozwiązanie ma tę wadę, że musimy dopasowywać komunikat systemowy do wła-


snych potrzeb, co nie zawsze jest łatwe. Pozostaje więc zrezygnować z definiowania błędu
użytkownika na rzecz stosowania bezpośrednio polecenia RAISE_APPLICATION_ERROR
lub opisanej poprzednio procedury Blad, która to polecenie zawiera — wtedy obsługa
nastąpi w sekcji OTHERS. Możliwe jest również korzystanie przy obsłudze błędu użyt-
kownika z polecenia RAISE_APPLICATION_ERROR w sekcji obsługi wyjątków.
Rozdział 7. ♦ Procedury składowane 177

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_APPLICATION_ERROR (-20001, 'Nie ma takich');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;

Wymuszenie wystąpienia błędu ad hoc w sekcji obsługi wyjątków spowoduje jednak


jego propagację do miejsca wywołania i wtedy powinniśmy umieścić, np. w bloku
anonimowym, sekcję obsługi wyjątków obsługującą taki przypadek — choćby na po-
ziomie zdarzenia OTHERS.
SET SERVEROUTPUT ON;
DECLARE ile NUMBER;
BEGIN
licz(1.8, ile);
DBMS_OUTPUT.PUT_LINE (ile);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLCODE);
END;

Z czasem, kiedy będziemy tworzyli coraz bardziej skomplikowane elementy procedu-


ralne PL/SQL (procedury, funkcje), coraz większe będzie ryzyko, że podczas ich ge-
nerowania popełnimy błędy formalne, składniowe. Standardowo, jeśli w procedurze
znajdą się takie błędy, otrzymamy komunikat: Procedura utworzona z błędami kom-
pilacji. Jeśli chcemy otrzymać bardziej złożoną informację o popełnionych podczas
tworzenia ostatniego elementu proceduralnego błędach, możemy wykonać polecenie:
SHOW ERRORS;

Pomimo że wygenerowana w ten sposób informacja jest zdecydowanie bardziej szcze-


gółowa, należy z dużym dystansem podchodzić do wskazywanych linii kodu, w których
wykryto nieprawidłowości. Bardzo często wskazanie to wynika z wcześniej popełnio-
nych błędów. Prostym wnioskiem jest ten, że procedurę powinniśmy poprawiać, po-
cząwszy od błędów najwcześniej wykrytych, co w większości przypadków daje po-
prawne rezultaty.
178 Część II ♦ ORACLE PL/SQL
Rozdział 8.
Funkcje w PL/SQL
Kolejnym elementem proceduralnym w PL/SQL są funkcje. Podobnie jak w przypadku
procedury składowanej, jeśli funkcja o nazwie, którą chcemy nadać, już istnieje, naj-
pierw musi ona zostać usunięta.
DROP FUNCTION Liczf;

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.

Rysunek 8.1. SQL Developer po wykonaniu zapytania tworzącego funkcję

Podstawową różnicą składniową między procedurą a funkcją jest fakt, że ta druga


zawsze zwraca wartość „przez nazwę” i dlatego po liście parametrów, po słowie klu-
czowym RETURN, musi mieć zdefiniowany typ zwracanej wartości. Dla przykładu prze-
kształćmy procedurę zliczającą osoby wyższe, niż określa to parametr, na funkcję, która
liczbę osób spełniających to kryterium zwróci „przez nazwę”.
180 Część II ♦ ORACLE PL/SQL

CREATE OR REPLACE FUNCTION Liczf (mini NUMBER)


RETURN INT
IS
ile INT;
BEGIN
SELECT COUNT(IdOsoby) INTO Ile FROM Osoby
WHERE Wzrost > mini;
RETURN ile;
END;

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;

Przykład efektu wywołania funkcji z SQL Developera w zależności od aktywnej za-


kładki przedstawiają rysunki 8.2 oraz 8.3.

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

Podczas pracy w środowisku klienckim należy pamiętać, że kolejne wykonania skryptu


nie powodują automatycznego czyszczenia standardowego wyjścia (rysunek 8.4). Tę
czynność użytkownik musi wykonywać manualnie.

Rysunek 8.4. SQL Developer po wykonaniu kolejnego skryptu

Podobnie wygląda sprawa, jeśli stosujemy końcówkę klienta w wersji 10g lub starszej,
czyli SQL*Plus Worksheet (rysunek 8.5).

W wielu środowiskach funkcja może przekazywać wartość do miejsca wywołania przez


nazwę, natomiast do jej wnętrza parametry przekazujemy przez ich listę. W PL/SQL
dozwolone jest stosowanie na liście parametrów typu OUT, tak więc i tą drogą dane mogą
być przekazywane do miejsca wywołania.
CREATE OR REPLACE FUNCTION Liczf
(mini NUMBER, ilep OUT NUMBER)
RETURN INT
IS
182 Część II ♦ ORACLE PL/SQL

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;

Powyżej przedstawiono dość akademicki przykład, gdzie wartość wyznaczona zapy-


taniem przypisana jest do parametru ilep. Wywołanie takiej funkcji spowoduje, że na
zewnątrz obie zmienne będą miały zawsze tę samą wartość.
SET SERVEROUTPUT ON;
DECLARE
ile NUMBER;
ilep number;
BEGIN
ile:=liczf(1.8, ilep);
DBMS_OUTPUT.PUT_LINE (ile);
DBMS_OUTPUT.PUT_LINE (ilep);
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;

Powróćmy do pomysłu przekazywania wartości tylko przez nazwę funkcji i utwórzmy


funkcję, która oblicza sumę wypłat Brutto dla pracownika wskazanego parametrem kto.
Parametr ten został zdefiniowany jako zmienna typu obiektowego Zarobki.IdOsoby%TYPE
Rozdział 8. ♦ Funkcje w PL/SQL 183

— 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;

Podobnie jak w przypadku procedur, również w funkcjach możemy dokonywać ob-


sługi wyjątków. Ogólna idea pozostaje niezmieniona, zarówno w stosunku do błędów
systemowych, błędów definiowanych przez użytkownika i ich inicjacji, jak i propaga-
cji błędów do poziomu wywołania funkcji. Jest tylko jedna istotna różnica. Ponieważ
funkcja zawsze musi przekazywać wartość przez nazwę, w sekcji obsługi wyjątków dla
każdego z nich musi pojawić się słowo kluczowe RETURN i określenie zwracanej war-
tości. Jeżeli nie dopełnimy tego dla któregoś z błędów, to w przypadku jego wystąpie-
nia, zamiast obsługi tego wyjątku, pojawi się błąd w miejscu wywołania funkcji, a prze-
twarzanie zostanie przerwane z komunikatem Powrót z funkcji bez wartości. Prosty
przypadek obsługi wyjątków w funkcji prezentuje kolejny przykład.
CREATE OR REPLACE FUNCTION Liczf
(mini NUMBER) RETURN INT
IS
ile INT;
Brakuje EXCEPTION;
BEGIN
SELECT COUNT(IdOsoby) INTO Ile FROM Osoby
WHERE Wzrost > mini;
IF (ile = 0) THEN
RAISE brakuje;
END IF;
RETURN ile;
EXCEPTION
WHEN brakuje THEN
DBMS_OUTPUT.PUT_LINE('nie ma takich');
DBMS_OUTPUT.PUT_LINE('kod - ' || SQLCODE);
DBMS_OUTPUT.PUT_LINE('opis - ' || SQLERRM);
RETURN ile;
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLCODE);
RETURN ile;
END;
184 Część II ♦ ORACLE PL/SQL

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;

W obsłudze wyjątków zastosowano dla błędów systemowych kodowanie z użyciem


funkcji SQLCODE, natomiast dla błędów użytkownika kodowanie statyczne (wartości do-
datnie), którego znaczenie powinno być zawarte w dokumentacji.

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

Zastosowano tu podwójną zagnieżdżoną konwersję: wewnętrzna konwersja TO_DATE


powoduje zamianę liczby na datę kalendarza juliańskiego, która na zewnątrz konwerto-
wana jest na napis TO_CHAR. W celu wyprowadzenia wartości w zapytaniu wybierają-
cym odwołano się do pomocniczej tabeli DUAL ze schematu SYSTEM. Jest to tabela
zawierająca jedno pole X o jednym wierszu, którego wartość stanowi ′D′. Skutek działa-
nia tego zapytania ilustruje tabela 8.1.

Tabela 8.1. Efekt wykonania zapytania dokonującego konwersji liczby na napis


TO_CHAR(TO_DATE(12345,’D’),’JSP’)
TWELVE THOUSAND THREE HUNDRED FORTY-FIVE

Drugi przykład wynika z braku zdefiniowania w Oracle funkcji wybierającej z daty


jej część. Jeśli chcemy uzyskać np. zawarty w dacie rok, musimy skorzystać z wielo-
krotnej konwersji. Nieco nadmiarowy kod powoduje wybranie roku z daty bieżącej,
którą zwraca funkcja systemowa SYSDATE.
Rozdział 8. ♦ Funkcje w PL/SQL 185

SELECT
TO_NUMBER(SUBSTR(TO_CHAR(SYSDATE, 'yyyymmdd'),1,4)) AS Rok
FROM DUAL;

W przykładzie najpierw dokonano konwersji daty na napis według formatu 'yyyymmdd'


(cztery cyfry reprezentujące rok, dwie oznaczające numer miesiąca oraz dwie repre-
zentujące numer dnia w miesiącu), po czym za pomocą funkcji SUBSTR wycięto z tego
napisu 4 znaki, począwszy od znaku 1 — czyli wybrano numer roku. Ponieważ jest to
napis, dokonano jego konwersji na liczbę przy użyciu TO_NUMBER. Ostatnia konwersja
była wymagana w starszych wersjach ze względu na kłopot z numeryczną reprezenta-
cją napisów, jednak od wersji 9. takie problemy nie występują i dlatego uproszczona
postać poprzednio prezentowanego zapytania może wyglądać następująco:
SELECT
TO_CHAR(SYSDATE, 'yyyy') AS Rok
FROM DUAL;

Pominięcie pozostałych elementów konwersji pozostawiam uważnemu czytelnikowi do


samodzielnego uzasadnienia. Według tego samego schematu możemy, rzecz jasna, wy-
dobyć z daty np. numer miesiąca.
SELECT
TO_CHAR(SYSDATE, 'mm') AS Miesiac
FROM DUAL;

Inne operacje na dacie z wykorzystaniem przedstawionego schematu wydają się dość


oczywiste.
186 Część II ♦ ORACLE PL/SQL
Rozdział 9.
Pakiety
Bardzo ważnym aspektem jest możliwość zapanowania nad dużą liczbą procedur i funkcji
stworzonych przez użytkownika. Przesłanka uporządkowania sposobu organizacji opro-
gramowania użytkownika tworzonego po stronie serwera jest ważnym argumentem
przemawiającym na korzyść tworzenia pakietów. Pakiet stanowi swego rodzaju „opa-
kowanie” łączące kilka (kilkanaście) procedur lub funkcji. Z reguły posiadają one
pewien zestaw łączących je cech, np. przeznaczenie, funkcjonalność, ale nie stanowi to
wymogu formalnego. Pakiet składa się z dwóch części: deklaracji oraz ciała, w związku
z czym jego tworzenie odbywa się w dwóch etapach. W pierwszym tworzymy jego de-
klarację, nagłówek. Podobnie jak poprzednie elementy programistyczne, możemy ist-
niejący już pakiet usunąć.
DROP PACKAGE pakiet;

Możliwe jest również zastosowanie składni CREATE OR REPLACE nazwa_pakietu.


CREATE OR REPLACE PACKAGE pakiet
IS
FUNCTION liczf1(mini INT, ile OUT INT) RETURN INT;
FUNCTION liczf2(ile OUT INT) RETURN INT;
END;

Pakiet zawiera nagłówki elementów proceduralnych (procedur składowanych, funkcji)


w nim zawartych. W przykładzie stworzono pakiet, w którym znajdują się dwie funkcje
o nazwach liczf1 i liczf2. Każdy pakiet wymaga też utworzenia ciała (PACKAGE BODY),
w którym umieszczane są ciała wszystkich funkcji i procedur zdefiniowanych w jego
deklaracji.
CREATE OR REPLACE PACKAGE BODY pakiet
IS

FUNCTION liczf1(mini INT, ile OUT INT) RETURN INT


IS
BEGIN
SELECT COUNT(Idosoby) INTO ile FROM Osoby
WHERE RokUrodz > mini;
RETURN 0;
EXCEPTION
WHEN OTHERS THEN
188 Część II ♦ ORACLE PL/SQL

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;

W zaprezentowanym przykładzie w ciele pakietu stworzone zostały, zgodnie z zawartą


w nim deklaracją, dwie funkcje. Pierwszą jest liczf1, która liczy osoby urodzone po
roku danym parametrem mini i przekazuje ich liczbę na zewnątrz za pomocą zmien-
nej ile; przez nazwę przekazywany jest status wykonania tej funkcji. Druga funkcja
to liczf2, która zlicza osoby powyżej średniego roku urodzenia i przekazuje tę war-
tość na zewnątrz z wykorzystaniem zmiennej ile; przez nazwę przekazywany jest status
jej wykonania. Każda ze zdefiniowanych funkcji posiada własną sekcję obsługi wy-
jątków, w obu przypadkach reprezentowaną przez obsługę błędu OTHERS.

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

Pozostaje wykonanie analogicznej zmiany w ciele pakietu. Również tam zastosowanie


takich samych nazw dla obu funkcji okazuje się dopuszczalne składniowo.
CREATE OR REPLACE PACKAGE BODY pakiet
IS

FUNCTION liczf(mini INT, ile OUT INT) RETURN INT


IS
BEGIN
SELECT COUNT(Idosoby) INTO ile FROM Osoby
WHERE RokUrodz > mini;
RETURN 0;
EXCEPTION
WHEN OTHERS THEN
RETURN SQLCODE;
END;
FUNCTION liczf(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;
BEGIN
NULL;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END;

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 prezentowanym przykładzie raz została wywołana funkcja zawierająca jeden pa-


rametr, natomiast funkcja mająca dwa parametry została wywołana dwukrotnie, za każ-
dym razem z inną wartością parametru wchodzącego IN. Idea przeciążania wydaje się
więc prosta. Niestety komplikacje pojawiają się wtedy, gdy w funkcjach (procedurach)
przeciążanych chcemy zdefiniować domyślne wartości parametrów. Właściwie kłopotu
nie sprawia zadeklarowanie wartości domyślnej, ale próba użycia jej przy wywoła-
niu. Jeżeli będziemy chcieli skorzystać z wywołania pozycyjnego (umieściliśmy warto-
ści domyślne na końcu listy parametrów), to może się okazać, że przy wywołaniu pro-
cedury (funkcje) przeciążone upodobniły się do siebie na tyle, że nie jest możliwe ich
rozróżnienie. Powoduje to powstanie błędu przetwarzania. Lepiej wygląda sytuacja
przy wywołaniu nazewniczym, gdzie jawnie podajemy nazwę zmiennej, do której przy-
pisujemy wartość, i gdzie do takiego upodobnienia nie powinno dochodzić. Możemy się
dodatkowo wspierać, różnicując nazwy parametrów przeciążanych procedur (funkcji).
Nie ma to wpływu na wymagania dotyczące przeciążania (różne typy), a może pomóc
przy identyfikacji podczas wywoływania procedur (funkcji) w obecności wartości do-
myślnych (przy zastosowaniu schematu nazewniczego).
Rozdział 9. ♦ Pakiety 191

Stosowanie funkcji (procedur) przeciążonych jest powszechnym zwyczajem progra-


mistycznym. W prezentowanych dotąd rozważaniach uważny czytelnik mógł już wy-
kryć przypadek ogólnie stosowanej systemowej procedury przeciążonej. Jest nią pro-
cedura o nazwie PUT_LINE występująca w systemowym pakiecie DBMS_OUTPUT.
Posiada ona trzy warianty: dla zmiennych typu numerycznego, znakowego oraz daty.
Można to sprawdzić, oglądając definicję tego pakietu zawartą w schemacie SYSTEM.

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;

Natomiast w ciele pakietu, oprócz obowiązkowych definicji funkcji (procedur), któ-


rych nagłówki w nim przedstawiono, oraz nieobowiązkowej sekcji inicjalnej, możliwe
jest zadeklarowanie zmiennych globalnych wewnętrznych. Do zmiennych tego typu
nie można odwoływać się z zewnątrz pakietu, ale każda funkcja (procedura) wystę-
pująca w nim może ich używać. Jest to dobra metoda przekazywania wartości między
elementami tego samego pakietu, bez potrzeby dodatkowego wywoływania procedury
czy funkcji. W przykładzie przedstawiono deklarację wewnętrznej zmiennej globalnej
o nazwie globalna_wew.
CREATE OR REPLACE PACKAGE BODY pakiet
IS
globalna_wew INT;
FUNCTION liczf(mini INT, ile OUT INT) RETURN INT
IS
BEGIN
SELECT COUNT(Idosoby) INTO ile FROM Osoby
WHERE RokUrodz > mini;
RETURN 0;
EXCEPTION
WHEN OTHERS THEN
RETURN SQLCODE;
END;
FUNCTION liczf(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;
BEGIN
globalna := 12;
globalna_wew := 11;
EXCEPTION
192 Część II ♦ ORACLE PL/SQL

WHEN OTHERS THEN


RAISE;
END;

W przykładzie umieszczone zostały instrukcje przypisania wartości obu zmiennym


globalnym w sekcji inicjalizacyjnej. Sekcja ta jest wykonywana zawsze przed pierw-
szym wywołaniem dowolnego elementu programistycznego (procedury, funkcji) w ra-
mach pojedynczej sesji. Każde następne wywołanie (do momentu jej zamknięcia) nie
pociąga za sobą ponownego wykonania sekcji inicjalizacyjnej, stąd została ona wyko-
rzystana do ustawienia wartości początkowych zmiennych globalnych obu rodzajów.
Poza zmiennymi globalnymi każda z procedur (funkcji) może zawierać dowolną liczbę
zmiennych lokalnych deklarowanych w sekcjach deklaracji każdej z nich. Przeanali-
zujmy teraz przykład wywołania pakietu z określoną zmienną globalną.
SET SERVEROUTPUT ON;
DECLARE
ile INT;
blad INT;
BEGIN
DBMS_OUTPUT.PUT_LINE(pakiet.globalna);
blad:=pakiet.liczf(ile);
DBMS_OUTPUT.PUT_LINE(ile);
DBMS_OUTPUT.PUT_LINE(blad);
DBMS_OUTPUT.PUT_LINE(pakiet.globalna);
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;

Zmodyfikujmy teraz ciało pakietu. Ponieważ w poprzednich przykładach pokazane


zostały definicje przeciążonych funkcji liczf, zostaną one zastąpione kropkami. Zde-
finiowana w ciele funkcja zmien ma dwa parametry: pierwszy ma przekazywać nazwę
pola typu znakowego z tabeli Osoby, a drugi w postaci jednoznakowej ma kodować
rodzaj operacji wykonywanej na tym polu (L — przepisanie małymi literami, U — prze-
pisanie dużymi literami, I — przepisanie od dużej litery; w przypadku podania innego
znaku pole powinno pozostać bez zmian). Funkcja powinna być niewrażliwa na wiel-
kość liter wprowadzanych przez parametr kod, jednak aby możliwe było wykonanie
tego zadania, konieczne jest odkodowanie symboli przekazywanych poprzez zmienną
kod do postaci nazw odpowiednich funkcji znakowych (LOWER, UPPER, INITCAP). Tę ope-
rację wykonuje funkcja dekoduj, która przyjmuje parametr kod przekształcony do po-
Rozdział 9. ♦ Pakiety 193

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

Jeśli uważnie przeanalizujemy kod pakietu, to stwierdzimy, że definicja funkcji dekoduj


w jego ciele występuje po definicji funkcji zmien. Gdyby nie poczyniono żadnych do-
datkowych kroków, próba stworzenia ciała takiego pakietu zakończyłaby się niepo-
wodzeniem, ponieważ wywołanie funkcji następuje przed miejscem, w którym jest
ona zdefiniowana. Najprostszym rozwiązaniem jest przeniesienie w obrębie ciała pa-
kietu funkcji dekoduj przed funkcję zmien. W prezentowanym przykładzie jest to do-
puszczalne, jednak nie zawsze takie działanie jest możliwe. Ma to miejsce wtedy, gdy
w ciele pakietu istnieją dwie funkcje wzajemnie siebie wywołujące (rekurencja). Pro-
stym rozwiązaniem takiego problemu jest przeniesienie funkcji rekurencyjnie się wy-
wołujących do różnych pakietów. Co prawda, przy kompilacji pierwszego z nich po-
jawią się błędy wynikające z odwołania się do nieistniejącego obiektu, jednak utworzenie
drugiego pakietu zawierającego drugą z funkcji i ponowna kompilacja pierwszego
skutecznie tę kwestię zamyka. Bardziej finezyjnym rozwiązaniem jest zastosowanie tzw.
forwardu, to jest zapowiedzi istnienia w ciele pakietu definicji funkcji (procedury).
Składa się on z jej nagłówka z zamieszczoną listą parametrów. Takiego rozstrzygnięcia
użyto w przykładzie zaprezentowanym powyżej. Forward (zapowiedź) ma zastosowanie
tylko do wewnętrznych funkcji (procedur) pakietu. Jego rolę dla pozostałych funkcji
(procedur) pełni ich definicja w pakiecie. Ponieważ ciało pakietu jest jego „następni-
kiem”, występujące tam funkcje już „wiedzą” o istnieniu wszystkich zadeklarowanych
w nim obiektów. Poniżej zaprezentowano prosty przykład wywołania funkcji zmien.
SET SERVEROUTPUT ON;
DECLARE
ile INT;
blad INT;
BEGIN
Blad := pakiet.zmien('Nazwisko', 'u');
END;

Jak już wspomniano, pakiety są powszechnie stosowanym sposobem kodowania opro-


gramowania po stronie serwera. Dotyczy to również oprogramowania oferowanego
razem z systemem. Omówienie, choćby szczątkowe, funkcjonalności wszystkich do-
stępnych w systemie pakietów wymagałoby opracowania książki znacznie obszerniej-
szej niż prezentowana. Szeroka skala funkcji, jakie spełniają pakiety w Oracle (admi-
nistracja, metody numeryczne, analiza danych multimedialnych, hurtownie danych,
zgłębianie danych, zapis i analiza danych graficznych itd.) spowodowałaby, że takie
opracowanie byłoby mało użyteczne dla czytelnika. Stąd, dla zachęty do poszukiwań
oraz jako zapowiedź kolejnych przygotowywanych przez autora publikacji, krótkie
przykłady zastosowania jednego z prostszych pakietów — DBMS_RANDOM. Zawiera
on kilka funkcji pozwalających na generowanie wartości pseudolosowych. Funkcja
RANDOM tworzy liczby pseudolosowe z przedziału jednostronnie domkniętego <0, 1).
Aby generator zapewniał tworzenie niepowtarzalnych ciągów wartości dla kolejnych
wywołań, powinien zostać zainicjowany przy użyciu funkcji SEED(liczba).
SET SERVEROUTPUT ON;
DECLARE
aaa NUMBER;
BEGIN
DBMS_RANDOM.SEED (TO_CHAR(SYSDATE, 'MM-DD-YYYY HH24:MI:SS'));
aaa := DBMS_RANDOM.RANDOM;
DBMS_OUTPUT.PUT_LINE(aaa);
END;
Rozdział 9. ♦ Pakiety 195

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;

Również funkcja VALUE(min, max) ma bezparametrową postać przeciążoną, która ge-


neruje liczby z takiego samego zakresu jak funkcja RANDOM. Można też generować liczby
zgodnie z rozkładem normalnym Gaussa.
SET SERVEROUTPUT ON;
DECLARE
aaa NUMBER;
BEGIN
aaa := DBMS_RANDOM.NORMAL
DBMS_OUTPUT.PUT_LINE(aaa);
END;

Poza wartościami numerycznymi możliwe jest generowanie pseudolosowych ciągów


znaków. Funkcjonalność taką oferuje funkcja STRING(dlugosc, format), gdzie dlugosc
jest liczbą określającą długość tworzonego łańcucha, natomiast format określa sposób
generowania znaków:
 ‘a’, ‘A’ — znaki alfabetu o różnej wielkości;
 ’l’, ‘L’ — znaki alfabetu, tylko małe litery;
 ’u’, ‘U’ — znaki alfabetu, tylko duże litery;
 ’x’, ‘X’ — znaki alfanumeryczne;
 ’p’, ‘P’ — jakiekolwiek widoczne znaki (bez znaków sterujących).
SET SERVEROUTPUT ON;
DECLARE
aaa VARCHAR2(5);
BEGIN
aaa := DBMS_RANDOM.STRING('U', 5);
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

już spowodowało przepisanie prawie wszystkich nazwisk do zadanej postaci. Przyj-


rzyjmy się teraz miejscu wyzwolenia triggera — przed wstawieniem rekordu (BEFORE
UPDATE). Zapytanie UPDATE wykona się wtedy, zanim nowy wiersz zostanie wstawiony.
Spowoduje to, że dużymi literami nie zostanie przepisane nazwisko tego wiersza (wier-
szy, jeśli zastosowano INSERT ... SELECT zamiast INSERT ... VALUES). Rozwiązanie
tego problemu wymaga tylko zmiany miejsca wyzwolenia triggera na AFTER.
CREATE OR REPLACE TRIGGER up_tr
AFTER INSERT
ON Osoby
BEGIN
UPDATE Osoby SET Nazwisko = UPPER(Nazwisko);
END;

Niestety, nadmiarowe przetwarzanie danych w dalszym ciągu występuje. Co więcej,


ochrona przed zapisaniem nazwiska inaczej niż dużymi literami nie jest pełna, ponie-
waż po wstawieniu wiersza możemy zawsze użyć polecenia UPDATE i przepisać wy-
brane nazwiska według innego formatu (małymi literami, od dużej etc.). Oczywiście
taki stan będzie trwał tylko do czasu wstawienia kolejnego rekordu. Możemy go zmie-
nić, dodając do listy zdarzeń aktualizację danych. W skrajnym przypadku trigger może
zostać wyzwolony wszystkimi trzema zapytaniami modyfikującymi. Nazwy zdarzeń
wyzwalających trigger muszą być połączone operatorem logicznym OR.
CREATE OR REPLACE TRIGGER up_tr
AFTER INSERT OR UPDATE
ON Osoby
BEGIN
UPDATE Osoby SET Nazwisko = UPPER(Nazwisko);
END;

Pomimo pozorów poprawności, takie rozwiązanie daje względnie pozytywny rezultat


dla wstawiania nowych wierszy, tak jak to pokazano poprzednio. Niestety, w Oracle
mamy do czynienia z wyzwalaniem wyzwalacza poleceniami występującymi w jego
wnętrzu. Jeżeli nastąpi modyfikacja danych UPDATE, to polecenie UPDATE występujące
we wnętrzu wyzwalacza spowoduje ponowne uruchomienie jego samego. Mamy tutaj
do czynienia z klasycznym przykładem zapętlenia. Teoretycznie pętla taka powinna
się wykonywać bez końca, jednak, na skutek zastosowania blokady maksymalnej liczby
zagnieżdżeń, po przekroczeniu wartości progowej (domyślnie 50 poziomów rekurencji)
przetwarzanie kończy się wielolinijkowym, złożonym komunikatem o błędzie.
Error starting at line 1 in command:
UPDATE Osoby SET Nazwisko=UPPER(Nazwisko)
Error report:
SQL Error: ORA-00036: przekroczono maksymalną liczbę poziomów rekurencji SQL (50)
ORA-06512: przy “TESTOWY.UP_TR”, linia 2
ORA-04088: błąd w trakcie wykonywania wyzwalacza ‘TESTOWY.UP_TR’
...
ORA-04088: błąd w trakcie wykonywania wyzwalacza ‘TESTOWY.UP_TR’
ORA-06512: przy “TESTOWY.UP_TR”, linia 2
00036. 00000 - „maximum number of recursive SQL levels (%s) exceeded”
*Cause: An attempt was made to go more than the specified number
of recursive SQL levels.
*Action: Remove the recursive SQL, possibly a recursive trigger.
Rozdział 10. ♦ Procedury wyzwalane 199

Ponieważ i tak zdążyło się wykonać polecenie z wnętrza wyzwalacza (wyzwolenie


AFTER), rekordy zostaną zmodyfikowane zgodnie z naszym życzeniem — oczywiście
o ile nie istnieje polecenie wycofania transakcji na skutek błędu przetwarzania. Jeżeli
nawet, pomimo wystąpienia komunikatu o błędzie i przerwania przetwarzania, uzy-
skamy oczekiwany skutek, to wykonanie wielu pętli będzie trwało bardzo długo.

W celu rozwiązania zasygnalizowanych problemów możemy wykorzystać procedurę


wyzwalaną dla każdego przetworzonego zapytaniem modyfikującym dane (INSERT,
UPDATE, DELETE) wiersza — FOR EACH ROW.
CREATE OR REPLACE TRIGGER up_tr
BEFORE INSERT OR UPDATE
ON Osoby
FOR EACH ROW
BEGIN
:NEW.Nazwisko := UPPER(:NEW.Nazwisko);
END;

W wyzwalaczu tego typu dostępne są dwie predefiniowane zmienne rekordowe o na-


zwach :NEW i :OLD. Obydwie zmienne (nazwy i typy) mają strukturę pól tabeli, dla
której procedura wyzwalana została utworzona. Prześledźmy ich zawartość na pod-
stawie działania prostego triggera. W celu ograniczenia liczby pól zawartych w zmien-
nych :NEW i :OLD został on oparty na tabeli Dzialy, która ma tylko dwa pola. Aby po-
kazać wszystkie możliwe przypadki, trigger ten jest wyzwalany wszystkimi zapytaniami,
które mogą zostać zdefiniowane dla tabeli. W ciele wyzwalacza zawarto dwie linie
powodujące wyświetlenie statycznego napisu (New — lub Old —), po którym wypisy-
wane są pola zmiennych rekordowych — odpowiednio :NEW oraz :OLD — separowane
znakiem spacji.
CREATE OR REPLACE TRIGGER sp_tr1
AFTER INSERT OR DELETE OR UPDATE
ON Dzialy
FOR EACH ROW
BEGIN
DBMS_Output.PUT_LINE('New - ' || :NEW.IdDzialu || ' ' || :NEW.Nazwa);
DBMS_Output.PUT_LINE('Old - ' || :OLD.IdDzialu || ' ' || :OLD.Nazwa);
END;

Możemy teraz prześledzić działanie procedury wyzwalanej, wykonując zapytanie wsta-


wiające do tabeli Dzialy nowy wiersz.
BEGIN
INSERT INTO Dzialy VALUES(11, ‘Nowy’);
END;

Zamieszczenie zapytania INSERT we wnętrzu bloku anonimowego zostało podyktowane


tym, że w wersji 11. odseparowane zostały wyjścia dla przetwarzania zapytania oraz
skryptów. Dodatkowo nie ma możliwości jawnego użycia polecenia SET SERVER OUTPUT
ON — załączenie uniwersalnego wyjścia dla skryptów wykonywane jest poprzez naci-
śnięcie odpowiedniego przycisku na zakładce DBMS Output. Tak więc takie potrak-
towanie zapytania jest tylko wybiegiem formalnym. We wcześniejszych realizacjach
Oracle wystarczyło poprzedzić jego wykonanie poleceniem przełączenia stanu stan-
dardowego wyjścia. Komunikat wyświetlony na skutek wykonania zapytania z wnętrza
triggera jest następujący:
200 Część II ♦ ORACLE PL/SQL

New - 11 Nowy
Old -

Wskazuje on na to, że podczas wstawiania wiersza zmienna rekordowa :NEW zawiera


wartości pól wstawianego rekordu, natomiast zmienna rekordowa :OLD jest pusta.
Przetestujmy ten wyzwalacz, wykonując zapytanie UPDATE zmieniające wartości pól ta-
beli Dzialy. Podobnie jak poprzednio, zapytanie jest zawarte w bloku anonimowym.
BEGIN
UPDATE Dzialy SET Nazwa = UPPER(Nazwa) WHERE Iddzialu = 11;
END;

Zawartość zmiennych rekordowych jest zilustrowana uzyskanym z triggera komuni-


katem o postaci:
New - 11 NOWY
Old - 11 nowy

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;

Tym razem informacja wyświetlona na skutek działania wyzwalacza będzie zdecydo-


wanie dłuższa.
New - 1 DYREKCJA
Old - 1 Dyrekcja
New - 2 ADMINISTRACJA
Old - 2 Administracja
New - 3 TECHNICZNY
Old - 3 Techniczny
New - 4 HANDLOWY
Old - 4 Handlowy
New - 6 POMOCNICZY
Old - 6 Pomocniczy

Podobnie jak podczas modyfikowania pojedynczego wiersza, zmienna :NEW zawiera


nowe, a :OLD stare wartości pól. Należy zwrócić uwagę na fakt cyklicznego, „kurso-
rowego” przetwarzania po kolei w ciele kursora każdego modyfikowanego wiersza.
Z porównania tych dwóch przykładów wynika, że trigger jest wyzwalany nie dla każ-
dego, ale tylko dla każdego z modyfikowanych wierszy. Pozostaje nam tylko spraw-
dzenie działania wyzwalacza dla zapytania DELETE usuwającego wiersze.
BEGIN
DELETE FROM Dzialy WHERE Iddzialu = 11;
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.

Powróćmy do poprzedniego przykładu, w którym chcieliśmy zapewnić, aby trigger „pil-


nował” wpisywania nazwisk pracowników tylko dużymi literami. Zauważmy, że w przy-
padku zapytania wstawiającego nowy wiersz INSERT zasilane są wszystkie jego pola,
natomiast w przypadku aktualizacji danych zapytaniem UPDATE możemy zmieniać za-
wartość tylko wybranego pola (pól). W szczególności modyfikacja może nie dotyczyć
pola Nazwisko, a wtedy takim przypadku uruchamianie triggera jest bezcelowe. Dla-
tego w definicji procedury wyzwalanej możemy wymienić po zdarzeniu UPDATE pole
(pola), których zmiana spowoduje jego uruchomienie.
CREATE OR REPLACE TRIGGER up_tr
BEFORE INSERT OR UPDATE OF Nazwisko
ON Osoby
FOR EACH ROW
BEGIN
:NEW.Nazwisko := UPPER(:NEW.Nazwisko);
END;

Jeżeli trigger ma być uruchamiany na skutek wystąpienia zmiany w przynajmniej jed-


nym z kilku pól, ich lista jest rozdzielana przecinkami. Taka definicja dozwolona jest
tylko w przypadku zdarzenia UPDATE i nie może być stosowana w przypadku pozosta-
łych (DELETE oraz INSERT).
202 Część II ♦ ORACLE PL/SQL

Wyzwalacz może być wykonywany warunkowo na skutek zdefiniowania w klauzuli


WHEN wyrażenia logicznego. Uruchomienie wyzwalacza następuje tylko wtedy, kiedy
warunek ten jest prawdziwy (TRUE). W prezentowanym przykładzie przetworzenie ciała
triggera nastąpi tylko dla tych wierszy, w których nowa wartość pola brutto przekro-
czy o 10 procent wartość dotychczas w tym polu wpisaną.
CREATE OR REPLACE TRIGGER sprawdz
BEFORE UPDATE
ON Zarobki
FOR EACH ROW
WHEN (New.Brutto/Old.Brutto > 1.1)
BEGIN
INSERT INTO spr
VALUES(:Old.IdZarobku, :Old.IdOsoby, :Old.Brutto);
END;
/
UPDATE Zarobki SET Brutto = 3000 WHERE IdZarobku < 3;
SELECT * FROM spr;

Należy zwrócić uwagę na to, że w przypadku tworzenia wyrażenia sprawdzającego


w klauzuli WHEN zmienne rekordowe :NEW i :OLD są zapisywane bez poprzedzającego
je dwukropka (NEW, OLD). Nie są więc tzw. zmiennymi wiązania (związanymi). W pre-
zentowanym przykładzie pokazano, w jaki sposób można w jednym skrypcie dokonać
„wywołania” wyzwalacza poprzez wykonanie zapytania modyfikującego i sprawdzić
skutek jego działania. Znak slash (/) jest separatorem dwóch wykonywanych kolejno
fragmentów skryptu. Dla poprawnego działania wyzwalacza konieczne jest utworze-
nie tabeli o nazwie spr, która ma trzy pola numeryczne: pierwsze całkowitoliczbowe,
np. int, a dwa pozostałe zmiennoprzecinkowe, np. real. Jak widać, w ciele triggera
zamieszczono zapytanie wstawiające wiersz z zawartością zmienianego rekordu tabeli
Zarobki.

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.

Możliwe jest użycie procedur wyzwalanych jako narzędzi do dynamicznego genero-


wania wartości domyślnych. Załóżmy, że w tabeli Dzialy pojawiła się trzecia kolumna
znakowa o nazwie kod. Zaproponowany trigger ma za zadanie wpisać do tego pola
pierwszą literę pola Nazwa, w przypadku gdy podczas wstawiania rekordu nie podano
Rozdział 10. ♦ Procedury wyzwalane 203

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;

Powyższy przykład pokazuje również miejsce deklaracji zmiennych pomocniczych


używanych w ciele triggera (tu zmiennej New_Kod). Sekcja deklaracji występuje po
słowie kluczowym DECLARE i bezpośrednio poprzedza słowo kluczowe BEGIN. Wobec
odmiennego sposobu deklarowania zmiennych w różnych obiektach PL/SQL, raz z uży-
ciem słowa DECLARE, innym razem bez niego, możemy zastosować pewną mnemonikę:
tam gdzie w składni obiektu występują słowa kluczowe IS lub AS, są one wyznaczni-
kiem początku sekcji deklaracji (procedury składowane, funkcje, pakiety), natomiast
tam gdzie nie pojawia się żadne z tych słów, do oznaczenia sekcji deklaracji konieczne
jest użycie słowa kluczowego DECLARE, a sekcja ta bezpośrednio poprzedza słowo klu-
czowe BEGIN (procedury wyzwalane, bloki anonimowe).

Jak wynika z przytoczonych przykładów, procedury wyzwalane są bardzo często wy-


korzystywane do „pilnowania” sposobu wprowadzania danych i weryfikacji ich po-
prawności — stanowią dynamiczne więzy integralności. Jednak chyba najczęściej
stosowane są do automatycznej inkrementacji pól; dotyczy to w zdecydowanej więk-
szości pól kluczy podstawowych. Pierwszy sposób na generowanie ich kolejnych war-
tości opiera się na odczytaniu maksymalnej wartości już istniejącej w tabeli i zwięk-
szeniu jej — zwykle o jeden.
CREATE OR REPLACE TRIGGER nowy_numer
BEFORE INSERT
ON Dzialy
FOR EACH ROW
DECLARE
New_Num number;
BEGIN
SELECT (NVL(MAX(IdDzialu), 0)+1) INTO New_Num
FROM Dzialy;
:New.IdDzialu := New_Num;
END;

W przykładzie zastosowano składnię SELECT ... INTO, która pozwala na przypisanie


wartości zwracanej przez zapytanie wybierające do zmiennej. W naszym przypadku
jest to pomocnicza zmienna numeryczna New_Num zadeklarowana w sekcji deklaracji
wyzwalacza. Zapytanie wybierające oblicza wartość maksymalną pola IdDzialu z tabeli
Dzialy i zwiększa ją o jeden. Ponadto została zastosowana funkcja NVL, która sprawdza,
czy pierwszy argument ma wartość NULL. Jeśli odpowiedź jest negatywna, zwracana
jest jego wartość, a w przypadku przeciwnym wartość parametru drugiego. Możemy
204 Część II ♦ ORACLE PL/SQL

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.

Drugim, konkurencyjnym rozwiązaniem jest zastosowanie w ciele triggera uprzednio


utworzonej sekwencji do generowania kolejnych numerów. W prezentowanym skryp-
cie dwie pierwsze linie odpowiadają za usunięcie sekwencji (zakładamy, że obiekt
o tej nazwie mógł istnieć w schemacie) oraz jej utworzenie. Zwróćmy uwagę na to, że
w przykładzie postanowiono rozpocząć generowanie przez sekwencję wartości od 7.
DROP SEQUENCE licznik;
/
CREATE SEQUENCE licznik START WITH 7;
/
CREATE OR REPLACE TRIGGER nowy_numer
BEFORE INSERT
ON Dzialy
FOR EACH ROW
DECLARE
New_Num number;
BEGIN
SELECT licznik.NEXTVAL INTO New_Num FROM DUAL;
:New.IdDzialu := New_Num ;
END;

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.

Jak widzimy, stworzyliśmy dwa konkurencyjne rozwiązania dla wyzwalaczy, które


robią to samo, ale nie tak samo. Wbrew pozorom nie chodzi tutaj o formalną stronę
składniową, ale o zasadę działania. Porównajmy skutki zastosowania obu triggerów
w razie wykasowania kilku ostatnich rekordów. W pierwszym przypadku kolejny nu-
mer nie uwzględni faktu ich istnienia, natomiast w drugim sekwencja „nic nie wie” o ka-
sowanych wierszach i kolejny numer będzie miał wartość je uwzględniającą. Tak więc
w pierwszej sytuacji mamy niepełną informację o kasowanych wierszach i w tabeli widać
tylko luki w numeracji powstałe na skutek ich usuwania z jej środka, a w drugim roz-
wiązaniu informacja ta jest pełna bez względu na to, który z wierszy został wykasowany.
Wybór rozwiązania zależy oczywiście od świadomego działania programisty — oso-
biście przychylam się do stosowania drugiego z nich.

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

możliwe tylko przez zastosowanie zapytania wybierającego korzystającego z tabeli


systemowej DUAL — zostało to już pokazane w tworzonych w tym rozdziale trigge-
rach, a w tym miejscu ilustruje to prosty blok anonimowy.
DECLARE
zm int;
BEGIN
SELECT licznik.nextval INTO zm FROM DUAL;
DBMS_OUTPUT.PUT_LINE(zm);
END;

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;

Powróćmy teraz do głównego toku wywodu dotyczącego procedur wyzwalanych. Oczy-


wiście trigger utworzony na jednej z tabel może wpływać na zawartość innych. Roz-
ważmy przykład, kiedy w momencie zatrudniania nowego pracownika chcemy mu jed-
nocześnie wypłacić pierwsze wynagrodzenie, np. zaliczkę w wysokości 100 zł, na zakup
narzędzia koniecznego do wykonywania pracy, odzieży ochronnej czy też w ramach
refundacji kosztów dojazdu. Bez względu na przeznaczenie tej zaliczki, ma ona zostać
wypłacona automatycznie, czyli w tabeli Zarobki ma się pojawić nowy rekord.
CREATE OR REPLACE TRIGGER zaliczka
BEFORE INSERT
ON Osoby
FOR EACH ROW
DECLARE
M_Num int;
BEGIN
SELECT (NVL(MAX(IdZarobku),0)+1) INTO M_Num
FROM ZAROBKI;
INSERT INTO ZAROBKI
VALUES(M_Num, :New.IdOsoby, 100) ;
END;

W przykładzie zastosowano automatyczną inkrementację klucza podstawowego we-


dług algorytmu opartego na najwyższej wartości istniejącej w tabeli, który został opi-
sany poprzednio. Oczywiście możliwe jest użycie drugiej z opisanych metod, opartej
na sekwencji. W zapytaniu wstawiającym nowy wiersz do tabeli Zarobki do wpisania
wartości klucza obcego (Zarobki.IdOsoby) wykorzystano wartość odczytaną z nowego
wiersza wpisywanego do tabeli Osoby na podstawie zawartości zmiennej rekordowej
:New.IdOsoby.
206 Część II ♦ ORACLE PL/SQL

Kolejny prosty przykład pochodzi z praktyki dydaktycznej. W celu jego prezentacji


posłużymy się tabelą przechowującą podstawowe informacje o studentach. Kluczem
podstawowym jest numer indeksu. Ostatnie pole zawiera login przypisany każdemu
ze studentów, a wykorzystywany później do tworzenia kont w Oracle. Założyłem, że
nie będę posługiwał się numerem indeksu jako nazwą użytkownika (chociaż jest to proste
i spełnia kryterium unikatowości nazw, to moim zdaniem trochę odczłowiecza studenta,
sprowadzając go do numeru na liście), ale zastosuję nazwy oparte na początkowych
literach nazwiska i imienia.
CREATE TABLE studenci
(Indeks NUMBER(6) PRIMARY KEY,
Imie VARCHAR2(17),
Nazwisko VARCHAR2(20),
Login VARCHAR2(6)
);

Pozostaje proces automatycznego generowania nazw użytkowników. Zadanie to zostało


zrealizowane za pomocą procedury wyzwalanej podczas wpisywania nowego rekordu
oraz modyfikacji w istniejących już rekordach nazwiska lub imienia. Trigger jest uru-
chamiany przed wymienionymi zdarzeniami (tak aby możliwe było odwołanie się do
zmiennej :NEW) dla każdego z przetwarzanych rekordów.
CREATE OR REPLACE TRIGGER N_log
BEFORE INSERT OR UPDATE OF Imie, Nazwisko
ON Studenci
FOR EACH ROW
BEGIN
:NEW.Imie := UPPER(:NEW.Imie);
:NEW.Nazwisko := UPPER(:NEW.Nazwisko);
:NEW.Login := UPPER(SUBSTR(:NEW.Nazwisko, 1, 5))
|| UPPER(SUBSTR(:NEW.Imie, 1, 1));
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.

Jeżeli z jakichkolwiek przyczyn konieczne jest czasowe zablokowanie wybranego wy-


zwalacza, możemy to zrealizować, wykonując polecenie:
ALTER TRIGGER N_log DISABLE;

Przywrócenie funkcjonowania triggera uzyskujemy poprzez użycie polecenia:


ALTER TRIGGER N_log ENABLE;
Rozdział 10. ♦ Procedury wyzwalane 207

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.

Analizowany przykład wyzwalacza może zostać wzbogacony o klauzulę jego warun-


kowego wykonania WHEN, ponieważ zasadne jest niezmienianie istniejącego już w ba-
zie loginu stworzonego lub zmodyfikowanego inną drogą, niż na skutek działania tri-
ggera. Rozwiązanie takie przedstawia kolejny przykład.
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
WHEN (Stary.Login IS NULL)
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;
208 Część II ♦ ORACLE PL/SQL

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;

W przedstawionym przykładzie ograniczono się do wyświetlenia komunikatu odpo-


wiadającego zapytaniu uruchamiającemu trigger. Prezentowane tu trzy funkcje mają
sens tylko i wyłącznie w ciele triggerów. Jak widać, możliwe jest zarówno tworzenie
wyzwalaczy uruchamianych wieloma zdarzeniami zachodzącymi na tej samej tabeli, jak
również wielu triggerów wyzwalanych tym samym zdarzeniem. W tym drugim przypadku
do wersji 11. mieliśmy kłopot z wymuszeniem kolejności uruchamiania się wyzwalaczy.
Można było założyć, że będą one wykonywane w kolejności ich tworzenia (w takiej,
w jakiej występowały wpisy o nich w tabeli systemowej), chociaż nie gwarantował tego
producent. Nawet przy takim założeniu nie było więc pewności, że przetwarzanie kolej-
nego z wyzwalaczy zacznie się po wykonaniu pierwszego z nich. Możemy powiedzieć,
że system „zlecał” silnikowi serwera przetworzenie pewnego zestawu triggerów, a ten
przetwarzał je tak, jak mu na to pozwalały zasoby. Od wersji 11. możliwe jest ścisłe de-
finiowanie kolejności uruchamiania triggerów wyzwalanych tym samym zdarzeniem.
W celu przedstawienia tego mechanizmu utwórzmy prostą tabelę o dwóch polach.
CREATE TABLE Test
(Id int,
opis varchar2(33)
);

Następnie tworzymy wyzwalacz, który automatycznie ustawia wartość pierwszego pola


na zwiększoną o jeden w stosunku do maksymalnej dotychczas tam występującej, z po-
czątkową wartością dla pustej tabeli równą 1. Do wstawianej zawartości pola dopisy-
wana jest fraza ' trig1'.
CREATE OR REPLACE TRIGGER trig1
BEFORE INSERT ON Test FOR EACH ROW
BEGIN
Rozdział 10. ♦ Procedury wyzwalane 209

SELECT NVL(max(id),0)+1 INTO :NEW.id FROM Test;


:New.opis:=:New.opis || ' trig1';
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;

Przede wszystkim każdy z triggerów dotyczy tego samego wiersza, co spowoduje, że


pojawi się tylko jeden wpis, chociaż potencjalnie wszystkie triggery wykonują auto-
matyczną inkrementację. Natomiast kolejność dopisywanych w polu opis łańcuchów
będzie zawsze taka sama i będzie wynikała z następstwa zdefiniowanego klauzulą
FOLLOWS.
Id opis
-------- ---------------------------------
1 wstawiam trig1 trig2 trig3

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

FOR EACH ROW WHEN (New.Login IS NULL)


BEGIN
:New.Imie := UPPER(:Nowy.Imie);
END;

W takim przypadku bezpośrednio po wykonaniu zapytania tworzącego błędny trigger,


tak samo jak dla innych elementów programistycznych, możemy posłużyć się polece-
niem:
SHOW ERRORS;

W analizowanym przypadku wyświetlony zostanie komunikat:


LINE/COL ERROR
-------- -----------------------------------------------------------------
2/18 PLS-00049: niepoprawna zmienna wiązania ‘NOWY.IMIE‘

Pozwala on na szybką lokalizację błędu, który w naszym przypadku polegał na użyciu


zmiennej wiązania NOWY zamiast standardowej nazwy NEW. Nie została tu użyta klau-
zula WHEN zmieniająca standardowe nazwy. Komunikat wskazuje na miejsce wystąpie-
nia błędu — 2. linia, 18. kolumna. Jak widać, nie odpowiada to zapisowi, natomiast
wskazuje na pierwszą linię po nagłówku definiującym trigger.

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]

Triggery tworzone na tabelach można zastosować do śledzenia operacji na nich wy-


konywanych. Stwórzmy w tym celu dwie tabele pomocnicze. Pierwsza z nich ma prze-
chowywać informację o tym, kto modyfikował daną tabelę. Poza kluczem głównym
zawiera ona pola, które mają przechowywać dane o użytkowniku, dacie modyfikacji,
terminalu, z którego ją wykonano, nazwie zmienianej tabeli, instrukcji modyfikującej
oraz wskaźniku modyfikowanego wiersza.
CREATE TABLE Audit_Table (
IdAudit NUMBER,
Kto VARCHAR2(10),
Time_now DATE,
Terminal VARCHAR2(10),
Tabela VARCHAR2(10),
Operacja VARCHAR2(10),
klucz NUMBER
);

Druga tabela ma, oprócz klucza, przechowywać nazwę modyfikowanej kolumny oraz
jej starą i nową wartość.
Rozdział 10. ♦ Procedury wyzwalane 211

CREATE TABLE Audit_Table_Values (


IdAudit NUMBER,
Kolumna VARCHAR2(10),
StaraW NUMBER,
NowaW NUMBER);

Rozważmy budowę wyzwalacza do śledzenia zmian uruchamianego przez wszystkie


zapytania modyfikujące na przykładzie tabeli Osoby.
CREATE OR REPLACE TRIGGER Audit_Osoby
AFTER INSERT OR UPDATE OR DELETE ON Osoby
FOR EACH ROW
DECLARE
Time_now DATE;
Terminal VARCHAR2(10);
Licz int;
BEGIN
Time_now := SYSDATE;
Terminal := USERENV('TERMINAL');
SELECT max(IdAudit)+1 INTO Licz FROM Audit_table;
IF INSERTING THEN
INSERT INTO Audit_Table
VALUES ( Licz,User, Time_now, Terminal, 'Osoby',
'INSERT', :NEW.IdOsoby);
ELSIF DELETING THEN
INSERT INTO Audit_Table
VALUES ( Licz, User, Time_now, Terminal, 'Osoby',
'DELETE', :OLD.IdOsoby);
ELSE
INSERT INTO Audit_Table
VALUES (Licz, User, Time_now, Terminal, 'Osoby',
'UPDATE', :OLD.IdOsoby);
IF UPDATING ('RokUrodz') THEN
INSERT INTO Audit_Table_values
VALUES (Licz, 'Rok_urodz', :OLD.RokUrodz,
:new.RokUrodz);
ELSIF UPDATING ('IdDzialu') THEN
INSERT INTO Audit_Table_values
VALUES (Licz, 'IdDzialu', :OLD.IdDzialu, :NEW.IdDzialu);
END IF;
END IF;
END;

Pod zmienne pomocnicze podstawiono wartość czasu systemowego uzyskaną za po-


mocą funkcji SYSDATE oraz nazwę terminalu, na skutek zastosowania funkcji USERENV
z parametrem 'TERMINAL'. Wartość klucza głównego dla tabeli pomocniczej ustalono
na podstawie najwyższej wpisanej do niej do tej pory wartości. Następnie, w zależno-
ści od wykrytej akcji wyzwalającej trigger, do pierwszej z tabel pomocniczych wpisano
informacje o wstawianym, usuwanym lub modyfikowanym rekordzie. Różnią się one
jedynie statycznym napisem reprezentującym nazwę operacji oraz kluczem do mody-
fikowanego wiersza wyznaczanego na podstawie zmiennych wiązania :NEW lub :OLD
w zależności od operacji. Nazwę użytkownika dokonującego modyfikacji uzyskujemy,
stosując wbudowaną funkcję USER. Jeżeli akcją wyzwalającą jest aktualizacja danych
(UPDATE), to w przypadku modyfikacji dotyczących pól RokUrodz oraz IdDzialu do-
konywane są wpisy do drugiej z tabel. Wykrycie modyfikowanego pola jest możliwe
212 Część II ♦ ORACLE PL/SQL

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

Dla perspektyw, poza wszystkimi omawianymi dotąd wyzwalaczami, dostępny jest


trigger typu INSTEAD OF, zastępujący zdarzenie, które go wywołało, instrukcjami za-
wartymi w jego ciele. Możliwe jest np. zastosowanie blokady usuwania rekordów
za pośrednictwem perspektywy Stud i umieszczenie w ciele tego typu triggera komu-
nikatu.
CREATE OR REPLACE TRIGGER st_tr
INSTEAD OF DELETE
ON Stud
BEGIN
DBMS_OUTPUT.PUT_LINE('Zabronione');
END;

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;

Pomimo że wyzwalacze są poprawne, próba ich pierwszego użycia, czyli wykonanie


zapytania usuwającego wiersze, przez niedoświadczonego programistę może przyspo-
rzyć wielu emocji. Pojawia się wtedy komunikat o postaci 5 rows deleted, który suge-
ruje, że usuwanie wierszy jednak się odbyło. Dopiero sprawdzenie zawartości tabeli
przekonuje nas o tym, że blokada kasowania jest jednak skuteczna.

Gdybyśmy skonstruowali taki trigger dla wszystkich trzech zapytań — INSERT OR


UPDATE OR DELETE — otrzymalibyśmy odpowiednik perspektywy z opcją WITH READ ONLY.
W takim wypadku chyba nie warto sięgać po wyzwalacz, lecz zastosować blokadę
w definicji perspektywy. Jednak za pomocą triggerów możliwe jest blokowanie tylko
wybranych typów modyfikacji tabeli, np. dopuszczanie tylko wstawiania nowych wier-
szy, a uniemożliwianie ich usuwania oraz zmiany ich zawartości. Tego typu blokady
mogą mieć zastosowanie praktyczne. W rzeczywistości oba zaprezentowane rozwią-
zania nie są satysfakcjonujące, ponieważ przy zastosowaniu polecenia pustego nie
otrzymujemy żadnego komunikatu, a w przypadku wypisania komunikatu za pomocą
procedury DBMS_OUTPUT.PUT_LINE będzie on widoczny tylko dla wykonywania zapytań
z wbudowanej końcówki klienta Oracle. Z kolei poprzez zastosowanie, co w praktyce
jest najczęstsze, końcówki klienta napisanej w języku wyższego rzędu również pozba-
wiamy się możliwości oglądania tego komunikatu. Możliwe jest wtedy ustanowienie
błędu przetwarzania aplikacji i jego przechwycenie w sekcji obsługi wyjątków.
CREATE OR REPLACE TRIGGER st_tr
INSTEAD OF DELETE
ON Stud
BEGIN
RAISE_APPLICATION_ERROR (-20001, 'Zakaz kasowania');
EXCEPTION
WHEN OTHERS THEN
RAISE;
END;

Zastosowanie obsługi wyjątków w obrębie wyzwalacza spowoduje, że po wykonaniu


zapytania usuwającego wiersz z tabeli za pośrednictwem perspektywy DELETE FROM
Stud ... otrzymamy komunikat o błędzie. Zostanie on wygenerowany nie z poziomu
triggera, ale z poziomu skryptu zawierającego zapytanie. Zawartość tego komunikatu
może mieć postać:
Error starting at line 1 in command:
DELETE FROM Stud WHERE indeks = 2222
Error report:
SQL Error: ORA-20001: 'Zakaz kasowania'
ORA-06512: przy „TESTOWY.ST_TR”, linia 12
ORA-04088: błąd w trakcie wykonywania wyzwalacza 'TESTOWY.ST_TR'

W prezentowanym przykładzie TESTOWY jest nazwą schematu, w którym utworzono per-


spektywę z triggerem. Oczywiście, zamiast definitywnie blokować którąś z modyfikacji,
możemy ją zablokować warunkowo. Prześledźmy takie działanie na przykładzie per-
spektywy opartej na tabeli Osoby, z której wybiera ona identyfikator (klucz podstawowy)
oraz podstawowe pola identyfikujące pracownika.
214 Część II ♦ ORACLE PL/SQL

CREATE OR REPLACE VIEW Dane


AS
SELECT Idosoby, Imie, Nazwisko
FROM Osoby

Tym razem trigger ma zablokować możliwość usunięcia rekordu tylko w przypadku,


kiedy pracownik ma jakąkolwiek wypłatę (Brutto w tabeli Zarobki). W przeciwnym
przypadku ma on zapewnić jego skasowanie. Jak widać, trigger taki jest uzupełnieniem
klucza obcego, ponieważ w Oracle domyślnym jego działaniem jest wpisanie w pole
klucza kasowanego w tabeli nadrzędnej wartości NULL. W wielu innych środowiskach
zastosowanie ograniczenia klucza obcego uniemożliwia kasowanie wierszy z tabeli
nadrzędnej, jeśli w tabeli podrzędnej istnieje jakikolwiek wpis odwołujący się do ka-
sowanego wiersza (powiązany z nim). Nasz trigger ma zapewnić właśnie taką funkcjo-
nalność.
CREATE OR REPLACE TRIGGER st_tr
INSTEAD OF DELETE
ON Dane FOR EACH ROW
DECLARE
ile NUMBER;
BEGIN
SELECT COUNT(IdOsoby) INTO ile FROM
Zarobki WHERE IdOsoby = :OLD.IdOsoby;
IF (ile = 0) THEN
DELETE FROM OsobyWHERE IdOsoby = :OLD.IdOsoby;
ELSE
DBMS_OUTPUT.PUT_LINE( Zabronione );
END IF;
END;

W przykładzie uzyskano liczbę wypłat dla usuwanego pracownika, korzystając z iden-


tyfikatora zawartego w zmiennej rekordowej :OLD — jeżeli liczba wierszy wyniosła zero
(brak wypłat dla kasowanego rekordu), zrealizowano usuwanie. Jednakże ze względu
na to, że w Oracle zdarzenia zawarte w ciele triggera wywołują trigger i grozi nam za-
pętlenie, a co za tym idzie, niemożność wykasowania rekordu, powtórzenie usuwania
zostało zrealizowane bezpośrednio na tabeli źródłowej Osoby, a nie na perspektywie,
dla której stworzono wyzwalacz. Jak widać, u podstaw braku możliwości tworzenia
triggerów typu INSTEAD OF dla tabel leży niemożność warunkowego wykonania akcji,
która ten trigger wyzwala, co bezpośrednio wynika z cechy rekurencyjnego wywoły-
wania triggerów z ciała.

Podobnie do czasowego wyłączenia pojedynczego triggera, możliwe jest wyłączenie,


a następnie ponowne włączenie wszystkich triggerów dla tabeli. Tym razem stosujemy
jednak zapytanie modyfikujące tabelę.
ALTER TABLE Dzialy DISABLE ALL TRIGGERS;
ALTER TABLE Dzialy ENABLE ALL TRIGGERS;

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.

Działanie wyzwalacza na poziomie bazy danych prześledzimy na przykładzie obsługi


zdarzenia logowania do bazy. W celu jego prezentacji zbudujmy tabelę pomocniczą
Loginy, która będzie zawierała, poza identyfikatorem wiersza, pola z nazwą użytkownika,
czasem logowania, terminalem, z którego następuje połączenie, nazwą hosta oraz ty-
pem zastosowanego uwierzytelnienia.
CREATE TABLE Loginy (
IdAudit NUMBER,
Kto VARCHAR2(20),
Time_now DATE,
Terminal VARCHAR2(20),
Host_N VARCHAR2(20),
Autent VARCHAR2(20)
);

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

CREATE OR REPLACE TRIGGER Polacz


AFTER LOGON
ON DATABASE
DECLARE
Time_now DATE;
Terminal VARCHAR2(20);
Host_N VARCHAR2(20);
Autent VARCHAR2(20);
Licz int;
BEGIN
Time_now := SYSDATE;
Terminal := USERENV('TERMINAL');
Host_N := SYS_CONTEXT('USERENV', 'HOST');
Autent := SYS_CONTEXT('USERENV', 'AUTHENTICATION_TYPE');
SELECT NVL(MAX(IdAudit)+1, 1) INTO Licz FROM Loginy;
INSERT INTO Loginy
VALUES (Licz, User, Time_now, Terminal, Host_N, Autent);
END;

W ciele triggera wprowadzono pomocnicze zmienne przeznaczone na odczytane in-


formacje systemowe oraz automatyczną inkrementację pola identyfikatora wiersza.
Data została odczytana przy użyciu funkcji SYSDATE, która zwraca datę zegara syste-
mowego. Do odczytania nazwy terminalu została użyta funkcja USERENV('TERMINAL'),
natomiast do odczytania nazwy hosta zastosowano jej następczynię o nieco większej
funkcjonalności — SYS_CONTEXT. Automatyczna inkrementacja jest wykonywana na
podstawie maksymalnej wartości wpisanej do tabeli, a nazwa użytkownika odczyty-
wana jest dzięki funkcji USER.

Takie rozwiązanie ma jednak wadę, ponieważ zakłada rejestrowanie wszystkich uda-


nych połączeń, stąd w tabeli Loginy będą dominowały wpisy związane z użytkowni-
kami systemowymi, którzy cyklicznie łączą się z bazą. Dotyczy to zwłaszcza użytkow-
ników SYSMAN oraz DBSNMP, którzy odpowiadają za regularne wykonywanie się
procesów drugoplanowych, oraz, w mniejszym stopniu, użytkownika SYS. Aby wy-
eliminować te cykliczne wpisy, opłaca się zastosować instrukcję warunkową, zawie-
rającą listę nazw użytkowników niepodlegających wpisaniu do tabeli.
CREATE OR REPLACE TRIGGER Polacz
AFTER LOGON
ON DATABASE
DECLARE
Time_now DATE;
Terminal VARCHAR2(20);
Host_N VARCHAR2(20);
Autent VARCHAR2(20);
Licz int;
BEGIN
IF (UPPER(USER) NOT IN ('SYSMAN', 'DBSNMP', 'SYS')) THEN
Time_now := SYSDATE;
Terminal := USERENV('TERMINAL');
Host_N := SYS_CONTEXT('USERENV', 'HOST');
Autent := SYS_CONTEXT('USERENV', 'AUTHENTICATION_TYPE');
SELECT NVL(MAX(IdAudit)+1, 1) INTO Licz FROM Loginy;
INSERT INTO Loginy
VALUES (Licz, User, Time_now, Terminal, Host_N, Autent);
END IF;
END;
Rozdział 10. ♦ Procedury wyzwalane 217

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.

W tym miejscu wypadałoby szerzej omówić funkcję SYS_CONTEXT, która umożliwia


odczytanie bardzo dużej ilości informacji systemowych. Pełna składnia jej wywołania
ma postać:
SELECT SYS_CONTEXT('USERENV', 'Atrybut') FROM DUAL;

Pierwszy atrybut wywołania może przyjąć jedną wartość — statycznego napisu


'USERENV'. Wydaje się, że twórcy Oracle przewidują rozszerzenie funkcjonalności,
ale od co najmniej trzech realizacji poza tą wartością nie pojawiło się nic nowego.
Istnieje natomiast możliwość zdefiniowania tego parametru przez użytkownika, co
pociąga za sobą zmianę zakresu (zawężenie) działania tej funkcji, np. ograniczenie go
tylko do sesji danego użytkownika. Drugi atrybut, podawany również jako łańcuch,
może przybierać wiele postaci, wskazując na interesujący nas parametr środowiska czy
sesji. Wykaz i opis wartości, jakie może on przyjąć, wraz ze skróconym ich opisem
zawiera tabela 10.1.

Tabela 10.1. Wykaz atrybutów funkcji SYS_CONTEXT


Atrybut Opis
TERMINAL Podaje identyfikator sieciowy komputera.
LANGUAGE Podaje język i region geograficzny używany przez sesję.
LANG Podaje skróconą nazwę języka używanego przez sesję.
SESSIONID Podaje unikalny identyfikator sesji.
INSTANCE Podaje numer identyfikacyjny bieżącej instancji bazy.
ENTRYID Podaje dostępny dla audytu identyfikator wejścia.
ISDBA Zwraca TRUE, jeśli bieżący użytkownik ma uprawnienia (rolę) DBA,
a FALSE w przeciwnym przypadku.
CLIENT_INFO Zwraca opisujących sesję użytkownika łańcuch o długości do 64 bajtów,
który może zostać wykorzystany w pakiecie DBMS_APPLICATION_INFO.
NLS_TERRITORY Zwraca nazwę regionu geograficznego bieżącej sesji.
NLS_CURRENCY Podaje symbol waluty dla bieżącej sesji.
NLS_CALENDAR Podaje narodowy kalendarz dla bieżącej sesji.
NLS_DATE_FORMAT Podaje narodowy format daty bieżącej sesji.
NLS_DATE_LANGUAGE Podaje język, w którym wyrażana jest data bieżącej sesji.
NLS_SORT Wskazuje, czy sortowanie opiera się na postaci binarnej, czy na alfabecie.
CURRENT_USER Podaje nazwę użytkownika bieżącej sesji — we wnętrzu procedur
wykorzystujących lub zmieniających uprawnienia może być inna niż
SESSION_USER.
218 Część II ♦ ORACLE PL/SQL

Tabela 10.1. Wykaz atrybutów funkcji SYS_CONTEXT — ciąg dalszy


Atrybut Opis
CURRENT_USERID Podaje identyfikator użytkownika bieżącej sesji — we wnętrzu procedur
wykorzystujących lub zmieniających uprawnienia może być inny niż
SESSION_USER.
SESSION_USER Podaje nazwę użytkownika bieżącej sesji.
SESSION_USERID Podaje identyfikator użytkownika bieżącej sesji.
CURRENT_SCHEMA Podaje nazwę domyślnego schematu użytkownika bieżącej sesji. Może ona
zostać zmieniona przez zastosowanie polecenia ALTER SESSION SET SCHEMA.
CURRENT_SCHEMAID Podaje identyfikator domyślnego schematu użytkownika bieżącej sesji.
Może on zostać zmieniony przez zastosowanie polecenia ALTER SESSION
SET SCHEMA.
PROXY_USER Podaje nazwę użytkownika PROXY bieżącej sesji.
PROXY_USERID Podaje identyfikator użytkownika PROXY bieżącej sesji.
DB_DOMAIN Podaje nazwę domeny bazy danych — parametr konfiguracyjny DB_DOMAIN.
DB_NAME Podaje nazwę instancji bazy danych — parametr konfiguracyjny DB_NAME.
HOST Podaje nazwę hosta, na którym zalogowany jest użytkownik.
OS_USER Podaje nazwę użytkownika systemu operacyjnego, pod którą zalogowany
jest użytkownik.
EXTERNAL_NAME Podaje nazwę użytkownika, który zalogował się do bazy przy użyciu
autoryzacji zewnętrznej.
IP_ADDRESS Podaje adres IP komputera, z którego klient jest podłączony do bazy.
NETWORK_PROTOCOL Podaje nazwę protokołu zawartego w łańcuchu połączeniowym
(PROTOCOL = protocol).
BG_JOB_ID Podaje identyfikator procesu drugoplanowego (tła).
FG_JOB_ID Podaje identyfikator procesu pierwszoplanowego.
AUTHENTICATION_TYPE Podaje sposób autoryzacji użytkownika sesji
(DATABASE, OS, NETWORK, PROXY).
AUTHENTICATION_DATA Podaje datę, z którą została zrealizowana autoryzacja użytkownika.
CURRENT_SQL Tekst polecenia SQL — atrybut dostępny tylko podczas obsługi zdarzeń.
CLIENT_IDENTIFIER Zdefiniowany przez użytkownika identyfikator sesji.
GLOBAL_CONTEXT_MEMORY Rozmiar pamięci współdzielonej SGA użyty przez aplikację (w bajtach).

Warto zauważyć, że jest bardzo duża liczba zdarzeń zdefiniowanych na poziomie


SCHEMA i DATABASE, które mogą zostać wykorzystane do wyzwalania triggerów.
Ich wykaz z krótkim opisem zawiera tabela 10.2.

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

Tabela 10.2. Wykaz zdarzeń dla triggerów o zasięgu SCHEMA i DATABASE


Zdarzenie Opis
BEFORE ALTER Występujące, kiedy modyfikowany jest obiekt bazy danych.
AFTER ALTER
BEFORE DROP Występujące, kiedy usuwany jest obiekt bazy danych.
AFTER DROP
BEFORE ANALYZE Występujące, kiedy załączamy obliczanie statystyk dla obiektu bazy.
AFTER ANALYZE
BEFORE ASSOCIATE STATISTICS Występujące, kiedy dołączamy kolejną statystykę wydajności
AFTER ASSOCIATE STATISTICS do statystyk istniejących.
BEFORE AUDIT Występujące, kiedy przechodzimy do pracy w trybie AUDIT
AFTER AUDIT — ze śledzeniem wykonywania poleceń.
BEFORE NOAUDIT Występujące, kiedy przechodzimy do pracy w trybie NOAUDIT
AFTER NOAUDIT — bez śledzenia wykonywania poleceń.
BEFORE COMMENT Występujące, kiedy do obiektu bazy danych dodawany jest komentarz.
AFTER COMMENT
BEFORE CREATE Występujące, kiedy tworzony jest obiekt bazy danych.
AFTER CREATE
BEFORE DDL Występujące, kiedy wystąpi większość poleceń SQL DDL.
AFTER DDL Zdarzenie to nie jest wyzwalane tylko w przypadku poleceń ALTER
DATABASE, CREATE CONTROLFILE, CREATE DATABASE oraz gdy zapytanie
modyfikujące jest dynamicznie tworzone za pomocą PL/SQL.
BEFORE DISASSOCIATE STATISTICS Występujące, kiedy odłączamy statystykę wydajności do statystyk
AFTER DISASSOCIATE STATISTICS istniejących.
BEFORE GRANT Występujące, kiedy przypisywane są uprawnienia.
AFTER GRANT
BEFORE RENAME Występujące, kiedy zmieniana jest nazwa obiektu bazy danych.
AFTER RENAME
BEFORE REVOKE Występujące, kiedy odbierane są uprawnienia.
AFTER REVOKE
BEFORE TRUNCATE Występujące, kiedy zawartość tabeli usuwana jest poleceniem
AFTER TRUNCATE TRUNCATE.
BEFORE LOGOFF Występujące przy rozpoczęciu wylogowywania się użytkownika.
AFTER LOGON Występujące po pomyślnym zalogowaniu się do bazy.
AFTER SUSPEND Występujące, kiedy proces, po wykonaniu polecenia SQL, ulega
zawieszeniu na skutek braku zasobów pamięci. Jeśli wyzwalacz
spowoduje zwiększenie przydziału zasobów, proces jest wznawiany.

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).

Wykorzystując oba zdarzenia związane z logowaniem, możemy udoskonalić „dziennik”


automatycznie zapisujący informacje o logowaniu do systemu. Należy jednak najpierw
wzbogacić tabelę dwoma polami przeznaczonymi na informacje dotyczące identyfi-
katora sesji użytkownika oraz daty wylogowania.
CREATE TABLE Loginy (
IdAudit NUMBER,
Sesja NUMBER,
Kto VARCHAR2(20),
Time_now DATE,
Time_off DATE,
Terminal VARCHAR2(20),
Host_N VARCHAR2(20),
Autent VARCHAR2(20)
);

Dotychczas istniejący trigger został zmodyfikowany na potrzeby logowania w ten spo-


sób, że została dodana nowa zmienna pomocnicza, która jest zasilana przy pomocy funk-
cji SYS_CONTEXT z parametrem wskazującym na identyfikator sesji bieżącego użytkow-
nika. W ciele konsekwentnie stosowane jest odwoływanie się do tej funkcji w celu
uzyskania potrzebnych informacji. Ponieważ tym razem nie są zasilane wszystkie pola
tabeli (pole Time_off pozostaje przy logowaniu puste), konieczne jest zastosowanie listy
pól w instrukcji INSERT.
CREATE OR REPLACE TRIGGER Polacz
AFTER LOGON
ON DATABASE
DECLARE
Time_now DATE;
USER_sid NUMBER;
Uzytkownik VARCHAR2(20);
Terminal VARCHAR2(20);
Host_N VARCHAR2(20);
Autent VARCHAR2(20);
Licz int;
BEGIN
IF (UPPER(USER) NOT IN ('SYSMAN', 'DBSNMP', 'SYS')) THEN
Time_now := SYSDATE;
USER_sid := SYS_CONTEXT('USERENV', 'SESSIONID');
Rozdział 10. ♦ Procedury wyzwalane 221

Uzytkownik := SYS_CONTEXT('USERENV', 'SESSION_USER');


Terminal := SYS_CONTEXT('USERENV', 'TERMINAL');
Host_N := SYS_CONTEXT('USERENV', 'HOST');
Autent := SYS_CONTEXT('USERENV', 'AUTHENTICATION_TYPE');
SELECT NVL(MAX(IdAudit)+1, 1) INTO Licz FROM Loginy;
INSERT INTO Loginy (IdAudit, Sesja, Kto, Time_now, Terminal,
Host_N, Autent)
VALUES (Licz, USER_sid, Uzytkownik, Time_now, Terminal, Host_N, Autent);
END IF;
END;

Pozostaje jeszcze obsłużenie zdarzenia związanego z wylogowaniem się. W tym celu


utworzony zostanie kolejny wyzwalacz, który uzupełni wpis dotyczący daty wylogo-
wania się dla bieżącej sesji użytkownika. Za pomocą uzyskanych przy użyciu funkcji
SYS_CONTEXT informacji (identyfikatora sesji użytkownika oraz jego nazwy) wybieramy
z tabeli Loginy wiersz, który został utworzony podczas logowania. Uzupełnienie bra-
kującej daty wylogowania wykonywane jest poleceniem UPDATE, wskazującym w klau-
zuli WHERE na znaleziony wiersz.
CREATE OR REPLACE TRIGGER Rozlacz
BEFORE LOGOFF
ON DATABASE
DECLARE
Time_now DATE;
USER_sid NUMBER;
Uzytkownik VARCHAR2(20);
Id_wiersza NUMBER;
BEGIN
IF (UPPER(USER) NOT IN ('SYSMAN', 'DBSNMP', 'SYS')) THEN
Time_now := SYSDATE;
USER_sid := SYS_CONTEXT('USERENV', ' SESSIONID ');
Uzytkownik := SYS_CONTEXT('USERENV', 'SESSION_USER');
SELECT IdAudit INTO Id_wiersza FROM Loginy
WHERE Sesja = USER_sid AND Kto = Uzytkownik;
UPDATE Loginy SET Time_off = Time_now WHERE IdAudit = Id_wiersza;
END IF;
END;

Niestety, z przyczyn przedstawionych poprzednio nie mamy pewności, że dla każdego


wiersza data wylogowania zostanie uzupełniona. Dla „porządnych” studentów mamy
więc informację o fakcie zalogowania i długości połączenia z bazą. Możemy też od-
szukać te osoby, które nie mają zwyczaju porządnego kończenia sesji. Trigger w tej
postaci nie pozwala na wyłowienie „leni”, ponieważ wystarczy zalogować się, dosta-
tecznie długo mieć włączoną sesję, a następnie przykładnie się wylogować, aby wpisy
w tabeli wyglądały „ładnie”. W międzyczasie można nie wykonać żadnego polecenia
SQL! Aby można było zweryfikować aktywność studenta, trzeba jeszcze uzupełnić
przechwytywane informacje.

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

Tabela 10.3. Wykaz zdarzeń dla triggerów tylko o zasięgu DATABASE


Zdarzenie Opis
AFTER STARTUP Występujące, kiedy baza danych jest otwierana START UP OPEN.
BEFORE SHUTDOWN Występujące tuż przed zatrzymaniem instancji bazy danych. Zezwala
na całkowite zatrzymanie bazy. Przy natychmiastowym zatrzymaniu
instancji (ABORT) wyzwalacz nie zostaje uruchomiony.
AFTER SERVERERROR Występujące, kiedy pojawia się błąd na poziomie serwera. Bez zdefiniowanych
dodatkowych warunków wyzwalacz uruchamia się dla każdego błędu.
Nie dotyczy to błędów ORA-1034, ORA-1403, ORA-1422, ORA-1423
oraz ORA-4030.
AFTER DB_ROLE_CHANGE Występujące, kiedy baza danych jest otwierana pierwszy raz po zmianie roli.

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.

Tabela 10.4. Wykaz błędów, które nie wyzwalają triggera SERVERERROR


Błąd Komunikat Opis
ORA−1034 Oracle non available Nie można połączyć się z serwerem, np. na skutek
zatrzymania instancji.
ORA−1403 No data found Zapytanie nie znalazło wierszy.
ORA−1422 Too many rows Zapytanie zwróciło przy podstawianiu do zmiennej
więcej niż jeden wiersz.
ORA−1423 Error encountered while checking Błąd podczas pozyskiwania przez kursor kolejnego
for extra rows in exact fetch rekordu z zestawu rekordów.
ORA−4030 Out of process memory Wyczerpanie zasobów pamięci dla procesu.

Spróbujmy skonstruować wyzwalacz dla przypadku wystąpienia błędu na serwerze.


W tym celu utwórzmy pomocniczą tabelę, która ma przechowywać informacje o dacie
wystąpienia błędu i użytkowniku, w którego sesji się pojawił, a także zawierać komu-
nikat i zapytanie, które ten błąd spowodowało.
CREATE TABLE Blady (
dt date,
kto varchar2( 30),
msg varchar2(512),
stmt varchar2(512)
);
Rozdział 10. ♦ Procedury wyzwalane 223

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

Tabela 10.5. Przykładowa zawartość tabeli Blady


dt kto msg stmt
08/11/14 TESTOWY ORA-00942: tabela lub perspektywa nie istnieje SELECT * FROM dddd
08/11/14 TESTOWY ORA-00904: „AAA”: niepoprawny identyfikator UPDATE loginy SET aaa=1

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.

W środowisku Oracle zdefiniowano dużą liczbę funkcji zdarzeń, które pozwalają na


odczytanie wielu interesujących informacji — ich wykaz (nazwy oraz parametry for-
malne) wraz z typem zwracanej zmiennej oraz syntetycznym opisem zawiera tabela 10.6.

Tabela 10.6. Wykaz funkcji zdarzeń


Nazwa funkcji Zwracany typ Opis
ora_client_ip_address VARCHAR2 Zwraca adres IP klienta podczas zdarzenia LOGON,
kiedy stosowany był protokół TCP/IP.
ora_database_name VARCHAR2(50) Zwraca nazwę instancji bazy danych.
ora_des_encrypted_password VARCHAR2 Zwraca hasło użytkownika zakodowane algorytmem,
kiedy jest ono tworzone lub zmieniane.
ora_dict_obj_name VARCHAR(30) Zawiera nazwę słownika, na którym wykonana
została operacja DDL.
ora_dict_obj_name_list BINARY_INTEGER Zwraca listę obiektów modyfikowanych w trakcie
(lista_nazw OUT wykonywania zdarzenia.
ora_name_list_t)
ora_dict_obj_owner VARCHAR(30) Zwraca nazwę właściciela obiektu, na którym
wykonywana jest operacja DDL.
ora_dict_obj_owner_list BINARY_INTEGER Zwraca listę właścicieli obiektów, które są
(lista_wlascicieli OUT modyfikowane podczas zdarzenia.
ora_name_list_t)
ora_dict_obj_type VARCHAR(20) Zwraca typ obiektu modyfikowanego za pomocą DDL.
ora_grantee BINARY_INTEGER Przez parametr OUT funkcja zwraca listę
(lista_uzytkownikow OUT użytkowników, którzy mają prawo do zdarzenia.
ora_name_list_t) Przez nazwę zwracana jest ich liczba.
ora_instance_num NUMBER Zwraca numer instancji bazy.
ora_is_alter_column BOOLEAN Zwraca wartość logiczną TRUE, jeśli kolumna,
(nazwa_kolumny IN VARCHAR2) której nazwa jest parametrem, była modyfikowana.
ora_is_creating_nested_table BOOLEAN Zwraca wartość logiczną TRUE, jeśli w bieżącym
zdarzeniu była tworzona tabela zagnieżdżona.
ora_is_drop_column BOOLEAN Zwraca wartość logiczną TRUE, jeśli kolumna,
(nazwa_kolumny IN VARCHAR2) której nazwa jest parametrem, została usunięta.
ora_is_servererror BOOLEAN Zwraca TRUE, jeśli dany błąd znajduje się na stosie
błędów.
ora_login_user VARCHAR2(30) Zwraca nazwę użytkownika.
Rozdział 10. ♦ Procedury wyzwalane 225

Tabela 10.6. Wykaz funkcji zdarzeń — ciąg dalszy


Nazwa funkcji Zwracany typ Opis
ora_partition_pos BINARY_INTEGER W wyzwalaczach INSTEAD OF dla zdarzenia CREATE
TABLE określa miejsce w tekście zapytania SQL,
gdzie może pojawić się klauzula PARTITION.
ora_privilege_list BINARY_INTEGER Zwraca jako parametr wyjściowy listę przywilejów
(lista_uprawnien OUT przyznanych lub odebranych użytkownikowi,
ora_name_list_t) a przez nazwę liczbę tych uprawnień.
ora_revoke BINARY_INTEGER Zwraca jako parametr wyjściowy listę użytkowników
(lista_uzytkownikow OUT odbierających uprawnienia, a przez nazwę ich liczbę.
ora_name_list_t)
ora_server_error NUMBER Dla podanej przez parametr pozycji (1 dla wierzchołka
(pozycja-na_stosie IN stosu) zwraca numer błędu pojawiający się na tej
binary_integer) pozycji stosu.
ora_server_error_depth BINARY_INTEGER Podaje całkowitą liczbę występujących na stosie
błędów.
ora_server_error_msg VARCHAR2 Dla podanej przez parametr pozycji (1 dla wierzchołka
(pozycja_na_stosie IN stosu) zwraca komunikat błędu pojawiający się na
binary_integer) tej pozycji stosu.
ora_server_error_num_params BINARY_INTEGER Dla podanej przez parametr pozycji (1 dla wierzchołka
(pozycja_na_stosie IN stosu) zwraca liczbę formatowań typu "%s", pod
binary_integer) którymi umieszczana jest zmienna (łańcuch).
ora_server_error_param VARCHAR2 Dla podanej przez parametr pozycji (1 dla wierzchołka
(pozycja_na_stosie in stosu) oraz numeru parametru zwraca wartość
binary_integer, parametr IN podstawioną pod formatowania typu "%s", "%d"
binary_integer) itp., pod którymi umieszczana jest zmienna (wartość).
ora_sql_txt (tekst_sql OUT BINARY_INTEGER Przez parametr zwraca tekst zapytania SQL, które
ora_name_list_t) wyzwoliło obsługę zdarzenia (trigger). Jeśli
polecenie SQL jest zbyt długie, jest ono dzielone
na kilka wierszy w tabeli wyjściowej. Przez nazwę
zwracana jest liczba wierszy w tej tabeli.
ora_sysevent VARCHAR2(20) Zwraca nazwę zdarzenia (zgodnie ze składnią przy
tworzeniu wyzwalacza), które wyzwoliło trigger.
ora_with_grant_option BOOLEAN Zwraca wartość TRUE, jeśli przywilej może być
przepisywany (z opcją GRANT OPTION).
space_error_info BOOLEAN Przez nazwę zwraca TRUE, jeśli błąd jest związany
(number_bledu OUT NUMBER, z wyczerpaniem się zasobów (out of space),
typ_bledu OUT VARCHAR2, i wypełnia parametry wyjściowe informacjami
wlasciciel_obiektu OUT o tym obiekcie.
VARCHAR2,
nazwa_przestrzeni_tabel OUT
VARCHAR2, nazwa_obiektu OUT
VARCHAR2, nazwa podobiektu
OUT VARCHAR2)

W wielu funkcjach zdarzeń występuje zmienna tablicowa ora_name_list_t, której de-


klaracja ma postać:
TYPE ora_name_list_t IS TABLE OF VARCHAR2(64);
226 Część II ♦ ORACLE PL/SQL

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.

Z opisywanych funkcjonalności pozostaje jeszcze odpowiedź na pytanie postawione


podczas omawiania triggerów, a dokładniej stosowania ich do śledzenia aktywności
użytkowników — jak monitorować ich aktywność podczas wykonywania zapytań?
Rozwiązanie tego problemu jest trudne, jeśli chcemy uwzględniać wszystkie zapytania,
a zwłaszcza wybierające. Pierwszym pytaniem, na które postaram się odpowiedzieć,
jest pytanie o to, jakie polecenie w bieżącej chwili wykonuje konkretny użytkownik. Aby
to stwierdzić, po pierwsze musimy określić identyfikator jego sesji. W tym celu od-
pytujemy systemową perspektywę v$session.
SELECT sid, schemaname, osuser, substr(machine, 1, 20) Machine
FROM v$session
ORDER BY schemaname;

Tak sformułowane zapytanie generuje zestaw rekordów, którego przykładowy fragment


zawiera tabela 10.7. W zestawie tym dominować będą informacje o sesjach związa-
nych z automatycznie uruchamianymi procesami administracyjnymi np. z użytkowni-
kiem SYSTEM.

Tabela 10.7. Przykładowy rezultat zapytania do perspektywy v$session


SID SCHEMANAME OSUSER MACHINE
103 SYS AP
143 SYS SYSTEM ORC
138 SYSMAN SYSTEM ORC
108 TESTOWY AP
122 TESTOWY AP

Możemy tę informację odfiltrować, budując odpowiednią klauzulę WHERE i ograniczając


listę tylko do danych związanych z interesującym nas użytkownikiem. Niestety, nie
daje to jednoznacznej odpowiedzi na pytanie, z której sesji korzysta wybrany użyt-
kownik — jak widać w tabeli 10.7, użytkownik TESTOWY ma otwarte dwie sesje.
W przypadku stosowania SQL Developera jest to stan naturalny, gdyż jedna z sesji jest
związana z odpytywaniem serwera w celu wyświetlenia informacji dotyczących sche-
Rozdział 10. ♦ Procedury wyzwalane 227

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);

Niestety, zapytanie to wyświetla tylko tekst zapytania wykonywanego w chwili wy-


woływania polecenia. Jeśli w sesji użytkownika wykonywane są polecenia o krótkim
czasie wykonania, prawdopodobieństwo uzyskania jakiegokolwiek rekordu jest mi-
nimalne, chyba że polecenie takie będzie cyklicznie wykonywane w bardzo krótkich
odstępach czasu. Nie jest to jednak rozwiązanie adekwatne do naszych oczekiwań.

Inną próbą rozwiązania dylematu jest odpytanie połączonych perspektyw systemowych


v_$sql oraz all_users. Perspektywa v_$sql zawiera informacje o czasie zbudowania
planów wykonania zapytania (prekompilacji) dla wszystkich poleceń, skryptów, pro-
cedur etc., przechowywanych w pamięci współdzielonej SGA. Stąd, poza tekstem pole-
cenia, zawiera ona informacje o elementach związanych z przetwarzaniem tego za-
pytania (liczba odczytów z dysku, liczba wykonań, czas pierwszego wykonania etc.)
niezbędne do oceny wydajności.
SELECT
module, sql_text, username, disk_reads_per_exec,
buffer_gets, disk_reads, parse_calls, sorts,
executions, rows_processed, hit_ratio, first_load_time,
sharable_mem, persistent_mem,
runtime_mem, cpu_time, elapsed_time, address, hash_value
FROM
(SELECT
module, sql_text , u.username ,
round((s.disk_reads/decode(s.executions,0,1, s.executions)),2) disk_reads_per_exec,
s.disk_reads, s.buffer_gets, s.parse_calls,
s.sorts, s.executions, s.rows_processed,
100 - round(100 * s.disk_reads/greatest(s.buffer_gets,1),2) hit_ratio,
s.first_load_time, sharable_mem, persistent_mem, runtime_mem, cpu_time,
elapsed_time, address, hash_value
FROM
sys.v_$sql s, sys.all_users u
WHERE
s.parsing_user_id = u.user_id
AND UPPER(u.username) NOT IN
('SYS', 'SYSTEM', 'WKSYS', 'DBSNMP', 'OWBSYS', 'SYSMAN', 'MDSYS', 'EXFSYS')
ORDER BY username DESC)
WHERE
ROWNUM <= 40;
228 Część II ♦ ORACLE PL/SQL

Należy zauważyć, że pole module przechowuje informację o aplikacji, z której wyko-


nano zapytanie, natomiast username zawiera dane użytkownika, który pierwszy raz to
zapytanie wykonał. Każde kolejne wykonanie takiego samego zapytania (zgodność spraw-
dzana jest co do znaku) powoduje tylko zwiększenie wartości w polu executions. Po-
nieważ prawdopodobieństwo zapisania zapytania dokładnie w ten sam sposób przez
dwóch pracujących równolegle, ale niezależnie użytkowników szybko maleje wraz ze
stopniem komplikacji składni (stopniem trudności), mamy dość dobre narzędzie do
sprawdzania aktywności użytkowników bez względu na charakter wykonywanych po-
leceń. Na nieszczęście dla osoby monitorującej, szybkie metody komunikacji (poczta
elektroniczna, komunikator) oraz możliwość kopiowania i wklejania gotowych rozwią-
zań prowadzą do niejednoznaczności otrzymywanych wyników. Dodatkowo w perspek-
tywie v_$sql zapisywane są polecenia automatycznie generowane przez procesy dru-
goplanowe (silnik bazy danych); stąd w naszym zapytaniu lista wyeliminowanych wpisów
dla użytkowników o nazwach typu %SYS%. Należy dodać, że ze względu na cyklicz-
ność wykonywania wielu zapytań generowanych przez użytkowników systemowych
to właśnie te wpisy są najliczniej reprezentowane. Możemy wspomnieć, że obszar pa-
mięci przydzielonej na te wpisy jest ograniczony i stare, rzadko wykonywane zapytania
są nadpisywane. Jak widać, pomimo starań nie udało nam się znaleźć wystarczająco
satysfakcjonującego rozwiązania.
Rozdział 11.
Kursory
Pojęcie kursora jest związane z nawigowaniem, przemieszczaniem się pośród rekordów.
W Oracle kursor jest obiektem, który zawiera jeden rekord z zestawu rekordów zdefi-
niowanego przez dowolne poprawne składniowo zapytanie wybierające. Przykładowa
deklaracja może mieć postać:
DECLARE
CURSOR sledz IS SELECT Nazwisko, RokUrodz FROM Osoby;

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

W sekcji deklaracji poza kursorem zadeklarowano dwie zmienne pomocnicze zgodne


co do typu z polami zapytania definiującego kursor. Liczba zmiennych wynika z liczby
wyprowadzanych zapytaniem pól. W ciele bloku anonimowego po otwarciu kursora
wykonano przejście do pierwszego rekordu z ich zestawu za pomocą polecenia FETCH.
Polecenie to po słowie kluczowym INTO zawiera listę pól, do których podstawione zo-
staną wartości pól bieżącego rekordu. Obowiązuje odpowiedniość pozycyjna pól zapy-
tania i zmiennych. Nawigacja po pozostałych rekordach zestawu wykonywana jest
w ciele pętli WHILE za pomocą polecenia FETCH. Należy zauważyć, że przesuwa ono kur-
sor zawsze o jedną pozycję do przodu. Zastosowana funkcja DBMS_OUTPUT.PUT_LINE,
wyświetlająca wartości zmiennych zainicjowane kursorem, ma tylko na celu zazna-
czenie miejsca, w którym powinny być one przetwarzane za pomocą bardziej złożo-
nego kodu. Przeniesienie przetwarzania poza polecenie FETCH występujące w pętli spo-
woduje, po pierwsze, utratę danych z pierwszego rekordu, a po drugie po wyjściu poza
zestaw rekordów przetwarzane będą wartości NULL. Sprawdzenie warunku zakończenia
pętli oparte jest na atrybucie kursora %FOUND, który zwraca wartość TRUE, kiedy kursor
jest we wnętrzu zestawu rekordów, oraz wartość FALSE, kiedy znajdzie się poza nim.
Stąd, aby wejść do pętli, konieczne było wykonanie przed jej rozpoczęciem inicjalnego
FETCH — inaczej atrybut %FOUND miałby wartość NULL. Pozostałe atrybuty kursorów wraz
z krótkim opisem zawiera tabela 11.1.

Tabela 11.1. Atrybuty kursorów


Atrybut Typ Opis
%ISOPEN Boolean TRUE, jeśli kursor jest otwarty.
%NOTFOUND Boolean TRUE, jeśli ostatnie polecenie FETCH nie zwróciło rekordu.
%FOUND Boolean TRUE, jeśli ostatnie polecenie FETCH zwróciło rekord.
%ROWCOUNT Number Podaje liczbę przetworzonych do tej pory rekordów.

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 przedstawionym przykładzie zastosowano wskazanie na bieżący rekord do zmody-


fikowania poleceniem UPDATE pola RokUrodz. Odwołanie następuje w klauzuli WHERE
zapytania aktualizującego dane, a aktualizacja wykonywana jest tylko wówczas, kiedy
odczytana wartość pola RokUrodz jest nieokreślona (NULL). Stosowanie kursora FOR
UPDATE wydaje się atrakcyjne, należy jednak pamiętać, że potrzebuje on większych za-
sobów niż klasyczny kursor. Dodatkowo, jeżeli w przetwarzanym zestawie rekordów
(tabeli) istnieje pole klucza podstawowego, to do identyfikacji bieżącego rekordu można
użyć jego. Stąd możemy przyjąć, że korzystanie z kursora FOR UPDATE jest wskazane
tylko wtedy, kiedy nie jesteśmy w stanie w sposób inny niż poprzez metodę CURRENT OF
wskazać na bieżący rekord. W praktyce taka sytuacja pojawia się dość rzadko.

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;

W przedstawionym przykładzie zadeklarowano typ pracownicy_type, który jest kur-


sorem referencyjnym, natomiast pracownik jest zmienną tego typu. Zmienna pomoc-
nicza pracownicy_rec ma strukturę rekordu tabeli Osoby. W ciele bloku anonimowego
kursor pracownik jest otwierany dla zestawu rekordów stanowiącego wszystkie rekordy
z tej tabeli. Dalej nawigacja jest realizowana, jak w innych kursorach, za pomocą po-
lecenia FETCH, a rezultat podstawiany jest do zmiennej pracownicy_rec.

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;

Na potencjalną potęgę kursora referencyjnego wskazuje kolejny przykład, w którym


zapytanie nie jest dane jawnie podczas otwierania kursora. Zastosowano tu pomocni-
czą zmienną znakową zap, do której przypisano frazę stanowiącą poprawne składniowo
zapytanie wybierające. Przy otwieraniu kursora zastosowano tę właśnie zmienną za-
miast statycznej definicji zapytania.
DECLARE
TYPE pracownicy_type IS REF CURSOR;
pracownik pracownicy_type;
pracownicy_rec Osoby%ROWTYPE;
zap varchar2(111);
BEGIN
zap := 'SELECT * FROM Osoby';
OPEN pracownik FOR zap;
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;

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

Powyższy przykład pokazał użycie kursora niejawnego do przetworzenia wszystkich


rekordów z tabeli Osoby. Zaletą kursora niejawnego jest jego prostota i zwartość pod-
czas pisania kodu. Wadą, w porównaniu z kursorem referencyjnym, jest brak możli-
wości zdefiniowania zapytania za pomocą zmiennej znakowej. Powoduje to, że kursor
niejawny jest zawsze konstrukcją statyczną o ściśle zdefiniowanym zestawie rekordów.

Rozważmy zadanie polegające na wyświetleniu w jednej linii informacji o szefie i jego


bezpośrednim podwładnym — jeden wiersz dla każdego pracownika. Aby je wykonać,
zadeklarujemy typ cur_t będący kursorem referencyjnym bez zdefiniowanego typu
zwracanych danych. Zadeklarowane zostaną dwie zmienne kursorowe cur oraz cur1,
będące jego dwoma instancjami, a także dwie zmienne rekordowe res i res1 o typie
zgodnym z rekordem w tabeli Osoby.
SET SERVEROUTPUT ON;
DECLARE
TYPE cur_t is REF CURSOR;
cur cur_t;
cur1 cur_t;
res Osoby%ROWTYPE;
res1 Osoby%ROWTYPE;
BEGIN
OPEN cur FOR SELECT * from Osoby;
FETCH cur INTO res;
WHILE cur%FOUND
LOOP
OPEN cur1 FOR
SELECT * FROM Osoby WHERE IdSzefa= res.IdOsoby;
FETCH cur1 INTO res1;
WHILE cur1%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(res.Nazwisko ||
' jest szefem ' || res1.Nazwisko);
FETCH cur1 INTO res1;
END LOOP;
FETCH cur INTO res;
CLOSE cur1;
END LOOP;
CLOSE cur;
END;

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

Tabela 11.2. Skutek wykonania skryptu z zagnieżdżonym kursorem


KOWALSKI jest szefem KOWALIK
KOWALSKI jest szefem KOW
NOWAK jest szefem NOWIK
NOWAK jest szefem KOWALSKI
NOWAK jest szefem NOWACKI
NOWAK jest szefem JAKOW
KOWALIK jest szefem WILK
⋅⋅⋅

Kursory referencyjne wykorzystywane są często w procedurach składowanych. Spró-


bujmy w ten sposób rozwiązać zadanie polegające na wyświetleniu pełnej hierarchii
pracowników. W tym celu zbudujmy procedurę inner mającą dwa parametry: wska-
zujący na identyfikator pracownika — kto, oraz na poziom hierarchii — poziom. Oba
są całkowite oraz mają zdefiniowany kierunek przekazywania IN OUT — przekazywa-
nie wartości do i z procedury. W ciele procedury zadeklarowany jest typ kursora refe-
rencyjnego cur_t, zmienna tego typu cur oraz zmienna przeznaczona na rezultat prze-
twarzania kursora res.
CREATE OR REPLACE PROCEDURE inner
(kto IN OUT int, poziom IN OUT int)
IS
TYPE cur_t IS REF CURSOR;
cur cur_t;
res Osoby%ROWTYPE;
BEGIN
poziom:=poziom+1;
OPEN cur FOR SELECT * FROM Osoby Where IdSzefa=kto;
FETCH cur INTO res;
WHILE cur%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(to_char(poziom)|| ' ' ||
res.nazwisko);
kto:=res.idosoby;
inner(kto, poziom);
FETCH cur INTO res;
END LOOP;
CLOSE cur;
poziom:=poziom-1;
END;

Procedura w pierwszym wierszu ciała zwiększa zmienną poziom o 1, wskazując na zmianę


poziomu hierarchii (drzewa) — kolejny stopień zagnieżdżenia. Następnie otwierany
jest zestaw rekordów zawierający dane osób, których szef ma identyfikator przekazany
pierwszym parametrem wywołania kto. We wnętrzu pętli WHILE, przetwarzanej do końca
zestawu rekordów, najpierw wyświetlany jest komunikat zawierający numer poziomu
oraz nazwisko pracownika (podwładnego szefa o identyfikatorze kto), po czym iden-
tyfikator tego pracownika zostaje podpisany pod zmienną kto. Następnie wywoływana
jest rekurencyjnie procedura inner dla nowego identyfikatora szefa. Oznacza to, że bę-
dziemy poszukiwać podwładnych bieżącego pracownika, czyli coraz bardziej zagłębiać
238 Część II ♦ ORACLE PL/SQL

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.

Jeżeli chcemy ograniczyć wyprowadzane rekordy tylko do pojedynczego drzewa za-


wierającego korzeń główny, wystarczy zmienić zestaw rekordów obsługiwany w pro-
cedurze outer, dodając odpowiednią klauzulę WHERE.
CREATE OR REPLACE PROCEDURE outer
IS
TYPE cur_t IS REF CURSOR;
cur cur_t;
res Osoby%ROWTYPE;
kto int;
poziom int;
BEGIN
poziom:=1;
OPEN cur FOR SELECT * FROM Osoby WHERE IdSzefa IS NULL;
FETCH cur INTO res;
WHILE cur%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(' #####################');
DBMS_OUTPUT.PUT_LINE(to_char(poziom)|| ' ' ||
240 Część II ♦ ORACLE PL/SQL

Tabela 11.4. Skutek wykonania bloku anonimowego wywołującego procedurę outer


#####################
1 Kowalski
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
....
#####################
1 Nowak
2 Janik
3 Kowalski
3 Nowicki
3 Lew
2 Jasiński
2 Pawlak
2 Gawlik
....
#####################
1 Nowicki
#####################
1 Kowalski

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

i wykorzystanie go w dowolnym kodzie PL/SQL w schematach użytkowników, którzy


mają prawo do posługiwania się tym pakietem. Można również zdefiniować zmienne
tego typu, które będą widoczne poza pakietem.
CREATE OR REPLACE PACKAGE pakiet
IS
TYPE prac_type IS REF CURSOR RETURN Osoby%ROWTYPE;
TYPE cur_type IS REF CURSOR;
cur cur_type;
globalna INT;
FUNCTION liczf(mini INT, ile OUT INT) RETURN INT;
FUNCTION liczf(ile OUT INT) RETURN INT;
FUNCTION koduj(pole VARCHAR, kod CHAR) RETURN INT;
END;

Podobnie globalny typ kursora referencyjnego możemy zdefiniować w ciele pakietu.


Będzie on wtedy typem wewnętrznym, czyli takim, który może być wykorzystywany
we wszystkich procedurach (funkcjach) ciała pakietu, nie będzie jednak widoczny po-
za nim. Możliwe jest również zadeklarowanie zmiennych tego typu lub typu kurso-
rowego zdefiniowanego w pakiecie. Zmiennymi globalnymi wewnętrznymi mogą być
także zmienne typów złożonych %TYPE lub %ROWTYPE.
CREATE OR REPLACE PACKAGE BODY pakiet
IS
TYPE prac_type_w IS REF CURSOR RETURN Osoby%ROWTYPE;
TYPE cur_type_w IS REF CURSOR;
cur_w cur_type;
cur_w1 cur_type_w;
gobal_wew INT;
gobal_wew1 Osoby.Nazwisko%TYPE;
gobal_wew2 Osoby%ROWTYPE;
FUNCTION liczf(mini INT, ile OUT INT)RETURN INT
IS
wew INT;
BEGIN
...
END;
...
END;

Rozważmy użycie kursora referencyjnego w pakiecie. Przykład ma za zadanie zilu-


strować możliwość stosowania procedur pakietu do otwierania i nawigacji kursorem
referencyjnym. Ze względu na to w pakiecie zadeklarowano typ kursora referencyjnego
oraz dwie procedury. Pierwsza, Open_Osoby_c, ma dwa parametry: jeden typu kursora
referencyjnego, a drugi całkowity, który ma reprezentować parametr w klauzuli WHERE
definicji zestawu rekordów tego kursora (w tym przypadku identyfikator działu). Druga
procedura, Fetch_Osoby_c, ma jako pierwszy parametr zmienną typu kursora referen-
cyjnego, a jako drugi zmienną typu zgodnego z rekordem zwracanym przez kursor.
Należy zwrócić uwagę na fakt, że w przypadku obu procedur zmienna typu kursora
ma możliwość przekazywania danych zarówno do, jak i z procedury — IN OUT. Wy-
nika to z istnienia konieczności przekazywania informacji o bieżącym stanie kursora
do miejsca wywołania i odwrotnie.
242 Część II ♦ ORACLE PL/SQL

CREATE OR REPLACE PACKAGE P_cur


AS
TYPE Osoby_T IS REF CURSOR RETURN Osoby%ROWTYPE;
PROCEDURE Open_Osoby_c (Osoby_c IN OUT Osoby_T,
Dzial IN INTEGER);
PROCEDURE Fetch_Osoby_c (Osoby_c IN Osoby_T,
Osoby_row OUT Osoby%ROWTYPE);
END P_cur;

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;

Zastosowanie takiego pakietu przedstawione zostało w bloku anonimowym. Ma w nim


miejsce otwarcie zestawu rekordów dla wybranego działu przez wywołanie procedury
P_cur.Open_Osoby_c. Następnie w pętli bez warunku LOOP ... END LOOP wywoływana
jest procedura P_cur.Fetch_Osoby_c. Wyjście z pętli następuje po przekroczeniu ostat-
niego rekordu na skutek wykonania instrukcji EXIT WHEN, po sprawdzeniu atrybutu
%NOTFOUND kursora.
DECLARE
Osoby_cur P_cur.Osoby_T;
Dzial Osoby.IdDzialu%TYPE;
Osoby_row Osoby%ROWTYPE;
BEGIN
Dzial := 2;
P_cur.Open_Osoby_c(Osoby_cur, Dzial);
LOOP
P_cur.Fetch_Osoby_c(Osoby_cur, Osoby_row);
EXIT WHEN Osoby_cur%NOTFOUND;
DBMS_OUTPUT.PUT(Osoby_row.Nazwisko || ' ');
DBMS_OUTPUT.PUT_LINE(Osoby_row.RokUrodz);
END LOOP;
END;

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

CREATE OR REPLACE PACKAGE Osoby_Dzialy_data


AS
TYPE Cur_type IS REF CURSOR;
PROCEDURE Open_Cur(Cur IN OUT Cur_type,
co IN POSITIVE);
END Osoby_Dzialy_data;

W pakiecie zadeklarowano typ kursora referencyjnego bez zdefiniowanego typu zwraca-


nego przez ten kursor oraz jedną procedurę z dwoma parametrami: pierwszym IN OUT
będącym instancją kursora oraz drugim — całkowitym dodatnim (dość rzadko uży-
wane ograniczenie typu całkowitego).
CREATE OR REPLACE PACKAGE BODY Osoby_Dzialy_data
AS
PROCEDURE Open_Cur(Cur IN OUT Cur_type,
co IN POSITIVE) IS
BEGIN
IF co = 1 THEN
OPEN Cur FOR SELECT * FROM Osoby
WHERE RokUrodz > 1970;
ELSIF co = 2 THEN
OPEN Cur FOR SELECT * FROM Dzialy;
END IF;
END Open_Cur;
END Osoby_Dzialy_data;

W ciele pakietu zdefiniowana została procedura, która w zależności od wartości pa-


rametru co otwiera kursor dla zestawu rekordów związanego z tabelą Osoby (co = 1)
lub Dzialy (co = 2). Oczywiście można sobie wyobrazić większą liczbę warunków
i związaną z tym większą liczbę typów zestawów rekordów. To, co dziwne w tym
przykładzie, ilustruje zamieszczone poniżej wywołanie tej procedury z bloku anoni-
mowego.
DECLARE
Osoby_rec Osoby%ROWTYPE;
Dzial_rec Dzialy%ROWTYPE;
Cur Osoby_Dzialy_data.Cur_type;
BEGIN
Osoby_Dzialy_data.open_cur(Cur, 1);
FETCH Cur INTO Dzial_rec;
DBMS_OUTPUT.PUT(Dzial_rec.Nazwa);
DBMS_OUTPUT.PUT_LINE(' ' || Dzial_rec.kod);
EXCEPTION
WHEN ROWTYPE_MISMATCH THEN
DBMS_OUTPUT.PUT_LINE(
'Niezgodny typ rekordowy dla działów,' ||
' nawiguję po osobach...');
FETCH Cur INTO Osoby_rec;
DBMS_OUTPUT.PUT(Osoby_rec.Nazwisko);
DBMS_OUTPUT.PUT_LINE(' ' || Osoby_rec.RokUrodz);
END;

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

ale w instrukcji FETCH próbujemy przypisać go do zmiennej zgodnej z wierszem tabeli


Dzialy. Powoduje to powstanie błędu i przeniesienie przetwarzania do sekcji obsługi
wyjątków. Właściwe przetworzenie instrukcją FETCH następuje w obsłudze wyjątku
ROWTYPE_MISMATCH, ponieważ tutaj dokonujemy przypisania do zmiennej zgodnej z wier-
szem tabeli Osoby. Gdyby wywołanie procedury Osoby_Dzialy_data.open_cur wyko-
nane zostało z drugim parametrem równym dwa, przetworzenie nie spowodowałoby
przejścia do sekcji obsługi wyjątków. Po raz drugi zastosowano przetworzenie wła-
ściwego kodu w tej sekcji (poprzednio przy omawianiu wyjątków w procedurze), ale
takie rozwiązanie w praktyce nie powinno być stosowane — stąd mowa o dziwności
przykładu.

Oczywiście można sobie wyobrazić całkowitą zmianę źródła danych w zależności od


parametru wywołania, ale w praktyce ma to miejsce w przypadku, kiedy typ zmiennej
zwracanej przez kursor referencyjny dla każdego źródła jest taki sam. Dzieje się tak,
kiedy chcemy np. wyznaczać różne funkcje agregujące, co do których możemy zało-
żyć, że zwracają zmienną numeryczną. Rozważmy to na przykładzie bliźniaczego pa-
kietu agregat.
CREATE OR REPLACE PACKAGE agregat
AS
TYPE Cur_type IS REF CURSOR;
PROCEDURE Open_Cur(Cur IN OUT Cur_type,
co IN POSITIVE);
END agregat;

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;

Teraz, bez względu na wartość drugiego parametru, przetwarzanie bloku anonimowego


zakończy się sukcesem, a w sekcji obsługi wyjątków ograniczono się do wyprowadze-
nia odpowiedniego komunikatu.
DECLARE
Wynik real;
Cur agregat.Cur_type;
BEGIN
Rozdział 11. ♦ Kursory 245

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;

Niestety, przedstawione w tym miejscu przykłady kursorów referencyjnych mają dość


akademicki charakter i mała jest szansa na ich praktyczne zastosowanie. Zaprezento-
wanie pełnej funkcjonalności tego typu wymaga wprowadzenia dynamicznego SQL,
dlatego dopiero w poświęconym mu rozdziale zostaną opisane przykłady bardziej
użyteczne z praktycznego punku widzenia.

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;

Wykonanie polecenia ROLLBACK TO punkt; powoduje wycofanie zmian występujących


między tym poleceniem a punktem wycofania transakcji, na który wskazuje instrukcja
wycofująca zmiany. W takiej sytuacji polecenia zawarte pomiędzy początkiem trans-
akcji a punktem wycofania nie są ani zatwierdzone, ani wycofane — pozostają w sta-
nie niestabilnym. Dlatego konieczne jest zatwierdzenie tych poleceń instrukcją COMMIT
występującą na samym końcu przykładowego kodu. Po jego wykonaniu nazwiska w ta-
beli Osoby będą pisane od dużej litery. O transakcjach z punktem wycofania, tak jak
to pokazano w przykładzie, można myśleć jako o dwóch transakcjach zagnieżdżonych:
podrzędnej, rozpoczynającej się od tego punktu, która jest wycofywana, i nadrzędnej,
ciągnącej się od początku przetwarzania transakcji, która jest zatwierdzana. Możemy
powiedzieć, że zatwierdzeniu podlega również częściowe wycofanie zmian.

Oczywiście wycofanie do punktu SAVEPOINT powinno odbywać się warunkowo. Moż-


liwe jest zastosowanie wielu warunków i wielu punktów wycofania. Schemat takiego
przetwarzania zapisany metajęzykiem mógłby wyglądać następująco:
BEGIN
SAVEPOINT punkt;
Polecenia SQL;
SAVEPOINT punkt1;
Polecenia SQL;
SAVEPOINT punkt2;
Polecenia SQL;
...
IF (warunek2) THEN
ROLLBACK TO punkt2;
END IF;
IF (warunek1) THEN
ROLLBACK TO punkt1;
END IF;
IF (warunek) THEN
ROLLBACK TO punkt;
END IF;
COMMIT;
END;
Rozdział 12. ♦ Transakcje 249

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;

Bardziej spektakularne będzie jednak zastosowanie zatwierdzenia bądź wycofania trans-


akcji w ciele procedury. Rozważymy ten przypadek na podstawie nieco zmodyfikowa-
nej procedury exe_tekst, która ma trzy parametry wejściowe typu znakowego. Pierw-
szy z nich reprezentuje nazwę funkcji modyfikującej dane, drugi ma zawierać nazwę
zmienianego pola, natomiast trzeci nazwę modyfikowanej tabeli. W sekcji deklaracji tej
procedury zadeklarowano dwie zmienne pomocnicze: znakową przeznaczoną na tekst
wykonywanego zapytania oraz zmienną reprezentującą błąd użytkownika.
CREATE OR REPLACE PROCEDURE exe_tekst
(typ varchar2, pole varchar2, tabela varchar2)
IS
zap varchar2(111);
blad EXCEPTION;
BEGIN
IF UPPER(typ) NOT IN('UPPER', 'LOWER', 'INITCAP') THEN
RAISE blad;
ELSE
zap:= 'UPDATE ' || tabela || 'SET ' || pole || '=' || typ || '(' || pole ||')';
EXECUTE IMMEDIATE zap;
COMMIT;
END IF;
EXCEPTION
WHEN blad THEN
RAISE;
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE(Transakcja została wycofana na skutek błędu: ' || SQLCODE);
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

W dalszej kolejności modyfikacja jest zatwierdzana poleceniem COMMIT. W sekcji ob-


sługi wyjątków obsłużono błąd użytkownika. W tym przypadku nie jest konieczne
wycofywanie zmian, ponieważ nie doszło do żadnej modyfikacji. Obsłużony został rów-
nież błąd OTHERS reprezentujący wszystkie inne błędy. W jego obsłudze zdecydowano
się na wycofanie transakcji, ponieważ może on również reprezentować zdarzenia zwią-
zane z niepowodzeniem przetwarzania.

Pozostaje komentarz dotyczący zastosowania dyrektyw kompilatora w przypadku prze-


twarzania transakcyjnego. Rozpocznijmy od utworzenia tabeli pomocniczej o jednym
polu numerycznym. Wpiszmy do jej pierwszego rekordu wartość 2.
DROP TABLE dummy;
CREATE TABLE dummy (x number);
INSERT INTO dummy VALUES (2);

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;

Zastosujmy teraz naszą procedurę w bloku anonimowym. W bloku tym zadeklarujmy


kursor FOR UPDATE nad wszystkimi rekordami tabeli pomocniczej (w pierwszej chwili
zawiera ona tylko jeden rekord). W ciele procedury kursor nawiguje po wszystkich
rekordach tabeli, modyfikując rekord bieżący tak, że wartość pola jest mnożona przez
dwa. Również w ciele pętli związanej z nawigacją kursora wywoływana jest uprzed-
nio opracowana procedura wstawiająca kolejny wiersz.
DECLARE
cursor c IS SELECT * FROM dummy FOR UPDATE;
BEGIN
FOR i IN c
LOOP
UPDATE dummy SET x = x * 2 WHERE CURRENT OF c;
autonomous_proc;
END LOOP;
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.

Omówiliśmy sposoby zatwierdzania zmian, które są wprowadzane „ręcznie” przez użyt-


kownika, jednak do tej pory wstawialiśmy, modyfikowaliśmy i usuwaliśmy wiersze, nie
troszcząc się o zatwierdzanie tych operacji, a i tak po zakończeniu pracy były one za-
Rozdział 12. ♦ Transakcje 251

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;

Równie dobrze możemy zastosować polecenie:


SET AUTOCOMMIT IMMEDIATE;

Teraz wszystkie modyfikacje danych będą zatwierdzane bezpośrednio po wykonaniu


zapytania. Po poprawnej zmianie ustawień autozapisu Oracle wyświetla komunikat:
COMMIT COMPLETE

Możemy zmienić częstotliwość automatycznego zapisywania, wykonując polecenie:


SET AUTOCOMMIT 10;

Oznacza ono, że zapis będzie wykonywany po 10 zapytaniach modyfikujących dane.


Należy zauważyć, że przy takim ustawieniu blok PL/SQL (procedura, funkcja) jest
traktowany jako pojedyncze zapytanie modyfikujące, bez względu na to, ile takich
zapytań faktycznie w swoim ciele zawiera. Liczba występująca po AUTOCOMMIT nie może
być mniejsza niż zero ani większa niż dwa miliony. W celu przywrócenia stanu domyśl-
nego należy wykonać polecenie:
SET AUTOCOMMIT OFF;

Aby zobaczyć aktualny stan zatwierdzania transakcji, możemy użyć polecenia:


SHOW AUTOCOMMIT;

Na skutek jego działania otrzymamy komunikat o postaci:


AUTOCOMMIT OFF
252 Część II ♦ ORACLE PL/SQL

Trochę na marginesie głównego toku wykładu należy wspomnieć w kilku słowach


o przypisywaniu nazw do obiektów. Czasami, np. w celu ujednolicenia nazewnictwa
dla całego realizowanego projektu, konieczne jest przypisanie do nich nowych nazw.
Zastosowanie polecenia RENAME spowoduje, że nie będziemy już mogli posługiwać się
starą nazwą. To wymusi na nas przejrzenie wszystkich obiektów, w których odwoły-
waliśmy się do obiektu o zmienianej nazwie, i zastąpienie jej nową. Wygodniej jest
jednak zastosować synonim, który tworzy równoważną nową nazwę, ale pozostawia
możliwość posługiwania się starą. Zilustrujmy to na przykładzie tabeli Osoby.
CREATE SYNONYM Osoby_s FOR Osoby;

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

Możliwe jest również usuwanie synonimu:


DROP PUBLIC SYNONYM Osoby_ps1

oraz nadpisywanie istniejącego synonimu nowym:


CREATE OR REPLACE PUBLIC SYNONYM Osoby_ps1 FOR Osoby

Domyślnie prawo do nadawania synonimów publicznych ma użytkownik z uprawnie-


niami DBA (DataBase Administrator). Użytkownicy o niższych uprawnieniach muszą
mieć jawnie przypisane uprawnienie do ich tworzenia.
Rozdział 13.
Dynamiczny SQL
Przejdźmy teraz do omówienia tak często już sygnalizowanego zastosowania dynamicz-
nego SQL. Pierwsze rozwiązania są nam już znane, chociażby z procedury exe_tekst
przedstawionej w rozdziale 7. Pomimo wszystko zacznijmy od początku. Polecenie
EXECUTE IMMEDIATE pozwala na wykonanie zapytania (za wyjątkiem zapytania wybie-
rającego), które jest zawarte w łańcuchu lub zmiennej znakowej. Przykładem niech bę-
dzie dokonanie modyfikacji danych w tabeli Osoby zapisanej w postaci łańcucha.
BEGIN
EXECUTE IMMEDIATE 'UPDATE Osoby
SET Nazwisko = INITCAP(Nazwisko)';
END;

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

EXECUTE IMMEDIATE 'INSERT INTO Dzialy


VALUES (:1, :2) ' USING ktory+1, 'Test';
COMMIT;
END;

Przekazanie wartości zmiennej z dynamicznie wykonywanego zapytania przy użyciu


polecenia EXECUTE IMMEDIATE odbywa się dzięki zastosowaniu po łańcuchu (zmiennej)
zawierającym to zapytanie operatora INTO, po którym następuje nazwa zmiennej, do
której wartość ta jest podstawiana. Zmienna musi być zgodna co do typu z wartością
zwracaną przez zapytanie. Jeśli zwraca ono więcej niż jedno pole, lista zmiennych, do
których podstawiane są wartości, jest separowana przecinkami. Obowiązuje podsta-
wianie pozycyjne — pierwsza wartość do pierwszej zmiennej, druga do drugiej itd.
Oczywiście ważna jest również zgodność liczby wartości i zmiennych oraz pozycyjna
odpowiedniość ich typów. W przypadku przekazywania zmiennych do dynamicznego
SQL sprawa jest nieco bardziej skomplikowana. Przede wszystkim konieczne jest uży-
cie w ciele zapytania zmiennych wiązania, to jest takich, których nazwa zaczyna się od
dwukropka. Ich nazwy mogą być dowolne — w analizowanym przykładzie użyto w tej
roli liczb :1 oraz :2. Taka konwencja ma tylko pokazać kolejność, z jaką będą do nich
podstawiane wartości. Natomiast listę wartości podstawianych do zapytania (również
zawartych w zmiennych) umieszczamy po operatorze USING. Jest ona separowana
przecinkami i, tak jak w przypadku poprzednim, obowiązuje podstawianie pozycyjne
oraz pozycyjna zgodność typów.

Jako element dynamicznego SQL może również występować wywołanie procedury.


Aby zilustrować ten przypadek, utwórzmy najpierw pomocniczą procedurę test, która
ma trzy parametry całkowite — w kolejności typów IN, OUT oraz IN OUT. Procedura li-
czy osoby pracujące w dziale, którego identyfikator jest przekazywany pierwszym pa-
rametrem. Drugi parametr przekazuje do miejsca wywołania obliczoną wartość, na-
tomiast trzeci jest wskaźnikiem poprawności wykonania procedury: 0 — wykonanie
poprawne, 1 — wystąpił wyjątek. Oczywiście zastosowanie typu IN OUT w stosunku
do trzeciego parametru nie jest konieczne; jest tylko uzasadnione chęcią pokazania
sposobu postępowania w takim przypadku.
CREATE OR REPLACE PROCEDURE test
(ktory IN int DEFAULT 1, ile OUT int, status IN OUT int)
AS
BEGIN
SELECT COUNT(IdOsoby) INTO ile FROM Osoby
WHERE IdDzialu = ktory;
status := 0;
EXCEPTION
WHEN OTHERS THEN
status := 1;
END;

Wywołanie procedury z poziomu dynamicznego SQL z przekazaniem zmiennych wszyst-


kich typów (IN, OUT, IN OUT) wykonujemy w ramach bloku anonimowego. Zadeklaro-
wane zostały cztery zmienne pomocnicze. Trzy pierwsze, całkowite, przeznaczone są
na przekazywanie wartości do i z procedury; trzecia z nich ma określoną wartość po-
czątkową 0. Czwarta zmienna, znakowa, została zainicjowana nazwą wywoływanej pro-
cedury.
Rozdział 13. ♦ Dynamiczny SQL 255

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;

Ponieważ wywołanie procedury wymaga co najmniej użycia bloku anonimowego, rów-


nież zawartością łańcucha przetwarzanego poleceniem EXECUTE IMMEDIATE musi być
przynajmniej blok anonimowy ze wszystkimi elementami składniowymi, zupełnie tak,
jakbyśmy nie stosowali dynamicznego SQL. Jedyną różnicą jest konieczność używania
zmiennych wiązania w miejsce parametrów formalnych. W celu przekazania zmiennych,
tak samo jak w przypadku zapytań modyfikujących dane, używamy operatora USING, po
którym następuje ich lista. Nazwy zmiennych poprzedzone są deskryptorami wskazują-
cymi na kierunek ich przekazywania. Obowiązuje nas zgodność typów deskryptorów
z listą formalną parametrów procedury. Wartości przekazywane są zgodnie z pozycją
na liście. Deskryptor IN, jako domyślny, może zostać pominięty (tak jak przy definicji
procedury); pozostałe muszą wystąpić jawnie. Podobne postępowanie ma miejsce w przy-
padku funkcji. Aby pokazać ten przypadek praktycznie, utwórzmy pomocniczą funkcję
testf, która przez nazwę zwraca stan wykonania: 0 — wykonanie poprawne, 1 — wy-
stąpił wyjątek. Analogicznie jak w przypadku procedury, liczona jest ilość osób pra-
cujących w dziale, którego identyfikator dany jest pierwszym parametrem. Wynik prze-
kazywany jest przez drugi z parametrów.
CREATE OR REPLACE function testf
(ktory IN int DEFAULT 1, ile OUT int) RETURN int
AS
BEGIN
SELECT COUNT(IdOsoby) INTO ile FROM Osoby
WHERE IdDzialu = ktory;
RETURN 0;
EXCEPTION
WHEN OTHERS THEN
RETURN 1;
END;

Podobnie jak w przypadku procedury, wywołanie funkcji z poziomu dynamicznego SQL


następuje w ramach bloku anonimowego. Zadeklarowane zostały cztery zmienne po-
mocnicze. Trzy pierwsze, całkowite, przeznaczone są na przekazywanie wartości do
i z funkcji. Czwarta zmienna, znakowa, została zainicjowana jej nazwą.
DECLARE
dzial int:=1;
stan int;
ok int;
256 Część II ♦ ORACLE PL/SQL

funkcja varchar(222):= 'testf';


BEGIN
EXECUTE IMMEDIATE 'DECLARE st int; BEGIN :st := ' ||
funkcja || '(:1, :2); END;' USING OUT ok, IN dzial, OUT stan;
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.

Powróćmy na chwilę do procedury. Miała ona zadeklarowaną wartość domyślną dla


pierwszego parametru. Odwołanie się do niej wymaga zastosowania nazewniczego wy-
wołania procedury we wnętrzu dynamicznego bloku anonimowego. Przykład takiego
wywołania przedstawia kolejny listing.
DECLARE
dzial int:=1;
stan int;
ok int := 0;
procedura varchar(222) := 'test';
BEGIN
EXECUTE IMMEDIATE 'BEGIN ' || procedura || ‘
(ile => :5, status => :9); END; '
USING 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;

Zasadnicza różnica pomiędzy wywołaniem pozycyjnym a nazewniczym polega na tym,


że w łańcuchu przetwarzanym dynamicznie w miejscu wywołania funkcji stosujemy
konwencję parametr => :ZmiennaZwiązana; parametr staje się więc zmienną związaną.
Rozdział 13. ♦ Dynamiczny SQL 257

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.

Powróćmy do zapytań wybierających, aby pokazać, że nie zawsze w dynamicznym SQL


konieczne jest stosowanie składni SELECT ... INTO. Wystarczy, aby zapytanie prze-
twarzane dynamicznie zwracało jeden wiersz (rekord).
DECLARE
TYPE rec_t is RECORD (dzial int, nazwa varchar2(20));
rec rec_t;
BEGIN
EXECUTE IMMEDIATE 'SELECT * FROM Dzialy
WHERE IdDzialu= ' || 3 INTO rec;
DBMS_OUTPUT.PUT_LINE(rec.nazwa);
END;

W bloku anonimowym zadeklarowano typ rekordowy z jawnie zdefiniowanymi polami


(tak jak to ma miejsce w definicji tabeli) oraz zmienną tego typu. Zamiast wymieniać
każde z pól tabeli oddzielnie, zastosowano znak *. W celu ograniczenia liczby wierszy
do jednego w definicji dynamicznego zapytania skorzystano z klauzuli WHERE. Po ope-
ratorze INTO zastosowano zmienną rekordową zgodną z definicją zapytania. Zamiast
stosować w definicji typu rekordowego statyczną deklarację dla każdego z pól, możemy
odwołać się do zmiennej typu %ROWTYPE, co nie tylko upraszcza zapis, ale również za-
pewnia zgodność z rekordem tabeli, nawet jeśli w dowolnym momencie zostałaby ona
zmodyfikowana.
SET SERVEROUTPUT ON;
DECLARE
TYPE rec_t is RECORD (dzial Dzialy%ROWTYPE);
rec rec_t;
BEGIN
EXECUTE IMMEDIATE 'SELECT * FROM Dzialy
WHERE IdDzialu= ' || 3 INTO rec.dzial;
DBMS_OUTPUT.PUT_LINE(rec.dzial.nazwa);
END;

Czasami dynamicznie wykonywane zapytanie wybierające może zarówno zwracać warto-


ści, jak i je pobierać, np. w celu zdefiniowania warunku w klauzuli WHERE. Zilustrujmy
taki przypadek na podstawie bloku anonimowego. W bloku tym zadeklarowano dwie
zmienne na dane pobierane z zapytania (numeryczną i znakową) oraz zmienną prze-
kazywaną do niego.
DECLARE
nr int;
nazwa varchar2(22);
ktory int;
BEGIN
ktory := 3;
EXECUTE IMMEDIATE 'SELECT IdDzialu, Nazwa
FROM Dzialy WHERE IdDzialu = :1'
INTO nr, nazwa
258 Część II ♦ ORACLE PL/SQL

USING ktory;
DBMS_OUTPUT.PUT_LINE(nazwa);
END;

Parametrem polecenia EXECUTE IMMEDIATE jest łańcuch zawierający zapytanie wybie-


rające jeden wiersz z tabeli Dzialy. Fakt, że zostanie zwrócony pojedynczy wiersz, jest
zapewniony przez zastosowanie w klauzuli WHERE warunku zawierającego klucz pod-
stawowy tabeli. Jak widać w przykładzie, przekazanie zmiennych z zapytania uzyskujemy
dzięki użyciu operatora INTO, a przekazanie danych do zapytania dzięki zastosowaniu
operatora USING. Wymogi formalne przy zastosowaniu jednocześnie obu operatorów są
takie same jak dla każdego z nich oddzielnie. Zachowanie kolejności używania ope-
ratorów nie jest obowiązkowe.

Podobną sytuację mamy w przypadku zapytania modyfikującego dane z potwierdze-


niem — klauzula RETURNING — w którym zarówno przekazujemy parametr do zapyta-
nia, jak również odczytujemy informację zwrotną o modyfikowanym wierszu.
DECLARE
zap VARCHAR2(200);
kto int := 11;
kiedy int;
BEGIN
zap := 'UPDATE Osoby SET RokUrodz = 1980
WHERE IdOsoby = :1 RETURNING RokUrodz INTO :2';
EXECUTE IMMEDIATE zap USING kto
RETURNING INTO kiedy;
DBMS_OUTPUT.PUT_LINE(kiedy);
END;

Analogicznie jak w przypadku jednowierszowego zapytania wybierającego, w celu


dynamicznego wykonania takiego zapytania używamy obydwu operatorów: INTO oraz
USING. W tym wypadku jednak wymagane jest poprzedzenie drugiego z nich klauzulą
RETURNING, tak samo jak w treści zapytania.

Do tej pory prezentowane były przykłady akademickie mające na celu przedstawienie


podstaw składni i potencjalnych możliwości dynamicznego SQL. Kilka kolejnych
może być użytecznych praktycznie, a przede wszystkim ma za zadanie pokazać siłę
stosowania dynamicznego SQL, który pozwala na realizację zadań nieosiągalnych przy
korzystaniu z postaci statycznej. Pierwszy z przykładów ma za zadanie wyświetlić li-
stę tabel użytkownika oraz podać liczbę wierszy w każdej z nich. Rozwiązanie tego za-
dania zrealizowano w postaci bloku anonimowego, w którym zadeklarowano zmienną
znakową v_sql przeznaczoną na przetwarzany dynamicznie kod (stąd jej duży rozmiar)
oraz zmienną numeryczną v_cnt przeznaczoną na obliczoną w zapytaniu ilość wierszy
każdej z tabel.
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:= 'DECLARE vvv int; BEGIN SELECT COUNT(*)' ||
Rozdział 13. ♦ Dynamiczny SQL 259

'INTO :vvv FROM ' || rec.table_name || ';END;';


EXECUTE IMMEDIATE v_sql USING OUT v_cnt;
DBMS_OUTPUT.PUT_LINE(rec.table_name || '-' ||v_cnt);
END LOOP;
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;

Kolejny przykład wywodzi się z problematyki zgłębiania danych, a dokładnie z opra-


cowanego algorytmu budowy drzew decyzyjnych. Problem wynika stąd, że drzewa
decyzyjne są konstrukcjami, które „lubią” wartości dyskretne, a nieszczególnie „prze-
padają” za zmiennymi ciągłymi, które trzeba dyskretyzować. Dlatego niezbędnym oka-
zało się opracowanie funkcji, która sprawdza, czy dane w wybranej kolumnie określo-
nej tabeli są dyskretne, czy też ciągłe. W przypadku wartości dyskretnych można było
ich użyć bezpośrednio do budowy drzewa, w przeciwnym przypadku poddać dalszemu
procesowi dyskretyzacji. Za próg pozwalający uznać, że wartości w kolumnie są dys-
kretne, przyjęto przypadek, kiedy przyjmują co najwyżej cztery różne wartości. Liczba
ta jest kwestią umowną i można ją zmienić, modyfikując tylko warunek w funkcji. W celu
rozwiązania problemu zbudowana została funkcja Sprawdz_ciaglosc, która ma dwa pa-
rametry znakowe. Pierwszy, pKolumna, zawiera nazwę analizowanej kolumny, natomiast
drugi, pTabela, nazwę tabeli. Napis TAK zwracany jest, gdy kolumna jest według okre-
ślonych kryteriów ciągła, natomiast napis NIE w przypadku przeciwnym. Zadeklarowane
zostały dwie zmienne pomocnicze: numeryczna vLicznik, przechowująca liczbę róż-
nych wartości w kolumnie, oraz znakowa vSelect, przechowująca dynamicznie wyko-
nywany kod. Realizację funkcji sprawdzającej liczbę różnych wartości danych zawar-
tych w kolumnie przedstawia kod.
260 Część II ♦ ORACLE PL/SQL

CREATE OR REPLACE FUNCTION Sprawdz_ciaglosc


(pKolumna IN VARCHAR2, pTabela IN VARCHAR2)
RETURN VARCHAR2 IS
vLicznik NUMBER := 0;
vSelect VARCHAR2(2000);
BEGIN
vSelect := 'DECLARE Licznik int; BEGIN SELECT ' ||
'COUNT (DISTINCT ' || pKolumna || ') INTO :Licznik FROM '
|| pTabela || '; END; ';
EXECUTE IMMEDIATE vSelect USING OUT vLicznik;
IF vLicznik <= 4 THEN
RETURN 'NIE';
ELSE
RETURN 'TAK';
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20005,
'Brak danych w kolumnie ' || pKolumna || ' w tabeli ' || pTabela);
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;

Do wykonania tego kodu zastosowano polecenie EXECUTE IMMEDIATE z operatorem


USING OUT, ze względu na użycie bloku anonimowego, który przypisuje policzoną
w dynamicznym bloku anonimowym wartość zmiennej Licznik do zmiennej pomoc-
niczej funkcji vLicznik. Na podstawie otrzymanej wartości generowany jest komuni-
kat stanowiący wartość zwracaną przez funkcję. Dodatkowo zastosowano obsługę wy-
jątku NO_DATA_FOUND występującego, kiedy w kolumnie nie ma żadnych wartości, nie
ma w tabeli kolumny o wybranej nazwie lub nie ma tabeli o wybranej nazwie. Uważ-
nemu czytelnikowi pozostawiam zadanie przekształcenia dynamicznego bloku ano-
nimowego na postać zapytania jednowierszowego. Kolejny kod zawiera przykład wy-
wołania wcześniej utworzonej funkcji.
SET SERVEROUTPUT ON;
DECLARE
stan VARCHAR2(111);
BEGIN
stan:=Sprawdz_ciaglosc('Nazwisko', 'Osoby');
DBMS_OUTPUT.PUT_LINE(stan);
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

mocnicze: znakową zap, przechowującą kody dynamicznie przetwarzanych zapytań,


znakową nazwa, przechowującą nazwę tabeli, oraz numeryczną suma, przechowującą
policzoną wartość.
SET SERVEROUTPUT ON;
DECLARE
zap varchar2(200);
nazwa varchar2(200);
suma int;
BEGIN
nazwa:= 'testowa';
zap:= 'CREATE TABLE ' || nazwa || '(nr1 int, nr2 int)';
EXECUTE IMMEDIATE zap;
zap:= 'INSERT INTO ' || nazwa || ' VALUES (:nr1 , :nr2 )';
EXECUTE IMMEDIATE zap USING 2, 3;
zap:= 'INSERT INTO ' || nazwa || ' VALUES (:nr1 , :nr2 ) ';
EXECUTE IMMEDIATE zap USING 5, 3;
zap:= 'SELECT SUM(nr1) FROM ' || nazwa;
EXECUTE IMMEDIATE zap INTO suma;
DBMS_OUTPUT.PUT_LINE(suma);
zap:= 'DROP TABLE ' || nazwa;
EXECUTE IMMEDIATE zap ;
zap:= 'COMMIT';
EXECUTE IMMEDIATE zap;
END;

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.

Kolejny przykład nawiązuje do sygnalizowanych wcześniej zastosowań dynamicznego


SQL w przypadku użycia typu kursora referencyjnego. Zadaniem do rozwiązania jest
utworzenie procedury, która pozwoli nawigować po dowolnym polu dowolnej tabeli.
Nazwy pola oraz tabeli stanowią dwa znakowe, wejściowe parametry procedury. Do-
datkowo zadeklarowane zostały: typ kursora referencyjnego, zmienna tego typu oraz
dwie zmienne znakowe — zap na przetwarzane zapytania oraz wart na zawartość ko-
lumny.
CREATE OR REPLACE PROCEDURE dyn_cur
(pole varchar2, tabela varchar2)
IS
262 Część II ♦ ORACLE PL/SQL

TYPE rcur IS REF CURSOR;


cur rcur;
zap varchar2(2000);
wart varchar2(20);
BEGIN
zap:= 'SELECT ' || pole || ' FROM ' || tabela;
OPEN cur FOR ZAP;
FETCH cur INTO wart;
WHILE cur%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(wart);
FETCH cur INTO wart;
END LOOP;
CLOSE cur;
END;

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;

Analogiczne rozwiązanie, jak to przedstawione przy użyciu procedury, przepisano do


postaci bloku anonimowego.
DECLARE
TYPE rcur IS REF CURSOR;
cur rcur;
zap varchar2(2000);
pole varchar2(20);
tabela varchar2(20);
wart varchar2(20);
BEGIN
pole := 'Nazwa';
tabela := 'Dzialy';
zap := 'SELECT ' || pole || ' FROM ' || tabela;
OPEN cur FOR ZAP;
FETCH cur INTO wart;
WHILE cur%FOUND
LOOP
DBMS_OUTPUT.PUT_LINE(wart);
FETCH CUR INTO wart;
Rozdział 13. ♦ Dynamiczny SQL 263

END LOOP;
CLOSE cur;
END;

W prezentowanych rozwiązaniach pojawił się problem polegający na tym, że rzadko


kiedy użyteczne jest wyświetlenie wyników na uniwersalnym urządzeniu wyjściowym
— ekranie. O wiele częściej będziemy chcieli przechować dane w zmiennej. W przy-
padku kursorów dynamicznych, dynamicznego SQL, bardzo często są to dane, które
należy składować w postaci tabelarycznej. Oczywiście możliwe jest, jak to pokazano
w jednym z poprzednich przykładów, zapisywanie wyników do tabel schematu relacyj-
nego, ale wygodniejsze wydaje się użycie zmiennych złożonych typu tabelarycznego.
Należy ponadto pamiętać, że operacje dyskowe związane z zapisywaniem i odczytywa-
niem danych z tabel schematu relacyjnego należą do najwolniej przetwarzanych (szyb-
kość dostępu do urządzeń fizycznych jest o kilka rzędów mniejsza niż w przypadku
danych przechowywanych w pamięci). Pierwszy typ tego rodzaju, VARRAY, może zo-
stać zadeklarowany następująco:
TYPE NazwaTypu IS VARRAY (rozmiar)
OF TypElementu NOT NULL:

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

 DELETE — kasuje wszystkie wiersze.


 DELETE(n) — kasuje n-ty wiersz.
 DELETE(m, n) — kasuje wiersze o indeksach od m do n; kiedy m > n,
nie jest wykonywana żadna akcja.
 EXTEND(n) — dodaje n pustych (NULL) elementów na końcu tabeli.
 EXTEND(n, m) — dodaje n elementów, które są kopią elementu m,
na końcu tabeli.
 TRIM — usuwa jeden wiersz z końca tabeli.
 TRIM(n) — usuwa n wierszy z końca tabeli.
 LIMIT — dla typu VARRAY zwraca jego rozmiar, czyli maksymalną liczbę
elementów, jaka może być w tym typie zapisana; w przypadku TABLE OF
zwracana jest wartość NULL.

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

WHERE tc.table_name = ' || '''' || tabela || '''';


OPEN cur FOR zap;
FETCH cur INTO TEMP;
WHILE cur%FOUND
LOOP
kol(licz) := temp;
DBMS_OUTPUT.PUT_LINE('KOLUMNA - ' || kol(licz).nazwa
|| ', OGRANICZENIE - ' || kol(licz).constr );
licz := licz+1;
FETCH cur INTO temp;
END LOOP;
CLOSE cur;
FOR i IN kol.FIRST .. kol.LAST
LOOP
DBMS_OUTPUT.PUT_LINE('KOLUMNA - ' || kol(i).nazwa ||
', OGRANICZENIE - ' || kol(i).constr );
END LOOP;
END;

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

Rysunek 13.1. OD DO ILE


Przykładowe A B 11
dane do zadania
A C 12
wyszukiwania
drogi w grafie A D 13
B
— ich reprezentacja B A 11
relacyjna C A 12
oraz graficzna D A 12 A
D E 9 C F
E D 8
C E 11
E C 11 D E
C F 7
F C 7
F E 7
E F 7

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.

Do rozwiązania tego zadania zastosowano algorytm zachłanny, który, wychodząc z węzła


startowego, wyszukuje wszystkie sąsiednie. Rekurencyjnie przesuwając się do każdego
kolejnego sąsiada węzła nadrzędnego, wyszukuje on następne pokolenie sąsiadów, aż
natrafi na węzeł końcowy. Aby zapobiec zapętleniu się procesu lub wycofywaniu się
raz już pokonaną drogą z listy sąsiadów, eliminowane są te węzły, które algorytm „prze-
szedł” w poprzednich krokach poszukiwań. Wyznacza to drugie z kryteriów zatrzy-
mania rekurencji — sytuację, kiedy węzeł nie ma już sąsiadów, których nie ma jeszcze
na liście do tej pory pokonanych. Numeryczna realizacja tego algorytmu wykonywa-
na jest za pomocą procedury find, która ma trzy parametry znakowe. Pierwszy z nich
zawiera nazwę węzła, w którym znaleźliśmy się na kolejnym kroku poszukiwań, drugi
zawiera węzeł końcowy, a trzeci listę węzłów, które zostały do tej pory przebyte. Li-
sta ta jest zbiorem nazw węzłów ograniczonych znakami apostrofu (ogranicznik łań-
cucha znaków) oraz separowanych przecinkami. Trzeci z parametrów jest przekazywany
zarówno do funkcji IN OUT, jak i z niej. W funkcji zadeklarowano typ kursora referen-
cyjnego, zmienną tego typu, zmienną zgodną z polem do tabeli opisującej graf oraz
zmienne znakowe — zap, zawierającą zapytanie definiujące zestaw rekordów kursora,
i war_old, zapamiętującą pierwotną wartość zmiennej war przekazywaną do funkcji.
CREATE OR REPLACE PROCEDURE find
(odP varchar2, doP varchar2, WAR IN OUT varchar2 )
AS
TYPE R_C IS REF CURSOR;
R R_C;
ODB GRAF.DO%TYPE;
ZAP VARCHAR2(222);
war_old VARCHAR2(222);
DROGA VARCHAR2(222);
BEGIN
war_old := war;
Rozdział 13. ♦ Dynamiczny SQL 267

zap:= 'SELECT DO FROM GRAF WHERE OD = ' || '''' || ODP || '''' ||


' AND DO NOT IN (' ||WAR || ')';
OPEN R FOR zap;
FETCH R INTO ODB;
WHILE R%FOUND
LOOP
WAR := WAR || ', ' || '''' || ODP || '''';
IF ODB = DOP THEN
DROGA := REPLACE(REPLACE(SUBSTR(WAR, 6,
LENGTH(WAR)), ''''), ', ', '-') || '-' || ODB ;
DBMS_OUTPUT.PUT_LINE('DROGA '||DROGA);
END IF;
find(odB, doP, WAR);
war := war_old;
FETCH R INTO ODB;
END LOOP;
END;

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.

Przedstawmy teraz wywołanie naszej procedury w postaci bloku anonimowego przy


poszukiwaniu dróg od węzła A do węzła E. Zadeklarowane zostały trzy znakowe zmienne
pomocnicze — pierwsze dwie przechowujące początkowy i końcowy węzeł drogi oraz
zmienna war, która ma przechowywać listę węzłów już odwiedzonych.
SET SERVEROUTPUT ON;
DECLARE
ODP VARCHAR2(2);
DOP VARCHAR2(2);
WAR VARCHAR2(222);
BEGIN
ODP := 'A';
DOP := 'E';
WAR := '''' || ' ' || '''';
find(odP, doP, WAR);
END;
268 Część II ♦ ORACLE PL/SQL

Pod zmienne ODP i DOP podstawione zostały wartości odpowiadające początkowemu


i końcowemu węzłowi, pomiędzy którymi szukamy drogi. Ponieważ nasz algorytm
rozpoczyna pracę, lista węzłów już odwiedzonych powinna być pusta. Niestety, ope-
rator listy zgłasza błąd w przypadku pozbawienia jej wartości (zm IN ()), dlatego jako
pierwszy jej element wpisywany jest łańcuch zawierający spację (' '). Oczywiście jest
to poprawne tylko wtedy, kiedy w tabeli nie ma węzła o takiej nazwie — a z reguły
nie ma. Wyznaczone rozwiązania — wszystkie możliwe drogi przejścia — dla punktu
startowego A oraz końcowego E w przypadku grafu z rysunku 13.1 przedstawione zo-
stały poniżej.
DROGA A-C-E
DROGA A-C-F-E
DROGA A-D-E

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.

Liczba potencjalnych przykładów zastosowania dynamicznego SQL i kursorów refe-


rencyjnych o wartości praktycznej jest bardzo duża, jednak aby przedstawić bardziej
złożone, jak choćby wspomniany algorytm Dijkstry czy metodę budowania drzew de-
cyzyjnych (czy to z zastosowaniem entropii, czy naiwnego klasyfikatora Bayesa),
należałoby zaprezentować o wiele obszerniejsze kody procedur (funkcji, pakietów).
Nie jest to uzasadnione na poziomie poznawania składni PL/SQL, dlatego nie zostaną
one przedstawione w tej książce — być może znajdą się w kolejnych opracowaniach
poświęconych bardziej zaawansowanym metodom przetwarzania z zastosowaniem
PL/SQL. Część z nich była już prezentowana w artykułach naukowych poświęconych
tematyce baz danych i ich zastosowań praktycznych.

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.

Rozpocznijmy zatem od utworzenia po stronie Oracle prostej klasy Javy zawierającej


metodę wyświetlającą komunikat.
CREATE or REPLACE JAVA SOURCE NAMED Hello
AS
public class Hello
{
public static String hello()
{
return "Witamy";
}
};
270 Część II ♦ ORACLE PL/SQL

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;

Przedstawiona funkcja hello przekazywała wartość przez nazwę. Spróbujmy stworzyć


prostą procedurę, do której i z której dane będą przekazywane przez listę parametrów.
Zadaniem kolejnej procedury będzie pobranie dwóch zmiennych, zamiana ich warto-
ści miejscami i przekazanie ich do miejsca wywołania. W tym celu utworzono źródło
Javy Zmien zawierające klasę Zmiana o jednej metodzie zamien typu void, ponieważ
planowana jest jej enkapsulacja do procedury PL/SQL. W deklaracji parametrów użyto
dwóch zmiennych tablicowych — obie są typu int[ ]. Przyczyną takiego podejścia
jest fakt, że w Javie wszystkie zmienne proste przekazywane są przez wartość, a tylko
zmienne tablicowe przez referencję (adres). Konieczność przekazania zmiennych do
miejsca wywołania (PL/SQL) wymusza ich przekazywanie referencyjne, a nie poprzez
wartości. Do klasy została zaimportowana biblioteka java.math.*. Nie jest to konieczne,
ponieważ jest ona importowana domyślnie. Zdecydowano się na ten krok, aby poka-
zać miejsce dyrektywy import w definicji źródła danych na przykładzie możliwie pro-
stego kodu.
CREATE OR REPLACE JAVA SOURCE NAMED Zmien
AS
import java.math.*;
public class Zmiana {
public static void zamien (int[ ] a, int[ ] b)
{
int temp;
temp = b[0];
b[0] = a[0];
a[0] = temp;
}
};

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

CREATE OR REPLACE PROCEDURE Zamien


(a IN OUT Number, b IN OUT Number)
AS
LANGUAGE JAVA
NAME 'Zmiana.zamien (int[], int[])';

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;
}
};

Ciało metody stanowi przypisanie do zwracanej wartości wyniku dzielenia wejściowych


parametrów. Jest oczywistym fakt, że takie działanie może doprowadzić do wystąpie-
nia wyjątku (dzielenie przez zero). W źródle Javy możliwe jest zastosowanie obsługi
wyjątków. W tym celu przekazywanie wyniku dzielenia umieszczamy w bloku try,
natomiast obsługa wyjątku umieszczona zostaje w bloku catch, który znajduje się
bezpośrednio po nim. Parametrem obowiązkowym bloku catch jest zmienna e klasy
Exception.

Oto klasa Javy z obsługą wyjątków:


CREATE OR REPLACE JAVA SOURCE NAMED Dziel
AS
public class Dzielenie
{
public static double Dziele (double a, double b)
{
try
{
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 273

return a/b;
}
catch (Exception e)
{
System.err.println(e.getMessage());
return 0;
}
}
};

Następnie przygotowaną w ten sposób klasę Javy enkapsulujemy do postaci funkcji


w PL/SQL zgodnie z poprzednio pokazanym szablonem. Teraz możliwe staje się jej
wywołanie z poziomu bloku anonimowego PL/SQL.
CREATE OR REPLACE FUNCTION dzieli
(a in real, b in real) RETURN real
AS
LANGUAGE JAVA
NAME 'Dzielenie.Dziele (double, double) return double';
/
SET SERVEROUTPUT ON;
DECLARE
com real;
BEGIN
COM:=dzieli(1,2);
DBMS_OUTPUT.PUT_LINE(com);
END;

Niestety, takie rozwiązanie nie jest satysfakcjonujące, ponieważ w obu rozwiązaniach


klasy Javy w przypadku błędu dzielenia przez zero nie jest generowany wyjątek,
a funkcja PL/SQL zwraca znak ~! Szerszą informację otrzymujemy przy wywołaniu tej
funkcji z poziomu zapytania wybierającego, które, jak w przypadku innych wielkości
skalarnych, odwołuje się do tabeli DUAL.
SELECT dzieli(1,0) FROM DUAL;

Po wykonaniu tego zapytania komunikat ma bardziej czytelną postać:


DZIELI(1,0)
----------------------
Infinity

Również ta informacja może budzić niemałe zdziwienie. Przecież funkcja ma zwracać


wartość numeryczną, a wyświetlany jest komunikat w postaci znakowej! Wyjaśnia to
fakt wprowadzenia w Oracle wartości reprezentujących największe dopuszczalne war-
tości zmiennych zmiennoprzecinkowych, które mogą „przybliżać” nieskończoność. Prze-
śledźmy to na przykładzie prostego bloku anonimowego, w którym zadeklarowano zmienną
numeryczną niecałkowitą. W ciele zmienna ta jest zasilana predefiniowanymi warto-
ściami „nieskończoności” dla typów float i double. W drugiej części skryptu zasto-
sowano konwersję dwóch wartości zmiennej typu raw na zmienną numeryczną. Po każ-
dym podstawieniu następuje wyświetlenie odpowiedniej wartości zmiennej.
DECLARE
x real;
BEGIN
274 Część II ♦ ORACLE PL/SQL

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;

Skutkiem wykonania tego bloku będzie szereg znaków ~, w dwóch przypadkach


poprzedzonych znakiem minus. Podobnie sprawa ma się po stronie Javy, gdzie
również zostało zdefiniowane przybliżenie nieskończoności dla typu niecałkowitego
Float.POSITIVE_INFINITY oraz jej wartość ujemna Float.NEGATIVE_INFINITY. Spróbujmy
teraz zbudować dwie proste klasy Javy zwracające te predefiniowane nazwami symbo-
licznymi wartości, a następnie enkapsulujmy je do funkcji PL/SQL.
CREATE OR REPLACE JAVA SOURCE NAMED StaleS AS
public class stale {
public static double p_inf () {
return Float.POSITIVE_INFINITY;
};
public static double n_inf () {
return Float.NEGATIVE_INFINITY;
};
};
/
CREATE OR REPLACE FUNCTION inff
RETURN number
AS
LANGUAGE JAVA NAME
'stale.p_inf() return double';
/
CREATE OR REPLACE FUNCTION ninff
RETURN number
AS
LANGUAGE JAVA NAME
'stale.n_inf() return double';

Wywołanie obu funkcji PL/SQL zrealizowano, stosując zapytanie wybierające odwo-


łujące się do systemowej tabeli DUAL.
SELECT inff FROM DUAL;
SELECT ninff FROM DUAL;

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

Kolejny przykład nawiązuje do reprezentacji symbolicznej nieskończoności oraz po-


kazuje sensowne wykorzystanie klas Javy do definiowania predefiniowanych w tym
języku stałych. W tym celu utworzona została klasa Javy, która wyznacza logarytm
naturalny oraz zwraca wartość stałej e — podstawy tego logarytmu. Na uwagę zasłu-
guje fakt, że aby pominąć jawny eksport biblioteki Math, użyto wywołania kwalifi-
kowanego metody z tej klasy w postaci Math.log(a). Klasa zawiera dwie metody en-
kapsulowane do funkcji PL/SQL, które wywoływane są w zapytaniach wybierających
odwołujących się do tabeli systemowej DUAL.
CREATE OR REPLACE JAVA SOURCE NAMED NaturalLogarithm AS
public class NaturalLogarithm{
public static double getE() {
return double(java.lang.Math.E);
};
public static double loge(double a) {
try{
double x;
x = Math.log(a);
return x;
}
catch (Exception e) {
System.err.println(e.getMessage());
return 0;
}
};
};
/
CREATE OR REPLACE FUNCTION get_e RETURN number AS
LANGUAGE JAVA NAME
'NaturalLogarithm.getE() return double';
/
CREATE OR REPLACE FUNCTION lne(a number) RETURN number AS
LANGUAGE JAVA NAME
'NaturalLogarithm.loge(double) return double';
/
SELECT get_e FROM DUAL;
SELECT lne(2) FROM DUAL;
SELECT lne(0) FROM DUAL;

Skutkiem wykonania przedstawionych zapytań wybierających jest wyświetlenie warto-


ści stałej e logarytmu naturalnego z liczb 2 oraz 0.
GET_E
----------------------
2,71828182845905

LNE(2)
----------------------
0,693147180559945

LNE(0)
----------------------
-Infinity
276 Część II ♦ ORACLE PL/SQL

Jak widać, w ostatnim przypadku uzyskaliśmy ponownie wartość –Infinity. Dlaczego


tyle miejsca poświęcam jednemu tematowi — przybliżeniu wartości nieskończonej?
Mamy tutaj do czynienia z jawnym „naciąganiem” praw matematyki. Zarówno dzielenie
przez zero, jak i logarytm zera mają wartość niezdefiniowaną. Nie jest prawdą, że wynik
dąży do ±∞, ponieważ dziedzina tych dwóch funkcji nie zawiera zera. Uważam, że
wprowadzenie aproksymacji ±∞ jest przydatne, ale wtedy, kiedy rzeczywiście mamy
do czynienia z przekroczeniem zakresu typu zmiennej, natomiast „naginanie” praw
matematyki jest po prostu szkodliwe. Zdecydowanie wolałbym, aby w takim przypadku
zgłaszany był odpowiedni wyjątek. Cóż można na to poradzić? Pierwszym rozwiąza-
niem jest przypisanie wartości do zmiennej całkowitej. Zgłoszony zostanie wyjątek, gdyż
zmienne całkowite nie mają reprezentacji „nieskończoności”. Drugim wyjściem jest
posłużenie się w obliczeniach zmiennymi całkowitymi, np. według prezentowanego
schematu.
CREATE OR REPLACE JAVA SOURCE NAMED Dziel AS
public class Dzielenie
{
public static void Dziele (int a, int b, int[] x)
{
try
{
x[0] = a/b;
}
catch (Exception e)
{
x[0] = -1;
System.err.println(e.getMessage());
}
}
};
/
CREATE OR REPLACE procedure dzieli
(a in number, b in number, x in out number)
AS
LANGUAGE JAVA
NAME 'Dzielenie.Dziele (int, int, int[])';
/
DECLARE
com int;
BEGIN
dzieli(5, 0, com);
DBMS_OUTPUT.PUT_LINE('aaa ' ||com);
END;

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ń!

Po dywagacjach na temat niezgodności predefiniowanych stałych z matematyką czas


powrócić do głównego nurtu rozważań. Najczęściej procedury tworzone na serwerze
bazy danych powstają po to, aby przetwarzały dane w tej bazie zawarte, a nie tylko
w celu wykonywania operacji matematycznych na zmiennych. Podobnie jest w przy-
padku wykorzystywania klas Javy. Problemem jest tu zdefiniowanie sposobu połącze-
nia z bazą. W tym celu stosowany jest mechanizm JDBC, a programiście pozostaje
określić dla niego łańcuch połączenia. Ponieważ najczęściej analiza dotyczy danych
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 277

zawartych w tej samej instancji bazy, prześledźmy to na takim przypadku. Chcemy


utworzyć klasę Javy, a następnie funkcję PL/SQL tę klasę wykorzystującą, która poli-
czy osoby o wzroście wyższym niż wartość dana parametrem. Aby możliwe było wy-
konywanie operacji na dowolnej bazie danych, konieczne jest zaimportowanie klasy
java.sql.*. Ponieważ z instancją serwera Oracle łączyć będziemy się za pośrednic-
twem JDBC, musimy również zaimportować oracle.jdbc.*. Dodatkowo konieczny
jest import biblioteki java.io.*, aby możliwe było wykonywanie operacji odczytu
i zapisu. W klasie LWzrost zdefiniowano metodę statyczną ilosc, która pobiera jeden
parametr rzeczywisty oraz zwraca wartość tego samego typu.
CREATE OR REPLACE AND COMPILE
JAVA SOURCE NAMED LWZROST AS
import java.sql.*;
import java.io.*;
import oracle.jdbc.*;
public class LWzrost
{
public static double ilosc(double param)
{
double ile = 0;
try
{
Connection conn =
DriverManager.getConnection ("jdbc:default:connection");
String sql =
"SELECT COUNT(wzrost) FROM Osoby WHERE wzrost
> " + param;
Statement stmt = conn.createStatement();
ResultSet rset = stmt.executeQuery(sql);
while (rset.next())
{
ile = (double)rset.getInt(1);
}
rset.close();
stmt.close();
}
catch (SQLException e)
{
System.err.println(e.getMessage());
}
return ile;
}
};

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

conn.createStatement definiowana jest zmienna stmt klasy Statement. Zestaw rekordów


— zmienna rset — jest uzyskiwany przez zastosowanie metody stmt.executeQuery,
której parametrem jest zmienna sql zawierająca zapytanie wybierające. Tak jak to
pokazano przed chwilą, zestaw rekordów jest obsługiwany w pętli while na zasadach
analogicznych do przetwarzania kursora w PL/SQL. Możemy powiedzieć, że zmienna
rset zawiera definicję kursora po stronie końcówki „klienta”, jaką stanowi klasa Javy.
Za nawigowanie oraz zatrzymanie procesu przetwarzania pętli odpowiada metoda
rset.next(), która powoduje przejście do kolejnego rekordu zestawu, jak i przez war-
tość wskazuje, czy jeszcze tego zestawu rekordów nie opuściliśmy. Pobranie wartości
z bieżącego rekordu wykonywane jest dzięki zastosowaniu rset.getInt(1). Jak wi-
dać w przykładzie, indeksowanie kolejnych pól rekordu rozpoczyna się od wartości 1.
Ze względu na typ zmiennej wyjściowej dokonano rzutowania typu całkowitego na
zmienną typu (double). Po przetworzeniu zestawu rekordów zamykane są zmienne de-
finiujące ten zestaw (rset) i przetwarzaną instrukcję (stmt). W sekcji obsługi wyjątków
catch Javy przechwycony został wyjątek przetwarzania zapytań SQLException; formal-
nie powinien zostać przechwycony również domyślny zestaw wyjątków Exception. Na
koniec została zwrócona policzona wartość ze zmiennej ile.

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

public class LWzrost


{
public static void ilosc(double param, double[] ile)
{
try
{
Connection conn =
DriverManager.getConnection("jdbc:default:connection");
String sql = "SELECT COUNT(wzrost) FROM Osoby
WHERE wzrost > " + param;
Statement stmt = conn.createStatement();
ResultSet rset = stmt.executeQuery(sql);
while (rset.next())
{
ile[0] = (double)rset.getInt(1);
}
rset.close();
stmt.close();
}
catch (SQLException e)
{
System.err.println(e.getMessage());
}
}
};

Podobnie jak w poprzednim przykładzie, przedstawiona została tu enkapsulacja klasy


Javy do procedury PL/SQL wraz z wywołaniem z poziomu bloku anonimowego.
CREATE OR REPLACE PROCEDURE ilWzr
(param IN REAL,ile IN OUT NUMBER)
AS
LANGUAGE JAVA
NAME 'LWzrost.ilosc(double, double[])';
/
SET SERVEROUTPUT ON;
DECLARE
a real;
BEGIN
ilWzr(1.7, a);
DBMS_OUTPUT.PUT_LINE(a);
END;

W pokazanych przykładach trudno zasymulować przypadek wystąpienia wyjątku. Ko-


lejny przykład, który ma to ułatwić, zawiera klasę Javy ilewierszy, zliczającą liczbę
rekordów w tabeli, której nazwa jest parametrem wejściowym str. W bloku try od-
wołano się do domyślnego połączenia, a następnie wykonano zapytanie wybierające
według już prezentowanego schematu. W sekcji obsługi wyjątków ustawiono wartość
zwracanej zmiennej na –1, co sygnalizuje pojawienie się wyjątku. Jest to uzasadnione,
ponieważ liczba wierszy tabeli nie może być ujemna. Przedstawiony skrypt zawiera
ponadto enkapsulację do funkcji PL/SQL oraz jej wywołanie dla tabeli o nazwie inna.
CREATE OR REPLACE
JAVA SOURCE NAMED ilewierszy
AS
import java.sql.*;
280 Część II ♦ ORACLE PL/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 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

Nie zawsze jest konieczne nawigowanie po wszystkich rekordach zestawu rekordów.


W przykładach, w których zapytanie zwracało jeden wiersz, nie jest niezbędna pętla,
a wystarczające jest pojedyncze użycie metody nawigującej, np.:
boolean ok = rset.next();
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 281

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');

Ewentualne odebranie uprawnienia może zostać wykonane poleceniem:


CALL dbms_java.revoke_permission
('AP', 'SYS:java.net.SocketPermission', 'host', 'connect,resolve');

Prawa do wykonania połączeń do serwera za pośrednictwem Javy mogą być poten-


cjalnie niebezpieczne, dlatego powinny być przydzielane z rozwagą.

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;

Ciekawym przekształceniem jest uzupełnienie kodu o przechwytywanie nie tylko wy-


jątku SQL, ale również wszystkich pozostałych. Taki schemat ułatwia wykrycie przy-
czyny wystąpienia błędu przetwarzania. W przykładzie zamiast liczby zwracany jest
łańcuch, który zawiera informacje o sposobie wykonania funkcji. Konstrukcja taka
umożliwia łatwe testowanie poprawności spełnienia wymagań związanych z realiza-
cją połączenia. Może być to szczególnie cenne dla osób, które zaczynają tworzyć opro-
gramowanie w Javie, ale bywa również pomocne dla bardziej zaawansowanych pro-
gramistów. Przytaczanie przykładów dla innych baz danych nie wnosi nic nowego.

Język Java umożliwia w zasadzie dowolne przetwarzanie danych. Co ważne, możemy


również wykonywać działania na poziomie systemu operacyjnego. W szczególności może
to dotyczyć operacji na zbiorach i folderach. Prześledźmy to, opierając się na klasie Javy
JZbior, zawierającej dwie metody: delete oraz dirListaZbiorow. Pierwsza z nich, sta-
tyczna, zwracająca wartość całkowitą, ma zadeklarowany obiekt typu File z parame-
trem, który stanowi nazwa zbioru. Klasa File zawiera szereg metod, między innymi
delete, usuwającą wskazany zbiór. Metoda ta zwraca zmienną typu boolean określa-
jącą powodzenie lub niepowodzenie wykonania polecenia. Druga z metod, również
statyczna, zwraca zmienną typu String. Tak samo jak poprzednio, zadeklarowano obiekt
File, dla którego parametrem jest w tym przypadku nazwa folderu. Tym razem po-
służono się metodą list, zwracającą tabelę przypisaną do zmiennej Zbior, której ko-
lejnymi wartościami (wierszami) są elementy folderu — podfoldery i zbiory. Zmienna
ta w pętli for, inkrementowanej od zera do liczby wierszy zmiennej Zbior zawartej we
Rozdział 14. ♦ Zastosowanie Javy do tworzenia oprogramowania po stronie serwera 285

właściwości length, jest przekształcana na zmienną łańcuchową listaZbiorow, w któ-


rej kolejne pozycje są separowane średnikiem. W obu metodach zastosowano sekcję
obsługi wyjątków catch.
CREATE OR REPLACE JAVA SOURCE NAMED Zbiory
AS
import java.io.File;
public class JZbior {
public static int delete (String fileName)
{
File mojZbior = new File (fileName);
try
{
boolean ok = mojZbior.delete();
if (ok) return 1;
else return 0;
}
catch (Exception e)
{
System.err.println(e.getMessage());
return -1;
}
public static String dirListaZbiorow (String folder)
{
try
{
File mojFolder = new File (folder);
String[] Zbior = mojFolder.list();
String listaZbiorow = new String();
for (int i = 0; i < Zbior.length; i++)
listaZbiorow = listaZbiorow + "; " + Zbior [i];
return listaZbiorow;}
catch (Exception e)
{
System.err.println(e.getMessage());
return e.getMessage();
}
}
};

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

PROCEDURE getDirListaZbiorow (Folder VARCHAR2,


ListaZbiorow OUT VARCHAR2);
END;
/
CREATE OR REPLACE PACKAGE BODY XZBIOR
AS
FUNCTION Idelete (Zbior varchar2) RETURN NUMBER
AS
LANGUAGE JAVA
NAME 'JZbior.delete (java.lang.String) return int';
FUNCTION delete (Zbior varchar2) RETURN NUMBER
AS
ok number;
BEGIN
ok := Idelete (file);
RETURN ok;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('blad' || SQLERRM);
RETURN 0;
END;

FUNCTION dirFolder (dir IN VARCHAR2)


RETURN VARCHAR2
AS
LANGUAGE JAVA
NAME 'JZbior.dirListaZbiorow (java.lang.String)
return java.lang.String';

PROCEDURE getDirFolder (Folder VARCHAR2,


ListaZbiorow OUT VARCHAR2)
IS
BEGIN
files := dirFolder (Folder);
END;
END;

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

xfile.getDirListaZbiorow ('d:', lista );


DBMS_OUTPUT.PUT_LINE('ok = ' || ok);
DBMS_OUTPUT.PUT_LINE('Zawartosc folderu = ' || lista);
END;

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;

Niestety, zastosowanie metody waitFor() nie pozwala na przetwarzanie poleceń, do


których zakończenia jest potrzebna interwencja operatora (programisty). Użycie ta-
kiego polecenia spowoduje przejście w stan oczekiwania oraz kłopoty z przerwaniem
wykonywania funkcji. Nie jest również przechwytywany strumień, który może być
związany z przetwarzaniem polecenia, jak to miało miejsce w przypadku komendy dir.
Rozszerzenie tych funkcjonalności pozostawiam czytelnikowi. Należy jednak pamiętać,
że użycie takiej funkcji przez niepowołaną osobę może doprowadzić do dużych pro-
blemów. Stąd też zabezpieczenie przed przetwarzaniem takich funkcji, które możemy
odblokować, nadając odpowiedniemu użytkownikowi uprawnienie:
call dbms_java.grant_permission( 'testowy', 'SYS:java.io.FilePermission',
'<<ALL FILES>>', 'execute' );

Przedstawione w rozdziale przykłady zastosowania Javy do budowy oprogramowania


po stronie serwera Oracle nie wyczerpują wszystkich potencjalnych możliwości. Nie
zawierają chociażby generowania dynamicznych stron WWW, dają natomiast szeroki
wybór najczęściej stosowanych schematów przetwarzania. Przedstawienie wszystkich
możliwych zastosowań oraz szczegółowe ich opisanie wymagałoby opracowania od-
dzielnej książki, co być może nastąpi w przyszłości. Poza znajomością przedstawio-
nych tu reprezentatywnych przykładów odzwierciedlających szerokie spektrum metod
przetwarzania, chciałbym, aby czytelnik miał świadomość potęgi, która kryje się za
stosowaniem Javy w Oracle.
Rozdział 15.
Elementy administracji
— zarządzanie
uprawnieniami
z poziomu SQL
Wypada w książce dotyczącej serwera bazy danych poświęcić kilka chwil zagadnieniom
związanym z administracją — przynajmniej w zakresie podstawowym, niezbędnym dla
osoby tworzącej oprogramowanie po stronie serwera. Jak wiadomo, podstawową strukturą
logiczną jest przestrzeń tabel. Jest ona zwykle tworzona przy użyciu narzędzi wizual-
nych, np. Database Console, możliwe jest jednak utworzenie jej z poziomu języka za-
pytań. Podstawowa składnia wymaga tylko podania nazwy logicznej przestrzeni tabel
oraz nazwy pliku systemu operacyjnego, który będzie ją zawierał. Jeśli nie podamy
pełnej ścieżki, plik ten zostanie utworzony w domyślnej lokalizacji plików przestrzeni
tabel. Zastosowanie rozszerzenia ora jest tylko zwyczajowe. Możliwe jest utworzenie
pliku o dowolnym rozszerzeniu lub bez rozszerzenia. Wskazany w poleceniu plik fi-
zyczny nie może jednak istnieć w systemie operacyjnym; w przeciwnym przypadku
wyświetlany jest komunikat o błędzie.
CREATE TABLESPACE tbs_01
DATAFILE 'c:\oracle\oradata\oracle\P_Tabel.ora';

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

CREATE TABLESPACE P_Tabel


LOGGING
DATAFILE 'c:\oracle\oradata\oracle\P_Tabel.ora'
SIZE 5M
EXTENT MANAGEMENT LOCAL
SEGMENT SPACE MANAGEMENT AUTO;

Aby usunąć przestrzeń tabel, należy wykonać polecenie:


DROP TABLESPACE P_Tabel;

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.

Aby utworzyć w Oracle użytkownika, musimy wykonać polecenie, którego minimalna


postać jest następująca:
CREATE USER Nowy IDENTIFIED BY "kajak";

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;

Aby zapewnić użytkownikowi podstawową funkcjonalność, tj. możliwość łączenia się


z bazą, konieczne jest przypisanie mu roli CONNECT.
GRANT CONNECT TO NOWY;
Rozdział 15. ♦ Elementy administracji — zarządzanie uprawnieniami z poziomu SQL 291

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;

Po utworzeniu użytkownika jest możliwe jego modyfikowanie poprzez wykonywanie


polecenia ALTER USER. W przykładowym skrypcie zmieniane są kolejno: hasło użyt-
kownika, domyślna przestrzeń tabel, tymczasowa przestrzeń tabel (przeznaczona np.
na stany pośrednie sortowań, stany kursorów etc.), dostęp do konta oraz profil.
ALTER USER Nowy IDENTIFIED BY "pass";
ALTER USER Nowy DEFAULT TABLESPACE N_przestrz;
ALTER USER Nowy TEMPORARY TABLESPACE Users;
ALTER USER Nowy ACCOUNT LOCK;
ALTER USER Nowy PROFILE N_profil;

Wszystkie przedstawione parametry mogą być też ustawiane bezpośrednio podczas


tworzenia użytkownika. W SQL*Plus dostępne jest polecenie PASSWORD zmieniające jego
hasło. Żaden z administratorów nie może go odczytać — można je tylko zmieniać (in-
formacja ta nie dotyczy możliwości skorzystania przez administratora z zaszyfrowa-
nego hasła).

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.

Użytkownik może, a w zasadzie powinien, mieć przypisane uprawnienia do obiektów


(wyjątkiem jest sytuacja, kiedy ma on prawo do ich tworzenia i będzie korzystał tylko
z obiektów własnych). Przyznanie użytkownikowi Nowy prawa do wszystkich mani-
pulacji danymi na tabeli Osoby zawartej w schemacie Testowy uzyskujemy na skutek
wykonania polecenia:
GRANT SELECT, INSERT, UPDATE, DELETE ON Testowy.Osoby TO Nowy;

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;

Odebranie nieprzyznanego wcześniej prawa nie powoduje wyświetlenia komunikatu


o błędzie. W przypadku polecenia UPDATE jest możliwe ograniczenie zasięgu upraw-
nień tylko do wybranych kolumn.
GRANT SELECT, UPDATE(Nazwisko, Imie)
ON AP.Osoby TO Nowy;

Na pewno należy wymienić kolumnę (kolumny) przy nadawaniu prawa do tworzenia


odwołujących się do niej kluczy obcych.
GRANT REFERENCES (IdOsoby)
ON AP.Osoby TO 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

Jeśli odbieramy uprawnienia do modyfikowania tabeli (perspektywy), to w przypadku


dokonywania zmiany pola klucza obcego powinna zostać użyta klauzula CASCADE
CONSTRAINTS.
REVOKE UPDATE ON AP.Osoby
FROM Nowy CASCADE CONSTRAINTS;

Podobnie możemy przyznać prawa do procedury. Jedynym sensownym uprawnieniem


jest EXECUTE, które dotyczy zarówno procedur oraz funkcji, jak i pakietów. Należy przy
tym pamiętać, że przyznanie prawa do wykonywania pakietu obejmuje zawsze całość,
to jest wszystkie zawarte w nim procedury i funkcje zewnętrzne (co było już zazna-
czane wcześniej, a powinno być jednym z ważniejszych argumentów decydujących
o umieszczeniu funkcji czy procedury w danym pakiecie). Nie każdy użytkownik po-
winien mieć możliwość wykonywania procedur o różnym stopniu potencjalnego za-
grożenia dla bazy danych. Szczególnie ważne jest to dla pakietów działających na po-
ziomie systemu operacyjnego (klasy Javy) oraz wykonujących operacje na poziomie
konfiguracji czy administracji bazą danych.
GRANT EXECUTE ON AP.wysocy TO Nowy;
REVOKE EXECUTE ON AP.wysocy FROM Nowy;

Poza operacjami na danych, na obiektach, możliwe jest przypisywanie praw do mani-


pulowania tymi obiektami — tworzenia ich, modyfikowania czy usuwania ze schematu.
Przykłady związane z prawami do tworzenia obiektów zawiera tabela 15.1.
Rozdział 15. ♦ Elementy administracji — zarządzanie uprawnieniami z poziomu SQL 293

Tabela 15.1. Przyznawanie praw do obiektów


CREATE SESSION Łączenie z bazą danych.
CREATE TABLE Tworzenie tabel w swoim schemacie.
CREATE SEQUENCE Tworzenie sekwencji w swoim schemacie.
CREATE VIEW Tworzenie perspektyw w swoim schemacie.
CREATE PROCEDURE Tworzenie procedury i funkcji składowanej oraz pakietu składowanego w swoim
schemacie.

Przykłady poleceń do tworzenia procedury (zawiera w sobie procedury, funkcje oraz


pakiety) przedstawiono w metaskrypcie. Jeżeli nie użyto prefiksu ANY, prawo dotyczy
tylko obiektów w danym schemacie (schemacie użytkownika). Jeżeli został on użyty,
dotyczy ono elementów niewystępujących w schemacie użytkownika, które użytkow-
nik ten „widzi”.
GRANT CREATE (ANY) PROCEDURE | TRIGGER | TYPE TO Nowy;
GRANT ALTER (ANY) PROCEDURE | TRIGGER | TYPE TO Nowy;
GRANT DROP (ANY) PROCEDURE | TRIGGER | TYPE TO Nowy;

Odebranie uprawnienia wykonywane jest za pomocą polecenia REVOKE według przy-


kładowej składni:
REVOKE CREATE PROCEDURE | TRIGGER | TYPE FROM Nowy;

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;

Rola może zostać przypisana do użytkownika — będzie on wtedy posiadał wszystkie


nadane jej prawa.
GRANT Rola TO Nowy;
294 Część II ♦ ORACLE PL/SQL

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;

Rolę odbieramy poprzez wykonanie polecenia REVOKE — dotyczy to zarówno użytkow-


ników, jak i ról. Odebranie roli pozbawia wszystkich uprawnień z nią związanych.
REVOKE Rola FROM Nowy;
REVOKE Rola FROM 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;

Po jej usunięciu użytkownik traci wszystkie wynikające z niej uprawnienia — nie


dotyczy to sytuacji, kiedy niektóre z nich zostały mu przypisane jawnie. Informację
o zdefiniowanych w instancji Oracle rolach uzyskujemy, odpytując perspektywę słow-
nikową.
SELECT * FROM DBA_ROLES;

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.

W momencie powstania pierwszego obiektu użytkownika tworzony jest schemat. Może


on także zostać wygenerowany na skutek wykonania polecenia:
CREATE SCHEMA AUTHORIZATION testowy;

Obiekty bez nazwy kwalifikowanej tworzone są w bieżącym schemacie. W ramach


sesji możliwe jest przełączanie się między schematami na skutek wykonania polecenia:
ALTER SESSION SET CURRENT_SCHEMA = scott;

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

Tabela 15.2. Wykaz wybranych ról systemowych


CONNECT
RESOURCE
DBA
SELECT_CATALOG_ROLE
EXECUTE_CATALOG_ROLE
DELETE_CATALOG_ROLE
EXP_FULL_DATABASE
IMP_FULL_DATABASE
RECOVERY_CATALOG_OWNER
GATHER_SYSTEM_STATISTICS
LOGSTDBY_ADMINISTRATOR
AQ_ADMINISTRATOR_ROLE
AQ_USER_ROLE
GLOBAL_AQ_USER_ROLE
OEM_MONITOR
HS_ADMIN_ROLE
WKUSER
WM_ADMIN_ROLE
JAVAUSERPRIV
JAVAIDPRIV
JAVASYSPRIV
JAVADEBUGPRIV
EJBCLIENT
JAVA_ADMIN
JAVA_DEPLOY
CTXAPP
XDBADMIN
AUTHENTICATEDUSER
OLAP_DBA
SALES_HISTORY_ROLE

Po zmianie nazwy prawa do obiektów są przepisywane automatycznie. W przypadku


tabel w sposób automatyczny modyfikowane są referencje w kluczach obcych, które
się do nich odwołują. Nie są zmieniane na tej samej zasadzie odwołania do tych
obiektów w procedurach, funkcjach czy też pakietach.

Profil jest zbiorem ograniczeń związanych z zasobami przydzielonymi na zadania oraz


z uwierzytelnianiem użytkownika. Polecenie tworzące profil o ograniczeniach równo-
ważnych profilowi domyślnemu ma postać:
CREATE PROFILE N_profil
LIMIT CPU_PER_SESSION DEFAULT
CPU_PER_CALL DEFAULT
296 Część II ♦ ORACLE PL/SQL

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;

Niestety z polecenia trudno jest precyzyjnie wywnioskować, w jakich jednostkach miary


wyraża się ograniczenie. Dotyczy to w szczególności sposobu definiowania ograniczeń
związanych z dostępnymi zasobami. W Oracle 10 i starszych do wizualnego określenia
profilu można było posłużyć się Oracle Enterprise Managerem; rysunek 15.1.

Widoczny jest tu podział na dwie zakładki odpowiadające dwóm typom ograniczeń.


W zakresie General, dotyczącym zasobów, możemy zauważyć, że przydział CPU/Session
wyrażony jest w setnych sekundy, czas połączenia i czas bezczynny w minutach itp.
W tym miejscu możliwe jest również zdefiniowanie maksymalnej liczby sesji, jaką jed-
nocześnie może otworzyć użytkownik. W zakładce Password możemy podać czas, po
którym hasło wygaśnie, okres, przez jaki po tym wygaśnięciu będzie ono blokowane,
czas, przez jaki pamiętana ma być historia haseł, oraz liczbę dozwolonych nie-
udanych prób logowania. Ważnym parametrem jest Complexity function (w zapytaniu
PASSWORD_VERIFY_FUNCTION), pozwalający na sprawdzenie formalnej strony hasła, np.
jego złożoności (liczby znaków, tego czy zawiera znaki różne od liter etc). Badanie
złożoności hasła jest związane z przypisanym użytkownikowi profilem. Od wersji 10.
Rozdział 15. ♦ Elementy administracji — zarządzanie uprawnieniami z poziomu SQL 297

Rysunek 15.1. Wizualne tworzenie profilu — Oracle 10 i wersje starsze

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.

Podobnie jak w Enterprise Managerze, konsola umożliwia przestawianie wartości ogra-


niczeń na wielkości różne od domyślnych. Jako ułatwienie sugerowanych jest kilka naj-
częściej spotykanych wartości (rysunek 15.3), choć, oczywiście, możliwe jest również
wpisanie każdej z nich ręcznie.

Przypisanie profilu do użytkownika (zmiana jego profilu) jest wykonywane za pomocą


polecenia:
ALTER USER Nowy PROFILE N_profil

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';

Na takich samych zasadach możliwe jest przypisywanie komentarzy do kolumn tych


obiektów.
COMMENT ON COLUMN [schema.]table.column IS 'tekst';
COMMENT ON COLUMN [schema.]view.column IS 'tekst';
COMMENT ON COLUMN [schema.]snapshot.column IS 'tekst';
298 Część II ♦ ORACLE PL/SQL

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;

W tym miejscu pozwolę sobie na zakończenie dywagacji związanych z elementami


administracji bazą Oracle. Poruszone tutaj zagadnienia ograniczają się do zakresu ściśle
związanego z programowaniem po stronie serwera i nie pretendują do miana pełnego
wykładu z tej tematyki. Tak jak zaznaczono w rozdziale poświęconym Javie, zasługują
one na odrębną książkę. O ile jednak poprzednio obiecałem czytelnikom kolejną pu-
blikację, to w przypadku administrowania odsyłam raczej do innych książek poświę-
conych tej tematyce, chociażby do wydanej przez to samo wydawnictwo pozycji pani
Jadwigi Gnybek.
Rozdział 15. ♦ Elementy administracji — zarządzanie uprawnieniami z poziomu SQL 299

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 ...

Zadeklarujmy przykładowy typ obiektowy t_adres_o, który posłuży nam do przecho-


wywania danych adresowych.
CREATE OR REPLACE TYPE t_adres_o AS OBJECT
(miejscowosc varchar2(10),
numer_domu number,
kraj varchar2(10));

Jak widać, podstawowa definicja typu jest analogiczna do definicji tabeli. Zdefinio-
wany typ obiektowy usuwa się przy pomocy polecenia:
DROP TYPE t_adres_o;

Utworzony typ jest zapisywany na serwerze, co pozwala na odwoływanie się do niego


w kolejnych sesjach. Możliwe jest zadeklarowanie jego instancji (zmiennych), a na-
stępnie przypisanie do nich konkretnych wartości. Odbywa się to przy użyciu nazwy
typu obiektowego, po której następuje lista wartości. W skrypcie przedstawiono przy-
kład deklaracji obiektu adres typu t_adres_o i przypisanie mu konkretnych wartości
oraz ich wyświetlenie.
302 Część II ♦ ORACLE PL/SQL

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);
END;

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;

Po stworzeniu obiektu, ale przed wywołaniem jego konstruktora, jest mu automatycz-


nie przypisywana wartość pusta — NULL. Nie oznacza to jednak, że tyle wynoszą
wartości jego atrybutów (pól). Należy odróżniać sytuację, gdy obiekt ma wartość NULL
(jest niezainicjalizowany), od takiej, gdy jego pola mają tę wartość.
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);
adres := NULL; -- adres staje się niezainicjalizowany
adres.kraj := 'Anglia';
DBMS_OUTPUT.PUT_LINE(adres.miejscowosc||' '|| adres.numer_domu||' '||adres.kraj);
END;
Rozdział 16. ♦ Obiektowość w Oracle 303

Wykonanie takiego skryptu spowoduje wywołanie wyjątku, ponieważ usiłujemy pod-


stawić wartość do niezainicjowanego obiektu adres := null.
BŁĄD w linii 1:
ORA-06530: Odwołanie do niezainicjowanego kompozytu
ORA-06512: przy linia 7

Tak jak w językach programowania, możliwe jest utworzenie metod operujących na


obiekcie. Mogą stanowić je zarówno procedury, jak i funkcje PL/SQL poprzedzone
słowem kluczowym MEMBER. W prezentowanym przykładzie dla obiektu t_adres_o
zdefiniowane zostały dwie metody.
CREATE OR REPLACE TYPE t_adres_o AS OBJECT
(miejscowosc varchar2(10),
numer_domu number,
kraj varchar2(10),
MEMBER PROCEDURE ustaw_numer(n_numer varchar2),
MEMBER FUNCTION pokaz_adres RETURN varchar2);

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;

W prezentowanym przykładzie procedura ustawia zadaną wartość numeru domu, na-


tomiast funkcja zwraca pełną informację o adresie w postaci łańcucha. Wartość zwra-
cana przez funkcję została uzyskana dzięki konkatenacji wszystkich pól. Odwołanie
się do wartości bieżącej pola odbywa się dzięki użyciu słowa kluczowego SELF. Ko-
lejny skrypt przedstawia zastosowanie zdefiniowanych metod do zmiany numeru do-
mu oraz wyświetlenia informacji obejmującej pełne dane po tej zmianie.
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.ustaw_numer(55);
DBMS_OUTPUT.PUT_LINE('numer po zmianie' || ' ' || adres.numer_domu);
DBMS_OUTPUT.PUT_LINE(adres.pokaz_adres);
END;
304 Część II ♦ ORACLE PL/SQL

Rezultat wykonania poprzedniego skryptu będzie wyglądał następująco:


Lodz 5 Polska
numer po zmianie 55
Lodz 55 Polska

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);

Pierwsza z funkcji ma za zadanie zwracać połączone w jeden łańcuch nazwisko i imię


pracownika, a druga datę urodzenia. Ich implementacja w ciele typu może wyglądać
następująco:
CREATE OR REPLACE TYPE BODY t_osoba_o IS
MAP MEMBER FUNCTION odwzoruj RETURN VARCHAR2 IS
n varchar2(30);
BEGIN
n:=SELF.nazwisko||' '||SELF.imie;
RETURN n;
END;
MEMBER FUNCTION lata RETURN DATE IS
begin
return self.dataurodz;
end;
END;

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

MEMBER FUNCTION rok return number,


MEMBER FUNCTION najmniej return number,
MEMBER FUNCTION adres_osoby return varchar2);

Implementacja poszczególnych funkcji w ciele typu może zostać zrealizowana tak,


jak pokazuje to skrypt:
CREATE OR REPLACE TYPE BODY t_osoba_o IS
MAP MEMBER FUNCTION odwzoruj RETURN VARCHAR2 IS
n varchar2(30);
BEGIN
n := SELF.nazwisko||' '||SELF.imie;
RETURN n;
END;
MEMBER FUNCTION lata RETURN DATE IS
BEGIN
RETURN self.dataurodz;
END;
MEMBER FUNCTION razem RETURN NUMBER IS
i INTEGER;
suma number(10,2) := 0;
BEGIN
FOR i IN 1..self.zarobki.last() loop
Suma := suma+self.zarobki(i).brutto;
END LOOP;
RETURN suma;
END;
MEMBER FUNCTION srednia RETURN NUMBER IS
i INTEGER;
suma NUMBER(10,2) := 0;
srednia NUMBER(10,2) := 0;
licznik INTEGER := 0;
BEGIN
for i in 1..self.zarobki.last() loop
licznik := licznik+1;
suma := suma+self.zarobki(i).brutto;
END LOOP;
Srednia := (suma/licznik);
RETURN srednia;
END;
MEMBER FUNCTION najwiecej RETURN NUMBER IS
i INTEGER;
temp1 number(10, 2) := 0;
temp2 number(10, 2) := 0;
BEGIN
FOR i IN 1..self.zarobki.last() loop
temp1 := 0;
temp1 := self.zarobki(i).brutto;
IF (temp1 > temp2) THEN
temp2 := temp1;
END IF;
END LOOP;
RETURN temp2;
END;
MEMBER FUNCTION rok RETURN NUMBER IS
r NUMBER(10, 0) := 0;
BEGIN
Rozdział 16. ♦ Obiektowość w Oracle 307

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;

Przejdźmy do utrwalania zmiennych obiektowych. Zrealizujemy to, tworząc tabelę


osoba_o, która będzie składowała dane w postaci typu obiektowego t_osoba_o, a klucz
podstawowy zostanie ustanowiony na polu idosoby tego obiektu.
CREATE TABLE osoba_o OF t_osoba_o
(idosoby PRIMARY KEY)
OBJECT id PRIMARY KEY
NESTED table zarobki
STORE as tabela_zarobkow;

Zastosowana w poleceniu klauzula NESTED TABLE wskazuje nazwę segmentu danych,


w którym będą składowane elementy kolekcji zarobki. Segment tabela_zarobkow jest
tworzony wraz z tabelą osoba_o i zostaje zapamiętany w systemie jako tabela obiek-
tów typu t_zarobki_o. W analogiczny sposób tworzymy znacznie prostszą tabelę
dzialy_o, która nie ma zdefiniowanego klucza oraz nie zawiera tabeli zagnieżdżonej.
CREATE TABLE dzialy_o OF t_dzialy_o;

W celu zapewnienia pseudoautomatycznej inkrementacji klucza utworzymy sekwencję.


Będzie ona zasilała pole idosoby w tabeli osoba_o.
CREATE SEQUENCE osoby_seq INCREMENT BY 1 START WITH 1;

Ze względu na przyjętą strukturę danych wstawianie pierwszych rekordów wyko-


najmy przy pomocy skryptu PL/SQL. W początkowym kroku wpisujemy do tabeli
dzialy_o pierwszą wartość. Jednocześnie przechwytujemy do zmiennej dzial_ref
wskaźnik do wstawionego rekordu. Pozwoli nam to wstawić z jego wykorzystaniem
wartości do tabelki osoby_o. Przy okazji wstawiane są dwa rekordy do zagnieżdżonej
tabeli zarobki.
308 Część II ♦ ORACLE PL/SQL

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');

W takim przypadku wstawienie kolejnego rekordu do tabeli osoba_o wymaga znale-


zienia wskaźnika do istniejącego już rekordu w tabeli dzialy_o. W tym celu, również
jako źródło danych dla zapytania INSERT, podaje się zapytanie wybierające, w którym
polami są wstawiane dane, natomiast tabelą źródłową jest tabela dzialy_o, do której
rekordu chcemy znaleźć wskaźnik; na konkretny wiersz wskazuje klauzula WHERE.
INSERT INTO osoba_o
SELECT (3, 'Jarosław', 'KOWALCZYK',
to_date('10-10-1975','DD-MM-YYYY'),
t_adres_o('Ulica2', '14', '34', 'Poznan', '10-001', 'Poland'),
REF(dzial_ref),
k_zarobki(
t_zarobki_o(125, to_date('2008-09-11', 'YYYY-MM-DD'))
)
FROM dzialy_o dzial_ref
WHERE dzial_ref.iddzialu=1;

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;

Według podobnych zasad usuwamy wybrany rekord.


DELETE TABLE
(SELECT zarobki FROM osoba_o
WHERE idosoby = 3) z
WHERE z.brutto = 225;

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);

Dość prostym zadaniem jest wykonywanie zapytań wybierających do tabeli obiektowej.


Rządzą się one takimi samymi prawami jak zwykłe zapytania wybierające — jedyna
różnica polega na odwoływaniu się do obiektów powiązanych poprzez referencje. W tym
celu należy zastosować operator DEREF, którego argumentem jest nazwa powiązanej w ten
sposób tabeli, czyli dzial.
SELECT idosoby,imie, nazwisko, DEREF(dzial)
FROM osoba_o;

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;

Poprzednio pokazywaliśmy modyfikację tabeli zagnieżdżonej. Tym razem skupmy się


na zmianie wartości wskazywanej przez referencję. W przedstawionym przykładzie zmie-
niamy wskaźnik do działu dla osoby o imieniu Jarosław i przypisujemy wskaźnik dla
działu o identyfikatorze 2.
UPDATE osoba_o
SET dzial=(SELECT REF(dzial_ref)
FROM dzialy_o dzial_ref
WHERE dzial_ref.iddzialu=2)
WHERE osoba_o.imie='Jarosław';

Do operacji na danych mogą być wykorzystywane również metody zdefiniowane dla


obiektu. W przykładzie do wybrania osoby o określonej dacie urodzenia została wy-
korzystana funkcja lata.
310 Część II ♦ ORACLE PL/SQL

SELECT VALUE(k)
FROM osoba_o k
WHERE k.lata() = TO_DATE('1980-09-08','YYYY-MM-DD');

W celu wyświetlenia informacji o obiektach możemy wykorzystać perspektywy słow-


nikowe. W naszym przypadku wyświetlone zostaną typy utworzone w bieżącym sche-
macie.
SELECT * FROM user_objects
WHERE object_type = 'TYPE';

Na koniec rozważań ogólnych dotyczących tabel obiektowych przedstawione zostaną


przykłady zastosowania innych opracowanych metod. Użycie funkcji razem pozwala
na podsumowanie zarobków wybranej osoby.
SET SERVEROUTPUT ON
DECLARE
ile_zarobil NUMBER;
numer_osoby int := 1;
BEGIN
SELECT o.razem() INTO ile_zarobil FROM osoba_o o
WHERE o.idosoby = numer_osoby;
DBMS_OUTPUT.PUT_LINE('Osoba o numerze id ' || numer_osoby ||' zarobiła łącznie' ||
' ' || ile_zarobil);
END;

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;

Działanie skryptu, w którym odwołujemy się do metody adres_osoby, spowoduje na-


tomiast wyświetlenie w postaci jednego łańcucha danych adresowych wybranej osoby.
SET SERVEROUTPUT ON
DECLARE
a varchar2(50);
numer_osoby int := 1;
BEGIN
SELECT o.adres_osoby() INTO a FROM osoba_o o
WHERE o.idosoby = numer_osoby;
DBMS_OUTPUT.PUT_LINE('Osoba o numerze id ' || numer_osoby || ' ' || ' mieszka w ' || a);
END;

Oczywiście można by zaprezentować bardziej złożone zastosowanie metod definiowa-


nych na obiektach, jednak wymagałoby to analizy o wiele bardziej złożonego kodu, co
ze względów dydaktycznych nie jest szczególnie wskazane.
Rozdział 16. ♦ Obiektowość w Oracle 311

Jednym z ciekawszych praktycznych zastosowań typów obiektowych jest możliwość


definiowania za ich pomocą funkcji agregujących opracowanych przez programistę
(użytkownika). W tym celu należy określić typ, którego pola stanowią zmienne nie-
zbędne do przeprowadzenia obliczeń, z czego koniecznie jedna przeznaczona jest na
rezultat obliczeń funkcji agregującej. Niezbędne jest zdefiniowanie dla tego typu czte-
rech funkcji (metod) — ich nazwy oraz zestaw parametrów są obowiązkowe. Możemy
mówić, że przeciążamy wbudowane metody Oracle. We wszystkich tych funkcjach przez
nazwę zwracany jest stan ich wykonania, natomiast wynik jest przekazywany przez
listę parametrów:
 ODCIAggregateInitialize — statyczna funkcja (niedostępna poza typem)
wykorzystywana do inicjalizacji zmiennych typu i wywoływana przy każdej
zmianie grupy rekordów wynikającej z użycia klauzuli GROUP BY albo
przynajmniej raz na początku przetwarzania, jeśli taka klauzula nie istnieje.
W ciele tej funkcji możliwe jest również inicjowanie zmiennych zapytaniami
wybierającymi SELECT INTO.
 ODCIAggregateIterate — metoda wykonywana dla każdego z rekordów
w ich zestawie, która stanowi odpowiednik kursora. Za definiowanie zestawu
rekordów odpowiada zmienna typu, zgodnego z typem obiektu, do której pól
(atrybutów) odwołujemy się za pomocą zmiennych kwalifikowanych. Należy
zauważyć, że wartości są wyznaczane w pojedynczym przebiegu kursora.
Nawigacja odbywa się tylko raz, od pierwszego do ostatniego rekordu.
 ODCIAggregateTerminate — metoda wykonywana raz na końcu każdej grupy
rekordów powstałej na skutek użycia klauzuli GROUP BY albo przynajmniej
raz na końcu przetwarzania, jeśli taka klauzula nie istnieje. Służy ona
do wyznaczania ostatecznej wartości funkcji agregującej w grupie.
 ODCIAggregateMerge — metoda stosowana podczas przetwarzania równoległego.
Aby zapewnić poprawność przetwarzania dla dwóch procesów równoległych
(większa ich liczba może zostać potraktowana jako wielokrotność takiej pary),
funkcja ta pokazuje, jak należy złożyć wyniki cząstkowe. Stąd definicja
dwóch parametrów zgodnych z typem wyjściowym, z których jeden obrazuje
proces podstawowy, a drugi proces zrównoleglony.

W przygotowanym przykładzie zostanie utworzona funkcja agregująca obliczająca średnią


geometryczną liczb nieujemnych wyrażoną wzorem:
n
xg = n ∏x
i =1
i

W utworzonym typie zadeklarowano dwa pola: pierwsze przeznaczone na wartość


obliczonej średniej geometrycznej, a drugie będące licznikiem przetworzonych rekordów.
Typ zawiera również wymienione obowiązkowe metody.
CREATE OR REPLACE TYPE GeomAVG AS OBJECT
(
GAVG NUMBER, -- wartość średniej geometrycznej
Licz number, -- licznik rekordów
STATIC FUNCTION ODCIAggregateInitialize(pocz IN OUT GeomAVG)
RETURN number,
312 Część II ♦ ORACLE PL/SQL

MEMBER FUNCTION ODCIAggregateIterate(self IN OUT GeomAVG,


biezaca IN number) RETURN number,
MEMBER FUNCTION ODCIAggregateTerminate(self IN GeomAVG,
wynik OUT number, flags IN number) RETURN number,
MEMBER FUNCTION ODCIAggregateMerge(self IN OUT GeomAVG,
rown IN GeomAVG) RETURN number
);

W ciele typu w funkcji ODCIAggregateInitialize zainicjowano pola wartościami GAVG


NULL oraz licz 0. W funkcji ODCIAggregateIterate sprawdzane jest, czy bieżąca war-
tość pola nie wynosi NULL. Dla takiego przypadku nie jest wykonywana żadna instruk-
cja, co odpowiada pominięciu wartości NULL przy wyznaczaniu funkcji agregujących
i ma miejsce również w przypadku systemowych funkcji agregujących (co omówiono
wcześniej). W kolejnej instrukcji sprawdzane jest, czy GAVG wynosi NULL. Jeśli jest to
prawda, oznacza to pierwszy rekord grupy — co wynika z przyjętej wartości inicjują-
cej. W takiej sytuacji wartość GAVG ustawiana jest na bieżącą wartość rekordu, natomiast
licznik rekordów licz na 1. W przeciwnym wypadku wartość GAVG jest przez wartość
bieżącą mnożona, a licznik jest zwiększany o jeden. W funkcji ODCIAggregateTerminate
obliczany jest pierwiastek rzędu n (potęga o wykładniku będącym odwrotnością licz-
nika). Gdy nie ma rekordów w grupie, wynik jest ustawiany na NULL. Ostatnia funkcja
ciała typu, ODCIAggregateMerge, jest analogiem funkcji ODCIAggregateIterate. Różnica
polega na tym, że tym razem omawiane operacje nie są wykonywane na instancji self
obiektu (podstawowy proces), a na jego instancji o nazwie rown (reprezentującej pro-
ces równoległy) i następuje złączenie wyników tam otrzymanych z wynikami z procesu
self. Nazwy obu procesów są przykładowe i można zastosować dowolne inne nazew-
nictwo, byle zachowana została zgodność pomiędzy nazwami parametrów w typie i w jego
ciele.
CREATE OR REPLACE TYPE BODY GeomAVG IS
STATIC FUNCTION ODCIAggregateInitialize(pocz IN OUT GeomAVG)
RETURN number IS
BEGIN
pocz := GeomAVG(NULL, 0);
RETURN ODCIConst.Success;
END;
MEMBER FUNCTION ODCIAggregateIterate(self IN OUT GeomAVG, biezaca IN number)
RETURN number IS
BEGIN
IF (biezaca IS NOT NULL) THEN
if(self.GAVG IS NULL) THEN
self.GAVG := biezaca;
self.Licz := 1;
ELSE
self.GAVG := self.GAVG*biezaca;
self.Licz := self.Licz+1;
END IF;
END IF;
RETURN ODCIConst.Success;
END;
MEMBER FUNCTION ODCIAggregateTerminate(self IN GeomAVG, wynik OUT
number, flags IN number) RETURN number IS
BEGIN
IF (self.Licz > 0) THEN
wynik := POWER(self.GAVG, 1/self.Licz);
ELSE
Rozdział 16. ♦ Obiektowość w Oracle 313

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;

W prezentowanym przykładzie, ze względu na chęć zachowania prostoty kodu, zre-


zygnowano ze sprawdzenia, czy wartości argumentów nie są ujemne, oraz z sekcji
obsługi wyjątków, która przynajmniej w minimalnym zakresie (WHEN OTHERS) powinna
zostać zastosowana. Aby zdefiniować funkcję agregującą, utworzony typ należy enkap-
sulować, podobnie jak to zostało pokazane dla klas Javy. W definicji funkcji musi po-
jawić się dokładnie jeden parametr zgodny z typem, dla którego jest ona obliczana (zwy-
kle jest to liczba, ale można sobie wyobrazić, chociaż to dziwne, funkcję agregującą
użytkownika przetwarzającą inny typ — łańcuch albo datę), oraz wartość zwracana
przez nazwę zgodna co do typu ze zmienną znajdującą się w pierwszym polu typu,
która zawiera wynik (tutaj też nie musi to być koniecznie zmienna numeryczna). Moż-
liwość definiowania funkcji agregujących dla zmiennych różnych od numerycznych jest
ciekawą cechą funkcji użytkownika. Zastosowanie opcji PARALLEL_ENABLE powoduje,
że jeśli to tylko będzie możliwe, do wyznaczania tej funkcji zostanie użyte przetwa-
rzanie równoległe, co poprawia wydajność. Korzystanie z tej opcji jest możliwe tylko
wtedy, gdy została zdefiniowana metoda ODCIAggregateMerge.
CREATE OR REPLACE FUNCTION AVGG(input number) RETURN number
PARALLEL_ENABLE AGGREGATE USING GeomAVG;

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.

Po raz kolejny namawiam także do odwiedzenia witryny PLOUG (http://ploug.org.pl/),


gdzie dostępnych jest wiele materiałów poświęconych tematyce Oracle.
Skorowidz
--, 155 aliasy pól, 29
%, 28 ALL, 31, 46
%FOUND, 230 ALLOCATE EXTENT, 115, 134
%ISOPEN, 230, 245 Allow only SELECT statements, 15
%NOTFOUND, 230 ALTER INDEX, 134
%ROWCOUNT, 230, 231 COALESCE, 134
&&, 91 DEALLOCATE UNUSED, 134
/* */, 155 REBUILD ONLINE, 134
:=, 155 REBUILD TABLESPACE, 134
||, 28, 29, 155 ALTER SEQUENCE, 120
ALTER SESSION, 118
SET CURRENT_SCHEMA, 294
A ALTER TABLE, 110
ACCESS_INTO_NULL, 173 ADD, 110
ADD CONSTRAINT, 113, 118 ADD CONSTRAINT, 113, 118
administracja bazą danych, 20 ALLOCATE EXTENT, 115
AFTER, 198, 218 CASCADE CONSTRAINTS, 112
AFTER ALTER, 219 CHECKPOINT, 112
AFTER ANALYZE, 219 CONTINUE, 112
AFTER ASSOCIATE STATISTICS, 219 DEALLOCATE UNUSED, 114
AFTER AUDIT, 219 DISABLE ALL TRIGGERS, 214
AFTER COMMENT, 219 DISABLE PRIMARY KEY CASCADE, 118
AFTER CREATE, 219 DROP COLUMN, 111
AFTER DB_ROLE_CHANGE, 222 DROP CONSTRAINT, 113
AFTER DDL, 219 DROP UNUSED COLUMNS, 112
AFTER DISASSOCIATE STATISTICS, 219 ENABLE ALL TRIGGERS, 214
AFTER DROP, 219 ENABLE NOVALIDATE UNIQUE, 118
AFTER GRANT, 219 ENABLE PRIMARY KEY, 118
AFTER LOGON, 215, 219 GLOBAL TEMPORARY, 116
AFTER NOAUDIT, 219 INVALID, 112
AFTER RENAME, 219 MODIFY, 110, 111
AFTER REVOKE, 219 MOVE TABLESPACE, 116
AFTER SERVERERROR, 222, 223 PRIMARY KEY, 111
AFTER STARTUP, 222 RENAME COLUMN, 114
AFTER SUSPEND, 219 SET UNUSED, 112
AFTER TRUNCATE, 219 SET UNUSED COLUMN, 113
algorytm zachłanny, 266 ALTER TRIGGER DISABLE, 206
318 Skorowidz

ALTER TRIGGER ENABLE, 206 blok systemu operacyjnego, 12


ALTER USER, 291 blokowanie wyzwalacza, 206
ALTER USER PROFILE, 297 błąd standardowy, 84
ALTER USER QUOTA, 291 błędy, 170
ALTER VIEW DROP CONSTRAINT, 122 BREAK, 88
ANALYZE INDEX MONITORING USAGE, 135 BREAKS, 89
ANALYZE INDEX NOMONITORING BTITLE, 89
USAGE, 136
ANALYZE INDEX VALIDATE
STRUCTURE, 135
C
AND, 28, 31, 35, 47 CACHE, 120
AND DATAFILES, 290 CALL, 170
ANY, 31, 46, 293 CASCADE CONSTRAINTS, 110, 112, 292
APPEND, 93 CASE, 36, 48
APPENDCHILDXML, 148, 149 CASE_NOT_FOUND, 173
aproksymacja liniowa, 82 catch, 272
AS, 29, 36, 109, 163, 203 CEIL, 75
ASC, 29, 66 CHAR, 11
ASCII, 29 CHECK, 104, 105, 106, 108, 109, 122
AUTOCOMMIT, 251 CHECKPOINT, 112
automatyczna inkrementacja klucza CHR(), 29
podstawowego, 205 ciało pakietu, 187, 189
automatyczne zarządzanie przestrzenią, 289 CLASSPATH, 283
AUTONOMOUS_TRANSACTION, 250 CLOB, 11
AVG, 37, 46 CLOSE, 229
COALESCE, 135
B COLUMN, 88
COLUMNS, 89
B-drzewa, 130 COMMENT, 297
BEFORE, 218 COMMIT, 117, 247, 250
BEFORE ALTER, 219 COMPUTE, 88
BEFORE ANALYZE, 219 COMPUTES, 89
BEFORE ASSOCIATE STATISTICS, 219 CONCAT, 140
BEFORE AUDIT, 219 CONNECT, 290
BEFORE COMMENT, 219 CONNECT BY, 94, 95, 239
BEFORE CREATE, 219 Connection, 277
BEFORE DDL, 219 CONSTRAINT, 104, 106, 122
BEFORE DISASSOCIATE STATISTICS, 219 CONSTRAINT VIOLATION, 116
BEFORE DROP, 219 CONSTRAINTS, 116
BEFORE GRANT, 219 CONTINUE, 112, 161, 162
BEFORE LOGOFF, 219 CONTINUE WHEN, 162
BEFORE NOAUDIT, 219 CORR_K, 87
BEFORE RENAME, 219 CORR_S, 86
BEFORE REVOKE, 219 COUNT, 37, 174, 263
BEFORE SHUTDOWN, 222 COVAR_POP, 68
BEFORE TRUNCATE, 219 COVAR_SAMP, 68
BEFORE UPDATE, 198 CREATE, 93
BEGIN, 156, 170, 203 CREATE BITMAP INDEX, 133
BETWEEN, 31, 32, 160 CREATE FUNCTION, 179
BFILE, 11 CREATE INDEX, 132, 133
BINARY_INTEGER, 263 CREATE OR REPLACE, 121
BLOB, 11 CREATE OR REPLACE FUNCTION, 179, 180,
blok anonimowy, 156 181, 270
blok danych, 12
Skorowidz 319

CREATE OR REPLACE JAVA SOURCE DBMS_RANDOM, 194


NAMED, 269, 271 DDL, 260
CREATE OR REPLACE PACKAGE, 187, 188 DEALLOCATE UNUSED, 114, 134
CREATE OR REPLACE PACKAGE BODY, DECLARE, 156, 194, 203, 256
187, 189 DECODE, 55, 57
CREATE OR REPLACE PROCEDURE, 164, 165 DEFAULT, 105, 108, 167, 290
CREATE OR REPLACE PUBLIC SYNONYM, 252 DEFAULT NULL, 111
CREATE OR REPLACE TRIGGER, 197, 209 DEFAULT TABLESPACE, 290
CREATE OR REPLACE TYPE, 301, 304, 305 DEFERRABLE, 116, 117, 118
CREATE OR REPLACE VIEW, 121 DEFERRED, 117
CREATE PROCEDURE, 163, 164, 293 DEFINE, 92
CREATE PROFILE, 295, 296 definiowanie
CREATE PUBLIC SYNONYM, 252 klucz obcy, 107
CREATE ROLE, 293 typ obiektowy, 301
CREATE SCHEMA AUTHORIZATION, 294 deklaracja
CREATE SEQUENCE, 119, 293, 307 kursory, 229
CREATE SESSION, 293 stałe, 156
CREATE SYNONYM, 252 wyjątki, 175
CREATE TABLE, 103, 109, 208, 293 zmienne, 156, 203
AS, 109 DELETE, 101, 197, 200, 264
CREATE TABLESPACE, 289 WHERE, 101
CREATE TRIGGER, 197 DELETEXML, 150
CREATE TYPE, 301, 304 DELETING, 208
CREATE UNIQUE INDEX, 132 DENSE_RANK, 71
CREATE USER, 290 DEREF, 309
CREATE USER PROFILE DEFAULT, 290 DESC, 29, 30, 66
CREATE VIEW, 121, 293 DICT_COLUMNS, 124
createStatement(), 278 DICTIONARY, 124
CROSS JOIN, 43, 48 DISABLE PRIMARY KEY CASCADE, 118
CUBE, 52, 53 dodawanie kolumn do tabeli, 110
CUME_DIST, 73, 74, 75, 78 dodawanie ograniczeń, 113
CURRENT OF, 232 DriverManager.getConnection(), 277, 282
CURRENT ROW, 60 DriverManager.registerDriver(), 282
CURRVAL, 120 DROP, 121
CURSOR, 229 DROP COLUMN, 111
CURSOR_ALREADY_OPEN, 173 DROP CONSTRAINT, 113, 122
CYCLE, 119 DROP FUNCTION, 179
DROP INDEX, 132
DROP JAVA SOURCE, 270
D DROP PACKAGE, 187
dane XML, 137 DROP PROCEDURE, 164
Data Modeler, 10 DROP PUBLIC SYNONYM, 252
DATE, 11 DROP ROLE, 294
daty, 185 DROP SEQUENCE, 120
DAYS, 60 DROP STORAGE, 102
DB_ROLE_CHANGE, 222 DROP TABLE, 104, 107, 116
DBA, 14, 215 CASCADE CONSTRAINTS, 110
DBA_DATA_FILES, 125 DROP TABLESPACE, 290
DBA_FREE_SPACE, 129 DROP TRIGGER, 197
DBA_OBJECTS, 124 DROP TYPE, 301
DBA_SEGMENTS, 129 DROP UNUSED COLUMNS, 112
DBA_USERS, 130 DROP USER, 291
DBMS_OUTPUT, 157 DROP VIEW, 121
320 Skorowidz

DUAL, 120 FIRST, 263


DUP_VAL_ON_INDEX, 173 first(), 281
dynamic performance views, 127 FIRST_VALUE, 66
dynamiczne perspektywy wydajności, 127 Float.NEGATIVE_INFINITY, 274
dynamiczne wykonywanie zapytania, 254 Float.POSITIVE_INFINITY, 274
dynamiczny SQL, 253 FLOOR, 75
EXECUTE IMMEDIATE, 253, 254 FOLLOWING, 60
plany wykonania, 268 FOLLOWS, 209
tabele tymczasowe, 260 FOR, 160, 231
USING OUT, 260 REVERSE, 231
wyszukiwanie drogi w grafie, 266 FOR EACH ROW, 199
wywołanie procedury, 254 FOR UPDATE, 250
zgłębianie danych, 259 FOREIGN KEY, 104, 106, 110, 113
zliczanie rekordów, 259 FORMAT, 88
działania na poziomie systemu operacyjnego, 284 forward, 194
FROM, 27, 28
FULL JOIN, 41
E funkcje, 179
ekstent, 12, 102, 115 obsługa wyjątków, 184
eliminowanie powtórzeń, 54 parametry, 181
ELSE, 35, 159 przeciążanie, 189
ELSEIF, 159 RETURN, 183
Enable DBMS Output, 157 tworzenie, 179
ENABLE NOVALIDATE UNIQUE, 118 usuwanie, 179
ENABLE PRIMARY KEY, 118 wywołanie, 180
END, 156, 164 zwracanie wartości, 179
END LOOP, 160 funkcje agregujące, 36, 58
enkapsulacja klasy Javy, 271, 279 AVG, 37
ESCAPE, 34 COUNT, 37
etykiety, 162 MAX, 37
EXCEPTION, 156, 157, 172, 175 MIN, 37
exec(), 287 STDDEV, 37
EXECUTE, 246 SUM, 37
EXECUTE IMMEDIATE, 169, 171, 249, 253, VARIANCE, 37
255, 256, 258, 259, 261 funkcje analityczne, 49, 63, 65
executeQuery(), 278 COVAR_POP, 68
EXISTS, 31, 263 COVAR_SAMP, 68
EXISTSNODE, 149 CUME_DIST, 73, 74
EXIT, 160 FIRST_VALUE, 66
EXIT WHEN, 242 LAG, 78
EXTEND, 264 LAST_VALUE, 66
EXTRACT, 142, 143 LEAD, 78
EXTRACTVALUE, 143, 150 STDDEV_POP, 66, 67
STDDEV_SAMP, 66, 67
VAR_POP, 66, 67
F VAR_SAMP, 66, 67
FALSE, 27 WIDTH_BUCKET, 79, 81
FETCH, 230, 231, 244, 262 funkcje rankingowe, 63, 69
File, 285 DENSE_RANK, 71
filtrowanie na poziomie grupy rekordów, 39 PERCENT_RANK, 72, 84
filtrowanie na poziomie rekordu, 38 RANK, 70
filtrowanie pól XML, 144, 146 funkcje zdarzeń, 223, 224
Skorowidz 321

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

NCHAR, 11 operatory, 28, 31


NCLOB, 11 algebraiczne, 31
NESTED TABLE, 307 ALL, 46
NEXT, 115, 263 ANY, 46
NEXTVAL, 120, 204 BETWEEN, 32
NO_DATA_FOUND, 172, 173, 174 DEREF, 309
NOCYCLE, 119, 120 IN, 32, 43, 44
NOMONITORING USAGE, 136 INTERSECT, 90
NOT, 28, 31 IS NULL, 32
NOT NULL, 104, 105, 108, 156, 302 LIKE, 33
NOT_LOGGED_ON, 173 logiczne, 31
NTILE, 75 MINUS, 90
NULL, 27, 31, 41, 69, 89, 99, 105, 107, 156, ON, 95
159, 162 OR, 32
NULLS FIRST, 69 PRIOR, 94
NULLS LAST, 69 UNION, 89
NUMBER, 11 USING, 45
numer wyjątku, 175 opis pól tabeli, 30
numeracja błędów, 170 oprogramowanie po stronie serwera, 269
numeracja rekordów w grupie, 48 OR, 28, 31, 32, 35
NVARCHAR2, 11 ORA, 12
ora_client_ip_address, 224
NVL, 204
ora_database_name, 224, 226
ora_des_encrypted_password, 224
O ora_dict_obj_name, 224
ora_dict_obj_name_list, 224
obiektowość, 301 ora_dict_obj_owner, 224
OBJECT, 311 ora_dict_obj_owner_list, 224
obsługa grafów, 94 ora_dict_obj_type, 224
obsługa wyjątków, 172, 174, 183 ora_grantee, 224
Java, 272 ora_instance_num, 224, 226
odbieranie zestawu uprawnień, 292 ora_is_alter_column, 224
odchylenie standardowe populacji, 67 ora_is_creating_nested_table, 224
odchylenie standardowe próbki, 67 ora_is_drop_column, 224
ODCIAggregateInitialize, 311 ora_is_servererror, 224
ODCIAggregateIterate, 311, 312 ora_login_user, 223, 224, 226
ODCIAggregateMerge, 311, 313 ora_partition_pos, 225
ODCIAggregateTerminate, 311, 312 ora_privilege_list, 225
ograniczenia, 104, 118 ora_revoke, 225
CHECK, 105, 109 ora_server_error, 225
CONSTRAINT, 104 ora_server_error_depth, 223, 225
DEFAULT, 105, 108 ora_server_error_msg, 223, 225
FOREIGN KEY, 106 ora_server_error_num_params, 225
NOT NULL, 105, 108 ora_server_error_param, 225
UNIQUE, 105 ora_sql_txt, 223, 225
okno fizyczne, 60 ora_sysevent, 225
okno logiczne, 60 ora_with_grant_option, 225
ON, 95, 96 Oracle 11g, 9
ON COMMIT, 116 Oracle Database 11g, 13
Oracle Enterprise Manager, 20
ON COMMIT DELETE, 116
drzewo obiektów, 21
ON DATABASE, 215
logowanie, 20
ON DELETE CASCADE, 107
rejestracja serwera, 20
OPEN, 229
SQL Worksheet, 21
Oracle Home, 13
324 Skorowidz

Oracle SQL Developer, 16 INDEX_STATS, 135


elementy schematu użytkownika, 18 modyfikacja, 122
logowanie do serwera, 17 procedury wyzwalane, 212
plan wykonania zapytania wybierającego, 19 słownik, 124
połączenie z serwerem, 16 systemowe perspektywy, 124
Results, 18 tworzenie, 121
Script Output, 19 USER_INDEXES, 132
śledzenie zapytania wybierającego, 20 USER_OBJECTS, 123
wykonywanie zapytań, 18 USER_PROCEDURES, 125
wyniki zapytań, 18 usuwanie, 121
ORDER, 27 V$CONTROLFILE, 129
ORDER BY, 29, 30, 72, 100, 130 V$INSTANCE, 128
organizacja serwera Oracle, 10 V$SESSION, 226
OTHERS, 172, 176, 177, 188, 190 V$SESSION_CONNECT_INFO, 128
otwieranie kursora, 229 V$SQL, 227
OUT, 166, 168, 182, 278 V$SQLAREA, 227
OVER, 58, 69, 87 V$VERSION, 128
WITH READ ONLY, 122
zapytania wstawiające wiersze, 123
P pętle, 160
pakiety, 187 CONTINUE, 161
ciało, 187 FOR, 160
forward, 194 LOOP, 160
kursory, 241 przejście do kolejnego przebiegu, 161
tworzenie, 187 WHILE, 160
usuwanie, 187 PL/SQL, 9, 155
wywołanie obiektu, 188 blok anonimowy, 156
zapowiedź istnienia definicji funkcji funkcje, 179
(procedury), 194 instrukcja warunkowa, 157
PARALLEL_ENABLE, 313 kursory, 229
parametry, 165, 181 pakiety, 187
IN, 166 pętle, 160
IN OUT, 182 procedury anonimowe, 156
OUT, 166, 182 przeciążanie, 189
PARTITION BY, 58, 61, 63, 68 składnia, 155
partycje, 58, 61 skok bezwarunkowy, 162
partycjonowanie, 59 stałe, 156
PASING BY, 144 symbole specjalne, 155
PASSWORD, 291 transakcje, 247
PASSWORD_VERIFY_FUNCTION, 296 zmienne, 155
PCTFREE, 115 plany wykonania, 163
PCTINCREASE, 115 zapytania wybierające, 19
PCTUSED, 115 pliki, 12, 285
PERCENT_RANK, 72, 73, 74, 84 ORA, 12
PERCENTILE_CONT, 75, 76, 77 PLS_INTEGER, 263
PERCENTILE_DISC, 75, 76, 77, 78 podział na partycje, 58, 61
percentyle, 75 pola XMLTYPE, 142
perspektywy, 15, 121 pola zmiennych rekordowych, 199
DBA_DATA_FILES, 125 połączenie z bazą danych, 16, 276
DBA_FREE_SPACE, 129 POWER, 84
DBA_OBJECTS, 124 PRAGMA AUTONOMOUS
DBA_SEGMENTS, 129 _TRANSACTION, 250
DBA_USERS, 130 PRAGMA EXCEPTION_INIT, 176
dynamiczne perspektywy wydajności, 127 PRAGMA SERIALLY_REUSABLE, 245
Skorowidz 325

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

sekwencja, 119, 204, 307 SQL Worksheet, 15, 21


modyfikacja, 120 SQL*Plus, 22
tworzenie, 119 sql_text, 223
usuwanie, 120 SQLCODE, 175, 184
SELECT, 27, 28, 100, 163 SQLERRM, 175
FROM, 29 SQLException, 278
GROUP BY, 37 SQRT, 84
HAVING, 38, 39 stałe, 156
LIKE, 33 standaryzowana R2, 84
ORDER BY, 29, 30 START WITH, 94, 95, 119, 120, 239
ROWNUM, 35 statystyka t dla współczynnika kierunkowego, 84
WHERE, 30 statystyka t dla wyrazu wolnego, 84
WITH, 58 STDDEV, 37
SELECT ANY TABLE, 127 STDDEV_POP, 66, 67
SELECT LEVEL, 95 STDDEV_SAMP, 66, 67
selekcja, 27 STORAGE_ERROR, 173
SELF, 303 STRING, 195
SERIALLY_REUSABLE, 245 SUBSTR, 161
SERVERERROR, 222 SUM, 37, 58
serwer, 10 suma kwadratów, 84
SET, 100 symbole specjalne PL/SQL, 155
SET AUTOCOMMIT IMMEDIATE, 251 synonimy, 252
SET AUTOCOMMIT ON, 251 SYS, 12, 120
SET CONSTRAINT, 117 SYS_CONTEXT, 216, 217, 220, 223
SET CONSTRAINT ALL IMMEDIATE, 118 atrybuty, 217
SET ECHO, 93 SYS_XMLAGG, 138
SET FEEDBACK, 93 SYS_XMLGEN, 137
SET HEADING, 93 SYSDATE, 156, 184, 211, 216, 223
SET LONG, 142 SYSDBA, 127
SET NULL, 108 SYSTEM, 12
SET PAGE, 88 System Global Area, 163
SET PAGESIZE, 93
SET SERVEROUTPUT ON, 157, 180, 199
SET TRIMSPOOL, 93 Ś
SET UNUSED, 112 śledzenie
SET UNUSED COLUMN, 113 aktywność użytkowników, 226
SGA, 163 zapytania wybierające, 20
SHOW ERRORS, 177, 210 średnia geometryczna, 311
SHOW ERRORS TRIGGER, 210
SHUTDOWN, 251
SHUTDOWN ABORT, 220, 222 T
SHUTDOWN IMMEDIATE, 220
tabele, 103
SIZE, 289
dodawanie kolumn, 110
SKIP N, 88
dodawanie ograniczeń, 113
skok bezwarunkowy, 162
klucz obcy, 106
słownik, 124
SOME, 31 klucz podstawowy, 104
sortowanie rekordów, 29 modyfikacja, 110
SPOOL, 92, 93, 138 ograniczenia, 104
SPOOL OFF, 92 tworzenie, 103
sposoby tworzenia pliku, 93 usuwanie, 104, 110
sprawdzenie acykliczności grafu, 94 usuwanie kolumn, 111
SQL, 9 więzy integralności, 103
SQL INJECTION, 268 tabele tymczasowe, 116, 260
Skorowidz 327

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

USER_PROCEDURES, 125, 126 tabele, 104, 110


USER_ROLE_PRIVS, 126 typ obiektowy, 301
USER_RULES, 126 użytkownicy, 291
USER_SEGMENTS, 126 uwierzytelnianie, 295
USER_SEQUENCES, 126 użytkownicy, 13, 14, 290
USER_SOURCE, 126
USER_STORED_SETTINGS, 126
USER_SUMMARIES, 126
V
USER_SYNONYMS, 127 V$CONTROLFILE, 129
USER_SYS_PRIVS, 127 V$FIXED_TABLE, 127
USER_TAB_COL_STATISTICS, 127 V$INSTANCE, 128
USER_TAB_COLS, 127 V$SESSION, 226
USER_TAB_COLUMNS, 127 V$SESSION_CONNECT_INFO, 128
USER_TAB_COMMENTS, 127 V$SQL, 227
USER_TAB_HISTOGRAMS, 127 V$SQLAREA, 227
USER_TAB_PARTITIONS, 127 V$VERSION, 128
USER_TAB_PRIVS, 127 VALIDATE STRUCTURE, 135
USER_TAB_PRIVS_MADE, 127 VALUE, 195
USER_TAB_PRIVS_RECD, 127 VALUES, 99, 100, 209
USER_TAB_SUBPARTITIONS, 127 VAR_POP, 66, 67
USER_TABLES, 127 VAR_SAMP, 66, 67
USER_TABLESPACES, 127 VARCHAR2, 11
USER_TRIGGER_COLS, 127 VARIANCE, 37
USER_TRIGGERS, 127 VARRAY, 263
USER_TS_QUOTAS, 127
USER_TYPE_ATTRS, 127
USER_TYPE_METHODS, 127 W
USER_TYPE_VERSIONS, 127 waitFor(), 287, 288
USER_TYPES, 127 walidacja więzów, 117
USER_UNUSED_COL_TABS, 127 wariancja populacji, 67
USER_UPDATABLE_COLUMNS, 127 wariancja próbki, 67
USER_USERS, 127 wartości NULL, 105
USER_USTATS, 127 wartość domyślna, 105
USER_VARRAYS, 127 WHERE, 27, 30, 40, 46, 100, 122, 137
USER_VIEWS, 127 WHILE, 160, 230, 231
USER_WORKSPACES, 127 widoki, 121
USERENV, 211, 216 WIDTH_BUCKET, 79, 80, 81
Users, 13 wielkość liter, 27
USING, 45, 50, 95, 258 wielkość pliku fizycznego, 289
USING OUT, 259, 260 wielopoziomowe zapytania wybierające, 96
usuwanie więzy dynamiczne niejawne, 103
dane, 101 więzy integralności, 103
funkcje, 179 więzy odroczone, 118
indeksy, 132 więzy statyczne jawne, 103
klasy Javy, 270 WITH, 58
kolumny z tabeli, 111 WITH CHECK OPTION, 122
komentarze, 298 WITH GRANT OPTION, 292
pakiety, 187 WITH READ ONLY, 122
perspektywy, 121 współczynnik determinacji, 83
procedury składowane, 163 współczynnik dopasowania, 83
przestrzeń tabel, 290 współczynnik korelacji Kendalla, 87
role, 294 współczynnik korelacji pola, 85
sekwencja, 120 współczynnik korelacji Spearmana, 86
synonimy, 252
Skorowidz 329

współczynnik tau Kendalla, 87


współczynniki aproksymacji liniowej, 82
Z
współczynniki regresji liniowej, 82 zagnieżdżone instrukcje warunkowe, 158
wstawianie danych, 99 zakleszczenia, 246
dane XML, 148 zależności między parametrami aproksymacji
wybieranie, 27 (regresji) liniowej, 84
pola typu XMLTYPE, 142 zapisywanie skutku wykonania zapytania
wycofanie transakcji, 247 wybierającego, 92
wyjątki, 171, 173 zapytania, 16, 18, 27
deklaracja, 175 dla struktur XML, 137
NO_DATA_FOUND, 172 do wielu tabel, 40
wykonywanie modyfikujące dane, 99
polecenia systemu operacyjnego, 287 modyfikujące tabelę, 110
skrypty SQL, 18 tworzące tabele, 103
zapytania SQL, 18, 22, 25 wybierające, 27
wylogowanie, 221 z parametrem, 91
wyłączanie klucza podstawowego, 118 zarządzanie
wypełnianie pustych miejsc zdefiniowanymi ekstenty, 289
wyrażeniami, 55 uprawnienia, 289
wyprowadzanie tekstu, 157 zatrzymanie serwera, 220
wyszukiwanie drogi w grafie, 266 zatwierdzanie transakcji, 247
wywołanie zdarzenia, 219, 221
funkcje, 180 funkcje zdarzeń, 223
metody klasy Javy, 270 ZERO_DIVIDE, 173
nazewnicze wywołanie, 234 zliczanie rekordów, 259
obiekt pakietu, 188 złączenia, 40
procedury, 170 CROSS JOIN, 43, 48
wyzwalacze, 103, 212 FULL JOIN, 41
INSTEAD OF, 212 INNER JOIN, 40, 41
wzajemna blokada dostępu do rekordu, 246 JOIN, 40, 41, 42
kierunek realizacji połączenia, 41
LEFT JOIN, 41, 45
X, Y NATURAL JOIN, 42, 45
XML, 137 RIGHT JOIN, 41
XMLAGG, 140, 141 USING, 45
XMLATTRIBUTES, 138, 139 złożony klucz główny, 104
XMLCOLATTVAL, 140 zmiana
XMLCONCAT, 140 hasła, 291
XMLDIFF, 152 nazwa obiektu, 114, 294
XMLELEMENT, 138, 140 zmienne, 155
XMLForest, 139, 140 nazwy, 155
XMLQuery, 144 obiektowe, 302
operatory, 146 rekordowe, 199, 202
XMLType, 146 wartość, 156
XMLTYPE, 141, 142, 143 znacznik wysokiej wody, 102, 114
XOR, 28
YEARS FOLLOWING, 60

You might also like