SQL Sztuka programowania(1)


SQL. Sztuka
programowania
Autorzy: St閜hane Faroult, Peter Robson
T艂umaczenie: Marek P臋tlicki
ISBN: 978-83-246-0895-9
Tytu艂 orygina艂u: The Art of SQL
Format: B5, stron: 472
Wypowiedz wojn臋 niewydajnym bazom danych
" Projektowanie wydajnych baz danych
" Uwzgl臋dnianie kontekstu dzia艂ania aplikacji bazodanowych
" Poprawa szybkoSci dzia艂ania xle zaprojektowanych system贸w
Twoje bazy danych dzia艂aj膮 zbyt wolno? Pora to zmieni膰! Wraz ze wzrostem wielkoSci
korporacyjnych baz danych czas dost臋pu do nich ma coraz wi臋ksze znaczenie. Napisanie
poprawnie dzia艂aj膮cego kodu w j臋zyku SQL nie jest trudne, jednak tworzenie wydajnych
aplikacji bazodanowych jest prawdziw膮 sztuk膮. Jak mo偶esz zg艂臋bi膰 jej tajniki i sta膰 si臋
lepszym programist膮? Zdaniem autora tej ksi膮偶ki nauka wydajnej pracy z bazami danych
przypomina poznawanie zasad prowadzenia wojny, dlatego wzorem klasycznej pozycji
 Sztuka wojny autorstwa Sun Tzu prowadzi Ci臋 on przez poszczeg贸lne etapy kampanii
przeciwko nieefektywnie zaprojektowanym i napisanym aplikacjom bazodanowym.
 SQL. Sztuka programowania to praktyczny podr臋cznik, dzi臋ki kt贸remu szybko
poszerzysz sw膮 wiedz臋 w zakresie efektywnego stosowania j臋zyka SQL. Nauczysz si臋
dba膰 o wydajnoS膰 aplikacji ju偶 na etapie ich projektowania, a tak偶e mySle膰 o pracy
z bazami danych w kategoriach proces贸w, wykraczaj膮c poza same zapytania j臋zyka
SQL. Dowiesz si臋, jak poprawnie u偶ywa膰 indeks贸w oraz jak monitorowa膰 szybkoS膰
dzia艂ania bazy. Poznasz standardowe scenariusze zwi臋kszania wydajnoSci, kt贸re
pozwol膮 Ci zastosowa膰 sprawdzone fortele we w艂asnych projektach oraz w bazach
zaprojektowanych przez innych programist贸w.
" Projektowanie pod k膮tem wydajnoSci
" Efektywne korzystanie z baz danych w programach
" Poprawne stosowanie indeks贸w
" Projektowanie optymalnych zapyta艅 SQL
" Praca z du偶ymi zbiorami danych
" Korzystanie ze struktur drzewiastych
Wydawnictwo Helion
" Monitorowanie wydajnoSci
ul. KoSciuszki 1c
" Obs艂uga wsp贸艂bie偶noSci
44-100 Gliwice
" Radzenie sobie z niewydajnymi projektami
tel. 032 230 98 63
e-mail: helion@helion.pl Poznaj praktyczne techniki poprawy wydajnoSci baz danych
S P I S T R E 艢 C I
Wst臋p 7
1. Plany strategiczne 15
Projektowanie baz danych pod k膮tem wydajno艣ci
2. Prowadzenie wojny 51
Wydajne wykorzystanie baz danych
3. Dzia艂ania taktyczne 87
Indeksowanie
4. Manewrowanie 113
Projektowanie zapyta艅 SQL
5. Ukszta艂towanie terenu 151
Zrozumienie implementacji fizycznej
6. Dziewi臋膰 zmiennych 179
Rozpoznawanie klasycznych wzorc贸w SQL
7. Odmiany taktyki 231
Obs艂uga danych strategicznych
8. Strategiczna si艂a wojskowa 273
Rozpoznawanie trudnych sytuacji i post臋powanie w nich
9. Walka na wielu frontach 307
Wykorzystanie wsp贸艂bie偶no艣ci
10. Gromadzenie si艂 337
Obs艂uga du偶ych ilo艣ci danych
11. Fortele 381
Jak uratowa膰 czasy reakcji
12. Zatrudnianie szpieg贸w 417
Monitorowanie wydajno艣ci
Ilustracje 451
O autorach 453
Skorowidz 455
ROZ DZ I A A DRUGI
Prowadzenie wojny
Wydajne wykorzystanie baz danych
Il existe un petit nombre de principes fondamentaux de la guerre,
dont on ne saurait s 閏arter sans danger, et dont l application au contraire
a 閠 presque en tous temps couronn閑 par le succ艁s.
Istnieje niewielka liczba fundamentalnych zagadnie艅 zwi膮zanych z prowadzeniem wojny,
kt贸rych nie wolno lekcewa偶y膰: zaprawd臋, stosowanie si臋 do nich prawie niezawodnie
prowadzi do sukcesu.
 Genera艂 Antoine-Henri de Jomini (1779  1869)
