Blog programisty w Oracle PL/SQL

Jest to blog eksperymentatora programisty w PL/SQL dla Oracle. Wszystkie kody tutaj zamieszczone mogą być dowolnie wykorzystywane i zmieniane. A jeśli Ktoś z Gości znajdzie błąd, będę niezwykle wdzięczny...
Zapisz

Szukaj na tym blogu

wtorek, 27 grudnia 2011

Kto koduje ten błądzi

Zdecydowaną większość z poniższych błędów i ja sam popełniałem

1.Niewłaściwa obsługa wyjątków

BEGIN
....
.....

EXCEPTION
WHEN OTHERS THEN
COMMIT ;

END;

Wprawdzie kompilator PL/SQL  w wersji 11g wykrywa instrukcje WHEN OTHERS THEN NULL, ale traktowane są jako ostrzeżenia i to przy włączonej opcji PLSQL_WARNINGS='ENABLE:ALL'. W skrajnych przypadkach dla zaharmonogramowanych jobów; brak obsługi wyjątków jest w stanie w wiecznej pętli uruchamiać błędny kod i skonsumować istotną część zasobów bazy
 
2. Brak NVL w algorytmach obliczeniowych
DECLARE
a INTEGER;
b INTEGER :=1;
BEGIN
DBMS_OUTPUT.PUT_LINE(a + b);
END:
inna sprawa, że wg mnie pola używane do obliczeń powinny być DEFAULT 0 NOT NULL. Nawet jeśli się to przestrzega, warto wstawiać NVL dla świętego spokoju..

Nieznajomość faktu, że niektóre funkcje agregujące np AVG zadziałają inaczej dla wartości NULL i NOT NULL
SELECT AVG(pole_liczbowe) FROM tabela
zadziała inaczej
SELECT AVG( NVL(pole_liczbowe,0)) FROM tabela
jeśli pole_liczbowe bedzie przyjmować wartości NULL

3 Nadużywanie DBMS_OUTPUT w  pętlach
4. Warunek LENGTH( zmienna_tekstowa) = 0 zamiast zmienna_tekstowa IS NULL

5. Deklarowanie zmiennych odpowiadających polom w tabeli bez korzystania z zakotwiczeń %TYPE. Szczególnie ciekawym przypadkiem jest inna maksymalna długość VARCHAR2 w polu tabeli i jako zmienna w kodzie

6. W obrębie bazy deklarowanie tabel z polami VARCHAR2 ( size CHAR)i VARCHAR2( size BYTE)

7. Nadużywanie dynamicznego SQL - chociaż czasami bywa niezbędny. Szczególnie niebezpieczny jest SQL wykonujący operacje DDL, np.  TRUNCATE TABLE, CREATE INDEX, DROP INDEX... Polecenia DDL wywołują niejawne zatwierdzanie transakcji, co może w sposób bardzo skuteczny zniszczyć logike transakcyjną aplikacji w PL/SQL

8. Nadawanie id rekordom w inny sposób niż przy użyciu sekwencji
9. Dokonywanie konwersji ze stringów na liczby i na daty bez podania masek - taki kod jest nieprzenośny zupełnie
10. Stosowanie zmiennych typu CHAR

11. Niepodawanie obiektom baz danych w kodzie PL/SQL nazw schematów. I to wszystkim bez wyjątku !!! Czasami w dużej bazie może pojawić się kilka tabel klienci w rożnych schematach i mamy pewne źródło problemów. Przy migracji i scalaniu baz podawanie ownerów mocno ułatwia życie... Dlatego też sądzę, że dla kluczowych tabel należy tworzyć publiczne synonimy coby mniej rozgarnięci userzy nie popełnili ich w swoich schematach...

12. Stosowanie wskazówek w zapytaniach.
SELECT /*+ INDEX( t idx_klienci_nazwa) */ FROM schemat.tbl_klienci t WHERE t.nazwa = vc_Nazwa
Po mojemu hintów już od bazy 10g nie powinno się używać - wbudowany optymalizator radzi sobie z optymalizacją dobrze. Zaszycie hinta niesie za sobą niebezpieczeństwa związane z przebudową, usunięciem, rozszerzeniem indeksów, przeprowadzką rzadkich indeksów na bitmapowe przy migracji na enterprise, zmianą liczebności zbiorów danych i sposobem ich przechowywania( np.partycjonowanie tabel) - co może spowodować spory spadek wydajności... Lepiej jest więcej czasu poświęcić na rozpracowanie opornego zapytania niż umieszczać wskazówki w kodzie...

