Zaczynamy ćwiczyć z DB2 Tworzymy procedurę, która wstawia do dwóch tabel: osoba i adres Autor: Artur Wroński, IBM [email protected] … i przy okazji poznamy mechanizmy pozwalające na tworzenie optymalnych zapytań SQL Wykonując to ćwiczenie dowiesz się: –Jak wygenerować identyfikator dla klucza głównego? –Jak napisać procedurę składowaną? –Jak sprawdzić, czy zapytanie SQL jest optymalnie wykonywane przez bazę? –Co ma wpływ na optymalizację zapytań? –Jak sprawdzić, jakie indeksy przydadzą się dla zapytania? –Jak przekazywać zbiory wynikowe z procedury do aplikacji? –Co to są wyrażenia common table expressions? 1 Projekt tabel tabela: osoba id_osoby bigint (PK) imie nazwisko miejscowosc ulica nr_domu kod_pocztowy imie varchar(50) nazwisko varchar(50) tabela: adres id_osoby bigint (FK) miejscowosc varchar(80) ulica varchar(80) nr_domu varchar(15) kod_pocztowy char(5) Założenia: • imię i nazwisko nie może się powtarzać • id_osoby musi być wygenerowane • usunięcie osoby powinno usunąć rekordy z adresem • osoba może posiadać tylko jeden adres Na przykładzie bardzo prostego ćwiczenia poznasz elementy przydatne dla dewelopera aplikacji działających na DB2. Ćwiczenie będzie polegało na napisaniu procedury składowanej, która dostaje na wejściu zestaw danych i wstawia je do dwóch tabel: osoba i adres. 2 Tworzymy tabelę OSOBA create table id_osoby imie nazwisko ); osoba ( bigint not null, varchar(50) not null, varchar(50) not null alter table osoba add constraint osoba_id_osoby_pk primary key (id_osoby); alter table osoba add constraint osoba_naz_uq unique (nazwisko,imie); Czy stosować reguły integralnościowe (constrains)? Jak wykonać przedstawione na slajdzie instrukcje? Jest wiele metod. Możesz uruchomić narzędzie graficzne Centrum Sterowania (DB2 Control Center), wybrać z menu Narzędzia -> Edytor Komend. W edytorze połącz się z wybraną bazą danych, wklej polecenie i wykonaj. Zwróć uwagę, że każda instrukcja zakończona jest średnikiem – w narzędziach DB2 jest to domyślny znak kończący instrukcję. Możesz uruchomić okno poleceń tekstowych DB2 (w systemie Windows: db2cmd, na Linuksie/Uniksach jest to po prostu powłoka systemu operacyjnego), następnie przygotować plik z w/w instrukcjami i go wywołać go: db2 connect to baza1 db2 -tvf plik_z_instrukcjami.sql Opcja -t oznacza, że instrukcja jest zakończona znakiem specjalnym (terminated), domyślne takim znakiem jest średnik (;). Opcja -v wyświetla echo instrukcji (verbose), natomiast opcja -f czyta polecenia z pliku (file) Jeśli, chciałbyś przygotować i zarządzać bardziej złożonym modelem danych, IBM ma do tego odpowiednie narzędzie: IBM Rational Data Architect (na bazie Eclipse). □ Niektórzy deweloperzy unikają reguł integralnościowych (constraints), ponieważ spowalniają modyfikacje danych. Jednak tylko w taki sposób jesteśmy zagwarantować spójność danych w bazie. Zwróć uwagę, że baza danych wykorzystuje reguły integralnościowe przy optymalizacji zapytań, co może przyspieszyć wykonywanie zapytań. Pewnie zdziwisz się, ale w DB2 można zaznaczyć, by reguły integralnościowe nie były sprawdzane przy modyfikacji (NOT ENFORCED), a były wykorzystywane do optymalizacji (ENABLE QUERY OPTIMIZATION) – zapytania działają szybciej, a modyfikacje nie są spowalniane. Takie więzy spójności (informational constraints) mają jednak sens, jeśli mamy 100% przekonanie, że dane są spójne. 3 Generowanie id alter table osoba alter column id_osoby set generated always as identity (start with 100); generated always - baza zawsze generuje dane insert into osoba (imie, nazwisko) values ('Adam', 'Mickiewicz'); Co zostało wygenerowane? values identity_val_local() Ostatnie w sesji wygenerowane ID. select id_osoby from new table ( insert into osoba (imie, nazwisko) values ('Adam', 'Mickiewicz') ) as t; SELECT z INSERT-a Jednym ze sposobów określenia nowej wartości klucza głównego są pola generowane. Pola generowane przechowują wartości, które zostały wygenerowane dowolną funkcją SQL. IDENTITY jest przykładem funkcji pozwalającej wygenerować kolejny numer, który bardzo przydaje się przy tzw. sztucznych kluczach głównych. Jeśli pole zdefiniowane było z atrybutem ALWAYS, wtedy w aplikacji nie mamy możliwości określenia wartości pola (musimy je pominąć w instrukcji wstawiającej, bądź modyfikującej). Jeśli pole zdefiniowane było z atrybutem BY DEFAULT, wtedy możemy mu także przypisać wartość jawnie. Wygenerowaną ostatnio wartość IDENTITY możemy pobrać specjalną funkcją identity_val_local(), ale także możemy wykorzystać specjalny rodzaj zapytania SELECT z instrukcji INSERT, który zwróci nam dane, które rzeczywiście zostały zapisane przez bazę danych. Na pierwszy rzut oka taka funkcjonalność może wydawać się zbędna, bo przecież wiemy, co zapisywaliśmy do bazy danych – jednak w praktyce taka konstrukcja bardzo się przydaje. Zwróć uwagę, że każdą próbę modyfikacji danych możemy podmienić wyzwalaczem TRIGGER … INSTEAD OF – wtedy jedną instrukcją SQL możemy sprawdzić, co było zapisane. Zapytanie SELECT z INSERTa jest dużo bardziej wydajne niż zapisanie danych instrukcją INSERT i odczytanie rekordu dodatkowym zapytaniem. Zwróć uwagę, że SELECT można robić także na instrukcjach UPDATE, DELETE i zwrócić rekordy modyfikowane, bądź sprzed modyfikacji. 4 Generowanie id Jeśli baza zawsze generuje wartości to jak migrować tak założone tabele IMPORT-em? alter table osoba alter column id_osoby set generated by default; generated by default - baza pozwala na INSERT, UPDATE import from plik1 of del insert into osoba IMPORT alter table osoba alter column id_osoby set generated always; alter table osoba alter column id_osoby restart with 999; Zmiana na ALWAYS oraz ustawienie licznika Skoro tylko baza danych może wygenerować wartość pojawia się pytanie „jak przenieść dane pomiędzy tak utworzonymi tabelami bądź pomiędzy bazami danych”? Klauzula BY DEFAULT pozwala nam także na ręczne modyfikowanie generowanych pól, dzięki czemu będziemy mogli użyć standardowych narzędzi do migracji danych wykorzystujących SQL INSERT, takich jak IMPORT. Na czas ładowania danych zmieniamy definicję pola generowanego na GENERATED BY DEFAULT. Ładujemy dane IMPORT-em. W przykładzie ładowany plik ma postać pliku tekstowego z separtorem – klauzula of del (delimited). Po załadowaniu danych zmieniamy definicję pola generowanego, tak by zagwarantować, że nikt ręcznie nie nadpisze wartości pola generowanego oraz zwiększamy wartość licznika związanego z polem IDENTITY. □ Import jest przykładem narzędzia do ładowania danych. Import czyta dane z pliku bądź z kursora. Plik może mieć postać tekstową (of del) bądź binarną ixf (integrated exchange format). Format ixf jest przenośny pomiędzy różnymi systemami DB2 i dodatkowo zawiera definicję tabeli, tzn. na podstawie pliku ixf możemy odtworzyć strukturę tabeli. Dane z bazy możesz wyeksportować poleceniem export. Oto przykłady exportu: db2 export to plik.del of del select * from tabela db2 export to plik.ixf of ixf select * from tabela 5 Generowanie id, cz. II Jeśli baza zawsze generuje wartości to jak migrować tak założone tabele IMPORT-em? alter table osoba alter column id_osoby drop identity; Usunięcie IDENTITY import from plik1 of del insert into osoba IMPORT alter table osoba alter column id_osoby set generated always as identity (start with 100); alter table osoba alter column id_osoby restart with 999; Założenie IDENTITY oraz ustawienie licznika Atrybut IDENTITY możemy także usunąć, a następnie odtworzyć po załadowaniu danych. Klauzula (start with 100) określa definicję pola generowanego (narzędzia do przenoszenia schematu bazy odczytają wartość początkową równą 100), natomiast klauzula restart with 999 określa bieżącą wartość licznika. Wartość licznika mogliśmy nadać tylko jednym poleceniem (start with 999), lecz w ten sposób tracilibyśmy informację, od jakiego numeru rozpoczęliśmy generowanie kluczy. □ Jeśli chciałbyś sprawdzić definicję tabeli możesz zrobić to przy pomocy narzędzia db2look (opcja –e pochodzi od słowa extract): db2look -d baza1 -e -t tabela1 DDL (data definition language) tabeli możesz wygenerować także w Centrum Sterowania DB2: kliknij prawym klawiszem na wybraną tabelą i wybierz „Generuj DDL…”. 6 Generowanie id, cz. III Jeśli baza zawsze generuje wartości to jak migrować tak założone tabele LOAD-em? load from plik1 of del modified by identityoverride insert into osoba LOAD alter table osoba alter column id_osoby restart with 999; Ustawienie licznika Tabelę z polem generowanym możemy także „nadpisać na siłę”, narzędziem LOAD, które zapisuje dane bezpośrednio na dysk z pominięciem dziennika transakcji oraz silnika SQL. W przykładzie wykorzystaliśmy modyfikator identityoverride, który pozwala na nadpisanie pól IDENTITY z opcja GENERATED ALWAYS. □ LOAD jest zupełnie inna metodą ładowania danych niż IMPORT. Przede wszystkim jest dużo szybszy. Narzędzie jest wielowątkowe i automatycznie powołuje odpowiednią ilość agentów, tak by w maksymalnym stopniu wykorzystać dostępne procesory. Indeksy budowane są dopiero po załadowaniu danych (mogą być budowane przyrostowo). Oczywiście zysk na szybkości musi odbywać się jakimś kosztem. Podczas ładowania danych obszar tabel, w którym znajduje się ładowana tabela dostępny jest tylko do odczytu. Dodatkowo, narzędzie LOAD nie sprawdza reguł integralnościowych, dlatego też po załadowaniu danych należy uruchomić specjalną komendę SET INTEGRITY, która sprawdza spójność danych i ponownie aktywuje więzy spójności (np. klucz główny – klucz obcy). 7 Czy są inne metody generacji ID? CREATE SEQUENCE seq1 AS BIGINT START WITH 1; VALUES NEXTVAL FOR seq1 ; VALUES PREVVAL FOR seq1 ; ALTER SEQUENCE seq1 RESTART WITH 100 ; Jak jest różnica pomiędzy INDENTITY as SEQUENCE? - INDENTITY związane jest z tabelą, a SEQUENCE jest niezależnym obiektem, wykorzystywanym przez aplikacje. Kolejny identyfikator możemy także wygenerować przy wykorzystaniu sekwencji. Sekwencje jednak nie są skojarzone z danym obiektem bazy danych – aplikacja musi być świadoma faktu istnienia sekwencji. Kolejne wartości sekwencji generuje się funkcją NEXTVAL (NEXT VALUE). Funkcja PREVVAL (PREVIOUS VALUE) podaje ostatnią wygenerowaną w sesji wartość dla danej sekwencji. Warto zaznaczyć, że sekwencje, jak i pola generowane generują po prostu następny numer i nie sprawdzają wartości, które są aktualnie w tabeli. Jeśli chcemy wygenerować wartość, która jest unikalna możemy skorzystać z funkcji GENERATE_UNIQUE(). 8 Po co wykorzystywać IDENTITY albo SEQUENCE? także dla WYDAJNOŚCI ! db2look -d baza1 -e -t osoba /.../ "ID_OSOBY" BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY ( START WITH +100 INCREMENT BY +1 MINVALUE +100 MAXVALUE +9223372036854775807 NO CYCLE CACHE - generacja kolejnej paczki jest CACHE 20 zapisywana do logu. Kluczowe dla wydajności! NO ORDER, "IMIE" VARCHAR(50) NOT NULL , "NAZWISKO" VARCHAR(50) NOT NULL Atrybut IDENTITY oraz sekwencje przede wszystkim ułatwiają tworzenie aplikacji, ale także są dużo bardziej wydajne, niż własnoręczne metody generowania wartości. Kluczowym atrybutem wpływającym na wydajność generowanych sekwencji jest CACHE. Silnik bazy danych przygotowuje w pamięci paczkę wartości, które następnie rozdziela pomiędzy aplikacje (klauzula NO ORDER oznacza, że aplikacje niekoniecznie muszą dostawać generowane numerki w określonej kolejności – wtedy jest jeszcze szybciej). 9 Tworzymy tabelę ADRES create table adres ( id_osoby bigint not null, miejscowosc varchar(80), ulica varchar(80), nr_domu varchar(15), kod_pocztowy char(5) ); alter table adres add constraint adres_id_osoby_fk foreign key(id_osoby) references osoba(id_osoby) on delete cascade on update restrict; alter table adres add constraint adres_id_uq unique (id_osoby); comment on table adres is 'Powiazane z tabela osoba'; Tworząc klucz obcy z klauzulą on delete cascase wymuszamy usuwanie rekordów zależnych, przy usunięciu rekordu głównego. Komentarze (instrukcja comment) opisujące znaczenie tabel i kolumn są przechowywane w słowniku systemowym bazy danych – warto z nich korzystać, gdyż schemat bazy danych staje się bardziej czytelny. Struktura tabel osoba, adres: 10 Tworzymy procedurę 3 2 1 Jeśli będziesz chciał tworzyć i śledzić procedury w narzędziu graficznym możesz sięgnąć po DB2 Developer Workbench. Narzędzie zostało oparte o środowisko Eclipse i jest bezpłatne, niezależnie od wykorzystywanej przez Ciebie edycji DB2. 1) Database Explorer – w tym oknie możesz nawiązać połączenie z bazą danych i zobaczyć, jakie obiekty zostały w bazie utworzone. 2) Data Project Explorer – tu tworzysz projekty, w których będziesz mógł budować procedury składowane, zapytania SQL oraz XQuery. 3) Okno pokazuje kod edytowanej procedury DODAJ_OSOBE. Procedurę możesz wdrożyć na dowolnej bazie danych (Deploy…) i wykonać. Procedurę możesz także debuggować po uruchomieniu odpowiedniej perspektywy. DB2 Developer Workbench możesz pobrać z adresu: http://ibm.com/db2/express 11 Tworzymy procedurę dodaj_osobe create procedure dodaj_osobe ( in p_imie varchar(50), in p_nazwisko varchar(50), in p_miejscowosc varchar(80), in p_ulica varchar(80), in p_nr_domu varchar(15), in p_kod_pocztowy char(5) ) specific dodaj_osobe begin atomic -- ciało procedury -- wstawienie do tabel: osoba i adres end @ begin atomic -- blok nierozerwalny, traktowany jak pojedyncza, choć złożona instrukcja Jeśli tworzymy procedurę z interfejsu tekstowego, musimy przygotować plik z treścią procedury, a na końcu procedury musimy umieścić unikalny znak końca instrukcji (lub dwa znaki, w przykładzie znak @). Procedura składowana jest przykładem tzw. złożonej instrukcji SQL. Utworzenie procedury z tak przygotowanego skryptu: db2 -td@ -f twój_plik (opcje: -t = terminated, -d = defined). Oczywiście wcześniej musisz połączyć się z bazą danych. W procedurze zdefiniowana sześć parametrów wejściowych. Nazwy parametrów poprzedzono przedrostkiem „p_” by później ułatwić rozróżnienie parametrów od zmiennych oraz nazw obiektów SQL. Blok: begin atomic Instrukcja 1 Instrukcja 2 end traktowany jest jak jedna złożona instrukcja, co oznacza, że wszystkie instrukcje SQL w bloku wykonają się w całości, albo się nie wykonają w ogóle. Jeśli wystąpi błąd, i błąd ten nie będzie obsłużony, wtedy żadna z elementarnych instrukcji zawartych w bloku nie wykona się. Działanie bloków nierozerwalnych można odnieść np. do pojedynczej instrukcji, która usuwa wiele rekordów z bazy (np. delete from tab1 where id > 1000). W wyniku działania takiej instrukcji usuną się wszystkie rekordy spełniające kryterium usuwania, albo żaden jeśli dla dowolnego rekordu wystąpi błąd. 12 Ciało procedury dodaj_osobe begin atomic declare v_id_osoby bigint; select id_osoby into v_id_osoby from new table( insert into osoba(imie,nazwisko) values (p_imie, p_nazwisko)) as t; insert into adres (id_osoby,miejscowosc,ulica,nr_domu,kod_pocztowy) values (v_id_osoby,p_miejscowosc,p_ulica, p_nr_domu, p_kod_pocztowy); end @ Dzięki wykorzystaniu bloku nierozerwalnego możemy wstawić w jednej logicznej części osobę i adres. Mamy gwarancję, że jeśli wystąpi błąd przy wstawianiu adresu to wstawienie osoby zostanie automatycznie anulowane. Zwróć uwagę, że nie jest to wycofanie transakcji (rollback). Na zewnątrz bloku można umieścić instrukcję COMMIT bądź ROLLBACK, która będzie obejmowała także instrukcje wywołane przed uruchomieniem procedury (bloku). W programach funkcjonalność podobną do BEGIN ATOMC można uzyskać poprzez punkty zapisu transakcji (SAVEPOINT, ROLLBACK TO SAVEPOINT). Dla pseudo kodu poniżej: OK oznacza poprawne wykonanie, BŁĄD oznacza zakończenie instrukcji z błędem. instrukcja1; -- OK begin atomic instrukcja2; -- OK instrukcja3; -- BŁĄD instrukcja4; end; instrukcja5; -- OK commit; Transakcja niejawnie rozpoczyna się w momencie wykonania instrukcji1. Błąd, który wystąpił w instrukcji3 spowoduje, że modyfikacje dokonane w instrukcji2 będą anulowane. Instrukcja4 nie będzie wykonana ze względu na nieobsłużony błąd w instrukcji3. Instukcja commit zatwierdzi modyfikacje z instukcji1 oraz instrukcji5. 13 Testowanie procedury dodaj_osobe call dodaj_osobe( 'Anna', 'Kalinowska', 'Nowa Iwiczna', 'Biała', '23/14', '12123 '); commit; call dodaj_osobe( 'Anna', 'Kalinowska', 'Nowa Iwiczna', 'Biała', '23/14', '12123 '); Czy drugie wywołanie się powiedzie? SQLCODE SQL0803N One or more values in the INSERT statement, UPDATE statement, or foreign key update caused by a DELETE statement are not valid because the primary key, unique constraint or unique index identified by "2" constrains table "PL38219.OSOBA" from having duplicate rows for those columns. SQLSTATE=23505 SQLSTATE O jaką regułę spójności chodzi? Powtórne wywołanie procedury dodaj_osobę () z tymi samymi argumentami nie powiedzie się, ze względu na wcześniej zdefiniowaną regułę niepowtarzalności. Baza w komunikacie o błędzie podaje kod błędu oraz SQLSTATE. Kod błędu ma charakter specyficzny dla DB2, natomiast SQLSTATE ma charakter ogólny (standard ANSI) i jest zgodny z bazami danych innych producentów. Zwróć uwagę na wyjaśnienie kodu błędu. 14 Sprawdzenie błędu dodaj_osobe db2 ? SQL0803N db2 ? 23505 Sprawdzenie błędu SELECT INDNAME, INDSCHEMA, COLNAMES FROM SYSCAT.INDEXES WHERE IID = 2 AND TABSCHEMA = 'PL38219' AND TABNAME = 'OSOBA' ; Wynika z opisu błedu PL38219.OSOBA_NAZ_UQ (+NAZWISKO+IMIE) Informacja o naruszonym warunku Podając w linii poleceń tekstowych znak zapytania i kod błędu bądź SQLSTATE możemy szybko uzyskać opis błędu. Zwróć uwagę, że komunikat błędu dla SQLSTATE jest bardzo lakoniczny. db2 ? 23505 SQLSTATE 23505: Wystąpiło naruszenie ograniczenia narzuconego przez indeks unikalny lub ograniczenie przez unikalność (UNIQUE). Dużo dokładniejszy opis błędu można uzyskać wykorzystując kod błędu, który będzie zawierał także szczegółowe instrukcje objaśniające, jak dowiedzieć się która reguła integralnościowa została naruszona. W opisie wygenerowanym poleceniem db2 ? SQL0803N podane zostało zapytanie pozwalające na identyfikację nazwy naruszonej reguły niepowtarzalności. 15 Sprawdzenie błędu w proced. dodaj_osobe Modyfikujemy procedurę (trochę inaczej☺): -jeśli OSOBA istnieje to uaktualnij ADRES begin atomic declare v_id_osoby bigint; zakres działania handler-a begin declare continue handler for sqlstate '23505' begin select id_osoby into v_id_osoby from osoba where imie = p_imie and nazwisko = p_nazwisko; end; select id_osoby into v_id_osoby from new table(insert into osoba(imie,nazwisko) values (p_imie, p_nazwisko)) as t; end; [..] end @ W powyższym przykładzie najpierw próbujemy wstawiać nowy rekord i jeśli okaże się, że dany rekord już istnieje (to samo imię i nazwisko), wtedy pobieramy z bazy id_osoby. W ciele procedury zdefiniowaliśmy handler, czyli kod obsługi wyjątków, który będzie wywołany, jeśli wystąpi dany błąd. Handler typu „kontynuuj” oznacza, że po wystąpieniu błędu zostanie wywołany kod obsługi błędu (na rysunku na niebiesko), a następnie procedura zostanie kontynuowana od instrukcji, gdzie wystąpił błąd. Zwróć uwagę na zakres działania handlera. Gdybyśmy zdefiniowali exit handler, wtedy po wykonaniu kodu handlera, opuścilibyśmy blok begin … end, a następnie dalej kontynuowalibyśmy wykonywanie procedury, ale od pierwszej instrukcji znajdującej się po bloku, z którego wyszliśmy. Innymi słowy, handler odnosi się do danego bloku, a nie całej procedury. Exit handler wychodzi z bloku begin..end a nie z całej procedury. Zwróć uwagę, że w naszym przykładzie wykorzystaliśmy dodatkowy, zagnieżdżony blok. Jeszcze jedna uwaga: deklaracje handlerów, jak i deklaracje zmiennych dopuszczane są tylko na początku bloku (zaraz po begin, a przed jakąkolwiek instrukcją SQL). 16 Zakres działania handlera begin atomic declare v_id_osoby bigint; begin declare continue handler for sqlstate '23505' [..] end; Na razie nie modyfikujmy tej części kodu insert into adres (id_osoby,miejscowosc,ulica,nr_domu,kod_pocztowy) values (v_id_osoby,p_miejscowosc,p_ulica, p_nr_domu, p_kod_pocztowy); end @ Czy teraz powtórne wywołanie procedury z tymi samymi argumentami powiedzie się? SQL0803N One or more values in the INSERT statement, UPDATE statement, or foreign key update caused by a DELETE statement are not valid because the primary key, unique constraint or unique index identified by "1" constrains table "PL38219.ADRES" from having duplicate rows for those columns. SQLSTATE=23505 Gdybyśmy teraz chcieli wykonać tak utworzoną procedurę z tymi samymi argumentami jak wcześniej, dostaniemy znów błąd związany z naruszeniem reguły niepowtarzalności, ale tym razem już innej niż poprzednio (reguły mówiącej, że dana osoba może mieć tylko jeden adres – sprawdź schemat tabeli adres ze wcześniejszych slajdów). 17 Uaktualnienie adresu merge into adres using table( values ( v_id_osoby, p_miejscowosc, p_ulica, p_nr_domu, p_kod_pocztowy) ) tmp (id_osoby,miejscowosc,ulica,nr_domu,kod_pocztowy) Zbiór wierszy, zwykle wynik SELECT-a on ( adres.id_osoby = tmp.id_osoby) when matched then update set (miejscowosc, ulica, nr_domu, kod_pocztowy) = (tmp.miejscowosc,tmp.ulica,tmp.nr_domu,tmp.kod_pocztowy) when not matched then insert (id_osoby,miejscowosc,ulica,nr_domu,kod_pocztowy) values (tmp.id_osoby,tmp.miejscowosc,tmp.ulica, tmp.nr_domu, tmp.kod_pocztowy); By nie dostawać błędu związanego z istnieniem danego adresu przyjmijmy, że w takiej sytuacji uaktualnimy dane adresowe. Można to zrobić jedną instrukcją SQL MERGE, która pozwala scalać nie tylko jeden rekord ale wiele rekordów z tabelą docelową. Po słowie kluczowym using można podać nazwę tabeli, albo zapytanie SQL.W przykładzie podaliśmy konstrukcje table, która tworzy wirtualną tabelę składającą się z jednego rekordu zbudowanego na podstawie parametrów wejściowych procedury: table( values ( v_id_osoby, p_miejscowosc, p_ulica, p_nr_domu, p_kod_pocztowy) ) tmp (id_osoby,miejscowosc,ulica,nr_domu,kod_pocztowy) Rekord ten będzie scalony z tabelą adres. Jeśli rekord o id_osoby będzie istniał w tabeli adres, wtedy wykona się uaktualnienie rekordu, jeśli nie będzie istniał, wtedy zostanie wstawiony nowy rekord. Klauzulę table(..) możemy zastąpić zapytaniem na specjalnej tabeli systemowej, która zawiera dokładnie jeden rekord: ( select v_id_osoby, p_miejscowosc, p_ulica, p_nr_domu, p_kod_pocztowy from sysibm.sysdummy1 ) Klauzula values dokonuje po prostu ewaluacji wyrażenia, np.: values 10.1+1 zwróci 11.1. 18 Wstawienie osoby - lepiej begin atomic declare v_id_osoby bigint default null; select id_osoby into v_id_osoby from osoba where imie = p_imie and nazwisko = p_nazwisko; if v_id_osoby is null then select id_osoby into v_id_osoby from new table(insert into osoba(imie,nazwisko) values (p_imie, p_nazwisko)) as t; end if; [..] Dlaczego lepiej niż sprawdzanie błędu? Ponieważ nieudana próba wstawienia niepotrzebnie zwiększa licznik pola generowanego i ogólnie jest kosztowna. Oczywiście fragment kodu związany ze sprawdzeniem, czy dana osoba już istnieje najlepiej poprawić, tak by nie wywoływać obsługi błędu, która z natury jest kosztowna. Dużo lepiej jest sprawdzić na wstępie, czy rekord istnieje, a dopiero potem ewentualnie go wstawić (chyba, że sytuacja, w której wstawiamy osobę, która istnieje już w bazie występuje niezmiernie rzadko). Przy wstawianiu osoby i pobraniu wygenerowanego identyfikatora wykorzystaliśmy klauzulę „zapytanie ze wstawienia”. 19 A co jeśli ktoś wprowadzi imię dużymi literami? call dodaj_osobe( 'Stanisława', 'Kowalska', ... call dodaj_osobe( 'Stanisława', 'KOWALSKA', ... alter table osoba drop constraint osoba_naz_uq; alter table osoba add column imie_szuk varchar(50) not null generated always as ( ucase( imie) ); alter table osoba add column nazwisko_szuk varchar(50) not null generated always as ( ucase( nazwisko) ); Teraz warunek unikalności dotyczy dużych liter alter table osoba add constraint osoba_naz_uq unique (nazwisko_szuk,imie_szuk) Tak jednak nie pójdzie A gdyby był warunek biznesowy mówiący, że nie możemy wstawić drugi raz osoby o tym samym imieniu i nazwisku, porównując dane dużymi literami? Rozwiązanie: wystarczy dodać pole generowane, które przechowuje imię i nazwisko dużymi literami a następnie założyć regułę niepowtarzalności na tak utworzonych polach. Pokazane na slajdzie instrukcje jednak nie zadziałają, ponieważ na czas dodania pola generowanego należy wyłączyć sprawdzanie integralności i przegenerować wartości dla istniejących rekordów (patrz następny slajd). 20 Teraz lepiej alter table osoba drop constraint osoba_naz_uq; set integrity for osoba off; alter table osoba add column imie_szuk varchar(50) not null generated always as ( ucase( imie) ); alter table osoba add column nazwisko_szuk varchar(50) not null generated always as ( ucase( nazwisko) ); set integrity for osoba, adres immediate checked force generated; Tabela adres jest powiązana kluczem z tabelą osoba, stąd wymienione są dwie tabele alter table osoba add constraint osoba_naz_uq unique (nazwisko_szuk,imie_szuk); Tak wygląda poprawny ciąg instrukcji SQL, pozwalający dodać nowe pole i wygenerować dla niego wartości. Zwróć uwagę na instrukcję, która włącza sprawdzanie integralności: set integrity for osoba, adres immediate checked force generated; Integralność należało włączyć nie tylko dla tabeli osoba, ale także dla tabeli adres. Dlaczego? Dlatego, iż tabele są powiązane ze sobą regułami referencyjnymi (klucz podstawowy – klucz obcy). Wyłączając integralność tabeli osoba automatycznie wyłączyliśmy integralność tabel powiązanych (tu: adres). By sprawdzić, które tabele są w stanie wyłączenia sprawdzania integralności, najlepiej sięgnąć do systemowego słownika bazy danych (system catalog). 21 Modyfikacja kodu procedury - obsługa dużych liter begin atomic declare v_id_osoby bigint default null; select id_osoby into v_id_osoby from osoba where ucase(imie) = ucase(p_imie) and ucase(nazwisko) = ucase(p_nazwisko); [..] A co z wydajnością? Teraz sprawdzimy, czy osoba istnieje w bazie danych w następujący sposób: Funkcją UCASE podniesiemy do dużych liter zmienne będące argumentami procedury, a następnie porównamy je zawartością pól imię i nazwisko podniesionych do dużych liter także funkcją UCASE. Pewnie zadajesz sobie pytanie, „a co z wydajnością?”. Czy baza nie będzie nie będzie musiała sekwencyjnie odczytać całą tabelę (by podnieść do dużych liter)? Odpowiedź na następnych slajdach. 22 Plan wykonania zapytania dla pola generowanego (tekstowo) db2 -tf "%DB2PATH%\misc\EXPLAIN.DDL" Utworzenie tabel dla wyjaśnionych zapytań set current explain mode = EXPLAIN; Zapytania będą wyjaśniane oraz nie wykonywane select id_osoby from osoba where ucase(imie) = ucase('Jerzy') and ucase(nazwisko) = ucase('Pasula'); Plan zapytania jest zapisany w utworzonych tabelach db2exfmt -d %DBNAME% -e %USERNAME% -n %% -s %% -g -o %1.exfmt -w -1 -# 0 TIC Prezentowanie planów zapytań w formacie tekstowym To, jak baza danych wykonuje zapytanie najlepiej sprawdzić przy pomocy tzw. planu wykonania zapytania. W środowisku graficznym DB2 Centrum Sterowania jest to trywialne (klikasz prawym klawiszem na bazą danych i wybierasz Wyjaśnij zapytanie). W środowisku tekstowym będziesz musiał najpierw utworzyć zestaw tabel, w których będą pamiętane wyjaśnione plany zapytań (narzędzie graficzne robi to za Ciebie). Jeśli wykonasz następującą instrukcję: set current explain mode = EXPLAIN wtedy zapytania dla bieżącej sesji będą tylko wyjaśniane (bez wykonywania). Plan wykonania zapytania zostanie przechowany we wcześniej utworzonych (skryptem EXPLAIN.DDL) tabelach. Narzędzie db2exfmt podłącza się do bazy danych, sięga do tabel z wyjaśnionymi zapytaniami i formatuje wyniki zapisując je do pliku tekstowego. W przykładzie wykorzystano zmienne ustawione w systemie Windows (tak naprawdę jest to fragment skryptu): %DBNAME% – nazwa bazy danych %USERNAME% – nazwa użytkownika %1.exfmt – plik, do którego będzie zapisany plan wykonania zapytania. W systemach Linux/UNIX użyj pojedynczego znaku % zamiast %%. 23 exfmt.ksh -- skrypt dla Linuksa/UNIX if [[ $1 = '' ]]; then echo „Brak argumentow: <plik SQL> <poziom optymalizacji>" exit fi DBNAME="BAZA1" QF=${1:-'sql.filename'} QO=${2:-'7'} db2 set current query optimization = $QO db2 set current explain mode = EXPLAIN db2 -tvf $QF db2exfmt -d ${DBNAME} -e ${USER} -n % -s % -g $QF.exfmt -w -1 -# 0 TIC -o db2 set current explain mode = NO Oto przykładowy skrypty shell-owy exfmt.ksh dla systemów Linux/UNIX, który można wykorzystać do generowania planu wykonania zapytania. Skrypt tworzy plik ze sformatowanym planem zapytania o takiej nazwie, jak plik zawierający analizowane zapytanie, z doklejonym rozszerzeniem .exfmt Przykład wywołania: exfmt.ksh zapytanie.sql 5 Plik wynikowy: zapytanie.sql.exfmt Oto odpowiednik skryptu dla systemu Windows: exfmt.bat (zmień nazwę bazy danych wskrypcie) set DBNAME=BAZA1 set OPTLEVEL=%2 db2 set current query optimization = %OPTLEVEL% db2 set current explain mode = EXPLAIN db2 -tvf %1 db2exfmt -d %DBNAME% -e %USERNAME% -n %% -s %% -g TIC -o %1.exfmt -w -1 -# 0 db2 set current explain mode = NO 24 Plan wykonania zapytania - skrypt exfmt.bat Rows RETURN ( 1) Original Statement Cost I/O select id_osoby from osoba | where ucase(imie) = ucase('Jerzy') and 1 ucase(nazwisko) = ucase('Pasula') FETCH ( 2) 12.871 1 /----+---\ 1 18 IXSCAN TABLE: PL38219 Optimized Statement ( 3) OSOBA 0.0165298 SELECT Q1.ID_OSOBY AS "ID_OSOBY" 0 FROM PL38219.OSOBA AS Q1 | WHERE ( 18 Q1.NAZWISKO_SZUK = UCASE('Pasula')) AND INDEX: PL38219 (Q1.IMIE_SZUK = UCASE('Jerzy')) OSOBA_NAZ_UQ Oto plan wykonania zapytania wyszukującego imię i nazwisko dużymi literami. Zwróć uwagę, że DB2 wykorzystało indeks OSOBA_NAZ_UQ. Na grafie węzeł (3) odszukuje dany rekord z wykorzystaniem indeksu, a następnie doczytuje z tabeli pozostałe kolumny – węzeł (2). Analizując plan wykonania zapytania zwróć uwagę na zoptymalizowaną postać zapytania. DB2 przepisało zapytanie do postaci, w której sięga do pól generowanych (pola nazwisko_szuk, imie_szuk) i dlatego mogło wykorzystać indeks, który był na nich założony. Jak czytać graf? Zwróć uwagę, że korzeń grafu zawiera wyjaśniającą legendę. Czytając graf analizuj, w których gałęziach drzewa przyrasta koszt. Liczba zwróconych wierszy 1 FETCH ( 2) 12.871 1 Wykonywana operacja Numer węzła, do którego nawiązuje dalsza część raportu Umowny koszt (jednostka: timeron) służący do porównywania różnych planów wykonania tego samego zapytania Umowny koszt operacji dyskowych IXSCAN - oznacza odczytanie wewnętrznego adresu rekordu z wykorzystaniem indeksu. FETCH - doczytuje pozostałe kolumny, dla rekordu o danym adresie wewnętrznym. 25 Co ma wpływ na optymalizację? Database Context: ---------------Parallelism: CPU Speed: [..] Package Context: --------------SQL Type: Optimization Level: [..] None 3.778754e-007 Dynamic 5 wynik działania exfmt.bat STATYSTYKI INDEKSY FRAGMENTACJA DANYCH REGUŁY INTEGRALNOŚCIOWE Jest wiele rzeczy, które wpływają na optymalizację zapytań. Analizując plik zawierający sformatowany schemat obsługi zapytania zwróć uwagę na kontekst bazy danych oraz kontekst zapytania – znajdziesz tam nazwy parametrów bazy danych, które mają wpływ na sposób optymalizacji. Bardzo ważnym parametrem jest także poziom optymalizacji. Poziom optymalizacji można ustawić niezależnie dla każdego zapytania, bądź akceptować domyślną wartość definiowaną na poziomie bazy danych (dft_queryopt). Bardzo ważnym, o ile nie najważniejszym, czynnikiem są statystyki dla optymalizatora, czyli informacje o samych danych. Statystyki zawierają nie tylko informacje o ilości rekordów, czy o rozkładzie wartości, ale także informacje o tym jak dane rozkładają się na dyskach (jak są pofragmentowane). Jeśli np. defragmentowałeś tabelę (polecenie REORG), wtedy także watro zebrać statystyki. 26 Zmienne sterujące kompilatorem SQL set current query optimization = 1 Poziom optymalizacji. Jaki ustawić? DB2_ANTIJOIN DB2_HASH_JOIN DB2_INLIST_TO_NLJN DB2_LIKE_VARCHAR DB2_MINIMIZE_LISTPREFETCH DB2_NEW_CORR_SQ_FF DB2_PRED_FACTORIZE DB2_REDUCED_OPTIMIZATION DB2_SELECTIVITY DB2_SQLROUTINE_PREPOPTS Zmienne sterujące kompilatorem Poziom optymalizacji można ustawić w aplikacji instrukcją SQL: set current query optimization. Zwykle dla zapytań o charakterystyce OLTP, czyli takich, które pobierają (aktualizują) niewielką liczbę rekordów bardzo dobre są niskie poziomy optymalizacji (0 lub 1), ponieważ baza powinna poświęcać stosunkowo mało czasu na samą optymalizację. Niskie poziomy dostępu preferują dostęp z wykorzystaniem indeksów (uproszczony model kosztowy). Wyższe poziomy optymalizacji (5,7) stosowane są głównie w zapytaniach analitycznych, które z natury wykonują się długo, dlatego też warto je optymalizować. Specjalnymi zmiennymi rejestrowymi można określić także algorytmy, jakie DB2 ma uwzględniać przy optymalizacji. Zmienne ustawia się narzędziem db2set, ale można je także ustawić w środowisku systemu operacyjnego. Po ustawieniu danej zmiennej należy powtórnie wystartować instancję DB2. Przykładowo zmienna DB2_HASH_JOIN, określa czy baza danych ma wykorzystywać funkcję haszującą do wykonywania złączeń. Warto zaznaczyć, że zmienne sterujące optymalizatorem ustawia się względnie rzadko. Optymalizator DB2 działa w oparciu o dojrzały model kosztowy, rozwijany od przeszło 20 lat. W innych bazach danych bardzo popularną techniką jest częste stosowanie tzw. podpowiedzi (hints), czyli dyrektyw, które określają w jaki sposób baza danych ma wykonywać zapytanie. W DB2 takie techniki nie są praktykowane, choć istnieje możliwość zewnętrznego skorygowania planu wykonania zapytania. Przygotowuje się profil optymalizacyjny w postaci pliku XML, w którym podaje się treść zapytania oraz dyrektywy optymalizacyjne. Wczytywany przez bazę plik pozwala na korektę działania aplikacji bez modyfikacji kodu aplikacji. 27 Dlaczego statystyki są tak ważne? To jest komenda administracyjna runstats on table pl38219.osoba with distribution on key columns and indexes all set profile only runstats on table pl38219.osoba use profile runstats on table pl38219.osoba with distribution on all columns tablesample system(20) update profile only util_impact_priority 10 Zbieranie statystyk da się zautomatyzować. Komentarz do slajdu z poprzedniej strony: Wykonując zapytanie SQL możemy podać, czy chcemy by DB2 szybko dostarczyło pierwszy rekord, albo optymalizowało pracę na dostarczenie całego zbioru wynikowego. Dla zapytania: select … optimize for 1 row pierwszy rekord dostarczony zostanie szybko – taka konstrukcja szczególnie jest przydatna, jeśli pobieramy kilka pierwszych rekordów z posortowanego zbioru. □ Do zbierania statystyk służy polecenie runstats (dostępne w C poprzez API bazy danych). Bardzo wygodną metodą zbierania statystyk jest przygotowanie profilu statystyk. Profil statystyk to nic innego jak odpowiednie opcje polecenia runstats przechowane w słowniku bazy danych, dla każdej tabeli z osobna. Operator odpowiedzialny za zbieranie statystyk używa później dokładnie tej samej składni dla każdej tabeli: runstats … use profile. W przypadku dużych tabel bardzo przydatne są opcje pozwalające zbierać statystyki na próbce danych (tablesample), oraz pozwalające określić priorytet, czyli procent wykorzystania procesora (util_impact_priority). W domyślnej konfiguracji bazy danych w wersji 9 statystyki zbierane są automatycznie . Jeśli tabela zostanie zmodyfikowana na tyle, że może to wpłynąć na poziom optymalizacji, wtedy baza zgłasza żądanie zebrania statystyk. Statystyki są zbierane w najbliższym czasowym oknie administracyjnym, które może określić administrator. Automatyczne zbieranie statystyk można włączyć/wyłączyć parametrami bazy danych: auto_maint, auto_tbl_maint, auto_runstats, choć najprościej można to zrobić przy pomocy Centrum Sterowania i opcji „konserwacja bazy danych”. 28 Jak sprawdzić czy statystyki były zebrane? select tabname, tabschema, stats_time from syscat.tables Jak zwykle takie informacje są dostępne poprzez widoki systemowe SYSCAT A co jeśli tabela zmienia swoją zawartość tak często, że statystyki nigdy nie są aktualne? alter table dane volatile Czy da się zebrać statystyki z aplikacji? call admin_cmd( 'runstats on table pl38219.osoba') By sprawdzić, czy statystyki były zebrane dla danej tabeli wystarczy wykonać odpowiednie zapytanie na słownikach systemowych. Jeśli tabele zawierają dane przejściowe (ładujemy dane a później je usuwamy), wtedy statystyki praktycznie zawsze będą nieaktualne. W takiej sytuacji należy tabelę opatrzyć atrybutem VOLATILE (ulotny) – baza danych będzie preferowała indeksy w dostępie do takiej tabeli i nie będzie uwzględniała statystyk. Warto jeszcze wspomnieć o tabelach przechowujących różnego rodzaje logi aplikacji. Dla takich tabel najlepiej wykonać: ALTER TABLE tabela1 APPEND ON Wtedy dane będą dopisywane na końcu tabeli, co będzie działało szybciej, ponieważ baza nie będzie poświęcała czasu na wyszukiwanie wolnego miejsca w środkowych obszarach tabeli (baza inaczej optymalizuje wstawianie danych). □ Statystyki można zebrać także poprzez wywołanie procedury admin_cmd (instrukcja SQL a nie polecenie administracyjne). 29 Zbieranie statystyk a transakcja Zebranie statystyk dokonywane jest w transakcji. Dopiero zatwierdzenie transakcji spowoduje uaktualnienie katalogów systemowych. db2 +c call admin_cmd( 'runstats on table pl38219.osoba') db2 commit Ważne dla tabel tymczasowych Zwróć uwagę, że statystyki zapisywane są do słownika systemowego w momencie zatwierdzania transakcji. 30 Czy można samemu uaktualnić statystyki? - TAK ! Do symulacji środowiska produkcyjnego, bez konieczności kopiowania danych. - Ale po co??? db2look -d baza1 -e -m -t tab1 system produkcyjny system deweloperski Narzędziem db2look (które głównie wykorzystywane jest do wygenerowania schematu bazy – DDL) można także wyeksportować statystyki do pliku. db2look z opcją -m generuje odpowiedni skrypt z instrukcjami UPDATE, które mogą potem posłużyć do uaktualnienia statystyk na innym systemie. Przenoszenie statystyk wykorzystywane jest głównie w sytuacji, jeśli w środowisku testowym chcemy odtworzyć zachowanie systemu produkcyjnego. Zbieramy statystyki na produkcji i przenosimy je na system testowy. Optymalizator DB2 zachowuje się tak samo, jak na systemie produkcyjnym, mimo iż system testowy zawiera zupełnie inne dane (trzeba pamiętać, by wyłączyć automatyczne zbieranie statystyk na systemie testowym!). 31 DB2 nie używa indeksu. Po reorganizacji już używa, dlaczego? reorgchk current statistics on table all reorg table pl38219.staff allow read access Trzeba mieć dużo miejsca w przestrzeni tymczasowej reorg table pl38219.staff inplace allow write access start reorg table pl38219.staff inplace pause list history reorg all for sample Ta reorganizacja jest typu ONLINE - nie tworzy dużych obiektów tymczasowych Jeśli w zapytaniu pytasz się o 5% danych, ale dane są tak pofragmentowane, że do realizacji takiego zapytania należy przetworzyć 80% stron z danymi (dane pasujące do twojego zapytania rozrzucone są na wiele stron), wtedy DB2 nie skorzysta z indeksu, ponieważ mniej kosztowny jest sekwencyjny odczyt całej tabeli. W takiej sytuacji pomaga reorganizacja danych. Jedną z metod jest reorganizacja offlineowa -- tabela udostępniana jest tylko do odczytu a system robi kopię tabeli (w obszarze, tam gdzie jest tabela, bądź we wskazanym obszarze tymczasowym). Reorganizacja online (składnia inplace) działa małymi porcjami, jest asynchroniczna, można ją wstrzymywać i wznawiać później, i co najważniejsze pozwala na pełen odczyt i zapis do reorganizowanej tabeli. Poleceniem list history reorg możemy sprawdzić stan reorganizacji online-owej. Poleceniem reorgchk możemy sprawdzić, czy dana tabela potrzebuje reorganizacji. Narzędzie analizuje statystyki i przygotowuje raport. W raporcie zwróć uwagę na ostatnie kolumny, które zawierają współczynniki mówiące, czy tabela (indeks) nadaje się do reorganizacji. Dla tabel prezentowane są trzy współczynniki F1, F2, F3: Tabela: DB2ADMIN.ADRES … F1 F2 F3 REORG … 63 99 -*- 0 Gwiazdka na drugiej pozycji oznacza, ze tabele powinna być poddana reorganizacji ze względu na współczynnik F2 . W nagłówku raportu współczynniki są opisane: F2: 100 * (Efektywne wykorzystanie obszaru stron danych) > 70 Wartość F2 równa 63 oznacza, że tabela ADRES zawiera dużą ilość (37%) pustych miejsc powstałych na skutek usuwania rekordów i powinna być zreorganizowana. 32 Jak sprawdzić czy dla mojego zapytania przydałby się jakieś indeksy? select imie, nazwisko from osoba o inner join adres a on o.id_osoby = a.id_osoby where a.miejscowosc = 'Bydgoszcz'; plik: zapyt.sql 1) Sprawdź plan wykonania 2) Uruchom db2advis db2advis -d sample -i zapyt.sql [-t #minutes] -o rekomendacje.out 2) Zastosuj rekomendacje korzysta z tabel utworzonych przez EXPLAIN.DDL 4) Sprawdź plan wykonania Narzędzie db2advis podpowiada, jaki indeksy warto utworzyć, by przyspieszyć działanie danego zapytania (dokładniej zbioru zapytań – „workload”). Narzędzie w raporcie przygotowuje listę najpotrzebniejszych indeksów, informując jednocześnie o procentowej poprawie kosztu zapytania oraz miejscu jakie potencjalnie zajmie dany indeks. Zamiast pliku (opcja -i zapyt.sql) możemy podać opcję -g, która sięgnie do pamięci podręcznej (cache) wykonywanych przez db2 zapytań. Pamięć podręczną możemy wyczyścić poleceniem: flush package cache dynamic. 33 Przykładowy fragment raportu wygenerowanego przez db2advis Zalecanie indeksów... łączne miejsce na dysku potrzebne dla zestawu początkowego: [ łączne miejsce na dysku ograniczone do [ 10,389] MB Wypróbowywanie wariacji zestawu rozwiązań. Optymalizacja ukończona. liczba indeksów w bieżącym rozwiązaniu: 2 Liczba jednostek timeron: [2314,0000] (bez zaleceń) liczba jednostek timeron: [801,0000] (w bieżącym rozwiązaniu) poprawa: [65,38%] 4,486] MB -- LISTA ZALECANYCH INDEKSÓW -- =========================== -- index[1], 1,837MB CREATE INDEX "DB2ADMIN"."IDX610181743370000" ON "DB2ADMIN"."ADRES" ("MIEJSCOWOSC" ASC, "ID_OSOBY" ASC) ALLOW REVERSE SCANS ; COMMIT WORK ; RUNSTATS ON TABLE "DB2ADMIN"."ADRES" FOR INDEX "DB2ADMIN"."IDX610181743370000" ; COMMIT WORK ; -- index[2], 2,649MB CREATE UNIQUE INDEX "DB2ADMIN"."IDX610181743560000" ON "DB2ADMIN"."OSOBA" ("ID_OSOBY" ASC) INCLUDE ("NAZWISKO", "IMIE") ALLOW REVERSE SCANS ; COMMIT WORK ; RUNSTATS ON TABLE "DB2ADMIN"."OSOBA" FOR INDEX "DB2ADMIN"."IDX610181743560000" ; COMMIT WORK ; Po wykonaniu db2advis -d baza1 -i zapyt.sql -o rekomendacje.out narzędzie zasugerowało dwa indeksy, jeden adres (miejscowość, id_osoby), oraz drugi osoba (id_osoby) z opcją include (nazwisko, imie). Przygotowany raport musimy poddać racjonalnej ocenie, uwzględniając korzyści, oraz zajmowane miejsce i koszt związany z utrzymywaniem indeksu. Pierwszy indeks ma sens ponieważ wyszukujemy po nazwisku. Do indeksu zostało dołączone pole id_osoby, ponieważ pole to jest wykorzystywane przy złączeniu, co także przyspieszy zapytanie, ponieważ baza nie będzie musiała sięgać do strony z danymi, by pobrać id_osoby (wystarczy jeden odczyt z indeksu). Drugi indeks właściwie już istnieje, ponieważ na polu id_osoby jest założony klucz główny. DB2 w rekomendacji proponuje dołączyć do klucza indeksowego dwa dodatkowe pola. Składnię INCLUDE można traktować dokładnie tak samo jak indeks kompozycyjny (w naszym przypadku na trzech polach: id_osoby, nazwisko, imie), z tą różnicą, że warunek unikalności będzie przestrzegany tylko na polu id_osoby. Zwróć uwagę, że narzędzie podaje ile miejsca będzie zajmował indeks, oraz na jaką poprawę wydajności można liczyć po założeniu ideksu. Po namyśle decydujemy się na utworzenie pierwszego z proponowanych indeksów: create index adres_miej_idx on adres ("MIEJSCOWOSC" asc, "ID_OSOBY" asc); runstats on table db2admin.adres for index db2admin.adres_miej_idx; Uwaga: nazwy obiektów w wygenerowanym skrypcie ujęte są w cudzysłów. Oznacza to, że baza będzie traktowała nazwy z rozróżnieniem dużych i małych liter (w takiej postaci są przechowywane w słownikach systemowych bazy danych). Jeśli będziesz modyfikował skrypt to usuń cudzysłów albo pisz nazwy obiektów dużymi literami. Jeśli nie użyjesz cudzysłowu baza przechowa nazwę obiektu w słownikach dużymi literami. 34 Rows Rows RETURN RETURN ( 1) ( 1) Cost Cost I/O I/O | | 51,9354 51,8829 NLJOIN NLJOIN ( 2) ( 2) 2313,79 997,775 1985,23 851,58 /-------+------\ /------+------\ 51,9354 1 51,8829 1 TBSCAN FETCH IXSCAN FETCH ( 3) ( 9) ( 3) ( 4) 1331,19 22,7168 15,1885 22,7168 1135,82 3 2 3 | /---+---\ | /---+---\ 51,9354 1 53311 53311 1 53311 SORT IXSCAN TABLE: DB2ADMIN INDEX: DB2ADMIN IXSCAN TABLE: DB2ADMIN ( 4) ( 10) OSOBA ADRES_MIEJ_IDX ( 5) OSOBA 1331,18 15,1505 15,1505 1135,82 2 2 | | | 51,9354 53311 53311 FETCH INDEX: DB2ADMIN INDEX: DB2ADMIN ( 5) OSOBA_ID_OSOBY_PK OSOBA_ID_OSOBY_PK 1331,16 1135,82 /---+---\ Plany wykonania zapytań przed i po 53311 53311 utworzeniu pierwszego indeksu RIDSCN TABLE: DB2ADMIN ( 6) ADRES rekomendowanego przez db2advis 444,681 286,808 | Łączny koszt zapytania zmniejszył się z 2313.79 na 997.775. W zapytaniu przed zastosowaniem rekomendacji największym czynnikiem kosztowym była konieczność sekwencyjnego skanowania całej tabeli (lewa strona diagramu; węzły (3),(4),(6)…). Po zastosowaniu rekomendacji wyszukanie miejscowości przestało być tak kosztowne, a największym czynnikiem kosztowym stało się złączenie z tabelą osoba -- węzeł (2) (co jest oczywiste, ponieważ w jednej miejscowości może mieszkać wiele osób). NLJOIN (nested loop join) jest takim rodzajem złączenia, w którym dla każdego rekordu ze zbioru wynikowego po lewej stronie doczytywany jest rekord po prawej stronie (na wzór pętli zagnieżdżonej w innej pętli). Po utworzeniu drugiego indeksu proponowanego w rekomendacjach (IDX610181743560000) koszt dodatkowo zmniejszy się, ponieważ złączenie będzie realizowane bez konieczności sięgania do stron z danymi (odczyt tylko z indeksu), tak jak prezentuje to diagram poniżej. Gdybyś dołączył do zapytania dodatkowy warunek filtrujący dane po nazwisku, wtedy koszt byłby jeszcze mniejszy. Pamiętaj, koszt danego zapytania zależy od rozkładu wartości. Dla danych testowych, które wygenerowałem w jednej miejscowości mieszkało kilkadziesiąt osób. W danych rzeczywistych możesz mieć tylko kilka miejscowości i setki tysięcy osób – wtedy tworzenie indeksu na miejscowości nie będzie miało większego sensu. W sytuacji kiedy dane są silnie zduplikowane przydaje się mechanizm wielowymiarowego klastrowania MDC albo partycjonowanie danych. Rows RETURN ( 1) Cost I/O | 51,8829 NLJOIN ( 2) 556,971 481,43 /------+------\ 51,8829 1 IXSCAN IXSCAN ( 3) ( 4) 15,1885 15,1507 2 2 | | 53311 53311 INDEX: DB2ADMIN INDEX: DB2ADMIN ADRES_MIEJ_IDX IDX610181743560000 35 Śledzenie procedury www.ibm.com/developerworks wyszukiwujemy "put_line" "A UDF for File Output and Debugging from SQL" Implementacja funkcji pozwalającej na zapisywanie komunikatów do pliku z wywołań SQL Jeśli będziesz chciał zapisywać komunikaty o wartości zmiennych do pliku, wtedy najlepiej jak sięgniesz po gotową funkcję napisaną w C.W DB2 możesz pisać procedury składowane w także w języku C i języku Java. Procedury napisane w języku C albo Java określane są jako zewnętrzne, ponieważ tworzone są przy wykorzystaniu zewnętrznego kompilatora. W wyniku kompilacji takiej procedury powstaje biblioteka, która jest wykonywana przez motor bazy danych. Procedury SQL PL (SQL Procedural Language) są interpretowane przez motor bazy danych (wewnętrzna kompilacja do pseudokodu). Na stornach portalu deweloperskiego technologii IBM możesz bez problemu odszukać kod funkcji put_line. 36 PUT_LINE - instalacja UDFa 1) Rozpakowujemy plik zip 2003-01-17 2003-02-24 2003-01-05 2003-01-17 2005-11-07 2003-02-24 2002-10-30 2003-01-17 17:32 19:46 20:05 22:57 22:13 20:59 15:28 17:28 1 153 crefunc.sql 13 638 put_line.c 138 put_line.def 77 877 PUT_LINE.dll 109 822 put_line.zip 30 096 put_line_w2k.ZIP 1 126 testcase.sqp 656 testcase2.sql 2) Rejestrujemy funkcję db2 connect to baza1 copy put_line.dll "%DB2PATH%\function\." db2 -tvf crefunc.sql 2) Definiujemy katalog gdzie funkcja będzie tworzyła pliki poprzez ustawienie zmiennej w systemie PUT_LINE_OUT na wartość, np. C:\temp Start-> Settings-> Control Panel-> System -> Advanced-> Environment Variables Na przedstawionym slajdzie przedstawiłem kroki, które szybko pozwolą ci utworzyć funkcję put_line. Dostarczony na stronie plik zawiera kody źródłowe oraz gotowe biblioteki (skompilowane dla wersji DB2 8). 37 PUT_LINE - instalacja UDFa 4) Restartujemy instancję db2stop force db2start 5) Testujemy funkcję db2 connect to baza1 db2 values put_line ('aaa','w','to jest tekst') db2 values put_line ('aaa','a','to jest drugi tekst') more aaa to jest tekst to jest drugi tekst Inną techniką śledzenia jest zapisywanie danych diagnostycznych do osobnej tabeli, choć niestety nie może to być tradycyjna tabela, ponieważ jeśli w procedurze wykonasz wycofanie transakcji, wtedy dane zostaną także usunięte z takiej tabeli. Rozwiązaniem jest utworzenie tabeli tymczasowej z klauzulami ON COMMIT PRESERVE ROWS i ON ROLLBACK PRESERVE ROWS. Niezależnie od wykonywanych w procedurze instrukcji COMMIT oraz ROLLBACK dane nie zostaną usunięte z tabeli tymczasowej. Oto przykład: declare global temporary table moj_log (id int, info varchar (150)) on commit preserve rows not logged on rollback preserve rows ; Byś mógł tworzyć tabele tymczasowe administrator musi wcześniej utworzyć przestrzeń tymczasową na dane użytkownika i nadać uprawnienie USE, które zezwala na tworzenie tabel w takiej przestrzeni. create user temporary tablespace usertemp1 managed by system using ('USERTEMP'); grant use of tablespace usertemp1 to public; Tabele tymczasowe zawsze tworzone są w schemacie SESSION i dlatego też należy podawać schemat operując na tabelach tymczasowych użytkownika, np. insert into session.moj_log values (1, 'rekord') ; select * from session.moj_log ; 38 Jak przekazać wartość? - poprzez parametry create procedure dodaj_osobe ( out p_id_osoby bigint, [..] set p_id_osoby = v_id_osoby; end @ create procedure wstawiaj() specific wstawiaj begin declare v_id_osoby bigint; declare v_status varchar(1); call dodaj_osobe( v_id_osoby, 'Dominik', 'Matejko', 'Bydgoszcz', 'Jasna', '100', '22345 '); values put_line( 'wstawiaj.out', 'a', char( v_id_osoby) ) into v_status; end @ Załóżmy, że nasza procedura dodała nową osobę wstawiając odpowiedni rekord to tabeli osoba i tabeli adres. W aplikacji (np. zewnętrznej procedurze) możesz chcieć dowiedzieć się jakie id_osoby zostało wygenerowane. Wygenerowany identyfikator można przekazać na zewnątrz wykorzystując parametr typu OUT, tak jak pokazałem na slajdzie (procedura dodaj_osobę). W zewnętrznym programie (w naszym przypadku w procedurze wstawiaj) zdefiniuj zmienną, którą podstaw pod parametr OUT w wywołaniu procedury dodaj_osobe. Procedura dodaj_osobe ustawi wartość tej zmiennej na wygenerowany identyfikator. 39 Parametry procedury CALL PROCEDURE1 IN INOUT OUT IN procedura nie zmieni wartości zmiennej wstawionej w to miejsce (w jęz. C przekazywanie parametrów przez wartość) OUT procedura nie odczyta wartości zmiennej wstawionej w to miejsce INOUT procedura potrafi odczytać wartość zmiennej, ale także zmienić wartość zmiennej (w jęz. C przekazywanie parametrów przez wskaźnik) W DB2 są trzy rodzaje parametrów procedur. Parametr IN kopiuje wartość podanego argumentu (tworzy lokalną kopię). Modyfikacja parametru IN jest dozwolona, choć nie wpłynie na wartość zmiennej podanej w argumencie wywołania. Parametr INOUT pracuje bezpośrednio na zadanym argumencie. Możemy go odczytać ale i możemy zmienić jego wartość. Taki rodzaj parametru można porównać do przekazywania parametrów przez wskaźnik w języku C. Parametr OUT wyłącznie przekazuje wartość. Można go porównać do parametru INOUT, który jest ustawiany na wartość nieokreśloną (tu: NULL) w momencie uruchomienia procedury. Jeśli chcesz przetestować wywołanie procedury mającej parametry IN oraz INOUT wtedy pod wartość takiego parametru podaj znak zapytania (?). W przykładzie poniżej w cele procedury P1 ustawiłem wartość parametru PAR1 na 923: db2 call p1(?) Wartości parametrów wyjściowych -------------------------Nazwa parametru: PAR1 Wartość parametru: 923 Status powrotu= 0 40 Przekazanie zbioru wynikowego create procedure zbior() specific zbior begin declare kur1 cursor with return for select id_osoby from osoba; open kur1; end @ Otwarcie kursora przekaże zbiór wynikowy oparty na zapytaniu Inną metodą przekazywania wyników w procedurze są tzw. zbiory wynikowe (result sets). Procedura DB2 może zwrócić wiele niezależnych zbiorów wynikowych. W ciele procedur definiujemy kursor z klauzulą with return i następnie otwieramy kursor. Kursor nie może być zamknięty. Zewnętrzny program odbiera rekord po rekordzie (fetch) zwrócony przez taką procedurę zbiór wynikowy. W przykładzie procedura zbior przekazuje wynik zapytania select id_osoby from osoba. Można sobie zadać pytanie, w jakim celu przekazywać zbiór wynikowy, skoro nadrzędny program może samemu wykonać to zapytanie?? Jednym z zastosowań zbiorów wynikowych jest kontrola dostępu. Użytkownik wykonujący procedurę nie musi posiadać uprawnienia do tabeli, wystarczy, że będzie miał uprawnienie do wykonywania procedury (EXECUTE). W takiej sytuacji za pośrednictwem procedury będzie mógł czytać dane z tabeli, do której nie ma bezpośredniego dostępu. 41 Przekazanie zbioru wynikowego create procedure zbior() specific zbior begin declare v_id_osoby bigint; set v_id_osoby = 999; begin declare kur1 cursor with return for with tmp(id_osoby) as ( values (v_id_osoby) ) select id_osoby from tmp; open kur1; end; end @ Tabela złożona z jednej wartości Dlaczego blok begin-end jest konieczny? Oto przykład prostej procedury, która buduje kursor na dynamicznie utworzonej niejawnej tabeli tymczasowej. Konstrukcja with tworzy niejawną tabelę tymczasową tmp (tzn. taką, która istnieje tylko na czas wykonywania tej instrukcji), która składa się z jednego rekordu utworzonego na postawie zmiennej v_id_osoby (tu: 999). Jak widać, można przekazywać zbiory wynikowe, które niekoniecznie budowane są na podstawie zapytania na tradycyjnej tabeli. Na slajdzie zadałem pytanie: „dlaczego blok begin-end jest konieczny?”. Odpowiedź jest prosta: sekcja deklaracji musi wystąpić zaraz na początku bloku, po słowie kluczowym begin. Wcześniej instrukcją set określiliśmy wartość zmiennej v_id_osoby, więc w tym samym bloku nie mogliśmy zadeklarować kursora. Zapytania rozpoczynające się od słowa kluczowego WITH określane są jako common table expressions (CTE). 42 Trochę o Common table expressions with temp AS ( select nazwisko, imie, row_number() over( order by ucase(nazwisko) desc) AS nr from osoba ) select nazwisko, imie, nr from temp where nr > 10 fetch first 5 rows only optimize for 1 roW ; Zapytania CTE pozwalają na przetwarzanie zbiorów wynikowych w z góry określonej kolejności. Najpierw przetwarzane są dane wewnątrz wyrażenia with (na slajdzie kod na niebiesko). Następnie przetwarzane jest zewnętrzne zapytanie select * from temp. Działają dużo bardziej wydajnie niż zapytania skorelowane. W przykładzie pole nr jest dynamicznie wygenerowane i zawiera koleje numery przypisane rekordom z tabeli osoba, które byłyby posortowane po nazwisku malejąco. W zewnętrznym zapytaniu pytamy się o te rekordy, dla który numer jest większy od 10. Klauzula fetch first powoduje, że baza zwróci tylko paczkę pięciu rekordów i będzie optymalizować zapytanie na jak najszybsze przekazanie początkowych rekordów, a nie wykonanie wewnętrznego zapytania w całości. Przeanalizujmy plan wykonania zapytania: Wysoki koszt 11296,5 odnosi się do całości zapytania, a nie do Rows RETURN ( 1) Cost I/O | 5 FILTER ( 2) 11269,5 1479 | 51591 FETCH ( 3) 11247,2 1479 /---+---\ 51591 51591 IXSCAN TABLE: DB2ADMIN ( 4) OSOBA 4819,22 632 | 51591 INDEX: DB2ADMIN OSOBA_NAZ_UQ pobrania pierwszych pięciu rekordów. Jak widać DB2 wykorzystało indeks, który był utworzony na polu generowanym – czyli jest dobrze. Jeśli przeanalizujemy plan wykonania zapytania, a dokładniej doczytamy szczegóły węzła (1), w którym opisane jest zwracanie wyników: 1) RETURN: (Return Result) Cumulative Total Cost: Cumulative First Row Cost: 11269,5 24,8634 zauważymy, że koszt pozyskania pierwszego rekordu jest bardzo niski (24). Zwrócenie więc pięciu rekordów tą metodą nie będzie kosztowne i wykona się bardzo szybko. 43 Przekazanie zbioru wynikowego - ciąg dalszy create procedure zbior() specific zbior begin declare global temporary table temp1 (id_osoby bigint) on commit preserve rows with replace; insert into session.temp1 values( 123); insert into session.temp1 values( 456); begin declare kur1 cursor with return for select id_osoby from session.temp1; open kur1; end; end @ Trzeba wcześniej utworzyć przestrzeń tymczasową dla danych użytkownika Ten przykład pokazuje, ze zbiór wynikowy można zbudować w całości dynamicznie. W cele procedury tworzymy tabelę tymczasową temp1. Ponieważ tabela tworzona jest z klauzulą with replace, powtórne uruchomienie procedury usunie i utworzy na nowo tabelę tymczasową temp1. W cele procedury budujemy zawartość tabeli tymczasowej a następnie przekazujemy zbiór wynikowy do nadrzędnej aplikacji. 44 Odebranie zbiorów wynikowych DECLARE rs1 RESULT_SET_LOCATOR VARYING; DECLARE rs2 RESULT_SET_LOCATOR VARYING; CALL procedure1(); ASSOCIATE RESULT SET LOCATOR (rs1, rs2) WITH PROCEDURE procedure1; ALLOCATE cursor1 CURSOR FOR RESULT SET rs1; ALLOCATE cursor2 CURSOR FOR RESULT SET rs2; FETCH FROM cursor1 INTO var1, var2 ; FETCH FROM cursor2 INTO var3 ; Na slajdzie pokazałem metodę odebrania zbiorów wynikowych w nadrzędnej procedurze. Najpierw deklarujemy lokatory zbiorów wynikowych (declare) Wykonujemy procedurę (call) Skojarzamy lokatory z nazwą procedury (associate) Alokujemy kursor dla każdego ze zbiorów wynikowych (allocate) Pobieramy rekordy ze zbiorów wynikowych, w taki sam sposób jak w przypadku zadeklarowanych kursorów (fetch). Każdy z języków programowania może mieć inny zestaw funkcji do przechwytywania zbiorów wynikowych. 45 Odebranie zbioru wynikowego create procedure przejmij() specific przejmij begin declare sqlstate char(5); declare v_id_osoby bigint; declare v_status varchar(1); declare rs_zbior result_set_locator varying; call zbior(); associate result set locator (rs_zbior) with procedure zbior; allocate kur1 cursor for result set rs_zbior; fetch from kur1 into v_id_osoby; while(sqlstate = '00000') do values put_line( 'przejmij.out', 'a',char( v_id_osoby) ) into v_status; fetch from kur1 into v_id_osoby; end while; close kur1; end Przykład procedury przechwytującej zbiór wynikowy przekazanych przez procedurę zbior, utworzoną na poprzednich slajdach. 46 Funkcja tabelaryczna create function id_osoby() returns table(id_osoby bigint) modifies sql data deterministic no external action language sql begin atomic declare v_id_osoby bigint; call dodaj_osobe( v_id_osoby, 'Ela', 'Matejko', 'Bydgoszcz', 'Jasna', '100', '22345 '); return( values(v_id_osoby)); end @ select id_osoby from table(id_osoby()) as t Inną ciekawą metodą na przekazywanie wyników jest użycie funkcji tabelarycznych (table function), czyli funkcji, które zwracają wirtualną tabelę. W przykładzie funkcja id_osoby wstawia osobę i jej adres za pomocą wcześniej utworzonej procedury, a następnie zwraca wygenerowany identyfikator w postaci dynamiczne tabeli, która zawiera jeden rekord. Oczywiście funkcja id_osoby powinna być sparametryzowana, by można było wstawić dowolne dane. Zapytanie select id_osoby from table(id_osoby()) as t pokazuje, w jaki sposób zewnętrzna aplikacja może odebrać wygenerowany przy wstawieniu identyfikator. W porównaniu z parametrami OUT, INOUT oraz zbiorami wynikowymi ta metoda ma jedną dużą zaletę. Aplikacja odbierająca wygenerowany identyfikator może użyć zwykłego zapytania SQL. 47 create function dodaj ( p_imie varchar(50), p_nazwisko varchar(50), p_miejscowosc varchar(80), p_ulica varchar(80), p_nr_domu varchar(15), p_kod_pocztowy char(5) ) returns table(id_osoby bigint) specific dodaj modifies sql data deterministic no external action language sql begin atomic declare v_id_osoby bigint default null; set v_id_osoby = ( select id_osoby from osoba where ucase(imie) = ucase(p_imie) and ucase(nazwisko) = ucase(p_nazwisko)); if v_id_osoby is null then set v_id_osoby = ( select id_osoby from new table(insert into osoba(imie,nazwisko) values (p_imie, p_nazwisko)) as t); end if; merge into adres [..] return( values(v_id_osoby)); end @ Oto odpowiednik procedury dodaj_osobe napisany w postaci funkcji tabelarycznej. Funkcje różnią się od procedur i nie wszystkie konstrukcje języka SQL PL, które można umieścić w procedurze będzie można umieścić w funkcji. Oto przykład wywołania takiej funkcji, które wstawi dane do tabeli osoba oraz adres i zwróci wygenerowany dla nich identyfikator. select id_osoby from table( dodaj( 'Dominik', 'Matejko', 'Bydgoszcz', 'Jasna', '100', char('22345')) char ) as t; W funkcji wykorzystałem instrukcję merge, dokładnie tą samą jak dla procedury dodaj_osobę. merge into adres using table( values ( v_id_osoby, p_miejscowosc, p_ulica, p_nr_domu, p_kod_pocztowy) ) tmp (id_osoby,miejscowosc,ulica,nr_domu,kod_pocztowy) on ( adres.id_osoby = tmp.id_osoby) when matched then update set (miejscowosc, ulica, nr_domu, kod_pocztowy) = (tmp.miejscowosc,tmp.ulica,tmp.nr_domu,tmp.kod_pocztowy) when not matched then insert (id_osoby,miejscowosc,ulica,nr_domu,kod_pocztowy) values (tmp.id_osoby,tmp.miejscowosc,tmp.ulica, tmp.nr_domu, tmp.kod_pocztowy); 48 Podsumowanie Przykład prostej procedury był tylko pretekstem do omówienia różnego rodzaju technik związanych z optymalizacją zapytań. Mam nadzieję, że przydadzą się Tobie. Uwagi bądź pytania dotyczące tego materiału możesz przesłać na adres: [email protected] COPYRIGHT/Prawa autorskie Dokument możesz wykorzystywać na własny użytek do pogłębiania wiedzy o DB2. Możesz udostępniać ten materiał innym osobom zainteresowanym tematyką DB2. Wykorzystanie części lub całości tego materiału we własnej publikacji wymaga zgody autora. Ostatnia aktualizacja: 20 październik 2006 49