Zarys sztuki wojennej
52 ROZDZIAA DRUGI
a偶dy, kto by艂 zaanga偶owany w proces przej艣cia projektu z fazy rozwoju
Kw faz臋 produkcyjn膮, przyzna z pewno艣ci膮, 偶e prawie czu艂 tumult i wrzaw臋
bitwy. Bardzo cz臋sto zdarza si臋, 偶e na kilka tygodni przed s膮dnym dniem
przeprowadzone testy wydajno艣ci ujawniaj膮 smutny fakt: system nie
b臋dzie dzia艂a艂 tak p艂ynnie, jak to zak艂adano. Zaprasza si臋 ekspert贸w,
optymalizuje zapytania SQL, w kryzysowych burzach m贸zg贸w bior膮 udzia艂
administratorzy baz danych i system贸w. W ko艅cowym rozrachunku na
sprz臋cie dwukrotnie bardziej kosztownym osi膮ga si臋 wydajno艣膰 jedynie
teoretycznie zbli偶on膮 do zak艂adanej.
Cz臋sto w zast臋pstwie dzia艂a艅 strategicznych stosuje si臋 dzia艂ania taktyczne.
Strategia wymaga zastosowania architektury i modelu przystosowanych do
wymaga艅 projektu. Podstawowych zasad stosowanych w czasie wojny jest
zaledwie kilka, ale zadziwiaj膮co cz臋sto bywaj膮 one ignorowane. B艂臋dy
w architekturze okazuj膮 si臋 niezwykle kosztowne, a programista SQL-a
musi wkracza膰 na pole bitwy dobrze przygotowany do walki, musi wiedzie膰,
gdzie chce dotrze膰 i kt贸r膮 drog膮. W tym rozdziale przeanalizujemy
podstawowe cele zwi臋kszaj膮ce szanse na sukces w pisaniu program贸w
w spos贸b efektywny wykorzystuj膮cych bazy danych.
Identyfikacja zapyta艅
Przez stulecia jedynym sposobem, w jaki genera艂 m贸g艂 艣ledzi膰 losy bitwy,
by艂a obserwacja oddzia艂贸w na podstawie kolor贸w umundurowania
i niesionych przez nich proporc贸w. Gdy jaki艣 proces w 艣rodowisku bazy
danych zu偶ywa nadmiern膮 ilo艣膰 mocy procesora, cz臋sto istnieje mo偶liwo艣膰
zidentyfikowania zapytania SQL odpowiedzialnego za to zadanie.
Nierzadko jednak trudne bywa odkrycie tego, kt贸ra cz臋艣膰 aplikacji
wywo艂a艂a problematyczne zapytanie, szczeg贸lnie w 艣rodowisku, w kt贸rym
wykorzystywane s膮 zapytania budowane w spos贸b dynamiczny. Mimo tego
偶e wiele solidnych produkt贸w wyposa偶onych jest w mechanizmy
monitoruj膮ce, cz臋sto zdumiewaj膮co trudno jest znalez膰 odniesienie mi臋dzy
zapytaniem SQL a 艣rodowiskiem, w kt贸rym ono dzia艂a. Z tego powodu
dobrze jest nabra膰 nawyku oznaczania program贸w i krytycznych modu艂贸w
przez w艂膮czanie komentarzy w kodzie SQL w taki spos贸b, aby 艂atwo by艂o
zidentyfikowa膰 zr贸d艂o k艂opot贸w. Na przyk艂ad:
/* REJESTRACJA KLIENTA */ select ...
PROWADZENIE WOJNY 53
Tego typu komentarze identyfikuj膮ce mog膮 by膰 pomocne w 艣ledzeniu
b艂臋dnie dzia艂aj膮cego kodu. Mog膮 by膰 r贸wnie偶 pomocne przy okre艣laniu
obci膮偶enia serwera bazy danych powodowanego przez ka偶d膮 korzystaj膮c膮
z niego aplikacj臋, szczeg贸lnie w przypadku, gdy spodziewamy si臋, 偶e
wprowadzane zmiany mog膮 spowodowa膰 zwi臋kszenie obci膮偶enia, i musimy
oszacowa膰, czy posiadany sprz臋t ma szans臋 sprosta膰 wi臋kszym wymaganiom.
Niekt贸re produkty posiadaj膮 specjalizowane mechanizmy rejestruj膮ce,
kt贸re pozwalaj膮 unikn膮膰 komentowania ka偶dego wyra偶enia. Na przyk艂ad
pakiet dbms_application_info serwera Oracle pozwala oznakowa膰 program
za pomoc膮 48-znakowej nazwy modu艂u, 32-znakowej nazwy akcji
i 64-znakowego pola informacji o kliencie. Zawarto艣膰 tych p贸l jest
kontrolowana przez tw贸rc臋 aplikacji. W 艣rodowisku Oracle mo偶na u偶y膰
tego pakietu do 艣ledzenia tego, jaka aplikacja wykorzystuje baz臋 danych
w danej chwili, jak r贸wnie偶 tego, co dana aplikacja robi. Do tego s艂u偶膮
dynamiczne perspektywy V$, za pomoc膮 kt贸rych mo偶na 艣ledzi膰 stan
pami臋ci.
Identyfikowanie wyra偶e艅 u艂atwia 艣ledzenie zale偶no艣ci obci膮偶enia.
Trwa艂e po艂膮czenia do bazy danych
Nowe po艂膮czenie do bazy danych tworzy si臋 szybko i 艂atwo, lecz ta
prostota czasem przes艂ania fakt, 偶e szybkie, cykliczne wywo艂ania po艂膮cze艅
wi膮偶膮 si臋 z konkretnym, niema艂ym kosztem. Dlatego po艂膮czenia z baz膮
danych warto traktowa膰 z rozwag膮. Konsekwencje wywo艂ywania wielu
cyklicznych po艂膮cze艅, by膰 mo偶e ukrytych w ramach aplikacji, mog膮 by膰
znacz膮ce, co zademonstruje kolejny przyk艂ad.
Jaki艣 czas temu trafi艂em na aplikacj臋, kt贸ra przetwarza艂a du偶膮 liczb臋
niewielkich plik贸w tekstowych o rozmiarach do stu wierszy. Ka偶dy wiersz
zawiera艂 dane oraz identyfikator bazy danych, do kt贸rej dane te mia艂y by膰
za艂adowane. W tym przypadku by艂 wykorzystywany tylko jeden serwer
bazy danych, ale prezentowana zasada obowi膮zuje r贸wnie偶 w przypadku
setek baz danych.
54 ROZDZIAA DRUGI
Przetwarzanie ka偶dego z plik贸w odbywa艂o si臋 zgodnie z nast臋puj膮c膮
procedur膮:
Otwarcie pliku
A偶 do napotkania ko艅ca pliku
Odczytaj wiersz
Po艂膮cz si臋 z serwerem zdefiniowanym w tre艣ci
Wpisz dane
Zamknij po艂膮czenie
Zamknij plik
Opisany proces dzia艂a艂 zadowalaj膮co z wyj膮tkiem przypadk贸w, gdy do
za艂adowania trafia艂a du偶a liczba niewielkich plik贸w w odst臋pach czasu
przekraczaj膮cych mo偶liwo艣膰 ich przetworzenia przez aplikacj臋.
To powodowa艂o znaczny przyrost dziennika zaleg艂o艣ci w bazie danych
(ang. backlog), kt贸ry nast臋pnie musia艂 by膰 przetworzony przez baz臋
danych, co zajmowa艂o dodatkowy czas.
U偶ytkownikowi bazy danych wyja艣ni艂em przyczyn臋 op贸znie艅, kt贸r膮 by艂a
nadmierna liczba otwieranych i zamykanych po艂膮cze艅. Jako demonstracj臋
problemu przygotowa艂em prosty program (napisany w C) emuluj膮cy
aplikacj臋 oraz proponowane przeze mnie rozwi膮zania. Wyniki dzia艂ania
demonstracji przedstawia tabela 2.1.
UWAGA
Program generuj膮cy wyniki z tabeli 2.1 wykorzystywa艂 proste instrukcje
wstawiaj膮ce wiersze do tabel. Klientowi wspomnia艂em r贸wnie偶 o technikach
bezpo艣redniego 艂adowania danych do tabel, kt贸re dzia艂aj膮 jeszcze szybciej.
TABELA 2.1. Wyniki testu nawi膮zywania i ko艅czenia po艂膮cze艅
Test Wynik
Nawi膮zanie i zako艅czenie po艂膮czenia dla ka偶dego wstawianego 7,4 wiersza na sekund臋
wiersza z pliku
Jedno po艂膮czenie, ka偶dy wiersz wstawiany indywidualnie 1681 wierszy na sekund臋
Jedno po艂膮czenie, wiersze wstawiane w porcjach po 10 5914 wierszy na sekund臋
Jedno po艂膮czenie, wiersze wstawiane w porcjach po 100 9190 wierszy na sekund臋
Ta demonstracja pokaza艂a, jak istotne jest, aby minimalizowa膰 liczb臋
osobnych po艂膮cze艅 z baz膮 danych. Z tego powodu kluczow膮 rol臋 odgrywa艂o
tu proste sprawdzenie, czy kolejne wprowadzenie danych ma odby膰 si臋 do
tej samej bazy, do kt贸rej po艂膮czenie ju偶 zosta艂o otwarte. Analiz臋 mo偶na by
PROWADZENIE WOJNY 55
poprowadzi膰 dalej, poniewa偶 liczba mo偶liwych baz danych by艂a oczywi艣cie
ograniczona. Teoretycznie mo偶na by艂o osi膮gn膮膰 dalszy zysk wydajno艣ci,
wykorzystuj膮c tablic臋 uchwyt贸w po艂膮cze艅, po jednym dla ka偶dej z mo偶liwych
baz danych, i nawi膮zuj膮c po艂膮czenie dopiero wtedy, gdy b臋dzie to konieczne.
W ten spos贸b wykorzystywanych by艂oby maksymalnie tyle uchwyt贸w, ile
istnieje baz danych. Jak wida膰 w tabeli 2.1, prosta technika pojedynczego
po艂膮czenia (lub minimalizacji liczby nawi膮zywanych po艂膮cze艅) usprawnia
wydajno艣膰 do dwustu razy. I to wszystko dzi臋ki niewielkiej modyfikacji kodu.
Oczywi艣cie przy tej okazji mog艂em zademonstrowa膰 r贸wnie偶 znacz膮ce
korzy艣ci wynikaj膮ce z ograniczenia liczby wywo艂a艅 mi臋dzy aplikacj膮
a baz膮 danych, wykorzystuj膮c 艂adowanie danych za pomoc膮 tablic.
Przez wstawianie wielu wierszy w pojedynczej operacji ca艂e zadanie
mo偶na przyspieszy膰 jeszcze pi臋ciokrotnie. Wyniki z tabeli 2.1 pokazuj膮
przyspieszenie o tysi膮c dwie艣cie razy w stosunku do pierwotnej
wydajno艣ci operacji.
Sk膮d bierze si臋 tak znaczne przyspieszenie?
Przyczyn膮 pierwszego przyspieszenia jest fakt, 偶e nawi膮zanie po艂膮czenia
z baz膮 danych jest operacj膮  ci臋偶k膮 , to znaczy wykorzystuj膮c膮 du偶o
zasob贸w systemowych.
We wci膮偶 popularnym 艣rodowisku klient-serwer nawi膮zanie po艂膮czenia
to prosta operacja, co powoduje, 偶e niewiele os贸b ma 艣wiadomo艣膰
kryj膮cego si臋 za ni膮 procesu. W pierwszym etapie klient musi nawi膮za膰
po艂膮czenie z modu艂em nas艂uchuj膮cym, wchodz膮cym w sk艂ad serwera,
po czym modu艂 nas艂uchuj膮cy uruchamia osobny proces lub w膮tek
serwera bazy danych albo przekazuje (bezpo艣rednio lub po艣rednio)
偶膮danie do istniej膮cego, oczekuj膮cego procesu serwera.
Niezale偶nie od liczby operacji (wywo艂ywania nowych proces贸w lub
w膮tk贸w i wywo艂ywania obs艂ugi zapytania) system bazy danych musi
utworzy膰 nowe 艣rodowisko dla ka偶dej sesji, dzi臋ki czemu b臋dzie ona
mog艂a 艣ledzi膰 realizowane w niej zadania. System obs艂ugi bazy danych
musi sprawdzi膰 poprawno艣膰 has艂a dla konta, za pomoc膮 kt贸rego zosta艂o
nawi膮zane po艂膮czenie i utworzona nowa sesja. System obs艂ugi bazy
danych cz臋sto musi r贸wnie偶 wykona膰 kod zwi膮zany z procedur膮
zalogowania do bazy danych (zaimplementowany w postaci wyzwalacza).
To samo dotyczy r贸wnie偶 kodu inicjalizacyjnego niezb臋dnego do
dzia艂ania procedur osadzonych i pakiet贸w. Na tym tle standardowy
56 ROZDZIAA DRUGI
protok贸艂 nawi膮zania po艂膮czenia mi臋dzy klientem a serwerem ma
niewielki wp艂yw na powsta艂e op贸znienia. Z powodu tej  zasobo偶erno艣ci
procesu nawi膮zania po艂膮czenia tak wielkie znaczenie z punktu widzenia
wydajno艣ci maj膮 rozmaite techniki pozwalaj膮ce na utrzymanie raz
nawi膮zanego po艂膮czenia z baz膮 danych, jak pule po艂膮cze艅 (ang.
connection pooling).
Druga przyczyna przyspieszenia wi膮偶e si臋 ze zmniejszeniem liczby cykli
przesy艂ania danych mi臋dzy aplikacj膮 a baz膮 danych (tzw. round-trips),
co, jak wida膰, r贸wnie偶 ma niebanalny udzia艂 w czasoch艂onno艣ci operacji.
Nawet w przypadku, gdy zosta艂o nawi膮zane tylko jedno po艂膮czenie
i jest ono wykorzystywane do realizacji wszystkich kolejnych operacji,
prze艂膮czanie kontekstu mi臋dzy programem a j膮drem systemu obs艂ugi
bazy danych r贸wnie偶 ma wp艂yw na dalsze op贸znienia. Je艣li zatem
system obs艂ugi bazy danych umo偶liwia wykorzystanie jakiego艣
mechanizmu 艂adowania danych wi臋kszymi porcjami (jak na przyk艂ad
tablice), warto wzi膮膰 pod uwag臋 jego u偶ycie. W rozwi膮zaniach, kt贸re
wykorzystuj膮 rzeczywiste tablice, warto sprawdzi膰, jaki jest domy艣lny
rozmiar tablicy, i dostosowa膰 go do potrzeb aplikacji. Ka偶dy rodzaj
operacji wykorzystuj膮cych przetwarzanie pojedynczymi wierszami
wi膮偶e si臋 oczywi艣cie z analogicznymi konsekwencjami, na co b臋dziemy
mieli sporo dowod贸w w tym rozdziale.
Po艂膮czenia z bazami danych i prze艂膮czenia kontekstu s膮 jak Chi艅skie
Mury  im ich wi臋cej, tym d艂u偶ej trwa przekazanie wiadomo艣ci.
Strategia przed taktyk膮
To strategia definiuje taktyk臋, nie odwrotnie. Dobry programista nie
postrzega procesu w kategoriach drobnych kroczk贸w, lecz z perspektywy
ostatecznego wyniku. Najefektywniejszy spos贸b uzyskania wyniku nie musi
wynika膰 z proces贸w biznesowych, cz臋sto sprawdza si臋 mniej bezpo艣rednie
podej艣cie. Nast臋pny przyk艂ad poka偶e, w jaki spos贸b nadmierne skupienie
si臋 na procesach proceduralnych mo偶e odwr贸ci膰 uwag臋 od najbardziej
efektywnych rozwi膮za艅.
PROWADZENIE WOJNY 57
Kilka lat temu otrzyma艂em zapytanie z pro艣b膮, abym spr贸bowa艂 je
zoptymalizowa膰.  Spr贸bowa艂 to by艂o s艂owo kluczowe tego kontraktu.
Wcze艣niej odby艂y si臋 dwa podej艣cia do tej optymalizacji, pierwsze
podejmowane przez autor贸w, drugie przez eksperta Oracle. Mimo tych
pr贸b ka偶de wywo艂anie tego zapytania trwa艂o oko艂o dwudziestu minut,
co zdaniem autor贸w by艂o nie do przyj臋cia.
Celem tej procedury by艂o wyliczanie ilo艣ci materia艂贸w zamawianych przez
centraln膮 jednostk臋 fabryki. Obliczenia wykorzystywa艂y istniej膮ce zapasy
i zam贸wienia pochodz膮ce z r贸偶nych zr贸de艂. Dane by艂y odczytywane z kilku
identycznych tabel, a nast臋pnie agregowane w jednej tabeli zbiorczej.
Procedura sk艂ada艂a si臋 z sekwencji wyra偶e艅 o nast臋puj膮cej filozofii dzia艂ania:
najpierw dane ze wszystkich tabel by艂y kopiowane do tabeli zbiorczej,
nast臋pnie wywo艂ywane by艂o zapytanie agreguj膮ce dane dotycz膮ce
materia艂贸w, nast臋pnie z tabeli by艂y usuwane nadmiarowe dane niemaj膮ce
znaczenia dla procedury. Ta sekwencja by艂a powtarzana dla ka偶dej tabeli
zr贸d艂owej. 呕adne z zapyta艅 SQL nie by艂o szczeg贸lnie skomplikowane,
偶adnego z nich z osobna nie mo偶na te偶 by艂o okre艣li膰 jako szczeg贸lnie
nieefektywnego.
Wi臋ksz膮 cz臋艣膰 dnia zaj臋艂o mi zrozumienie procesu, co w ko艅cu zaowocowa艂o
postawieniem pytania:  Dlaczego procedur臋 wykonywano w kilku etapach? .
Wystarczy艂oby podzapytanie z operatorem unii (UNION), za pomoc膮 kt贸rego
mo偶na po艂膮czy膰 wszystkie wyniki z tabel zr贸d艂owych. Nast臋pnie
pojedyncza instrukcja SELECT pozwoli艂aby za jednym zamachem zape艂ni膰
tabel臋 wynikow膮. R贸偶nica w wydajno艣ci by艂a imponuj膮ca: z dwudziestu
minut czas wykonania zadania skr贸ci艂 si臋 do dwudziestu sekund.
Skuteczno艣膰 modyfikacji by艂a tak zdumiewaj膮ca, 偶e spor膮 cz臋艣膰 czasu
zaj臋艂a mi weryfikacja, czy aby nowa procedura generuje dok艂adnie takie
same wyniki jak poprzednia.
Nie by艂y tu potrzebne nadzwyczajne umiej臋tno艣ci, wystarczy艂a umiej臋tno艣膰
my艣lenia poza schematami. Poprzednie pr贸by optymalizacji procesu
zako艅czy艂y si臋 niepowodzeniem, poniewa偶 by艂y za bardzo zbli偶one do
sedna problemu. Cz臋sto warto zdoby膰 si臋 na 艣wie偶e spojrzenie, zrobi膰
krok wstecz, aby poszerzy膰 perspektyw臋. Warto by艂o zada膰 dwa pytania:
 Co mamy dost臋pnego przed wykonaniem procedury? oraz  Jakie wyniki