13. Pogrzebmy w bebechach
select UPPER(username), UPPER (osuser), UPPER (program), UPPER( machine)
into vc_UserName, vc_OSUser, vc_Program, vc_Machine
from v$session
where sid = USERENV('sid');
Staranie się, aby unikać odwołań do tabeli słownikowych, a jeśli się tego nie uda to takowe wywołania wsadzić do osobnego pakietu i korzystać tylko i wyłącznie z funkcji pakietowych... Tak samo w przypadku pakietów systemowych - wywołania należy opakować

14. Uprawnienia
GRANT SELECT, INSERT, DELETE, UPDATE on fk.tbl_faktury TO PANI_BASIA;
GRANT SELECT ON fk.tbl_faktury TO PANI_JOLA.
Wszelakie uprawnienia do obiektów bazodanowych rozdzielać wyłącznie za pomocą ról, użytkownikom nie wolno nadawać ról bezpośrednio, to prowadzi do trudnego do opanowania chaosu w kodzie. Bywają czasami pojętni użytkownicy piszący własne procedury, dla nich można zrobić wyjątek, dla pozostałych lamerów nie

15.Przedrostki
select * from schemat.klienci... Stosowanie standaryzowanych przedrostków (tbl_, vw_, seq_, mv_, pdb_, pckg_ ) do obiektów baz danych - aby w wypadku choroby genialnego kodera lub błędów runtime dało się cokolwiek zrozumieć, co genialny twórca miał na myśli
16. Nadużywanie zapytań z dual. Widziałem złośliwy test:
CREATE TABLE DUAL AS
SELECT * FROM DUAL
  UNION ALL
SELECT * FROM DUAL
17. Używanie w kodzie funkcji  SYSDATE ( za wyjątkiem pieczątek czasowych), zamiast tego należy użyć parametrów 

piątek, 1 lipca 2011

Algorytmy rekurencyjne w SQL

 Możliwe?? Tak, wystarczy polubić klauzulę model..
Poniżej w dwóch zapytaniach są  algorytmy zapisane w wersji rekurencyjnej
  • ciąg Fibonacciego
  • silnia
  • algorytm Euklidesa do wyznaczania największego wspólnego podzielnika