chcemy uzyska膰 z procedury? . W po艂膮czeniu ze 艣wie偶ym spojrzeniem
odpowiedzi na te pytania doprowadzi艂y do tak wielkiego udoskonalenia procesu.
58 ROZDZIAA DRUGI
Sp贸jrz na problem z szerszej perspektywy, zanim zag艂臋bisz si臋
w drobniejsze szczeg贸艂y rozwi膮zania.
Najpierw definicja problemu,
potem jego rozwi膮zanie
Brak wiedzy mo偶e by膰 niebezpieczny. Cz臋sto zdarza si臋, 偶e ludzie s艂yszeli
lub czytali o nowych czy nietypowych technikach, w niekt贸rych przypadkach
nawet do艣膰 ciekawych, kt贸re natychmiast chcieli wdro偶y膰 w ramach
rozwi膮zywania swoich problem贸w. Programi艣ci i architekci system贸w
nader cz臋sto zachwycaj膮 si臋 tego typu  rozwi膮zaniami , kt贸re w gruncie
rzeczy jedynie przyczyniaj膮 si臋 do powstawania nowych problem贸w.
Na szczycie listy  gotowych rozwi膮za艅 z regu艂y mo偶na znalez膰 denormalizacj臋.
Nie艣wiadomi praktycznych zagro偶e艅 zwi膮zanych z modyfikacj膮 nadmiarowych
danych zwolennicy denormalizacji cz臋sto sugeruj膮 j膮 jako pierwsze podej艣cie
przy  optymalizacji wydajno艣ci. Niestety, zdarza si臋 to cz臋sto na etapie
rozwoju aplikacji, gdy jeszcze nie jest za p贸zno na zmiany projektu (lub
przynajmniej nauk臋 konstruowania wydajnych z艂膮cze艅 tabel). Szczeg贸lnie
popularnym panaceum na problemy wydaje si臋 specjalna forma denormalizacji,
jak膮 jest zmaterializowana perspektywa. Zmaterializowane perspektywy
czasem okre艣la si臋 nazw膮 migawek (ang. snapshot). Jest to mniej
spektakularna nazwa, ale za to bardziej zbli偶ona do nagiej prawdy o tym
zjawisku: chodzi bowiem ni mniej, ni wi臋cej o kopie danych wykonania
w okre艣lonym punkcie czasu. Nie chc臋 tu sugerowa膰, 偶e nigdy nie zdarza si臋,
偶e od czasu do czasu, przyparty do muru, nie jestem zmuszony do zastosowania
kontrowersyjnych technik. Jak stwierdzi艂 Franz Kafka:  Logiki nie da si臋
podwa偶y膰, ale nie ma ona szans w obliczu cz艂owieka, kt贸ry po prostu
pr贸buje przetrwa膰 .
Jednak przyt艂aczaj膮c膮 wi臋kszo艣膰 problem贸w daje si臋 rozwi膮za膰 dzi臋ki
inteligentnemu zastosowaniu do艣膰 tradycyjnych technik. Warto nauczy膰
si臋 wyci膮ga膰 wszystko, co dobre, z takich prostych rozwi膮za艅. Gdy ju偶 si臋
je opanuje, cz艂owiek uczy si臋 docenia膰 ich ograniczenia. Dopiero potem
mo偶na pr贸bowa膰 os膮dzi膰 potencjalne zalety (o ile istniej膮) nowych rozwi膮za艅
technicznych.
PROWADZENIE WOJNY 59
Wszystkie technologiczne rozwi膮zania s膮 zaledwie 艣rodkiem do osi膮gni臋cia
celu. Wielkim zagro偶eniem dla niedo艣wiadczonego programisty jest poci膮g
do najnowszej technologii, kt贸ry szybko zamienia si臋 w cel dla samego
siebie. A zagro偶enie jest tym wi臋ksze w przypadku os贸b ciekawych,
obdarzonych du偶ym entuzjazmem, o technicznym zaci臋ciu.
Fundamenty s膮 wa偶niejsze od mody: naucz si臋 podstaw fachu, zanim
zaczniesz bawi膰 si臋 nowinkami techniki.
Stabilny schemat bazy danych
Wykorzystanie j臋zyka DDL (ang. data definition language) do tworzenia,
modyfikowania czy usuwania obiekt贸w bazy danych jest bardzo nagann膮
praktyk膮, kt贸ra powinna zosta膰 oficjalnie zakazana. Nie ma powodu, aby
dynamicznie tworzy膰, modyfikowa膰 i usuwa膰 obiekty bazy. Z wyj膮tkiem
partycji, o czym wspomn臋 w rozdziale 5., oraz tabel tymczasowych,
z zaznaczeniem, 偶e maj膮 by膰 zadeklarowane jako tymczasowe w systemie
zarz膮dzania bazami danych (istnieje jeszcze kilka wa偶nych wyj膮tk贸w
od tej regu艂y, o czym wspomn臋 w rozdziale 10.).
Zastosowania j臋zyka DDL wykorzystuj膮 podstawowy s艂ownik danych bazy.
Ten s艂ownik jest r贸wnie偶 centralnym obiektem wszystkich operacji w bazie
danych, a wykorzystanie go prowadzi do wywo艂ywania globalnych blokad,
kt贸re powoduj膮 znaczne konsekwencje w przypadku wydajno艣ci bazy.
Jedyn膮 dopuszczaln膮 operacj膮 DDL jest przycinanie tabeli (TRUNCATE),
kt贸re jest bardzo szybk膮 metod膮 usuwania danych z tabeli (ale nale偶y
pami臋ta膰, 偶e w przypadku tej operacji nie mamy mo偶liwo艣ci jej wycofania
za pomoc膮 instrukcji ROLLBACK!).
Tworzenie, modyfikacja i usuwanie obiekt贸w baz danych to zadania
etapu projektowego, a nie codzienne operacje wykonywane przez
aplikacj臋 klienck膮.
60 ROZDZIAA DRUGI
Operacje na rzeczywistych danych
Wielu programist贸w ch臋tnie pos艂uguje si臋 tymczasowymi tablicami
roboczymi, do kt贸rych na przyk艂ad 艂aduj膮 dane do dalszego przetwarzania.
Tego typu podej艣cie z regu艂y uznaje si臋 za niew艂a艣ciwe, poniewa偶 mo偶e
ono prowadzi膰 do zamkni臋cia percepcji w zakresie proces贸w biznesowych
i uniemo偶liwi膰 szersze spojrzenie. Nale偶y pami臋ta膰, 偶e tabele tymczasowe
nie daj膮 mo偶liwo艣ci zastosowania pewnych zaawansowanych technik
dost臋pnych w przypadku rzeczywistych tabel (cz臋艣膰 z tego typu opcji
om贸wi臋 w rozdziale 5.). Indeksowanie tabel tymczasowych (o ile w og贸le
jest dost臋pne) mo偶e na przyk艂ad by膰 mniej optymalne. W wyniku tych
ogranicze艅 zapytania wykorzystuj膮ce tabele tymczasowe mog膮 dzia艂a膰
mniej wydajnie od prawid艂owo napisanych zapyta艅 na rzeczywistych
tabelach. Wad膮 zastosowania tabel tymczasowych jest ponadto konieczno艣膰
wykonania dodatkowego zapytania wype艂niaj膮cego tabel臋 tymczasow膮
danymi.
Nawet w przypadku, gdy zastosowanie tabel tymczasowych jest uzasadnione,
nie nale偶y nigdy wykorzystywa膰 w tym charakterze rzeczywistych tabel
udaj膮cych tabele tymczasowe, szczeg贸lnie gdy liczba zapisywanych w nich
wierszy jest du偶a. Jeden z problem贸w le偶y tu w mechanizmie gromadzenia
statystyk: je艣li statystyki dotycz膮ce tabel nie s膮 gromadzone w czasie
rzeczywistym, to system zarz膮dzania baz膮 danych wykorzystuje do tego
chwile mniejszego obci膮偶enia. Natura tabel roboczych polega na tym, 偶e
z regu艂y w takich przestojach bywaj膮 puste, co powoduje, 偶e optymalizator
uzyskuje zupe艂nie b艂臋dne informacje. W efekcie system podejmuje b艂臋dne
decyzje w wyniku nieodpowiednich plan贸w wykonawczych przygotowywanych
przez optymalizator zapyta艅, co prowadzi bezpo艣rednio do obni偶onej
wydajno艣ci. Je艣li kto艣 jest naprawd臋 zmuszony do u偶ycia tabel
tymczasowych, powinien u偶ywa膰 do tego celu tabel, kt贸re system
zarz膮dzania bazami danych jest w stanie zidentyfikowa膰 jako tymczasowe.
Wykorzystanie tabel tymczasowych oznacza przepychanie danych do
mniej optymalnego zasobu.
PROWADZENIE WOJNY 61
Przetwarzanie zbior贸w w SQL-u
SQL przetwarza dane w postaci zbior贸w. W przypadku operacji
modyfikacji (UPDATE) lub usuwania danych (pod warunkiem, 偶e te
modyfikacje nie s膮 dokonywane na ca艂ych tabelach) u偶ytkownik musi
zdefiniowa膰 zbi贸r wierszy, kt贸rych dana modyfikacja dotyczy. W ten
spos贸b definiuje si臋 pewn膮 granularno艣膰 procesu, kt贸ry mo偶na okre艣li膰
jako zgrubn膮, je艣li zmiany dotycz膮 wi臋kszej liczby wierszy, lub drobn膮,
gdy przetwarzamy jedynie kilka wierszy.
Ka偶da pr贸ba modyfikacji du偶ej liczby wierszy tabeli, ale ma艂ymi porcjami
jest z regu艂y z艂ym pomys艂em i zwykle okazuje si臋 bardzo niewydajna.
Takie podej艣cie jest dopuszczalne jedynie w przypadku, gdy na bazie danych
b臋d膮 dokonywane bardzo rozleg艂e zmiany, kt贸re na czas transakcji mog膮
spowodowa膰 tymczasowe zaj臋cie bardzo du偶ej ilo艣ci zasob贸w oraz zajmuj膮
bardzo du偶o czasu w przypadku wycofania transakcji (ROLLBACK). Istniej膮
r贸wnie偶 opinie, 偶e w przypadku modyfikacji du偶ych porcji danych nale偶y
w ramach kodu DML (ang. data manipulation code) umieszcza膰 co jaki艣
czas instrukcje zatwierdzaj膮ce zmiany (COMMIT). Tego typu podej艣cie mo偶e
jednak nie sprawdzi膰 si臋 w przypadku, gdy dane s膮 艂adowane z pliku
i procedura zostanie przerwana w trakcie. Z czysto praktycznego punktu
widzenia cz臋sto o wiele pro艣ciej i szybciej jest wznowi膰 proces od pocz膮tku,
ni偶 pr贸bowa膰 zlokalizowa膰 miejsce w danych wej艣ciowych, do kt贸rego
zosta艂y ju偶 za艂adowane, i od niego wznawia膰 proces.
Natomiast bior膮c pod uwag臋 rozmiar loga transakcji (ang. transaction log)
wykorzystywanego do wycofania zmian transakcji, r贸wnie偶 istniej膮 opinie,
偶e fizyczna implementacja bazy danych powinna by膰 przygotowana
do przyjmowania zmian wykonywanych przez aplikacj臋, a aplikacja nie
powinna by膰 zmuszona do omijania ogranicze艅 fizycznej implementacji.
Je艣li wymagana ilo艣膰 zasob贸w niezb臋dnych do zapisania loga transakcji jest
rzeczywi艣cie bardzo du偶a, zapewne nale偶y zastanowi膰 si臋, czy cz臋stotliwo艣膰
dokonywania tego typu zmian jest odpowiednia do konstrukcji bazy.
Mo偶e si臋 bowiem okaza膰, 偶e zmiana strategii z miesi臋cznych, gigantycznych
aktualizacji danych na kilkakrotnie mniejsze, tygodniowe, albo zupe艂nie
niewielkie, dzienne modyfikacje spowoduje, 偶e problem zupe艂nie
przestanie istnie膰.
62 ROZDZIAA DRUGI
Pracowite zapytania SQL
SQL nie jest j臋zykiem proceduralnym. Cho膰 w zapytaniach SQL istnieje
mo偶liwo艣膰 zastosowania logiki j臋zyk贸w proceduralnych, nale偶y traktowa膰
je z rozwag膮. Problemy z rozr贸偶nieniem logiki proceduralnej od
przetwarzania deklaratywnego najlepiej wida膰 w przypadkach, gdy kto艣
pr贸buje wydoby膰 dane z bazy, dokona膰 na nich modyfikacji, po czym
ponownie zapisa膰 w bazie. Gdy program lub procedura dzia艂aj膮ca
w ramach programu otrzyma dane wej艣ciowe, cz臋sto zdarza si臋, 偶e s膮 one
wykorzystywane do odczytu innych danych z bazy, po czym nast臋puje
p臋tla lub inna forma logiki funkcyjnej (z regu艂y p臋tla if...then...else)
zastosowana do wydobywania kolejnych danych z bazy. W wi臋kszo艣ci
przypadk贸w jest to efekt g艂臋boko zakorzenionych nawyk贸w lub s艂abej
znajomo艣ci SQL-a w po艂膮czeniu z niewolniczym oddaniem w stosunku do
specyfikacji funkcjonalnej. Wiele stosunkowo skomplikowanych operacji
mo偶na wykona膰 za pomoc膮 pojedynczego zapytania w SQL-u. Je艣li
u偶ytkownik poda warto艣膰, cz臋sto mo偶na poda膰 interesuj膮cy go wynik bez
konieczno艣ci rozbijania logiki na poszczeg贸lne instrukcje o niewielkim
zwi膮zku z wynikiem ko艅cowym.
Istniej膮 dwa g艂贸wne powody, aby unika膰 stosowania logiki proceduralnej
w SQL-u:
Ka偶dy dost臋p do bazy danych wi膮偶e si臋 z operacjami na wielu r贸偶nych
warstwach programowych, w艂膮czaj膮c w to operacje sieciowe.
Nawet w przypadku, gdy sie膰 nie jest wykorzystywana, wchodz膮 w gr臋
operacje wymiany danych mi臋dzy procesami. Wi臋cej operacji dost臋pu
oznacza wi臋cej wywo艂a艅 funkcji, wi臋cej przepustowo艣ci, a co si臋 z tym
wi膮偶e, d艂u偶sze oczekiwanie na odpowiedz. Gdy tego typu wywo艂ania s膮
cz臋sto powtarzane, op贸znienia staj膮 si臋 wyraznie zauwa偶alne.
 Proceduralny znaczy, 偶e wydajno艣膰 i utrzymanie zale偶膮 od programu,
nie od bazy danych.
Wi臋kszo艣膰 system贸w baz danych do wykonywania operacji, jak na przyk艂ad
z艂膮czenia, wykorzystuje zaawansowane algorytmy przekszta艂caj膮ce
zapytania w ich inne formy tak, aby wykonywa艂y si臋 w wydajniejszy
spos贸b. Optymalizatory oparte na koszcie (ang. cost-based optimizers,
CBO) to skomplikowane modu艂y, kt贸re pokona艂y d艂ug膮 drog臋. Na
pocz膮tku swojego istnienia mechanizmy tego typu by艂y praktycznie
PROWADZENIE WOJNY 63
bezu偶yteczne, ale z czasem nabra艂y dojrza艂o艣ci i obecnie daj膮 doskona艂e
wyniki. Dobry mechanizm CBO bywa bardzo skuteczny w znajdowaniu
najbardziej odpowiedniego planu wykonania. Jednak偶e CBO analizuje
ka偶de zapytanie SQL, nic ponad to. Wrzucaj膮c jak najwi臋ksz膮 liczb臋
operacji w pojedyncze wyra偶enie, przerzucamy na baz臋 danych
odpowiedzialno艣膰 za znalezienie najbardziej optymalnego wykonania.
Dzi臋ki temu program mo偶e wykorzysta膰 r贸wnie偶 przysz艂e usprawnienia
w dzia艂aniu silnika systemu zarz膮dzania baz膮 danych. W ten spos贸b
r贸wnie偶 przysz艂e usprawnienia aplikacji s膮 cz臋艣ciowo przerzucane
na dostawc臋 bazy danych.
Jak to zwykle bywa, r贸wnie偶 od zasady unikania logiki proceduralnej
istniej膮 dobrze uzasadnione wyj膮tki. Dotyczy to sytuacji, gdy tego typu
podej艣cie pozwala na znacz膮ce przyspieszenie w uzyskiwaniu wynik贸w.
Rozbudowane, monstrualne zapytania SQL nie zawsze stanowi膮 wzorzec
wydajno艣ci. Jednak nale偶y pami臋ta膰, 偶e proceduralny kod sklejaj膮cy
kolejno wykonywane zapytania SQL operuj膮ce na tych samych danych
(tabelach i wierszach) to dobry materia艂 na pojedyncze zapytanie SQL.
Mechanizm CBO analizuje pojedyncze zapytanie skonstruowane w zgodzie
z regu艂ami modelu relacyjnego jako jedn膮 ca艂o艣膰 i jest w stanie opracowa膰
efektywny spos贸b jego wykonania.
Jak najwi臋cej pracy zrzucaj na optymalizator zapyta艅, aby skorzysta膰
z jego mo偶liwo艣ci.
Maksymalne wykorzystanie dost臋pu
do bazy danych
Gdy planujemy wizyt臋 w wielu sklepach, w pierwszym kroku musimy
zdecydowa膰 si臋 na to, co chcemy kupi膰 w ka偶dym z nich. Dzi臋ki temu
mo偶na zaplanowa膰 tras臋 podr贸偶y i zminimalizowa膰 konieczno艣膰
przechodzenia mi臋dzy sklepami tam i z powrotem. Odwiedzamy pierwszy
sklep, dokonujemy zakup贸w, po czym odwiedzamy kolejny sklep.
To zdrowy rozs膮dek, a jednak zasada obowi膮zuj膮ca w tym przyk艂adzie
wydaje si臋 obca wielu praktycznym zastosowaniom baz danych.
64 ROZDZIAA DRUGI
Gdy z pojedynczej tabeli chcemy odczyta膰 kilka r贸偶nych informacji, nawet
niezbyt powi膮zanych (co rzeczywi艣cie wydaje si臋 do艣膰 powszechnym
przypadkiem), nieoptymalne jest nawi膮zywanie wielu po艂膮cze艅, po jednym
dla odczytania ka偶dej porcji danych. Na przyk艂ad nie nale偶y odczytywa膰
pojedynczych kolumn, je艣li potrzebujemy warto艣ci z kilku z nich. Nale偶y
do tego wykorzysta膰 pojedyncz膮 operacj臋. Niestety, dobre praktyki
programowania zorientowanego obiektowo z zasady pojedynczego odczytu
warto艣ci atrybut贸w zrobi艂y zalet臋, nie wad臋. Nie wolno jednak myli膰 metod
obiektowych z przetwarzaniem danych w bazach. Mieszanie tych poj臋膰
nale偶y do najpowa偶niejszych b艂臋d贸w, tabel nie wolno 艣lepo traktowa膰
jako klas, a kolumn jako atrybut贸w.
Ka偶d膮 wizyt臋 w bazie danych wykorzystuj do wykonania jak
najwi臋kszej ilo艣ci pracy.
Zbli偶enie do j膮dra systemu DBMS
Im bli偶ej j膮dra systemu zarz膮dzania baz膮 danych mo偶e dzia艂a膰 kod, tym
b臋dzie dzia艂a艂 wydajniej. To w艂a艣nie sedno si艂y baz danych. Na przyk艂ad
niekt贸re systemy zarz膮dzania bazami danych pozwalaj膮 na rozszerzanie
swoich mo偶liwo艣ci za pomoc膮 nowych funkcji, kt贸re mo偶na pisa膰
w niskopoziomowych j臋zykach programowania, jak na przyk艂ad C. J臋zyki
niskiego poziomu operuj膮ce na wskaznikach maj膮 istotn膮 cech臋 negatywn膮:
je艣li w procedurze zostanie pope艂niony b艂膮d, mo偶na doprowadzi膰
do uszkodzenia danych w pami臋ci. Problem by艂by powa偶ny ju偶 w przypadku,
gdyby z programu korzysta艂 tylko jeden u偶ytkownik. K艂opot z bazami
danych polega jednak na tym, 偶e mog膮 obs艂ugiwa膰 du偶膮 liczb臋 u偶ytkownik贸w.
W przypadku uszkodzenia danych w pami臋ci, mo偶na uszkodzi膰 dane
innego, zupe艂nie  niewinnego programu. W praktyce bywa tak, 偶e solidne
systemy zarz膮dzania bazami danych wykonuj膮 kod w pewnej izolacji
(technika okre艣lana mianem piaskownicy, ang. sandbox), dzi臋ki czemu
w przypadku awarii procesu nie poci膮ga on za sob膮 ca艂ej bazy danych.
Przyk艂adem jest tu system Oracle, kt贸ry do wymiany danych mi臋dzy
procesami a baz膮 danych wykorzystuje specjalny mechanizm komunikacji.
Ten proces jest podobny do 艂膮czy mi臋dzy bazami danych pracuj膮cych na
r贸偶nych serwerach. Je艣li zysk uzyskany z wydajno艣ci zewn臋trznych funkcji
PROWADZENIE WOJNY 65
w C w stosunku do osadzonych procedur PL/SQL rekompensuje koszt
skonstruowania zewn臋trznego 艣rodowiska i prze艂膮cze艅 kontekstu, warto
wykorzysta膰 funkcje zewn臋trzne. Jednak nie warto ich stosowa膰 w przypadku,
gdy maj膮 by膰 wykorzystywane do wywo艂ywania pojedynczo dla ka偶dego
wiersza tabeli. Jak to zwykle bywa, decyzja jest kwesti膮 r贸wnowagi
i znajomo艣ci wszystkich konsekwencji stosowania r贸偶nych strategii
rozwi膮zywania tego samego problemu.
Je艣li funkcje maj膮 by膰 jednak wykorzystane, warto w pierwszej kolejno艣ci
rozwa偶y膰 wykorzystanie funkcji standardowych, dost臋pnych w mechanizmie
zarz膮dzania baz膮 danych. Nie chodzi tu wy艂膮cznie o kwesti臋 unikania
 ponownego wynajdowania ko艂a , lecz przede wszystkim o to, 偶e funkcje