Przedstawione są dwie techniki:
  •  modele iteracyjne -znakomicie nadają się do implementacji jednoargumentowych funkcji rekurencyjnych, a ich zapis jest niemal intuicyjny. Niestety przy większej liczbie parametrów pojawiają się problemy cyklicznego wyliczania komórek
  • modele nieiteracyjne  - formuły są nieco bardziej zawiłe, ale można obsłużyć funkcje wieloargumentowe
    SELECT                                                    
          d  , f AS FIBBONACCI,
          S AS SILNIA
      FROM   (SELECT   0 d  FROM DUAL)
    MODEL
       DIMENSION BY (0 d)
       MEASURES (0 f, 0 s)
       RULES
          ITERATE (10000000) UNTIL (ITERATION_NUMBER = 50)
           (f [ITERATION_NUMBER] =
                                CASE ITERATION_NUMBER
                                    WHEN 0 THEN 0
                                    WHEN 1 THEN 1
                                  ELSE
                                    f[ITERATION_NUMBER - 2] + f[ITERATION_NUMBER - 1]
                                END,
          s [ITERATION_NUMBER] =
                      CASE ITERATION_NUMBER
                        WHEN 0 THEN 1
                        ELSE           
                        ITERATION_NUMBER * s[ITERATION_NUMBER - 1]
                      END)




    Poniższy przykład można jeszcze nieco uprościć :)

    SELECT   L1, L2, NWD
      FROM   (SELECT   L1, l2
                FROM   (    SELECT   LEVEL - 1 L1
                              FROM   DUAL
                        CONNECT BY   LEVEL <= 20),
                       (    SELECT   LEVEL - 1 L2
                              FROM   DUAL
                              CONNECT BY   LEVEL <= 20))
    MODEL
       DIMENSION BY (L1, L2)
       MEASURES (0 NWD )
       RULES SEQUENTIAL ORDER
          (NWD [ANY, ANY] =
                CASE
                   WHEN CV (l2) = 0
                   THEN
                      CV (l1)
                   WHEN CV (l2) <= CV (l1)
                   THEN
                      NWD[CV (l2), MOD (CV (l1), CV (l2))]
                END,
          NWD [ANY, ANY] =
                CASE
                   WHEN CV (l2) > CV (l1) THEN
                            NWD[CV (l2), CV (l1)]
                   ELSE NWD[CV (l1), CV (l2)]
                END)

    niedziela, 29 maja 2011

    Trzy pułapki w PL/ SQL

    1. Modyfikator result_cache dla funkcji niedeterministycznych przekształca je na deterministyczne
    Funkcja deterministyczna jest to funkcja zwracająca dla takiego samego zbioru parametrów identyczny  wynik. Deterministyczności wymaga sie  od wyrażeń w indeksach  funkcyjnych i polach wyliczeniowych dla tabel.. Deterministyczność wyklucza użycie funkcji SYSDATE, funkcji losowych...

    Rozważmy poniższy przykład:
     Skompilujmy poniższą funkcję:
     CREATE OR REPLACE FUNCTION Fdb_Test RETURN DATE
    RESULT_CACHE
        IS
    BEGIN
        RETURN SYSDATE;
    END;




    Następnie wywołajmy pięć  razy w odstępach kilkusekundowych:
    SELECT Fdb_Test  FROM DUAL
    O ile bufor dla result cache jest nieprzepełniony, to funkcja zwróci nam za każdym wywołaniem taką samą wartość. Poniższe sprawdzenie pokaże czterokrotne użycie bufora(za pierwszym razem rzeczywiście wywoła się SYSDATE):
    SELECT SCAN_COUNT
       FROM  V$RESULT_CACHE_OBJECTS WHERE  NAMESPACE ='PLSQL' AND NAME LIKE '%FDB_TEST%'

    2. Struktury zakotwiczone %ROWTYPE zapewniają uproszczenie wstawiania..
    I z reguły jest to prawda
    Stwórzmy tabelę:
    CREATE TABLE TBL_TEST
          (ID NUMBER(4),
            OPIS VARCHAR2(50 CHAR)
           )
    Można wówczas dokonać wstawiania w bardzo elegancki sposób bez podawania listy kolumn...
    DECLARE
      vr_Test TBL_TEST%ROWTYPE;
    BEGIN
      vr_Test.ID  := 1;
      vr_Test.Opis := 'Opis';
      INSERT INTO TBL_TEST VALUES vr_Test;
      COMMIT;
    END;Zakotwiczenie przy wstawianiu będzie  dalej działało  przy dodaniu nowych pól, chyba, że to będą pola obliczeniowe..

    Po dodaniu formuły
    ALTER TABLE TBL_TEST  ADD FORMULA VARCHAR2(100 CHARGENERATED ALWAYS  AS ( TO_CHAR(ID) || ' ' || OPIS)
    przy próbie wykonania powyższego bloku pojawi się komunikat
    ORA-54013: INSERT operation disallowed on virtual columns
    Szkoda, że obie możliwości nie mogą działać jednocześnie (:

    3, Triggery DML blokują jedynie operacje DML
    Jeśli chcemy zablokować  w sposób dynamiczny np. usuwanie z tabel, to nie można użytkownikom końcowym nadawać żadnych uprawnień DDL do tabeli..
    Poniższy trigger
    CREATE OR REPLACE TRIGGER .TRG_TBL_TEST__NO_DELETE
    BEFORE DELETE
    ON
    TBL_TEST
    --w celu lepszej wydajności powinien to być triger  on statement, a nie FOR EACH ROW
    BEGIN
      IF EXTRACT
    ( HOUR FROM LOCALTIMESTAMP ) BETWEEN 8 AND 18 THEN
        RAISE_APPLICATION_ERROR (- 20100, 'Usuwanie w godzinach pracy z tabeli jest niedozwolone' );
      END IF;
    END TRG_TBL_TEST__NO_DELETE;
    nie zabezpieczy przed instrukcją DDL 
     TRUNCATE TABLE TBL_TEST
    jeśli użytkownik końcowy posiadać będzie bezpośrednio nadane uprawnienie ALTER TABLE

    środa, 27 kwietnia 2011

    NULL a poprawnośc algorytmów obliczeniowych..

    Wartość NULL bywa przyczyną wielu błędów w algorytmach  obliczeniowych - większość  z nich wynika  braku inicjacji zmiennych:
     Rozważmy trzy przykłady:
    1.Nie działa inicjalizacja dla typów zakotwiczonych w PL/SQL
    CREATE TABLE TBL_TEST
     (FIELD NUMBER DEFAULT 0 NOT NULL);

     DECLARE
       vn_Number TBL_TEST.FIELD%TYPE;
     BEGIN
       IF vn_Number IS NULL THEN
        DBMS_OUTPUT.PUT_LINE('Inicjalizacja nie zadziałała');
       END IF;
     END;
     Powyższy przypadek jest dość podstępny- zakotwiczenia pobierają tylko informację o typie bez checków...
    2.Funkcje agregujące

     CREATE TABLE TBL_TEST
     (FIELD NUMBER  );


    ciąg instrukcji insert, takze z wartością NULL;
    INSERT INTO TBL_TEST (FIELD) VALUES (NULL);
    INSERT INTO TBL_TEST (FIELD) VALUES (1);
    INSERT INTO TBL_TEST (FIELD) VALUES (8); 
    COMMIT;
    Wykonajmy teraz zapytanie:
    SELECT SUM(FIELD), AVG(FIELD), AVG(NVL(FIELD,0)) FROM TBL_TEST
    Otrzymane wyniki to
    9        4.5         3
    Powyższy wynik świadczy o tym, że funkcja AVG do liczenia średniej wybiera wartości tylko  NOT NULL.  Dlatego warto dokładnie przestudiować dokumentację opisującą działanie funkcji agregujących..

    3. Operatory arytmetyczne

    DECLARE
     vn_A NUMBER(2) :=1;
     vn_B NUMBER(2) ;
     vn_C NUMBER(3);
    BEGIN
      vn_C := vn_A + vn_B;
      IF vn_C IS NULL THEN 
        DBMS_OUTPUT.PUT_LINE('RESULT IS NULL');
       END IF;
    END
    Dla operatorów arytmetycznych i wbudowanych funkcji matematycznych np. ABS i SIN, jeśli co najmniej jeden parametr jest NULL, to zwracana jest także wartość NULL..

    Rozpatrując powyższe przykłady, nasuwa się oczywisty wniosek - należy unikać wartości NULL w algorytmach obliczeniowych.... są one źródłem bardzo złośliwych i podstępnych błędów
    Proponuję zastosowanie poniższych zaleceń:
    • na etapie projektowania struktury baz danych dla pól tabel wykorzystywanych w algorytmach numerycznych powinno się wymuszać wartość  NOT NULL.  Wartość domyślna jest niekonieczna, niech lepiej zostanie bezpośrednio zainicjowana w kodzie
    •  należy zamiast zmiennych typu NUMBER, BINARY_INTEGER stosować zmienne typu SIMPLE_DOUBLE, SIMPLE_INTEGER, SIMPLE_FLOAT oraz inne z przedrostkiem SIMPLE, które wymagają inicjacji na poziomie deklaracji.. Wykonanie poniższego bloku
           DECLARE
               vn_Test SIMPLE_INTEGER;
           BEGIN
               vn_Test := 0;
           END;
    zakończy się błędem: PLS-00218: a variable declared NOT NULL must have an initialization assignment. oprócz tego jakiekolwiek przypisanie wartości NULL do zmiennej vn_Test też wygeneruje wyjątek... Wyjątki wyłowią niebezpieczne fragmenty kodu w algorytmach..
    • Jeśli musimy użyć typu NUMBER(12,2) to wówczas najlepiej stworzyć  nagłówek pakietu do definicji własnych typów
         CREATE OR REPLACE PACKAGE PCKG_MyTypes IS
              SUBTYPE t_Number_12_2 IS NUMBER(12,2) NOT NULL;
         END;
         wówczas wykonanie poniższego bloku
         DECLARE
            vn_Test PCKG_MyTypes.t_Number_12_2;
         BEGIN
            vn_test := 0;
       END;
       także zakończy się wyjątkiem : PLS-00218: a variable declared NOT NULL must have an initialization assignment

    środa, 13 kwietnia 2011

    DBMS_METADATA - generowanie skryptów DDL

    Bardzo ciekawym pakietem dostarczonym z bazą danych jest DBMS_METADATA – jego głównym zadaniem jest tworzenie skryptów DDL dla różnych obiektów bazodanowych..

    Dokumentacji nie będę przepisywać, ale przedstawię kilka zastosowań

    Należy zauważyć, że wywołania w instrukcjach SQL umożliwiają ograniczone możliwości wykorzystania API, skrypty PL/SQL pozwalają na bardziej zaawansowane mozliwości tworzenia skryptów DDL

    • Skrypt do tworzenia listy uprawnień dla bieżącego użytkownika:
    SELECT
    DBMS_METADATA.GET_GRANTED_DDL('ROLE_GRANT', USER)
                ||
    DBMS_METADATA.GET_GRANTED_DDL('SYSTEM_GRANT', USER)
                ||
    DBMS_METADATA.GET_GRANTED_DDL('OBJECT_GRANT', USER)
    FROM DUAL
    • Skrypt do tworzenia indeksów w bieżącym schemacie
    SELECT DBMS_METADATA.GET_DDL('INDEX', INDEX_NAME, USER) ||';' FROM USER_INDEXES WHERE VISIBILITY = 'VISIBLE'
    • Skrypt do tworzenia bieżącego użytkownika
    SELECT DBMS_METADATA.GET_DDL('USER', USER)
    FROM DUALpod warunkiem, że użytkownik nie jest SYS/SYSDBA
    • Przeniesienie tabel  ze schematu SOURCE_SCHEMA  do schematu DESTINATION_SCHEMA wszystkich z tabel z nazwami zaczynającymi się na B, z pominięciem atrybutów storage i  kluczy obcych…
    zacznijmy od  stworzenia tabeli na wynikowy skrypt
    CREATE  TABLE TBL_DDL_DATA(
    DDL_SCRIPT CLOB DEFAULT EMPTY_CLOB()
    );

    poniżej skrypt
    DECLARE
    v_hdl NUMBER;
    v_th1 NUMBER;
    v_th2 NUMBER;
    v_doc sys.ku$_ddls := sys.ku$_ddls();
       BEGIN
          delete from T_BLOB;
          v_hdl := DBMS_METADATA.OPEN('TABLE', 'LATEST');
          DBMS_METADATA.SET_FILTER (v_hdl,'SCHEMA_EXPR','
    IN( ''SOURCE_SCHEMA'')');
          DBMS_METADATA.SET_FILTER (v_hdl,'NAME_EXPR','LIKE ''B%''');
          v_th1 := DBMS_METADATA.ADD_TRANSFORM (v_hdl,'MODIFY', NULL, 'TABLE');
          DBMS_METADATA.SET_REMAP_PARAM(v_th1,'REMAP_SCHEMA', 'SOURCE_SCHEMA', 'DESTINATION_SCHEMA');
          v_th2:=DBMS_METADATA.ADD_TRANSFORM(v_hdl,'DDL');
          DBMS_METADATA.SET_TRANSFORM_PARAM(v_th2,    'SQLTERMINATOR', TRUE);
    DBMS_METADATA.SET_TRANSFORM_PARAM(v_th2,    'SEGMENT_ATTRIBUTES', FALSE);
    DBMS_METADATA.SET_TRANSFORM_PARAM(v_th2,'REF_CONSTRAINTS', FALSE, 'TABLE');
    DBMS_OUTPUT.PUT_LINE(DBMS_METADATA.GET_QUERY(v_hdl));
    LOOP
       
        v_doc := DBMS_METADATA.FETCH_DDL(v_hdl);
        IF v_doc IS NOT EMPTY THEN
            IF v_doc.COUNT >0 THEN
                INSERT INTO TBL_DDL_DATA(DDL_SCRIPT)
                VALUES (v_doc(v_doc.last).DDLTEXT);
            END IF;
        END IF;
    EXIT
    WHEN v_doc IS NULL;
    END LOOP;
    COMMIT; 
    DBMS_METADATA.CLOSE(v_hdl);
    END;

    Jeśli chcemy wykonać skrypt, zaleca się posiadać rolę CREATE TABLE do schematu docelowego

    Moje uwagi:
    • z poziomu SQL nie ma możliwości parametryzacji skryptów - pominięcia parametrów storage, ustawienia filtra, zmiany schematu - to wszystko można zrobić w PL/SQL
    • Skrypty  dla lokalnych dla tabeli partycjonowanych działają dobrze pod warunkiem, że owe tabele są puste(zgodnie z założeniem pakiety – tworzenie  struktur, a nie ich rozbudowywanie)
    • Skrypty DDL dla tabel partycjonowanych  interwałowo niekiedy są generowane błędnie
    • Dłuższe skrypty opłaca się zapisywać do pól CLOB
    • Pakiet jest dość powolny