wbudowane dzia艂aj膮 znacznie bli偶ej j膮dra systemu zarz膮dzania baz膮 danych
ni偶 zewn臋trzny kod, a w zwi膮zku z tym s膮 bardziej wydajne.
Oto prosty przyk艂ad wykorzystania kodu SQL w bazie Oracle, za pomoc膮
kt贸rego zademonstruj臋 wydajno艣膰 uzyskan膮 dzi臋ki zastosowaniu funkcji
wbudowanych. Za艂贸偶my, 偶e mamy dane tekstowe wprowadzone r臋cznie
przez u偶ytkownika i 偶e te dane zawieraj膮 zb臋dne ci膮gi znaku spacji.
Potrzebna jest nam funkcja, kt贸ra zast膮pi takie ci膮gi wielu znak贸w spacji
pojedynczym znakiem. Przyjmijmy, 偶e nie b臋dziemy korzysta膰 z obs艂ugi
wyra偶e艅 regularnych, dost臋pnej w Oracle Database 10g, a zamiast tego
napiszemy w艂asn膮 funkcj臋:
create or replace function squeeze1(p_string in varchar2)
return varchar2
is
v_string varchar2(512) := '';
c_char char(1);
n_len number := length(p_string);
i binary_integer := 1;
j binary_integer;
begin
while (i <= n_len)
loop
c_char := substr(p_string, i, 1);
v_string := v_string || c_char;
if (c_char = ' ')
then
j := i + 1;
while (substr(p_string || 'X', j, 1) = ' ')
loop
j := j + 1;
66 ROZDZIAA DRUGI
end loop;
i := j;
else
i := i + 1;
end if;
end loop;
return v_string;
end;
/
Uwaga na marginesie: na ko艅cu ci膮gu znak贸w dopisywany jest znak X, aby
unikn膮膰 por贸wnania warto艣ci j z d艂ugo艣ci膮 tego ci膮gu.
Istniej膮 r贸偶ne metody eliminacji ci膮g贸w spacji, w kt贸rych mo偶na wykorzysta膰
funkcje udost臋pniane w ramach bazy Oracle. Oto jedna z alternatyw:
create or replace function squeeze2(p_string in varchar2)
return varchar2
is
v_string varchar2(512) := p_string;
i binary_integer := l;
begin
i := instr(v_string, ' ');
while (i > 0)
loop
v_string := substr(v_string, 1, i)
|| ltrim(substr(v_string, i + l));
i := instr(v_string, ' ');
end loop;
return v_string;
end;
/
Trzecia z metod mo偶e by膰 nast臋puj膮ca:
create or replace function squeeze3(p_string in varchar2)
return varchar2
is
v_string varchar2(512) := p_string;
len1 number;
len2 number;
begin
len1 := length(p_string);
v_string := replace(p_string, ' ', ' ');
len2 := length(v_string);
while (len2 < len1)
loop
len1 := len2;
PROWADZENIE WOJNY 67
v_string := replace(v_string, ' ',' ');
len2 := length(v_string);
end loop;
return v_string;
end;
/
Gdy te trzy alternatywne metody zostan膮 przetestowane na prostym
przyk艂adzie, ka偶da, zgodnie z oczekiwaniem, da dok艂adnie takie same
wyniki i nie b臋dzie znacz膮cej r贸偶nicy w wydajno艣ci:
SQL> select squeeze1('azeryt hgfrdt r')
2 from dual
3 /
azeryt hgfrdt r
Elapsed: 00:00:00.00
SQL> select squeeze2('azeryt hgfrdt r')
2 from dual
3 /
azeryt hgfrdt r
Elapsed: 00:00:00.01
SQL> select squeeze3('azeryt hgfrdt r')
2 from dual
3 /
azeryt hgfrdt r
Elapsed: 00:00:00.00
Za艂贸偶my jednak, 偶e operacja oczyszczania ci膮g贸w znak贸w z wielokrotnych
spacji b臋dzie wywo艂ywana tysi膮ce razy dziennie. Poni偶szy kod mo偶na
zastosowa膰 do za艂adowania bazy danych danymi testowymi, za pomoc膮
kt贸rych mo偶na w nieco bardziej realistycznych warunkach przetestowa膰
wydajno艣膰 trzech przedstawionych wy偶ej funkcji oczyszczaj膮cych ci膮gi
znak贸w z wielokrotnych spacji:
create table squeezable(random_text varchar2(50))
/
declare
i binary_integer;
j binary_integer;
k binary_integer;
v_string varchar2(5O);
68 ROZDZIAA DRUGI
begin
for i in 1 .. 10000
loop
j := dbms_random.value(1, 100);
v_string := dbms_random.string('U', 50);
while (j < length(v_string))
loop
k := dbms_random.value(l, 3);
v_string := substr(substr(v_string, 1, j) || rpad(' ', k)
|| substr(v_string, j + 1), 1, 50);
j := dbms_random.value(i, 100);
end loop;
insert into squeezable values(v_string);
end loop;
commit;
end;
/
Ten skrypt tworzy tabel臋 testow膮 z艂o偶on膮 z dziesi臋ciu tysi臋cy wierszy
(to do艣膰 niewiele, bior膮c pod uwag臋 艣redni膮 liczb臋 wywo艂a艅 zapyta艅 SQL).
Test wywo艂uje si臋 nast臋puj膮co:
select squeeze_func(random_text)
from squeezable;
Gdy wywo艂ywa艂em ten test, wy艂膮czy艂em wy艣wietlanie wynik贸w na
ekranie. Dzi臋ki temu upewni艂em si臋, 偶e czasy dzia艂ania algorytm贸w
oczyszczaj膮cych wielokrotne spacje nie s膮 zafa艂szowane przez czas
niezb臋dny na wy艣wietlenie wynik贸w. Testy by艂y wywo艂ane wielokrotnie,
aby upewni膰 si臋, 偶e nie wyst膮pi艂 efekt przyspieszenia wykonania dzi臋ki
zbuforowaniu danych.
Czasy dzia艂ania tych algorytm贸w prezentuje tabela 2.2.
TABELA 2.2. Czas wykonania funkcji oczyszczaj膮cych ci膮gi spacji na danych testowych (10 000 wierszy)
Funkcja Mechanizm Czas wykonania
squeeze1
PL/SQL p臋tla po elementach ci膮gu znak贸w 0,86 sekund
squeeze2 instr() + ltrim()
0,48 sekund
squeeze3
replace() wywo艂ana w p臋tli 0,39 sekund
Cho膰 wszystkie z tych funkcji wykonuj膮 si臋 dziesi臋膰 tysi臋cy razy w czasie
poni偶ej jednej sekundy, squeeze2() jest 1,8 razy szybsza od squeeze1(),
a squeeze3() jest ponad 2,2 razy szybsza. Jak to si臋 dzieje? Po prostu
PROWADZENIE WOJNY 69
PL/SQL nie jest tak  blisko j膮dra , jak funkcja SQL-a. R贸偶nica wydajno艣ci
mo偶e wygl膮da膰 na niewielk膮 w przypadku sporadycznego wywo艂ywania
funkcji, lecz w programie wsadowym lub na obci膮偶onym serwerze OLTP
r贸偶nica mo偶e ju偶 by膰 powa偶na.
Kod uwielbia j膮dro SQL-a  im jest bli偶ej, tym jest gor臋tszy.
Robi膰 tylko to, co niezb臋dne
Programi艣ci cz臋sto wykorzystuj膮 instrukcj臋 count(*) do implementacji
testu istnienia. Z regu艂y dochodzi do tego w celu implementacji
nast臋puj膮cej procedury:
Je艣li istniej膮 wiersze spe艂niaj膮ce warunek
Wykonaj na nich dzia艂anie
Powy偶szy schemat jest implementowany za pomoc膮 nast臋puj膮cego kodu:
select count(*)
into counter
from tabela
where
if (counter > 0) then
Oczywi艣cie w 90% tego typu wywo艂ania instrukcji count(*) s膮 zupe艂nie
zb臋dne, dotyczy to r贸wnie偶 powy偶szego przyk艂adu. Je艣li jakie艣 dzia艂anie
musi by膰 wykonane na podzbiorze wierszy tabeli, dlaczego go po prostu nie
wykona膰 od razu? Je艣li nie zostanie zmodyfikowany ani jeden wiersz, jaki
w tym problem? Nikomu nie stanie si臋 偶adna krzywda. Ponadto w sytuacji,
gdy operacja wykonywana w bazie danych sk艂ada si臋 z wielu zapyta艅,
po wywo艂aniu pierwszego z nich liczb臋 zmodyfikowanych wierszy mo偶na
odczyta膰 ze zmiennej systemowej (@@ROWCOUNT w Transact-SQL, SOL%ROWCOUNT
w PL/SQL itp.), specjalnego pola SQL Communication Area (SQLCA)
w przypadku wykorzystania osadzonego SQL (embedded SQL) lub
za po艣rednictwem specjalizowanych API, jak na przyk艂ad funkcji
mysql_affected_rows() j臋zyka PHP. Liczba przetworzonych wierszy jest
czasem zwracana z funkcji, kt贸ra wykonuje operacj臋 w bazie danych, jak
metoda executellpdate() biblioteki JDBC. Zliczanie wierszy bardzo cz臋sto
70 ROZDZIAA DRUGI
nie s艂u偶y niczemu opr贸cz zwi臋kszenia ilo艣ci pracy, jak膮 musi wykona膰 baza,
poniewa偶 wi膮偶e si臋 ono z dwukrotnym przetworzeniem (a raczej: odczytaniem,
a potem przetworzeniem) tych samych danych.
Ponadto nie nale偶y zapomina膰, 偶e je艣li naszym celem jest aktualizacja
lub wstawienie wierszy (cz臋sty przypadek: wiersze s膮 zliczane po to, by
stwierdzi膰 istnienie klucza), niekt贸re systemy zarz膮dzania bazami danych
oferuj膮 specjalne instrukcje (jak MERGE w Oracle 9i Database), kt贸re
dzia艂aj膮 bardziej wydajnie ni偶 w przypadku zastosowania osobnych zapyta艅
zliczaj膮cych.
Nie ma potrzeby, aby jawnie kodowa膰 to, co baza danych wykonuje
w spos贸b niejawny.
Instrukcje SQL-a
odwzorowuj膮 logik臋 biznesow膮
Wi臋kszo艣膰 system贸w baz danych udost臋pnia mechanizmy monitoruj膮ce,
za pomoc膮 kt贸rych mo偶na sprawdza膰 stan wykonywanych aktualnie
instrukcji, a nawet 艣ledzi膰 liczb臋 ich wywo艂a艅. Przy okazji mo偶na
u艣wiadomi膰 sobie liczb臋 przetwarzanych jednocze艣nie  jednostek
biznesowych : zam贸wie艅 lub innych zg艂osze艅, klient贸w z wystawionymi
fakturami lub dowolnych innych zdarze艅 istotnych z punktu widzenia
biznesowego. Mo偶na zweryfikowa膰, czy istnieje sensowne (a nawet
absolutnie precyzyjne) prze艂o偶enie mi臋dzy dwoma klasami aktywno艣ci.
Innymi s艂owy: czy dla zadanej liczby klient贸w liczba odwo艂a艅 do bazy
danych za ka偶dym razem jest taka sama? Je艣li zapytanie do tabeli klient贸w
jest wywo艂ywane dwadzie艣cia razy cz臋艣ciej, ni偶 wskazywa艂aby na to liczba
przetwarzanych klient贸w, to z pewno艣ci膮 wskazuje na jaki艣 b艂膮d. Taka
sytuacja sugerowa艂aby, 偶e zamiast jednorazowego odczytu danych z tabeli,
program dokonuje du偶ej ilo艣ci zb臋dnych odczyt贸w tych samych danych
z tabeli.
Nale偶y sprawdzi膰, czy dzia艂ania w bazie danych s膮 sp贸jne z realizowanymi
funkcjami biznesowymi aplikacji.
PROWADZENIE WOJNY 71
Programowanie logiki w zapytaniach
Istnieje kilka sposob贸w implementacji logiki biznesowej w aplikacji
wykorzystuj膮cej baz臋 danych. Cz臋艣膰 logiki proceduralnej mo偶na
zaimplementowa膰 w ramach instrukcji SQL-a (cho膰 ze swej natury SQL
m贸wi o tym, co nale偶y zrobi膰, a nie jak). Nawet w przypadku dobrej
integracji SQL-a w innych j臋zykach programowania zaleca si臋, aby jak
najwi臋cej logiki biznesowej ujmowa膰 w SQL-u. Taka strategia pozwala
na uzyskanie wy偶szej wydajno艣ci przetwarzania danych ni偶 w przypadku
implementacji logiki w aplikacji. J臋zyki proceduralne to takie, w kt贸rych
mo偶na definiowa膰 iteracje (p臋tle) oraz stosowa膰 logik臋 warunkow膮
(konstrukcje if...then...else). SQL nie potrzebuje p臋tli, poniewa偶 ze swojej
natury operuje na zbiorach danych. Potrzebuje jedynie mo偶liwo艣ci
okre艣lania warunk贸w wykonania okre艣lonych dzia艂a艅.
Logika warunkowa wymaga obs艂ugi dw贸ch element贸w: IF i ELSE. Obs艂uga
IF w SQL-u to prosta sprawa: warunek WHERE zapewnia dok艂adnie tak膮
semantyk臋. Natomiast z obs艂ug膮 logiki ELSE jest pewien problem. Na
przyk艂ad mamy za zadanie pobra膰 z tabeli zbi贸r wierszy, po czym wykona膰
r贸偶ne typy operacji, w zale偶no艣ci od typ贸w zbior贸w. Fragment tej logiki
mo偶na zasymulowa膰 z u偶yciem wyra偶enia CASE (Oracle od dawna obs艂uguje
odpowiednik tej operacji w postaci funkcji decode()1). Mi臋dzy innymi
mo偶na modyfikowa膰 w locie warto艣ci zwracane w ramach zbioru wynikowego
w zale偶no艣ci od spe艂nienia okre艣lonych warunk贸w. W pseudokodzie mo偶na
to zapisa膰 nast臋puj膮co2:
CASE
WHEN warunek THEN
WHEN warunek THEN
WHEN warunek THEN
ELSE
END
Por贸wnywanie warto艣ci liczbowych i dat to operacje intuicyjne. W przypadku
ci膮g贸w znak贸w mog膮 by膰 przydatne funkcje znakowe, jak greatest()
czy least() znane z Oracle, czy strcmp() z MySQL-a. Czasem te偶 bywa
1
Funkcja decode() jest nieco bardziej  surowa w stosunku do konstrukcji CASE.
Do uzyskania tych samych efekt贸w mo偶e by膰 konieczne wykorzystanie dodatkowych
funkcji, na przyk艂ad sign().
2
Istniej膮 dwa warianty konstrukcji CASE, przedstawiona wersja jest bardziej zaawansowana.
72 ROZDZIAA DRUGI
mo偶liwe zastosowanie pewnej formy logiki w instrukcjach za pomoc膮
wielokrotnych i logicznych operacji wstawiania do tabel oraz za pomoc膮
wstawiania 艂膮cz膮cego3 (merge insert). Nie nale偶y unika膰 takich instrukcji,
o ile s膮 dost臋pne w posiadanym systemie zarz膮dzania bazami danych.
Innymi s艂owy, polecenia SQL-a mo偶na wyposa偶y膰 w du偶膮 ilo艣膰 element贸w
kontrolnych. W przypadku pojedynczej operacji korzy艣膰 z tych mechanizm贸w
by膰 mo偶e nie jest wielka, lecz z zastosowaniem instrukcji CASE i wielu
instrukcji wykonywanych warunkowo jest ju偶 o co walczy膰.
O ile to mo偶liwe, warto implementowa膰 logik臋 aplikacji
w zapytaniach SQL zamiast w wykorzystuj膮cej je aplikacji.
Jednoczesne wielokrotne modyfikacje
Moje g艂贸wne za艂o偶enie w tym podrozdziale opiera si臋 na stwierdzeniu,
偶e kolejne modyfikacje danych w pojedynczej tabeli s膮 dopuszczalne pod
warunkiem 偶e dotycz膮 roz艂膮cznych zbior贸w wierszy. W przeciwnym razie
nale偶y 艂膮czy膰 je w ramach pojedynczego zapytania. Oto przyk艂ad
z rzeczywistej aplikacji4:
update tbo_invoice_extractor
set pga_status = 0
where pga_status in (1, 3)
and inv_type = 0;
update tbo_invoice_extractor
set rd_status = 0
where rd_status in (1, 3)
and inv_type = 0;
W tej samej tabeli dokonywane s膮 dwie kolejne operacje modyfikuj膮ce.
Czy te same wiersze b臋d膮 wykorzystywane dwukrotnie? Nie ma
mo偶liwo艣ci, aby to stwierdzi膰. Zasadniczym pytaniem jest tu jednak, jak
wydajne s膮 kryteria wyszukiwania? Atrybuty o nazwach type (typ) lub
status z du偶ym prawdopodobie艅stwem gwarantuj膮 s艂ab膮 dystrybucj臋
warto艣ci. Jest zatem ca艂kiem mo偶liwe, 偶e najefektywniejszym sposobem
odczytu tych danych b臋dzie pe艂ne, sekwencyjne przeszukiwanie tabeli.
3
Dost臋pny na przyk艂ad w Oracle od wersji 9.2.
4
Nazwy tabel zosta艂y zmienione.
PROWADZENIE WOJNY 73
Mo偶e te偶 by膰 tak, 偶e jedno z zapyta艅 wykorzysta indeks, a drugie b臋dzie
wymaga艂o pe艂nego przeszukiwania. W najkorzystniejszym przypadku
obydwa zapytania skorzystaj膮 z wydajnego indeksu. Niezale偶nie jednak
od tego, nie mamy prawie nic do stracenia, aby nie spr贸bowa膰 po艂膮czy膰
obydwu zapyta艅 w jedno:
update tbo_invoice_extractor
set pga_status = (case pga_status
when 1 then 0
when 3 then 0
else pga_status
end),
rd_status = (case rd_status
when 1 then 0
when 3 then 0
else rd_status
end)
where (pga_status in (1, 3)
or rd_status in (1, 3))
and inv_type = 0;
Istnieje prawdopodobie艅stwo wyst膮pienia niewielkiego narzutu
spowodowanego aktualizacj膮 kolumn o warto艣ci ju偶 przez nie posiadanej.
Jednak w wi臋kszo艣ci przypadk贸w jedna z艂o偶ona aktualizacja danych jest
o wiele szybsza ni偶 sk艂adowe wywo艂ane osobno. Warto zauwa偶y膰
zastosowanie logiki warunkowej z u偶yciem instrukcji CASE. Dzi臋ki temu
przetworzone zostan膮 tylko te wiersze, kt贸re spe艂niaj膮 kryteria, niezale偶nie
od tego, jak wiele kryteri贸w b臋dzie zastosowanych w zapytaniu.
Operacje modyfikuj膮ce warto wykonywa膰 w pojedynczej, z艂o偶onej
operacji, aby zminimalizowa膰 wielokrotne odczyty tej samej tabeli.
Ostro偶ne wykorzystanie
funkcji u偶ytkownika
Gdy w zapytaniu jest wykorzystana funkcja u偶ytkownika, istnieje
mo偶liwo艣膰, 偶e b臋dzie wywo艂ywana wielokrotnie. Je艣li funkcja wyst臋puje
w li艣cie SELECT, b臋dzie wywo艂ywana dla ka偶dego zwr贸conego wiersza. Je艣li
wyst膮pi w instrukcji WHERE, b臋dzie wywo艂ywana dla ka偶dego sprawdzonego
74 ROZDZIAA DRUGI
wiersza, kt贸ry spe艂nia kryteria sprawdzone wcze艣niej. To mo偶e oznacza膰
bardzo wiele wywo艂a艅 w przypadku, gdy wcze艣niej sprawdzane kryteria
nie s膮 bardzo mocno selektywne.
Warto si臋 zastanowi膰, co si臋 stanie, gdy taka funkcja wywo艂uje inne
zapytanie. To zapytanie b臋dzie wywo艂ywane przy ka偶dym wywo艂aniu
funkcji. W praktyce jej wynik b臋dzie taki sam jak w przypadku wywo艂ania
podzapytania, z t膮 r贸偶nic膮, 偶e w przypadku zapytania ukrytego w funkcji
optymalizator nie ma mo偶liwo艣ci lepszego zoptymalizowania zapytania
g艂贸wnego. Co wi臋cej, procedura osadzona jest wykonywana na osobnej
warstwie abstrakcji w stosunku do silnika SQL, wi臋c b臋dzie dzia艂a膰 mniej
wydajnie ni偶 bezpo艣rednie podzapytanie.
Zaprezentuj臋 przyk艂ad demonstruj膮cy zagro偶enia wynikaj膮ce z ukrywania
kodu SQL w funkcjach u偶ytkownika. Wezmy pod uwag臋 tabel臋 flights
opisuj膮c膮 loty linii lotniczych. Tabela ta zawiera kolumny: flight_number,
departure_time, arrival_time i iata_airport_codes5. S艂ownik kod贸w (oko艂o
dziewi臋膰 tysi臋cy pozycji) jest zapisany w osobnej tabeli zawieraj膮cej nazw臋
miasta (lub lotniska, je艣li w jednym mie艣cie znajduje si臋 kilka lotnisk),
nazw臋 kraju itp. Oczywi艣cie ka偶da informacja o locie wy艣wietlana
u偶ytkownikom powinna zawiera膰 nazw臋 miasta i lotniska docelowego
zamiast nic niem贸wi膮cego kodu IATA.
W tym miejscu trafiamy na jedn膮 ze sprzeczno艣ci w in偶ynierii nowoczesnego
oprogramowania. Do  dobrych praktyk programowania zaliczana jest
mi臋dzy innymi modularno艣膰, polegaj膮ca w uproszczeniu na opracowaniu
kilku odosobnionych warstw logiki. Ta zasada sprawdza si臋 doskonale
w og贸lnym przypadku, lecz w kontek艣cie baz danych, w kt贸rych kod
stanowi element wsp贸lny mi臋dzy programist膮 a baz膮 danych, potrzeba
zastosowania modularno艣ci kodu jest znacznie mniej wyrazna. Zastosujmy
jednak zasad臋 modularno艣ci, tworz膮c niewielk膮 funkcj臋 zwracaj膮c膮 pe艂n膮
nazw臋 lotniska na podstawie kodu IATA:
create or replace function airport_city(iata_code in char)
return varchar2
is
city_name varchar2(50);
begin
select city
5
IATA: International Air Transport Association.
PROWADZENIE WOJNY 75
into city_naine
from iata_airport_codes
where code = iata_code;
return(city_name);
end;
/
Dla czytelnik贸w niezaznajomionych ze sk艂adni膮 Oracle: wywo艂anie
trunc(sysdate) zwraca dzisiejsz膮 dat臋, godzin臋 00:00, arytmetyka dat jest
oparta na dniach. Warunek dotycz膮cy czas贸w odlotu odnosi si臋 zatem do
czas贸w mi臋dzy 8:30 a 16:00 dnia dzisiejszego. Zapytania wykorzystuj膮ce
funkcj臋 airport_city() mog膮 by膰 bardzo proste, na przyk艂ad:
select flight_number,
to_char(departure_time, 'HH24:MI') DEPARTURE,
airport_city(arrival) "TO"
from flights
where departure_time between trunc(sysdate) + 17/48
and trunc(sysdate) + 16/24
order by departure_time
/
To zapytanie wykonuje si臋 z zadowalaj膮c膮 pr臋dko艣ci膮. Z zastosowaniem
losowej pr贸bki na mojej maszynie siedemdziesi膮t siedem wierszy jest
zwracanych w czasie 0,18 sekundy (艣rednia z kilku wywo艂a艅). Taka
wydajno艣膰 jest do przyj臋cia. Statystyki informuj膮 jednak, 偶e podczas
wywo艂ania zosta艂y odczytane trzysta trzy bloki w pi臋膰dziesi臋ciu trzech
operacjach odczytu z dysku. A nale偶y pami臋ta膰, 偶e ta funkcja jest
wywo艂ywana rekurencyjnie dla ka偶dego wiersza.
Alternatyw膮 dla funkcji pobieraj膮cej dane z tabeli (s艂ownika) mo偶e by膰
z艂膮czenie tabel. W tym przypadku zapytanie nieco si臋 skomplikuje:
select f.flight_number,
to_char(f.departure_time, 'HH24:MI') DEPARTURE,
a.city "TO"
from flights f,
iata_airport_codes a
where a.code = f.arrival
and departure_time between trunc(sysdate) + 17/48
and trunc(sysdate) + 16/24
order by departure_time
/
76 ROZDZIAA DRUGI
To zapytanie wykonuje si臋 w czasie 0,05 sekundy (te same statystyki, ale
nie mamy do czynienia z rekurencyjnymi wywo艂aniami). Takie oszcz臋dno艣ci
mog膮 wyda膰 si臋 niewiele warte  trzykrotne przyspieszenie zapytania w wersji
nieoptymalnej trwaj膮cego u艂amek sekundy. Jednak do艣膰 powszechne jest,
偶e w rozbudowanych systemach (mi臋dzy innymi na lotniskach) niekt贸re
zapytania s膮 wywo艂ywane setki tysi臋cy razy dziennie. Za艂贸偶my, 偶e nasze
zapytanie musi by膰 wywo艂ywane pi臋膰dziesi膮t tysi臋cy razy dziennie. Gdy
zostanie u偶yta wersja zapytania wykorzystuj膮ca funkcj臋, ca艂kowity czas
wykonania tych zapyta艅 wyniesie oko艂o dwie godziny i trzydzie艣ci minut.
W przypadku z艂膮czenia b臋d膮 to czterdzie艣ci dwie minuty. Oznacza to
usprawnienie rz臋du 300%, co w 艣rodowiskach o du偶ej liczbie zapyta艅
oznacza znacz膮ce przyspieszenie, kt贸re mo偶e przek艂ada膰 si臋 na konkretne
oszcz臋dno艣ci finansowe. Bardzo cz臋sto zastosowanie funkcji powoduje
niespodziewany spadek wydajno艣ci zapytania. Co wi臋cej, wyd艂u偶enie czasu
wykonania zapyta艅 powoduje, 偶e mniej u偶ytkownik贸w jest w stanie
korzysta膰 z bazy danych jednocze艣nie, o czym wi臋cej pisz臋 w rozdziale 9.
Kod funkcji u偶ytkownika nie jest poddawany analizie
optymalizatora.
Oszcz臋dny SQL
Do艣wiadczony programista baz danych zawsze stara si臋 wykona膰 jak
najwi臋cej pracy za pomoc膮 jak najmniejszej liczby instrukcji SQL-a.
Klasyczny programista natomiast stara si臋 dostosowa膰 sw贸j program
do ustalonego schematu funkcyjnego. Na przyk艂ad:
-- Odczyt pocz膮tku okresu ksi臋gowego
select closure_date
into dtPerSta
from tperrslt
where fiscal_year=to_char(Param_dtAcc,'YYYY')
and rslt_period='1' || to_char(Param_dtAcc,'MM');
-- Odczyt ko艅ca okresu na podstawie daty pocz膮tku
select closure_date
into dtPerClosure
from tperrslt
where fiscal_year=to_char(Param_dtAcc,'YYYY')
and rslt_period='9' || to_char(Param_dtAcc,'MM');
PROWADZENIE WOJNY 77
To jest przyk艂ad kodu o bardzo niskiej jako艣ci, mimo tego 偶e szybko艣膰 jego
wykonania jest zadowalaj膮ca. Niestety, taka jako艣膰 jest typowa dla wi臋kszo艣ci
kodu, z kt贸rym musz膮 mierzy膰 si臋 specjali艣ci od optymalizacji. Dlaczego
dane s膮 odczytywane z zastosowaniem dw贸ch osobnych zapyta艅? Ten
przyk艂ad by艂 uruchamiany na bazie Oracle, w kt贸rej 艂atwo zaimplementowa膰
zapytanie zapisuj膮ce odpowiednie warto艣ci w tabeli wynikowej. Wystarczy
odpowiednio zastosowa膰 instrukcj臋 ORDER BY na kolumnie rslt_period:
select closure_date
bulk collect into dtPerStaArray
from tperrslt
where fiscal_year=to_char(Param_dtAcc,'YYYY')
and rslt_period in ('1' || to_char(Param_dtAcc,'MM'),
'9' || to_char(Param_dtAcc,'MM'))
order by rslt_period;
Dwie odczytane daty s膮 zapisywane odpowiednio w pierwszej i drugiej
kom贸rce macierzy. Operacja bulk collect jest specyficzna dla j臋zyka
PL/SQL, lecz w pozosta艂ych j臋zykach obs艂uguj膮cych pobieranie danych
do macierzy obowi膮zuje podobna zasada.
Warto zauwa偶y膰, 偶e macierz nie jest tu niezb臋dna, a te dwie warto艣ci mo偶na
pobra膰 do zmiennych skalarnych, wystarczy zastosowa膰 nast臋puj膮c膮 sztuczk臋6:
select max(decode(substr(rslt_period, 1, 1), -- sprawdzenie pierwszego znaku
'1', closure_date,
-- je艣li to '1', zwracamy dat臋
to_date('14/10/1066', 'DD/MM/YYYY'))),
-- w przeciwnym razie
max(decode(substr(rslt_period, 1, 1),
'9', closuredate, -- o t臋 dat臋 chodzi
to_date('14/10/1066', 'DD/MM/YYYY'))),
into dtPerSta, dtPerClosure
from tperrslt
where fiscal_year=to_char(Param_dtAcc, 'YYYY')
and rslt_period in ('1' || to_char(Param_dtAcc,'MM'),
'9' || to_char(Param_dtAcc,'MM'));
6
Funkcja decode() baz danych Oracle dzia艂a jak instrukcja CASE. Dane por贸wnywane
podaje si臋 w pierwszym argumencie. Je艣li warto艣膰 jest r贸wna drugiemu argumentowi,
zwracany jest trzeci. Je艣li nie zostanie podany pi膮ty argument, w takim przypadku
czwarty jest traktowany jako warto艣膰 ELSE; w przeciwnym razie, je艣li pierwszy
argument jest r贸wny czwartemu, zwracany jest pi膮ty i tak dalej w odpowiednich
parach warto艣ci.
78 ROZDZIAA DRUGI
W tym przyk艂adzie wynik b臋dzie dwuwierszowy, a oczekujemy wyniku
jednowierszowego zawieraj膮cego dwie kolumny (tak, jak w przyk艂adzie
z macierz膮). Dokonamy tego, sprawdzaj膮c za ka偶dym razem warto艣膰
w kolumnie rozr贸偶niaj膮cej warto艣ci z ka偶dego wiersza, czyli rslt_period.
Je艣li odnaleziony wiersz jest tym, kt贸rego szukamy, zwracana jest odpowiednia
data. W przeciwnym razie zwracana jest dowolna data (w tym przypadku
data bitwy pod Hastings), znacznie starsza (z punktu widzenia por贸wnania
 mniejsza ) od jakiejkolwiek daty w tej tabeli. Wybieraj膮c maksimum,
mamy pewno艣膰, 偶e otrzymamy odpowiedni膮 dat臋. Ten trik jest bardzo
praktyczny i mo偶na go z powodzeniem stosowa膰 do danych znakowych
lub liczbowych. Wi臋cej tego typu technik om贸wi臋 w rozdziale 11.
SQL jest j臋zykiem deklaratywnym, zatem nale偶y zachowa膰 dystans
do proceduralno艣ci zastosowa艅 biznesowych.
Ofensywne kodowanie w SQL-u
Programistom cz臋sto doradza si臋 programowanie defensywne polegaj膮ce
mi臋dzy innymi na sprawdzaniu poprawno艣ci wszystkich parametr贸w przed
ich zastosowaniem w wywo艂aniu. Przy korzystaniu z baz danych wi臋ksze
zalety ma jednak kodowanie ofensywne, polegaj膮ce na wykonywaniu kilku
dzia艂a艅 r贸wnolegle.
Dobrym przyk艂adem jest mechanizm obs艂ugi kontroli poprawno艣ci
polegaj膮cy na wykonywaniu serii sprawdze艅 i zaprojektowany w ten
spos贸b, 偶e w przypadku wyst膮pienia cho膰 jednego wyniku negatywnego
wywo艂ywany jest wyj膮tek. Za艂贸偶my, 偶e mamy przetworzy膰 p艂atno艣膰 kart膮
p艂atnicz膮. Kontrola takiej transakcji sk艂ada si臋 z kilku etap贸w. Nale偶y
sprawdzi膰, 偶e poprawny jest identyfikator klienta i numer karty oraz 偶e s膮
prawid艂owo ze sob膮 powi膮zane. Nale偶y r贸wnie偶 zweryfikowa膰 dat臋
wa偶no艣ci karty. No i oczywi艣cie bie偶膮cy zakup nie mo偶e spowodowa膰
przekroczenia limitu karty. Gdy wszystkie testy zako艅cz膮 si臋 pomy艣lnie,
mo偶e zosta膰 przeprowadzona operacja obci膮偶enia konta karty.
Niedo艣wiadczony programista m贸g艂by napisa膰 co艣 takiego:
select count(*)
from customers
where customer_id = id_klienta
PROWADZENIE WOJNY 79
W tym miejscu nast臋puje sprawdzenie wyniku, a je艣li wynik jest pomy艣lny,
nast臋puje wywo艂anie:
select card_num, expiry_date, credit_limit
from accounts
where customer_id = id_klienta
Tutaj r贸wnie偶 nast膮pi sprawdzenie wyniku, po kt贸rym (w przypadku
powodzenia) wywo艂ywana jest transakcja finansowa.
Do艣wiadczony programista zapewne napisze to nieco inaczej
(zak艂adaj膮c, 偶e today() to funkcja zwracaj膮ca bie偶膮c膮 dat臋):
update accounts
set balance = balance  wielkosc_zamowienia
where balance >= wielkosc_zamowienia
and credit_limit >= wielkosc_zamowienia
and expiry_date > today()
and customer_id = id_klienta
and card_num = numer_karty
Tutaj nast臋puje sprawdzenie liczby zmodyfikowanych wierszy. Je艣li jest to
zero, przyczyn臋 takiej sytuacji mo偶na sprawdzi膰 za pomoc膮 jednego zapytania:
select c.customer_id, a.card_num, a.expiry_date,
a.creditlimit, a.balance
from customers c
left outer join accounts a
on a.customer_id = c.customer_id
and a.cardnum = numer_karty
where c.customer_id = id_klienta
Je艣li zapytanie nie zwr贸ci 偶adnego wiersza, oznacza to, 偶e warto艣膰
customer_id jest b艂臋dna, je艣li w wyniku card_num jest NULL, oznacza to,
偶e numer karty jest b艂臋dny itd. Jednak w wi臋kszo艣ci przypadk贸w to drugie
zapytanie nie b臋dzie nawet uruchomione.
UWAGA
Warto zwr贸ci膰 uwag臋 na wywo艂anie count(*) w pierwszym fragmencie kodu
niedo艣wiadczonego programisty. To doskona艂a ilustracja b艂臋dnego u偶ycia funkcji
count(*) do sprawdzenia, czy w tabeli istniej膮 pozycje spe艂niaj膮ce warunek.
Zasadnicz膮 cech膮 programowania ofensywnego jest opieranie swoich za艂o偶e艅
na rozs膮dnym prawdopodobie艅stwie. Na przyk艂ad nie ma wi臋kszego sensu,
by sprawdza膰 istnienie klienta. Je艣li nie istnieje, w bazie danych nie
znajdzie si臋 偶aden dotycz膮cy go rekord (zatem w wyniku wywo艂ania
zapytania bez wcze艣niejszej kontroli i tak nie zostan膮 zmodyfikowane
80 ROZDZIAA DRUGI
偶adne dane)! Zak艂adamy, 偶e wszystko zako艅czy si臋 powodzeniem, a nawet
je艣li tak si臋 nie stanie, przygotowujemy mechanizm ratuj膮cy nas z opresji
w tym jednym punkcie  i tylko w tym jednym. Co interesuj膮ce, tego
typu strategia przypomina nieco  optymistyczn膮 kontrol臋 wsp贸艂dzielenia
zastosowan膮 w niekt贸rych bazach danych. Chodzi o to, 偶e za艂o偶ono z g贸ry,
i偶 z du偶ym prawdopodobie艅stwem nie b臋dzie sytuacji konfliktu dost臋pu
do danych, a je艣li jednak si臋 to zdarzy, dopiero w贸wczas uruchamiane s膮
stosowne konstrukcje kontrolne. W wyniku zastosowania tej strategii
wydajno艣膰 systemu jest znacznie wy偶sza ni偶 w przypadku system贸w
stosuj膮cych strategi臋 pesymistyczn膮.
Nale偶y kodowa膰 w oparciu o rachunek prawdopodobie艅stwa.
Zak艂ada si臋, 偶e najprawdopodobniej wszystko zako艅czy si臋
pomy艣lnie, a dopiero w przypadku niepowodzenia uruchamia si臋
plany awaryjne.
艢wiadome u偶ycie wyj膮tk贸w
Mi臋dzy odwag膮 a zapalczywo艣ci膮 r贸偶nica jest do艣膰 subtelna. Gdy zalecam
stosowanie agresywnych metod kodowania, nie sugeruj臋 bynajmniej szar偶y
w stylu Lekkiej Brygady pod Ba艂ak艂aw膮7. Programowanie z u偶yciem wyj膮tk贸w
r贸wnie偶 mo偶e by膰 konsekwencj膮 brawury, gdy dumni programi艣ci decyduj膮 si臋
 i艣膰 na ca艂o艣膰 . Maj膮 bowiem prze艣wiadczenie, 偶e testy i mo偶liwo艣膰 obs艂ugi
wyj膮tk贸w b臋d膮 ich tarcz膮 w tym boju. No tak, odwa偶ni umieraj膮 m艂odo!
Jak sugeruje nazwa, wyj膮tki to zdarzenia wyst臋puj膮ce w niecodziennych
sytuacjach. W programowaniu z u偶yciem baz danych nie wszystkie wyj膮tki
wykorzystuj膮 te same zasoby systemowe. Nale偶y pozna膰 te uwarunkowania,
aby korzysta膰 z wyj膮tk贸w w spos贸b inteligentny. Mo偶na wyr贸偶ni膰 dobre
wyj膮tki, wywo艂ywane, zanim zostaje wykonane dzia艂anie, oraz z艂e wyj膮tki,
kt贸re s膮 wywo艂ywane dopiero po fakcie wyrz膮dzenia powa偶nych zniszcze艅.
7
Podczas Wojny Krymskiej, w 1854 roku, odby艂a si臋 bitwa mi臋dzy wojskami Anglii,
Francji i Turcji a si艂ami Rosji. W wyniku nieprecyzyjnego rozkazu oraz osobistych
animozji mi臋dzy niekt贸rymi z dow贸dc贸w si艂 sprzymierzonych dosz艂o do szar偶y ponad
sze艣ciuset 偶o艂nierzy kawalerii brytyjskiej wprost na bateri臋 rosyjskiej artylerii. Na skutek
starcia zgin臋艂o oko艂o stu dwudziestu kawalerzyst贸w oraz po艂owa koni, bez jakiegokolwiek
dobrego rezultatu. Odwaga ludzi zosta艂a wkr贸tce wys艂awiona przez wiersz Tennysona,
a potem w kilku filmach hollywoodzkich, dzi臋ki czemu zwyk艂a g艂upota jednej militarnej
decyzji obr贸ci艂a si臋 w mit.
PROWADZENIE WOJNY 81
Zapytanie wykorzystuj膮ce klucz g艂贸wny, kt贸re nie znajdzie 偶adnych
wierszy, wykorzystuje niewiele zasob贸w  sytuacja jest identyfikowana
ju偶 na etapie przeszukiwania indeksu. Je艣li jednak w celu stwierdzenia, 偶e
dane spe艂niaj膮ce warunek nie wyst臋puj膮 w tabeli, zapytanie nie mo偶e u偶y膰
indeksu, zachodzi konieczno艣膰 dokonania pe艂nego przeszukiwania tabeli
(full scan). W przypadku wielkich tabel czas potrzebny do odczytu
sekwencyjnego w systemie dzia艂aj膮cym w danym monecie na granicy
swojej wydajno艣ci mo偶na potraktowa膰 jako czynnik katastrofalny.
Niekt贸re wyj膮tki s膮 szczeg贸lnie kosztowne, nawet przy najbardziej
sprzyjaj膮cych okoliczno艣ciach. Wezmy na przyk艂ad wykrywanie
duplikat贸w. W jaki spos贸b w bazie danych jest obs艂ugiwany mechanizm
unikalno艣ci? Prawie zawsze s艂u偶y do tego unikalny indeks i gdy wyst膮pi
pr贸ba wprowadzenia do tabeli warto艣ci zawieraj膮cej klucz wyst臋puj膮cy ju偶
w indeksie, zadzia艂a mechanizm zabezpieczaj膮cy przed zduplikowaniem
klucza, co efektywnie zablokuje zapis duplikatu. Jednak偶e zanim nast膮pi
pr贸ba zapisu indeksu (weryfikacji duplikatu), w tabeli musi zosta膰
fizycznie zapisana odpowiednia warto艣膰 (do procedury indeksuj膮cej
przesy艂any jest fizyczny adres wiersza w tabeli). Z tego wynika, 偶e
naruszenie ograniczenia unikalno艣ci klucza nast臋puje po fakcie zapisu
w tabeli danych, kt贸re musz膮 by膰 wycofane, czemu dodatkowo towarzyszy
komunikat informuj膮cy o wyst膮pieniu b艂臋du. Wszystkie te operacje wi膮偶膮
si臋 z okre艣lonym kosztem czasowym. Najwi臋kszym jednak grzechem jest
podejmowanie samodzielnych pr贸b dzia艂ania na poziomie wyj膮tk贸w.
W takim przypadku przejmujemy od systemu zadanie obs艂ugi operacji
na poziomie wierszy, nie ca艂ych zbior贸w danych, czyli sprzeciwiamy si臋
fundamentalnej koncepcji relacyjnego modelu danych. Konsekwencj膮
wyst臋powania cz臋stych narusze艅 ogranicze艅 w bazie b臋dzie w takim
przypadku stopniowa degradacja jej wydajno艣ci.
Przyjrzyjmy si臋 przyk艂adowi opartemu na bazie Oracle. Za艂贸偶my, 偶e
pracujemy nad integracj膮 system贸w informatycznych dw贸ch po艂膮czonych firm.
Adres e-mail zosta艂 ustandaryzowany w postaci wzorca
i ma zawiera膰 co najwy偶ej dwana艣cie znak贸w, wszystkie spacje i znaki
specjalne s膮 zast臋powane znakami podkre艣lenia8.
8
Przyk艂ad nie uwzgl臋dnia obs艂ugi polskich znak贸w diakrytycznych, niedozwolonych
w adresach e-mail  przyp.red.
82 ROZDZIAA DRUGI
Za艂贸偶my, 偶e now膮 tabel臋 pracownik贸w nale偶y wype艂ni膰 trzema tysi膮cami
wierszy z tabeli employees_old. Chcemy te偶, 偶eby ka偶dy pracownik
posiada艂 unikalny adres e-mail. Z tego powodu musimy zastosowa膰
okre艣lon膮 zasad臋 nazewnictwa: Jan Kowalski b臋dzie mia艂 e-mail o postaci
jkowalski, a J贸zef Kowalski (偶adnego pokrewie艅stwa) jkowalski2 itd.
W naszych danych testowych znajdziemy trzydzie艣ci trzy potencjalne
pozycje konfliktowe, co przy pr贸bie 艂adowania danych da nast臋puj膮cy efekt:
SQL> insert into employees(emp_num, emp_name,
emp_firstname, emp_email)
2 select emp_num,
3 emp_name,
4 emp_firstname,
5 substr(substr(EMP_FIRSTNAME, 1, 1)
6 ||translate(EMP_NAME, ' ''', '_'), 1, 12)
7 from employees_old;
insert into employees(emp_num, emp_name, emp_firstname, emp_email)
*
ERROR at line 1:
ORA-0000l: unique constraint (EMP_EMAIL_UQ) violated
Elapsed: 00:00:00.85
Trzydzie艣ci trzy duplikaty ze zbioru trzech tysi臋cy to troch臋 powy偶ej 1%,
by膰 mo偶e zatem warto by艂oby obs艂u偶y膰 te 99%, a elementy problemowe
obs艂u偶y膰 z u偶yciem wyj膮tk贸w? W ko艅cu 1% danych nie powinien
powodowa膰 znacznego obci膮偶enia bazy w wyniku procedury obs艂ugi
wyj膮tk贸w. Poni偶ej kod realizuj膮cy ten optymistyczny scenariusz:
SQL> declare
2 v_counter varchar2(l2);
3 b_ok boolean;
4 n_counter number;
5 cursor c is select emp_num,
6 emp_name,
7 emp_firstname
8 from employees_old;
9 begin
10 for rec in c
11 loop
12 begin
13 insert into employees(emp_num, emp_name,
14 emp_firstname, emp_email)
PROWADZENIE WOJNY 83
15 values (rec.emp_num,
16 rec.emp_name,
17 rec.emp_firstname,
18 substr(substr(rec.emp_firstname, 1, 1)
19 ||translate(rec.emp_name, ' ''', ' '), 1, 12));
20 exception
21 when dup_val_on_index then
22 b_ok := FALSE;
23 n_counter := 1;
24 begin
25 v counter := ltrim(to_char(n_counter));
26 insert into employees(emp_num, emp_name,
27 emp_firstname, emp_email)
28 values (rec.emp_num,
29 rec.emp_name,
30 rec.emp_firstname,
31 substr(substr(rec.emp_firstname, 1, 1)
32 ||translate(rec.emp_name, ' ''', '__'), 1,
33 12 - length(v_counter)) || v_counter);
34 b_ok : = TRUE;
35 exception
36 when dup_val_on_index then
37 n_counter := n_counter + 1;
38 end;
39 end;
40 end loop;
41 end;
40 /
PL/SOL procedure successfully completed.
Elapsed: 00:00:18.41
Jaki jest jednak rzeczywisty koszt obs艂ugi wyj膮tk贸w? Gdyby ten sam test
przeprowadzi膰 na danych pozbawionych duplikat贸w, oka偶e si臋, 偶e koszt
rzeczywistej obs艂ugi wyj膮tk贸w (ich wyst膮pie艅) jest pomijalny. Procedura
wywo艂ana na danych z duplikatami dzia艂a oko艂o osiemna艣cie sekund,
podobnie jak na danych bez duplikat贸w. Jednak gdy wykonamy ten test
(dane bez duplikat贸w) na naszej oryginalnej procedurze nieobs艂uguj膮cej
wyj膮tk贸w (insert...select), zauwa偶ymy, 偶e wykona si臋 znacznie szybciej
od p臋tli. Prze艂膮czenie si臋 w tryb  wiersz po wierszu powoduje oko艂o
50-procentowy narzut czasu przetwarzania. Czy w takim razie mo偶liwe
jest unikni臋cie tego trybu? To kwestia tego, czy zdecydujemy si臋 na rezygnacj臋
z mechanizmu obs艂ugi wyj膮tk贸w, kt贸ry to w艂a艣nie zmusi艂 nas do obs艂ugi
danych w trybie wierszowym.
84 ROZDZIAA DRUGI
Innym sposobem mog艂oby by膰 zidentyfikowanie wierszy powoduj膮cych
powstanie duplikat贸w i uzupe艂nienie w nich adres贸w e-mail kolejnymi
liczbami.
Aatwo okre艣li膰 liczb臋 problematycznych wierszy, wystarczy odpowiednio
je zgrupowa膰 w zapytaniu SQL. Jednak偶e uzupe艂nienie o unikalne liczby
mo偶e by膰 trudne bez zastosowania funkcji analitycznych dost臋pnych
w niekt贸rych zaawansowanych systemach baz danych. Okre艣lenie  funkcje
analityczne pochodzi z nomenklatury Oracle. W DB2 funkcje te znane s膮
jako funkcje OLAP (ang. online analytical processing), w Microsoft SQL
Server jako funkcje rankingu (ang. ranking functions). Warto przyjrze膰
si臋 bli偶ej rozwi膮zaniom tego typu z punktu widzenia czystego SQL-a.
Ka偶dy adres e-mail mo偶e mie膰 dopisany unikalny numer przy wykorzystaniu
do tego rankingu wed艂ug wieku pracownika. Numer 1 otrzyma najstarszy
pracownik w danej grupie duplikat贸w, numer 2 kolejny pod wzgl臋dem
wieku z tej grupy itd. Umieszczaj膮c t臋 liczb臋 w podzapytaniu, mamy
mo偶liwo艣膰 unikni臋cia dopisania czegokolwiek do pierwszego znalezionego
adresu e-mail w ka偶dej grupie, natomiast pozosta艂ym przypisywane s膮
kolejne liczby sekwencji. Spos贸b realizacji tego zadania demonstruje
poni偶szy kod:
SQL> insert into employees(emp_num, emp_firstname,
2 emp_name, emp_email)
3 select emp_num,
4 emp_firstname,
5 emp_name,
6 decode(rn, 1, emp_email,
7 substr(emp_email,
8 1, 12 - length(ltrim(to_char(rn))))
9 || ltrim(to_char(rn)))
10 from (select emp_num,
11 emp_firstname,
12 emp_name,
13 substr(substr(emp_firstname, 1, 1)
14 ||translate(emp_name, ' ''', '_'), 1, 12)
15 emp_email,
16 row_number()
17 over (partition by
18 substr(substr(emp_firstname, 1, 1)
PROWADZENIE WOJNY 85
19 ||translate(emp_name,' ''','_'), 1, 12)
20 order by emp_num) rn
21 from employees_old)
22 /
3000 rows created.
Elapsed: 00:00:11.68
Unikamy kosztu przetwarzania w trybie wierszowym, dzi臋ki czemu to
rozwi膮zanie zajmuje oko艂o 60% czasu w por贸wnaniu z p臋tl膮.
Obs艂uga wyj膮tk贸w zmusza do zastosowania logiki proceduralnej.
Zawsze warto bra膰 pod uwag臋 obs艂ug臋 wyj膮tk贸w, jednak w zakresie
niezmuszaj膮cym do rezygnacji z deklaratywnej specyfiki SQL-a.


Wyszukiwarka

Podobne podstrony:
C Sztuka programowania cpszpr
Java Sztuka programowania jaszpr
Asembler Sztuka programowania Wydanie II asesz2
Rails Sztuka programowania
UNIX Sztuka programowania unszpr
Asembler Sztuka programowania
Programowanie w SQL
Serwer SQL 2008 Administracja i programowanie
Oracle?tabaseg Programowanie w jezyku PL SQL or10ps
Bazy Danych J臋zyk Zapyta艅 SQL Programowanie Proceduralne
Zaawansowane programowanie w T SQL
Procedury, funkcje, wyzwalacze programowanie w j臋zyku T SQL

wi臋cej podobnych podstron