Jemný úvod do jazyka PL/pgSQL PostgreSQL

Návrh UDF funkcí

Pavel Stěhule

České vysoké učení technické v Praze, Fakulta stavební

Obsah

1. Úvod
2. Návrh vlastních PL/pgSQL funkcí
3. Použití PL/pgSQL pro správu databáze
4. Použití PL/pgSQL funkcí v CHECK podmínkách
5. Použití PL/pgSQL funkcí při návrhu vlastních operátorů
6. Použití PL/pgSQL funkcí při návrhu vlastních agregačních funkcí
7. Návrh funkcí triggrů v PL/pgSQL
8. Použití PL/pgSQL funkcí s parametry typu tabulka
9. Funkce vracející tabulky
10. Rekurzivní volání SRF funkcí
11. Návrh polymorfních funkcí, používání polymorfních typů
12. Změny ve verzi 8.x.
13. Kdy nepoužívat PL/pgSQL

Abstrakt

RDBMS PostgreSQL umožňuje (stejně jako většina obdobných RDBMS) navrhovat a používat tzv. uložené procedury (Stored procedure). Uložené procedura je kód, který je uložen a spouštěn SQL serverem. Pro PostgreSQL můžeme uložené procedury psát v některém z následujících programovacích jazyků: SQL, Perl, Python, TCL, PL/pgSQL. Ačkoliv je PL/pgSQL pouze jedním z jazyků, který můžeme použít při návrhu uložených procedůr, je patrně nejpoužívanější. Jedná se o jednoduchý programovací jazyk navržený pouze pro psaní uložených procedur RDBMS PostgreSQL. Je to jazyk ne nepodobný Module, s úzkou vazbou na vlastní RDBMS.

1. Úvod

PL/pgSQL nezavádí nové typy a vlastní funkce. Obojí sdílí s RDBMS. Funkce v PL/pgSQL mohou obsahovat většinu parametrizovaných SQL příkazů: pro správu tabulek, databází i jednotlivých záznamů. PL/pgSQL má konstrukci pro iteraci napříč množinou záznamů specifikovanou příkazem SELECT. V PL/pgSQL můžeme konstruovat SQL příkazy a pak je nechat provádět. Autoři PL/pgSQL se zjevně inspirovali jazykem PL/SQL, který je nativní prog. jazyk pro RDBMS Oracle, a tak není příliš obtížné konvertovat uložené procedury z Oracle do PostgreSQL a naopak.

V PostgreSQL můžeme PL/pgSQL použít k implementaci vlastních agregačních i normálních funkcí, operátorů, k implementaci procedur triggerů. Možnosti vytváření uložených procedur se každou novou verzí zvětšují, proto se i vyvýjí příkaz CREATE FUNCTION, kterým definujeme novou funkci. Popis se vztahuje na verzi 7.3.

CREATE [OR REPLACE] FUNCTION název (typ_argumenty [,...]) 
  RETURNS návr_typ AS '
    tělo fce
  'LANGUAGE 'plpgsql'
  [EXTERNAL SECURITY INVOKER | EXTERNAL SECURITY DEFINER]
[Tip]Tip

Počínaje verzí 7.2.1 můžeme použít frázi CREATE OR REPLACE. V předchozích verzích jsme museli před opakovaným vytvořením funkce funkci zrušit příkazem DROP FUNCTION. Ve verzi 7.3 můžeme určit s jakými právy se bude funkce vykonávat. Funkce získá buďto práva uživatele, který funkci spouští (EXTERNAL SECURITY INVOKER) nebo práva vlastníka funkce (EXTERNAL SECURITY DEFINER).

[Tip]Tip

Zřejmě počínaje verzí 7.3 již není nutné uvádět název jazyka v apostrofech, tj. je možné a doporučené psát funkci ve tvaru

CREATE [OR REPLACE] FUNCTION název (typ_argumenty [,...]) 
  RETURNS návr_typ AS '
    tělo fce
  'LANGUAGE plpgsql

Doporučuji psát kód funkcí v libovolném textovém editoru produkujícím čistý text. Kód uložit do souboru a psql příkazem \i název_souboru provést příkaz (příkazy) uložené v souboru vedoucí k vytvoření funkce. Rozhodně nedoporučuji psát funkce přímo v psql.

[Caution]Výstraha

PL/pgSQL nelze použít k návrhu vlastních datových typů.

K tomu abychom mohli používat PL/pgSQL musí být tento jazyk tzv. povolen pro danou databázi. Příkaz createlang -l vaše_db by měl vypsat tabulku obsahující řádek plpgsql | t. Pokud vrácená tabulka tento řádek neobsahuje, musí se používání PL/pgSQL pro danou databázi umožnit příkazem (příkaz může provést pouze uživatel s právy Postgres super uživatele).

createlang plpgsql vaše_db

Jestliže PL/pgSQL používat můžete, zkuste si napsat jednoduchou funkci Součet:

CREATE OR REPLACE FUNCTION Soucet(int,int) RETURNS int AS '
BEGIN
  /*
    Moje první triviální PL/pgSQL funkce.
  */
  RETURN $1 + $2;
END;
' LANGUAGE 'plpgsql'; 

Pokud funkci přepíšete bezchybně, pak můžete funkci otestovat příkazem SELECT soucet(10,10). V sloupci soucet (jmenuje se stejně, jako funkce) se objeví hodnota 20. Komentáře se v PL/pgSQL zapisují stejně, jako v SQL - jednořádkové začínají zdvojenou pomlčkou, víceřádkové mají C notaci, tj. /* komentář */. Provádění každé funkce v PL/pgSQL musí být ukončeno příkazem RETURN

Funkce má dvě části - deklaraci proměnných (ta zatím nebyla použitá) a vlastní tělo, tj. seznam příkazů (blok) oddělených středníkem vložený mezi dvojici klíčových slov BEGIN a END. Příkaz RETURN ukončí vykonávání funkce a jako výsledek vrátí hodnotu výrazu. Symboly $1 a $2 se používají ve významu hodnoty prvého a druhého argumentu funkce. Parametry funkce nelze modifikovat. Lze ale snadno vytvořit lokální kopie funkčních argumentů (verze 7.5 a vyšší). V stavajích verzích PostgreSQL včetně verze 7.3 nelze vrátit typ tabulka. Blok může obsahovat jiné - vnořené bloky. Bloky se, na rozdíl od Pascalu, nepoužívají k vymezení seznamu příkazů v konstrukcích IF, WHILE, FOR, LOOP, ale pouze k vymezení existence některých lokálních proměnných.

V PostgreSQL lze funkce přetěžovat, tj. můžeme mít definováno několik funkcí lišících se od sebe počtem a typy argumentů.

U identifikátorů, stejně tak i u klíčových slov, lze bez omezení používat velká i malá písmena. Při kompilaci se veškeré řetězce vyjma řetězců mezi zdvojenými apostrofy převádí na malá písmena.

Operátor přiřazení má v PL/pgSQL podobu symbolu :=. Kromě toho lze proměnné přiřadit výsledek SQL dotazu konstrukcí SELECT INTO

SELECT INTO cíl výraz FROM ...;

Cílem může být proměnná typu record nebo row, nebo seznam proměnných. Při provádění se kontroluje zda cíl odpovídá výrazu (počtem a typy). Pokud je výsledkem prázdná množina, pak všechny cílové proměnné nabudou hodnoty NULL. Pokud je výsledkem více řádků, pak se použíjí hodnoty z prvého řádku.

Kromě testu na NULL, který nemusí být vždy jednoznačný, můžeme testovat hodnotu vestavěné proměnné FOUND, která obsahuje hodnotu TRUE, pokud dotaz vrátil alespoň jeden řádek. Také lze po každém SQL příkazu uložit počet zpracovaných řádek do proměnné příkazem GET

GET DIAGNOSTICS promenna = ROW_COUNT;

Test proměnné FOUND lze provést pouze po příkazu SELECT INTO. Následující dva příklady vrací počet řádek v tabulce jména. V prvním případě se pro provedení SQL příkazu použila konstrukce PERFORM. Ta slouží k vykonání SQL příkazu nebo funkce v těch případech, kdy nedochází k dalšímu zpracování vrácené hodnoty.

CREATE OR REPLACE FUNCTION radku1() RETURNS int AS '
  DECLARE r int;
  BEGIN
    PERFORM * FROM jmena;
    GET DIAGNOSTICS r = ROW_COUNT;
    RETURN r;
  END;
' LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION radku2() RETURNS int AS '
  DECLARE r int;
  BEGIN
    SELECT INTO r count(*) FROM jmena;
    RETURN r;
  END;
' LANGUAGE 'plpgsql';

Za příkazem DECLARE můžeme deklarovat seznam dvojic proměnná typ oddělených středníkem. Kromě toho lze proměnné přiřadit hodnotu (počítá se při každém vstupu do bloku), lze ji zákazát přiřazení hodnoty NULL a také ji můžeme označit jako konstantní. Při porušení těchto pravidel,prostředí PL/pgSQL přeruší běh procedury. Všechny proměnné označené jako NOT NULL musí mít určenou DEFAULT hodnotu.

DECLARE jméno [CONSTANT] typ [NOT NULL][DEFAULT|(:=)hodnota]
[Note]Poznámka

Počínaje verzí 7.5 může hodnota obsahovat výraz obsahující parametry funkce.

CREATE OR REPLACE foo(varchar, integer) RETURNS varchar AS '
    DECLARE v varchar DEFAULT $1; i integer := $2;
  BEGIN
    RETURN v || '' '' || CAST(i AS VARCHAR);
  END;
' LANGUAGE plpgsql;

Kromě běžných typů můžeme použít typ RECORD, který může obsahovat řádek libovolné tabulky nebo tzv. odvozené typy. Ty vycházejí buďto z řádku konkrétní tabulky (název_tab%ROWTYPE), sloupce (název_tab.jm_sloupce%TYPE) nebo dříve deklarované proměnné (proměnná%TYPE). K jednotlivým položkám typu RECORD nebo řádek se přistupuje skrze tečkovou notaci, tj. proměnná.položka.

Kód PL/pgSQL může obsahovat libovolný SQL příkaz. Příkladem může být funkce vracející název měsíce na základě jeho indexu.

CREATE OR REPLACE FUNCTION MonthName1 (INTEGER) RETURNS VARCHAR(10) AS '
  DECLARE vysledek RECORD;
  BEGIN
    IF $1 <1 OR $1 > 12 THEN
      RAISE EXCEPTION ''Parametr je mimo přípustné meze\n'';
    END IF;
    SELECT INTO vysledek 
      CASE $1
        WHEN  1 THEN ''Leden''
        WHEN  2 THEN ''Únor''
        WHEN  3 THEN ''Březen''
        WHEN  4 THEN ''Duben''
        WHEN  5 THEN ''Květen''
        WHEN  6 THEN ''Červen''
        WHEN  7 THEN ''Červenec''
        WHEN  8 THEN ''Srpen''
        WHEN  9 THEN ''Září''
        WHEN 10 THEN ''Říjen''
        WHEN 11 THEN ''Listopad''
        WHEN 12 THEN ''Prosinec''
      END::VARCHAR(10) AS retval;
    RETURN vysledek.retval;
  END;
' LANGUAGE 'plpgsql';
[Note]Poznámka

Řetězce v PL/pgSQL se zapisují mezi zdvojené apostrofy. Není to příliš čitelné, díky tomu ale PostgreSQL může nabízet víc programovacích jazyků pro UDF funkce. Pokud by se Vám zdálo, že apostrofů je přílis, můžete dvojici apostrofů nahradit \', tj. zpětné lomítko a apostrof.

Tím libovolným SQL příkazem byl v tomto příkladě příkaz CASE. Ačkoliv se vrací pouze řetězec, je nutno použít typ RECORD, jelikož příkaz CASE vždy vrací RECORD (byť o jednom prvku). Kromě příkazu CASE se v příkladu použije konstrukce IF THEN END IF a příkaz RAISE EXCEPTION. Konstrukce IF THEN END IF je jasná. Počínaje verzí 7.2 můžeme používat podmínky ve tvaru IF THEN ELSIF THEN ELSE END IF. Prvním argumentem příkazu RAISE je úroveň vyjímky. K dispozici jsou tři možnosti: DEBUG - zapíše se do logu, NOTICE - oznámí se uživateli, EXCEPTION - přeruší se vykonávání funkce. Druhým parametrem je text chybového hlášení (text zapisujeme mezi zdovojené apostrofy). Pokud se v textu objeví symbol %, pak se tento symbol nahradí odpovídající proměnnou, která se předá jako třetí, čtvrtý, atd. argument. V PL/pgSQL není možné zachytit vyjímku, tj. každá výjimka na úrovni EXCEPTION vede k přerušení provádění funkce.

[Caution]Výstraha

jako argumenty lze použít pouze proměnné, nikoliv výrazy, tj. pokud chceme zobrazit výraz, musíme jej napřed přiřadit do proměnné.

Všimněte si, že příkaz CASE je parametrizován, tj. obsahuje proměnnou. Parametry lze použít u všech SQL příkazů, všude tam, kde se vyskytuje nějaká hodnota. Nelze parametrizovat názvy sloupců a tabulek. Toto omezení lze obejít (za cenu sníženého výkonu a zvýšené pracnosti) tzv. dynamickými dotazy (viz. níže příklad EXECUTE).

[Caution]Výstraha

Pokud chceme přistupovat z PL/pgSQL funkcí do dočasných tabulek, je použití EXECUTE nezbytné. PL/pgSQL funkce jsou předkompilované a drži si absolutní identifikátory tabulek (včetně dynamických, která se ovšem mění).

Následující příklad vytvoří tabulku s hodnotami fce sin v zadaném rozsahu. Používá konsrukci WHILE a parametrizovaný SQL příkaz INSERT. Dále ukazuje, jak lze ve funkcích vytvářet tabulky. Tabulka tabsin se vytvoří pouze v případě, že dosud neexistovala.

CREATE OR REPLACE FUNCTION tabsin(float,float,float) RETURNS BOOL AS '
  DECLARE i NUMERIC(5,4); krok NUMERIC(5,4); do NUMERIC(5,4);
  BEGIN
    IF NOT EXISTS(SELECT relname FROM pg_class
      WHERE relname = ''tabsin'' AND relkind=''r'') THEN
      RAISE NOTICE ''Vytvářím tabulku tabsin'';
      CREATE TABLE tabsin (x NUMERIC(5,4) PRIMARY KEY, fx float);
    ELSE 
      RAISE NOTICE ''Ruším všechny zaznamy v tabulce tabsin'';
      TRUNCATE TABLE tabsin;
    END IF;
    i := CAST($1 AS NUMERIC(5,4));
    do := CAST($2 AS NUMERIC(5,4));
    krok := CAST($3 AS NUMERIC(5,4));
    WHILE i <do LOOP
      INSERT INTO tabsin VALUES(CAST(i AS NUMERIC(5,4)), SIN(CAST(i AS float)));
      i := i + krok;
    END LOOP;
    RETURN TRUE;
  END;
' LANGUAGE 'plpgsql';

Vytvoření tabulky pro interval od -1 do 1 s krokem 0.0001 provede příkaz select tabsin(-1.0, 1.0, 0.0001). Provádění cyklu je rychlé. INSERT dvaceti tisíců řádek trvá na zhruba dvacet vteřin na mé staříčkém 2xP160. Požadovanou hodnotu funkce zjistíme příkazem SELECT

SELECT * FROM tabsin WHERE x=0.1234::NUMERIC(5,4);
[Note]Poznámka

Příkaz TRUNCATE nelze použít v PL/pgSQL ve verzi 7.3. Ve verzi 7.2 není jeho použití doporučeno. Opět je lze používat ve verzi 7.4.

Cyklus LOOP ... END LOOP můžeme opustit příkazem EXIT s volitelným návěstím (specifikuje cyklus, který má být přerušen) a volitelnou podmínkou.

<<hlavni>>
  LOOP
    EXIT hlavni WHEN c > 10;
    RAISE NOTICE ''%'', c;
    c := c + 1;
  END LOOP;

2. Návrh vlastních PL/pgSQL funkcí

To nejdůležitější na PL/pgSQL funkcích (resp. všech UDF funkcích) je fakt, že je můžeme použít v dotazech - zjednodušší se tím celý proces zpracování dat. Údaje můžeme zprocesovat ještě na serveru.

Jedna z funkcí, která v PostgreSQL chybí je určení velikonoc. Používá se pro určení státních svátků v plánovacích funkcích. Použil jsem Carterterův algoritmus pro určení velikonoční neděle.

CREATE OR REPLACE FUNCTION velnedele(INTEGER) RETURNS DATE AS '
DECLARE B INTEGER; D INTEGER; E INTEGER; Q INTEGER;
  DD INTEGER; MM INTEGER;
BEGIN
    IF $1 < 1900 OR $1 > 2099 THEN
      RAISE EXCEPTION ''Out of range'';
  END IF;
  B := 255 - 11 * ($1 % 19); D := ((B - 21) % 30) + 21;
  IF D > 38 THEN D := D - 1; END IF;
  E := ($1 + $1/4 + D + 1) % 7; Q := D + 7 - E;
  IF Q < 32 THEN DD:=Q; MM := 3; ELSE DD := Q - 31; MM := 4; END IF;
  RETURN to_date(to_char(DD, ''00'') || 
    to_char(MM, ''00'') || to_char($1,''0000''), '' DD MM YYYY'');
END;
' LANGUAGE plpgsql; 

Dalším případem, kdy je použití UDF funkce prakticky nezbytností, je použití čistících funkcí pro párování záznamů v tabulce. Tyto funkce představují poslední záchranu, když máme provádět výběry nad historickými databázemi, kde si nikdo nelámal hlavu s doménovou integritou. V přikladu pracuji s sloupcem hodnot, kde ekvivalentní by mohly být záznamy: Vyhl, Vyhl., Vyhláška, Vyhl.č.j., vyhl. čj., občas chybí mezera, sem tam jsou navíc.

CREATE OR REPLACE FUNCTION clean(VARCHAR) RETURNS VARCHAR AS '
DECLARE pom varchar DEFAULT $1;
BEGIN pom := to_ascii(lower(trim(both FROM pom)));
  pom = replace(pom,''c.'','' '');
  pom = replace(pom,''j.'','' '');
  pom = replace(pom,''cj.'','' '');
  pom = replace(pom,''cj '','' '');

  pom = replace(pom,''vyhl.'',''vyhlaska '');
  pom = replace(pom,''vyhl '',''vyhlaska '');
  pom = replace(pom,''.'','''');

  WHILE position(''  '' IN pom) <> 0 LOOP
    pom := replace(pom, ''  '','' '');
  END LOOP;
  RETURN trim(both FROM pom);
END;
' LANGUAGE plpgsql;

3. Použití PL/pgSQL pro správu databáze

Vhodně navržené uložené procedury znatelně zjednoduší správu databáze. Následující funkci používám pro vyčištění RDBMS od databází, které vytvoří studenti během semestru. Pokud se funkce spustí bez parametrů zobrazí nápovědu, stejně tak, pokud nesouhlasí počet argumentů.

CREATE OR REPLACE FUNCTION drop_students_databases() RETURNS INTEGER AS '
DECLARE
  helpstr VARCHAR(300);
BEGIN
  helpstr  := ''Funkce pro zrušení všech databází uživatelů jejichž\n'' ||
    ''jméno (jméno uživatele) vyhovuje masce příkazu LIKE.\n\n'' ||
    ''např. SELECT drop_students_databases('' || quote_literal(''group%'') || '')\n\n'' ||
    ''pozn. musíte být přihlášen jako uživatel postgres.\n'' ||
    ''autor: Pavel Stěhule, stehule@kix.fsv.cvut.cz 19.1.2002\n'';

  RAISE NOTICE ''%'', helpstr;
  RETURN 0;
END;
' LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION drop_students_databases(varchar(100)) RETURNS INTEGER AS '
DECLARE 
  db RECORD;
  deleted INTEGER := 0;
  helpstr VARCHAR(300);
BEGIN 
  IF length($1) = 0 OR $1 ISNULL OR current_user() <> ''postgres'' THEN
    helpstr  := ''Funkce pro zrušení všech databází uživatelů jejichž\n'' ||
      ''jméno (jméno uživatele) vyhovuje masce příkazu LIKE.\n\n'' ||
      ''např. SELECT drop_students_databases('' || quote_literal(''group%'') || '')\n\n'' ||
      ''pozn. musíte být přihlášen jako uživatel postgres.\n'' ||
      ''autor: Pavel Stěhule, stehule@kix.fsv.cvut.cz 19.1.2002\n'';
    IF current_user <> ''postgres'' THEN
      RAISE EXCEPTION ''Funkci může volat pouze uživatel postgres\n\n%'', helpstr;
    ELSE
      RAISE EXCEPTION ''Argument funkce musí být neprázdný řetězec\n\n%'', helpstr;    
    END IF;
  END IF;
  
  FOR db IN 
    select datname, usename from pg_database, pg_user 
      where datdba=usesysid and usename like $1
  LOOP
    deleted := deleted + 1;
    RAISE NOTICE ''Drop database %,%'', db.datname, db.usename;
    EXECUTE ''drop database "'' || db.datname || ''"'';
  END LOOP;
  RETURN deleted;
END;
' LANGUAGE 'plpgsql';

Všimněte si konstrukce FOR IN LOOP END LOOP, která iteruje napříč výsledkem SQL dotazu (v tomto případě seznamu databází jejiž vlastnící vyhovují masce LIKE). Jelikož tabulky mohou obsahovat i mezery, je třeba název tabulky vložit do uvozovek. PL/pgSQL má problémy s vykonáváním příkazu DROP DATABASE pokud název databáze obsahuje mezeru. Problém můžeme obejít tak, že celý SQL příkaz uložíme do řetězce a necháme provést příkazem EXECUTE. EXECUTE můžeme použít ve všech případech, kdy PL/pgSQL odmítne SQL příkaz provést.

[Note]Poznámka

Počínaje verzí 7.3 již nebude nutné uvnitř funkce zjišťovat, zda-li funkci spouští oprávněný uživatel. V této verzí můžeme příkazem GRANT určit, kdo má oprávnění spouštět funkci.

4. Použití PL/pgSQL funkcí v CHECK podmínkách

Funkce v PL/pgSQL můžeme použít jako kontrolní v CHECK podmínkách. Tyto funkce musí mít pouze jeden argument odpovídajícího typu a pokud je hodnota argumentu NULL, pak musí vrátit NULL. Toto chování lze ošetřit programově, nebo použít atribut isstrict, který zajistí, že kdykoliv je alespoň jeden z argumentů funkce NULL, pak jako výsledek funkce se použije hodnota NULL aniž by se vykonala funkce. Následující příklad testuje validitu ISBN kódu (sice existuje doplněk isbn_issn, ten však ve většině případů nebude nainstalován). Všimněte si použití proměnné weight typu pole.

CREATE OR REPLACE FUNCTION check_ISBN(CHAR(12)) RETURNS boolean AS '
  DECLARE 
    ISBN ALIAS FOR $1;
    pos INTEGER; asc INTEGER; suma INTEGER DEFAULT 0;
    weight INTEGER[] DEFAULT ''{10,9,8,7,6,5,4,3,2,1}'';  -- pro ISSN {8,7,6,5,4,3,2,1}
    digits INTEGER DEFAULT 1;
  BEGIN 
    FOR pos IN 1..length(ISBN) LOOP
      asc := ascii(substr(ISBN,pos,1));
      IF asc IN (88, 120) THEN -- ISDN muze obsahovat kontrolni cislo X
        suma := suma + 10;
        digits := digits + 1;
      ELSIF asc >= 48 AND asc <= 57 THEN
        suma := suma + (asc - 48)*weight[digits];
        digits := digits + 1;
      END IF;
    END LOOP;
    IF digits <> 11 THEN -- pro ISSN <> 9
      RETURN ''f'';
    ELSE
      RETURN (suma % 11) = 0;
    END IF;
  END;
' LANGUAGE 'plpgsql' WITH (isstrict);

Definovanou funkci můžeme použít CHECK podmínce, např (sloupec isbn):

CREATE TABLE ZCHU_RezervacniKnihy (
  id INTEGER PRIMARY KEY DEFAULT nextval('doc_seq'),
  idoo INTEGER NOT NULL,  -- id objektu ochrany
  taboo CHAR(1) NOT NULL CHECK(taboo IN ('m','v','s')),
  isign varchar(50) NOT NULL UNIQUE CHECK (isign <> ''),
  typ CHAR(2) NOT NULL REFERENCES cv_TypDocRK(kod),
  autor_prijmeni VARCHAR(30) NOT NULL CHECK (autor_prijmeni <> ''), 
  autor_jmeno VARCHAR(30) NULL CHECK (autor_jmeno <> ''),
  nazev VARCHAR(100) NOT NULL CHECK (nazev <> ''),
  poznamka VARCHAR(300) NULL,
  rok_vydani INTEGER NULL CHECK (rok_vydani > 1918 AND rok_vydani <EXTRACT(year FROM current_date)),
  zalozeno DATE DEFAULT current_date NOT NULL,
  vyrazeno DATE NULL CHECK (vyrazeno > zalozeno),
  isbn CHAR(12) NULL CHECK (check_ISBN(isbn)),
  url VARCHAR(100) NULL CHECK (url ~* '^(file|http|ftp)://.+')
); 

5. Použití PL/pgSQL funkcí při návrhu vlastních operátorů

PostgreSQL nepodporuje operaci dělení intervalu intervalem. Není ovšem žádným problémem tento nedostatek překonat a definovat si vlastní operátor / pro tuto kombinaci operandů.

omega=# select '1hour'::interval / '10min'::interval;
ERROR:  operator does not exist: interval / interval
CREATE OR REPLACE FUNCTION div_op(interval, interval) 
RETURNS double precision AS $$
BEGIN
  RETURN EXTRACT(EPOCH FROM $1) / EXTRACT(EPOCH FROM $2);
END;
$$ LANGUAGE plpgsql IMMUTABLE RETURNS NULL ON NULL INPUT;

CREATE OPERATOR / (procedure = div_op, leftarg = interval, rightarg = interval);

omega=# select '1hour'::interval / '10min'::interval;
 ?column?
----------
        6

V CHECK výrazech můžeme používat binární operátory OR a AND. V standardní distribuci chybí operátory XOR a implikace. Nicméně není žádným problémem tyto chybějící operátory do systému doplnit.

CREATE OR REPLACE FUNCTION op_xor (boolean, boolean) RETURNS boolean AS '
  BEGIN
    RETURN ((NOT $1) AND $2) OR ($1 AND (NOT $2));
  END;
' LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION op_imp (boolean, boolean) RETURNS boolean AS '
  BEGIN
    RETURN ($2 OR NOT $1);
  END;
' LANGUAGE 'plpgsql';

Obě funkce musíme zaregistrovat a přiřadit jim nějaký symbol. To provede příkaz CREATE OPERATOR. Jméno pro operátor vytvoříme kombinací následujících znaků +-*/<>=~!@#%^&|'?$.

DROP OPERATOR # (boolean, boolean);

CREATE OPERATOR # (
  procedure = op_xor,
  leftarg = boolean,
  rightarg = boolean,
  commutator = #
);

DROP OPERATOR >>> (boolean, boolean);

CREATE OPERATOR >>> (
  procedure = op_imp,
  leftarg = boolean,
  rightarg = boolean,
  commutator = >>>
);

Oba operátory si můžeme vyzkoušet na tabulce:

DROP TABLE logtab;

CREATE TABLE logtab (l boolean, p boolean);

INSERT INTO logtab VALUES(FALSE,FALSE);
INSERT INTO logtab VALUES(TRUE,FALSE);
INSERT INTO logtab VALUES(FALSE,TRUE);
INSERT INTO logtab VALUES(TRUE,TRUE);

SELECT l, p, l # p AS XOR FROM logtab;
SELECT l, p, l >>> p AS IMPL FROM logtab;

Použití obou těchto operátorů významně zjednoduší CHECK podmínky. V tabulce Hlaseni požaduji, aby bylo zadáno buďto id uživatele nebo jméno uživatele. Dále je požadováno, aby byl vyplněn sloupec popis chyby, pokud typ hlašení je chyba.

DROP TABLE Hlaseni;
DROP SEQUENCE Hlaseni_id_seq;

CREATE TABLE Hlaseni (
  id SERIAL PRIMARY KEY,
  zalozeno DATE DEFAULT current_date NOT NULL,
  zalozil_neprihlasen VARCHAR(60) CHECK (zalozil_neprihlasen <> ''),
  zalozil INTEGER REFERENCES Uzivatele(id) CHECK (
    zalozil_neprihlasen IS NOT NULL # zalozil IS NOT NULL),
  trida_chyby CHAR(2) NOT NULL REFERENCES TridaChHlaseni(kod),
  chybove_hlaseni TEXT CHECK (
    (trida_chyby IN ('ch','zc') >>> chybove_hlaseni IS NOT NULL) AND
    (chybove_hlaseni <> '')),
  podrobny_popis TEXT NULL CHECK (podrobny_popis <> '')
);

INSERT INTO Hlaseni (zalozil, trida_chyby, podrobny_popis)
  VALUES (1,'po', 'Nainstaloval jsem novou verzi.'); -- ok

INSERT INTO Hlaseni (zalozil, zalozil_neprihlasen, trida_chyby, podrobny_popis)
  VALUES (1, 'Pavel Stěhule','po', 'Nainstaloval jsem novou verzi.'); -- selže

INSERT INTO Hlaseni (zalozil_neprihlasen, trida_chyby, podrobny_popis)
  VALUES ('Pavel Stěhule','po', 'Nainstaloval jsem novou verzi.'); -- ok

INSERT INTO Hlaseni (zalozil_neprihlasen, trida_chyby, podrobny_popis)
  VALUES ('Pavel Stěhule','ch', 'Nainstaloval jsem novou verzi.'); -- selže 
[Note]Poznámka

Neexistenci operátoru XOR lze obejít poměrně jednoduše (bez nutnosti psaní PL/pgSQL funkce).

 zalozil INTEGER REFERENCES Uzivatele(id) CHECK (
    (zalozil_neprihlasen IS NOT NULL) <> (zalozil IS NOT NULL))

6. Použití PL/pgSQL funkcí při návrhu vlastních agregačních funkcí

Porovnáme počet vestavěných agregačních funkcí v PostgreSQL s jinými srovnatelnými RDBMS, zjistíme, že v PostgreSQL mnohé agregační funkce chybí. Naštěstí si v PostgreSQL si můžeme agregační funkci navrhnout sami.

V PostgreSQL agregační funkci vytvoříme pomocí dvou funkcí. První, která se spouští pro každou hodnotu (prvním parametrem je mezivýsledek, druhým pak samotná hodnota) a finální, jejíž parametrem je stávající mezivýsledek. První příklad tuto finální funkci nepotřebuje - vytvoří seznam čárkou oddělených položek. U agregačních funkcích je třeba (aby se chovali, tak jak se očekává) aby ignorovaly hodnotu NULL.

CREATE OR REPLACE FUNCTION comma_aggreg (text, text) RETURNS text AS '
  BEGIN
    IF $2 IS NULL THEN
      RETURN $1;
    END IF
    IF length($1) > 0 THEN
      RETURN $1 || ', ' || $2;
    ELSE
      RETURN $2;
    END IF;
  END;
' LANGUAGE 'plpgsql';

DROP AGGREGATE comma(text);

CREATE AGGREGATE comma (
  basetype = text,
  sfunc = comma_aggreg,
  stype = text,
  initcond = ''
);

SELECT comma(jmeno) FROM jmena;
[Tip]Tip

Pro tuto agregační funkci nepotřebujeme nutně PL/pgSQL. Lze si vystačit i s SQL, viz příklad (Autor Aleš Zíka)

CREATE FUNCTION text_sum(text,text) RETURNS text
      AS '
SELECT CASE WHEN $1 IS NULL THEN '''' ELSE $1 || '','' END || $2;
' LANGUAGE SQL;

CREATE AGGREGATE sum (
  BASETYPE=text,
  SFUNC=text_sum,
  STYPE=text
);

Drobnou úpravou agregační funkce sum získáme hladovou funkci - tj. pokud je daný text již agregován, tak se bude ignorovat. Při použití této funkce zpracování dotazu nebude nejrychlejší, je to ale nejkratší cesta, jak docílit chtěného výsledku.

CREATE OR REPLACE FUNCTION comma_aggreg(text,text) RETURNS text AS '
BEGIN
  IF $2 IS NULL THEN
    RETURN $1;
  END IF;
  IF length($1) > 0 THEN
    IF position($2 in $1) = 0 THEN
      RETURN $1 || '', '' || $2;
    ELSE
      RETURN $1;
    END IF;
  ELSE
    RETURN $2;
  END IF;
END; 
' LANGUAGE plpgsql;

Tabulka obsahuje působnosti poboček v okresech a krajích. Pokud bych nepoužil hladové sčítání řetězců, tak by se mi kraj ve výpisu objevil dvakrát.

aopk=> select * from pusobnost;
    pobocka    |     okres     | kraj
---------------+---------------+------
 Benešov       | Benešov       | SČ
 Benešov       | Kutná Hora    | SČ
 Č. Budějovice | Č. Budějovice | JČ
(3 řádek)

aopk=> select pobocka, sum(okres), sum(kraj) from pusobnost group by pobocka;
    pobocka    |         sum         | sum
---------------+---------------------+-----
 Č. Budějovice | Č. Budějovice       | JČ
 Benešov       | Benešov, Kutná Hora | SČ
(2 řádek)

Následující, mírně komplikovanější, agregační funkce vrací seznam položek v HTML formátu.

CREATE OR REPLACE FUNCTION html_li_aggr (text, text) RETURNS text AS '
  BEGIN
    IF $2 IS NULL THEN
      RETURN $1;
    ELSE
      RETURN $1 || ''<li>'' || $2 || ''</li>\n'';
    END IF;
  END;
' LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION html_ul_final (text) RETURNS text AS '
  BEGIN
    IF $1 <> '''' THEN
      RETURN ''<ul>\n'' || $1 || ''</ul>'';
    ELSE
      RETURN '''';
    END IF;
  END;
' LANGUAGE 'plpgsql';

DROP AGGREGATE html_ul (text);

CREATE AGGREGATE html_ul (
  basetype = text,
  sfunc = html_li_aggr,
  stype = text,
  initcond = '',
  finalfunc = html_ul_final
);

Výsledkem dotazu SELECT html_ul(jmeno) FROM jmena je jedna hodnota (i když více řádků textu), kterou můžeme přímo vložit do HTML stránky.

<ul>
<li>Stěhule</li>
<li>Rusňák</li>
<li>Chromečka</li>
<li>Bilíček</li>
<li>Bilak</li>
<li>Gottwald</li>
</ul> 

Dvě agregační funkce, které v PostgreSQL nenajdete a např. v MS Access nobo MSQL najdete, jsou First a Last. First vrací první hodnotu z množiny, Last poslední.

CREATE OR REPLACE FUNCTION First_aggr (text, text) RETURNS text AS '
  BEGIN
    IF $1 <> '''' THEN
      RETURN $1;
    ELSE
      RETURN $2;
    END IF;
  END;
' LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION Last_aggr (text, text) RETURNS text AS '
  BEGIN
    RETURN $2;
  END;
' LANGUAGE 'plpgsql';

DROP AGGREGATE First (text);

CREATE AGGREGATE First (
  basetype = text,
  sfunc = First_aggr,
  stype = text,
  initcond = ''
);

DROP AGGREGATE Last (text);

CREATE AGGREGATE Last (
  basetype = text,
  sfunc = Last_aggr,
  stype = text
);

SELECT First(jmeno) FROM jmena; -- -> Stěhule
SELECT Last(jmeno) FROM jmena;  -- -> Gottwald

7. Návrh funkcí triggrů v PL/pgSQL

Starší verze PostgreSQL nepodporovali kontrolu referenční integrity. Pokud bylo žadoucí (žádoucí je vždy) referenční integritu zajistit, bylo nutné navrhovat vlastní triggery. Od verze 7.0 PostgreSQL kontrolu referenční integrity zabezpečuje (i když pomocí generovaných triggerů) viz. klauzule REFERENCES. Což však neznamená, že se bez triggerů obejdeme. Použijeme je v případech, kdy je standardní model referenční integrity nedostačující nebo když chceme akce prováděné na jedné tabulce promítnout i do jiných tabulek. Počínaje verzí 7.3 se lze odkazovat nejen na primární klíč, ale i na sloupec typu UNIQUE.

Druhý případ je jednodušší a proto s ním začnu. Dovolím si malou vsuvku o triggerech. Trigger, česky spoušť je uložená procedura, kterou RDBMS aktivuje před nebo po provedení příkazů INSERT, UPDATE a DELETE na nějaké tabulce, které předtím určíme trigger. Jako trigger můžeme použít libovolnou PL/pgSQL proceduru bez parametrů vracející hodnotu typu OPAQUE.

[Caution]Výstraha

Ve verzi 8.0 došlo ke změně okamžiku volání AFTER triggerů. Před touto verzí se AFTER triggery aktivovali těsně po ukončení transakce, takže se procedury spouštěly až po dokončení funkcí, které způsobily aktivaci triggeru. Ve verzi 8.0 díky možnosti ošetřit vyjímky se již AFTER triggery volají tak, jak se očekává, tj. hned po dokončení BEFORE triggerů. Chování verze 8.0 je korektní, nicméně tato změna může způsobit problémy s portací aplikací na verzi 8.0.

==============7.4============   ==============8.0==================== 
NOTICE:  test_fx start          NOTICE:  test_fx start
NOTICE:  trig_fx [BEFORE]       NOTICE:  trig_fx[BEFORE] start
NOTICE:  trig_fx end            NOTICE:  trig_fx end
NOTICE:  test_fx end            NOTICE:  trig_fx[AFTER] start
NOTICE:  trig_fx [AFTER] start  NOTICE:  trig_fx end 
NOTICE:  trig_fx end            NOTICE:  test_fx end

[Note]Poznámka

Pro verzi 7.3 a vyšší jako návratový typ nepoužijeme OPAQUE ale nově vytvořený pseudotyp TRIGGER.

Představme si situaci, kdy z nějakého důvodu chceme mít data rozdělená do dvou tabulek. Pracovat však chceme se sjednocením těchto tabulek. Pokud použijeme pohled, vzdáme se možnosti používat referenční integritu. Další řešení je používat pomocnou tabulku, která bude obsahovat hodnoty obou zdrojových tabulek.

CREATE TABLE zdroj1 (
  kod CHAR(2) PRIMARY KEY,
  popis VARCHAR(100) CHECK (popis <> '')
);

CREATE TABLE zdroj2 (
  kod CHAR(2) PRIMARY KEY,
  popis VARCHAR(100) CHECK (popis <> '')
);

CREATE TABLE cil (
  kod CHAR(2) PRIMARY KEY,
  popis VARCHAR(100) CHECK (popis <> ''),
  zdroj CHAR(1) NOT NULL CHECK (zdroj = '1' OR zdroj = '2')
);

Předesílám, že jakákoliv funkce PL/pgSQL běží pod implicitní transakcí, tj. selže-li libovolný příkaz v proceduře, pak se veškeré změny v datech provedených funkcí anulují. Na tomto chování je postavena funkce triggeru. Zkusím napřed provést požadovanou funkci na cílové tabulce, pokud příkaz selže, pak se nepodaří provést příkaz ani na zdrojové tabulce.

CREATE OR REPLACE FUNCTION trig_build_cil_F() RETURNS OPAQUE AS '
BEGIN
  IF TG_OP = ''DELETE'' THEN
    DELETE FROM cil WHERE kod = OLD.kod;
    RETURN OLD;
  ELSE
    IF TG_OP = ''UPDATE'' THEN
      UPDATE cil SET kod = NEW.kod, popis = NEW.popis WHERE kod = OLD.kod;
      RETURN NEW;
    ELSE 
      INSERT INTO cil VALUES(NEW.kod, NEW.popis,
        CASE TG_RELNAME WHEN 'zdroj1' THEN 1 WHEN 'zdroj2' THEN 2 END);
      RETURN NEW;
    END IF;
  END IF;
END;
' LANGUAGE 'plpgsql';

Při provádění PL/pgSQL funkce jako triggeru máme k dispozici několik vestavěných proměnných. TG_OP popisuje příkaz vedoucí k spuštění triggeru, TG_RELNAME nese název tabulky převedený na málá písmena, na které se trigger spustil, NEW obsahuje řádek s novou verzí hodnot (pouze pro INSERT a UPDATE), OLD obsahuje řádek s původní verzí hodnot (pouze pro DELETE a UPDATE). Pokud procedura vrátí NULL, pak se neprovede změna dat. Trigger řeší pouze přenos dat ze zdrojových tabulek do cílové tabulky. Pokud budeme měnit přímo cílovou tabulku, pak cílová tabulka nebude odpovídat zdrojovým tabulkám. Této nekonzistentnosti lze spolehlivě zabránit až od verze 7.3, kdy uložená procedura může běžet s právy vlastníka. Vlastník bude jediný, kdo bude moci modifikovat cílovou tabulku. Ostatní budou moci cílovou tabulku pouze číst.

Funkci musíme přiřadit triggeru a tabulce příkazem CREATE TRIGGER. V zásadě můžeme vytvořit dva základní typy: ty které se provádějí před provedením příkazu, který vyvolal trigger, a ty, které se spouštějí po provedení příkazu.

CREATE TRIGGER t_zdroj1 
  BEFORE INSERT OR UPDATE OR DELETE ON zdroj1
  FOR EACH ROW EXECUTE PROCEDURE trig_build_cil_F();

CREATE TRIGGER t_zdroj2
  BEFORE INSERT OR UPDATE OR DELETE ON zdroj2
  FOR EACH ROW EXECUTE PROCEDURE trig_build_cil_F();

V některých případech chceme zamezit UPDATE některých sloupců tabulek (většinou se jedná o sloupce typu SERIAL, kde UPDATE nemá smysl). Opět existuje doplněk noupdate, který řeší tento problém, ale který není defaultně nainstalován. Pokud pokusů o UPDATE nebude mnoho, můžeme bez rozpaků použít trigger a krátkou PL/pgSQL proceduru:

DROP SEQUENCE ciselnik_id_seq;
DROP TABLE ciselnik;

CREATE TABLE ciselnik (
  id SERIAL PRIMARY KEY,
  popis VARCHAR(100) CHECK (popis <> '')
);

CREATE OR REPLACE FUNCTION trig_static_id() RETURNS OPAQUE AS '
BEGIN
  IF TG_OP = ''UPDATE'' AND NEW.id <> OLD.id THEN
    RAISE EXCEPTION ''You can not update PRIMARY KEY column on table %'', TG_RELNAME;
  ELSE
    RETURN NEW;  
  END IF;
END;
' LANGUAGE 'plpgsql';

CREATE TRIGGER static_id_ciselnik
  BEFORE UPDATE ON ciselnik
  FOR EACH ROW EXECUTE PROCEDURE trig_static_id();
[Note]Poznámka

Počínaje verzí 7.3 RDBMS při rušení tabulky zruší i všechny implicitní posloupnosti vytvořené pro sloupce typu SERIAL.

Pomocí triggerů můžeme zajistit integritu n x (1 ku m). Mějme entity maloplošné rezervace, velkoplošné rezervace a památné stromy. Ke každé této entitě chci mít přiřazen 0 až m dokumentů. Z určitých důvodů chci mít jednoznačný primární klíč přez všechny tabulky chráněných území (tj. entity: maloplošné rezervace (mchu), velkoplošné rezervace (vchu)a památné stromy (pp)). Z praktického hlediska si u entity rezervační knihy eviduji kromě primárního klíče i název tabulky, na kterou se odkazuji (rychlost). Standardní referenční integrita dovoluje se odkazovat pouze na jednu tabulku, tudíž ji nemůženme použit. Musíme za ní zajistit aby:

  • se nezrušil záznam z tabulek chráněných území, pokud na něj bude existovat odkaz z tabulky dokumentů (tabulky tzv. rezervačních knih).

  • do tabulky rezervačníh knih nesmíme přidat záznam s odkazem na neexistující klíč v tabulkách chráněných území.

DROP SEQUENCE zchu_seq; 
DROP SEQUENCE doc_seq;

CREATE SEQUENCE zchu_seq; -- Vytvoří posloupnost pro sdílený prim. key
CREATE SEQUENCE doc_seq;  -- Vytvoří posloupnost pro dokumenty.

DROP TABLE MCHU;

CREATE TABLE MCHU(
  id INTEGER PRIMARY KEY DEFAULT nextval('zchu_seq'),
  cis INTEGER NULL UNIQUE,
  status CHAR(1)
    DEFAULT 'n' CHECK (status IN ('n','v','z') AND (
      (status = 'n' AND zalozeno IS NOT NULL AND vyhlaseno IS NULL AND zruseno IS NULL) OR
      (status = 'v' AND vyhlaseno IS NOT NULL AND zruseno IS NULL) OR
      (status = 'z' AND zruseno IS NOT NULL AND vyhlaseno IS NOT NULL))),
  zalozeno DATE DEFAULT current_date NULL,
  vyhlaseno DATE NULL CHECK (vyhlaseno >= zalozeno),
  zruseno DATE NULL CHECK (zruseno >= zalozeno AND zruseno >= vyhlaseno),
  rozloha NUMERIC(11,2) NULL CHECK (
    (status = 'v' >>> (CAST(rozloha AS DOUBLE PRECISION) > 0.0 AND rozloha IS NOT NULL))),
  nazev VARCHAR(100) NOT NULL UNIQUE CHECK (nazev <> ''),
  vyska_min INTEGER NULL CHECK ((vyska_min > 150 AND vyska_min <1602) 
    AND (status = 'v' >>> vyska_min IS NOT NULL)),
  vyska_max INTEGER NULL CHECK ((vyska_max > 150 AND vyska_max <1602) 
    AND (status = 'v' >>> vyska_max IS NOT NULL)),
  stav_ochr CHAR(1) NULL REFERENCES StavOchr(kod) 
    CHECK (status = 'v' >>> stav_ochr IS NOT NULL),
  charakter_uzemi VARCHAR(250) NULL,
  lesni_rezervace BOOLEAN NULL CHECK (status = 'v' >>> lesni_rezervace IS NOT NULL)
);

DROP TABLE VCHU;

CREATE TABLE VCHU(
  id INTEGER PRIMARY KEY DEFAULT nextval('zchu_seq'),
  cis INTEGER NULL UNIQUE,
  status CHAR(1)
    DEFAULT 'n' CHECK (status IN ('n','v','z') AND (
      (status = 'n' AND zalozeno IS NOT NULL AND vyhlaseno IS NULL AND zruseno IS NULL) OR
      (status = 'v' AND vyhlaseno IS NOT NULL AND zruseno IS NULL) OR
      (status = 'z' AND zruseno IS NOT NULL AND vyhlaseno IS NOT NULL))),
  zalozeno DATE DEFAULT current_date NULL,
  vyhlaseno DATE NULL CHECK (vyhlaseno >= zalozeno),
  zruseno DATE NULL CHECK (zruseno >= zalozeno AND zruseno >= vyhlaseno),
  rozloha NUMERIC(11,2) NULL CHECK (
    (status = 'v' >>> (CAST(rozloha AS DOUBLE PRECISION) > 0.0 AND rozloha IS NOT NULL))),
  nazev VARCHAR(100) NOT NULL UNIQUE CHECK (nazev <> ''),
  vyska_min INTEGER NULL CHECK ((vyska_min > 150 AND vyska_min <1602) 
    AND (status = 'v' >>> vyska_min IS NOT NULL)),
  vyska_max INTEGER NULL CHECK ((vyska_max > 150 AND vyska_max <1602) 
    AND (status = 'v' >>> vyska_max IS NOT NULL)),
  stav_ochr CHAR(1) NULL REFERENCES StavOchr(kod) 
    CHECK (status = 'v' >>> stav_ochr IS NOT NULL),
  charakter_uzemi VARCHAR(250) NULL,
  lesni_rezervace BOOLEAN NULL CHECK (status = 'v' >>> lesni_rezervace IS NOT NULL)
);

DROP TABLE PS;

CREATE TABLE PS (
  id INTEGER PRIMARY KEY DEFAULT nextval('zchu_seq'),
  cis INTEGER NULL UNIQUE,
  puv_data BOOLEAN NOT NULL DEFAULT false CHECK (puv_data OR zalozeno IS NOT NULL),
  status CHAR(1)
    DEFAULT 'n' CHECK (status IN ('n','v','z') AND (
      (status = 'n' AND zalozeno IS NOT NULL AND vyhlaseno IS NULL AND zruseno IS NULL) OR
      (status = 'v' AND vyhlaseno IS NOT NULL AND zruseno IS NULL) OR
      (status = 'z' AND zruseno IS NOT NULL AND vyhlaseno IS NOT NULL))),
  zalozeno DATE DEFAULT current_date NULL,
  vyhlaseno DATE NULL CHECK (vyhlaseno >= zalozeno),
  zruseno DATE NULL CHECK (zruseno >= zalozeno AND zruseno >= vyhlaseno)
);

DROP TABLE ZCHU_RezervacniKnihy;

CREATE TABLE ZCHU_RezervacniKnihy (
  id INTEGER PRIMARY KEY DEFAULT nextval('doc_seq'), -- teoreticky by bylo možné wf i na dokumentech v RK
  idoo INTEGER NOT NULL,  -- id objektu ochrany
  taboo CHAR(1) NOT NULL CHECK(taboo IN ('m','v','s')), -- bez parků
  isign varchar(50) NOT NULL UNIQUE CHECK (isign <> ''),
  typ CHAR(2) NOT NULL REFERENCES TypDocRK(kod),
  autor_prijmeni VARCHAR(30) NOT NULL CHECK (autor_prijmeni <> ''), 
  autor_jmeno VARCHAR(30) NULL CHECK (autor_jmeno <> ''),
  nazev VARCHAR(100) NOT NULL CHECK (nazev <> ''),
  poznamka VARCHAR(300) NULL,
  rok_vydani INTEGER NULL CHECK (rok_vydani > 1918 AND rok_vydani <EXTRACT(year FROM current_date)),
  zalozeno DATE DEFAULT current_date NOT NULL,
  vyrazeno DATE NULL CHECK (vyrazeno > zalozeno),
  isbn CHAR(12) NULL CHECK (check_ISBN(isbn)),
  zalozil INTEGER NOT NULL REFERENCES AOPKUzivatele(id),
  vyradil INTEGER NULL REFERENCES AOPKUzivatele(id)
    CHECK(vyradil IS NOT NULL AND vyrazeno IS NOT NULL),
  url VARCHAR(100) NULL CHECK (url ~* '^(file|http|ftp)://.+')
); 

V definici tabulek používám operátor implikace, viz. definice vlastních operátorů výše. Následující funkce bude sloužit jako UPDATE, DELETE trigger pro tabulky MCHU, VCHU a PS. V případě, že budu rušit záznam na který se odkazuji z tabulky rezervačních knih, vyvolá výjimku.

CREATE OR REPLACE FUNCTION trig_refintegr_zchu_P() RETURNS OPAQUE AS '
DECLARE ktab CHAR(1);
BEGIN
  IF TG_OP = ''UPDATE'' THEN
    IF NEW.id <> OLD.id THEN
      RAISE EXCEPTION ''referential integrity violation - You can not update PRIMARY KEY column on table %'', 
        TG_RELNAME;
    END IF;
    RETURN NEW;
  END IF;

  ktab := CASE TG_RELNAME 
    WHEN ''mchu'' THEN ''m'' 
    WHEN ''vchu'' THEN ''v''
    WHEN ''ps''   THEN ''s'' 
  END;  

  IF EXISTS(SELECT id FROM ZCHU_RezervacniKnihy WHERE idoo = OLD.id
    AND tanoo = ktab) THEN
    RAISE EXCEPTION ''referential integrity violation - key in % still referenced from ZCHU_rezervacniKnihy'', 
      TG_RELNAME;
  END IF;
  RETURN OLD;
END;
' LANGUAGE 'plpgsql'

CREATE TRIGGER refintegr_zchu_p_mchu
  BEFORE DELETE OR UPDATE ON MCHU
  FOR EACH ROW EXECUTE PROCEDURE trig_refintegr_zchu_P();

CREATE TRIGGER refintegr_zchu_p_vchu
  BEFORE DELETE OR UPDATE ON VCHU
  FOR EACH ROW EXECUTE PROCEDURE trig_refintegr_zchu_P();

CREATE TRIGGER refintegr_zchu_p_ps
  BEFORE DELETE OR UPDATE ON PS
  FOR EACH ROW EXECUTE PROCEDURE trig_refintegr_zchu_P(); 

PL/pgSQL funkce trig_refintegr_zchu_F() nedovolí přiřadit do tabulky rezervačních knih odkaz na neexistující zákonem chráněnné území.

CREATE OR REPLACE FUNCTION trig_refintegr_zchu_F() RETURNS OPAQUE AS '
DECLARE r RECORD; tab VARCHAR(20);
BEGIN
  IF TG_OP = ''UPDATE'' THEN
    IF NEW.id <> OLD.id THEN
      RAISE EXCEPTION ''referential integrity violation - You can not update PRIMARY KEY column on table %'', TG_RELNAME;
    END IF;
  END IF;
  IF NEW.taboo IS NULL THEN
    RETURN NEW;  -- nelze pokračovat
  END IF;
  IF NOT NEW.taboo IN (''m'',''v'',''s'') THEN
    RETURN NEW;
  END IF;
  SELECT INTO r CASE NEW.taboo
    WHEN ''m'' THEN (SELECT id FROM MCHU WHERE id=NEW.idoo)
    WHEN ''v'' THEN (SELECT id FROM VCHU WHERE id=NEW.idoo)
    WHEN ''s'' THEN (SELECT id FROM PS WHERE id=NEW.idoo)
  END AS id;
  IF r.id IS NULL THEN
    tab := CASE NEW.taboo
      WHEN ''m'' THEN ''MCHU''
      WHEN ''v'' THEN ''VCHU''
      WHEN ''s'' THEN ''PS''
    END;   
    RAISE EXCEPTION ''referential integrity violation - key referenced from % not found in %'', TG_RELNAME, tab;
  END IF;
  RETURN NEW;
END;
' LANGUAGE 'plpgsql';

CREATE TRIGGER refintegr_zchu_f_ZCHU_RK
  BEFORE INSERT OR UPDATE ON ZCHU_rezervacniKnihy
  FOR EACH ROW EXECUTE PROCEDURE trig_refintegr_zchu_f();

Triggery typu BEFORE se spouští ještě před vyhodnocením CHECK podmínek. To je logické. Díky tomu máme možnost v funkcích triggerů doplňovat některé chybějící hodnoty. Na druhou stranu v triggeru můžeme detekovat hodnoty nevyhovující CHECK podmínkám. Řešením této situace není vyvolat výjimku, ale normálně ukončit provádění triggeru. RDBMS pak při následující kontrole zjistí nevyhovující hodnotu a výjimku vyvolá sám.

Trigger v PostgreSQL může mít specifikovány i statické parametry - můžeme pak jednu proceduru použít v různých kontextech, atd. Parametry se ovšem nepředávají klasicky prostřednictvím parametrů funkce obsluhující trigger, ale jsou přístupné v externí proměnné TG_ARGV[].

Mějme například požadavek na logování zápisu do tabulkek, kdy hodnotu, která se bude zapisovat do logu, nelze jednoduše odvodit z názvu tabulky. Potřebovali bychom komplikované větvení.

CREATE TABLE foo_a(i integer);
CREATE TABLE foo_b(i integer);

CREATE OR REPLACE FUNCTION trig() RETURNS trigger AS '
BEGIN
  RAISE NOTICE ''Trigger: %,Insert do tabulky: %,Parametr: %'', 
    TG_NAME, TG_RELNAME, TG_ARGV[0];
  RETURN NULL;
END;
' LANGUAGE plpgsql;

CREATE TRIGGER foo_a_trg AFTER INSERT ON foo_a
  FOR EACH ROW EXECUTE PROCEDURE trig('a');
CREATE TRIGGER foo_b_trg AFTER INSERT ON foo_b
  FOR EACH ROW EXECUTE PROCEDURE trig('b');

INSERT INTO foo_a VALUES(1);
INSERT INTO foo_b VALUES(1);

Vysledkem je hlášení

psql:testtr.sql:16: NOTICE:  Trigger: foo_a_trg,Insert do tabulky: foo_a,Parametr: a
INSERT 336812 1
psql:testtr.sql:17: NOTICE:  Trigger: foo_b_trg,Insert do tabulky: foo_b,Parametr: b
INSERT 336813 1

8. Použití PL/pgSQL funkcí s parametry typu tabulka

V některých případech potřebujeme spouštět funkci na celý řádek tabulky, např. když potřebujeme nahradit opakující se výraz CASE v příkazu SELECT. Mějme následující tabulky prodejců a prodeje. Tabulka prodeje obsahuje sloupce c1, c2, c3, t2, ot, oec1, oec2, oec3, t1, b1, b2, c050, c150, c300, t1n, t2n, které obsahují počet prodaných kusů v daném tarifu v rámci jednoho prodeje. Tarify jsou stanovené direktivně a nemění se.

CREATE TABLE prodejci (
  id SERIAL PRIMARY KEY,
  rc VARCHAR(10) NOT NULL UNIQUE CHECK (rc ~ '^[0-9]{9,10}$'),
  jmeno VARCHAR(20) NOT NULL CHECK (jmeno <> ''),
  prijmeni VARCHAR(20) NOT NULL CHECK (prijmeni <> '')
);

CREATE TABLE prodej (
  id SERIAL PRIMARY KEY,
  prodejce int REFERENCES prodejci(id),
  closing_date DATE NOT NULL DEFAULT current_date::DATE,
  c1   integer NULL CHECK(c1 > 0),
  c2   integer NULL CHECK(c2 > 0),
  c3   integer NULL CHECK(c3 > 0),
  t2   integer NULL CHECK(t2 > 0),
  ot   integer NULL CHECK(ot > 0),
  oec1 integer NULL CHECK(oec1 > 0),
  oec2 integer NULL CHECK(oec2 > 0),
  oec3 integer NULL CHECK(oec3 > 0),
  t1   integer NULL CHECK(t1 > 0),
  b1   integer NULL CHECK(b1 > 0),
  b2   integer NULL CHECK(b2 > 0),
  c050 integer NULL CHECK(c050 > 0),
  c150 integer NULL CHECK(c150 > 0),
  c300 integer NULL CHECK(c300 > 0),
  t1n  integer NULL CHECK(t1n > 0),
  t2n  integer NULL CHECK(t2n > 0)
);

Zákazník bude požadovat rozpis prodaných kusů, nikoliv však podle jednotlivých tarifů, ale podle celkového počtu kusů rozděleného do tříd podle objemu na jeden provedený prodej. Třídy jsou:

0 - 4, 5 - 9, 10 - 19, 20 - 49, nad 50

Z rozpisu pak mohu určit jednak výši prodeje jedním prodejcem, ale i jeho zaměření na určitý segment trhu. Rozpis můžeme realizovat jako výběr s použitím příkazu CASE (níže), nebo výběr s PL/pgSQL funkcí, která má jako argument řádek tabulky.

CREATE OR REPLACE FUNCTION isnz(integer) RETURNS integer AS '
  BEGIN
    IF $1 IS NOT NULL THEN
      RETURN $1; 
    ELSE 
      RETURN 0;
    END IF;
  END;
' LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION c_prodej_seg (prodej, int, int) RETURNS integer AS '
  DECLARE s INTEGER;
  BEGIN 
    s := isnz($1.c1) + isnz($1.c2) + isnz($1.c3) + isnz($1.t2)
      + isnz($1.oec1) + isnz($1.oec2) + isnz($1.oec3) + isnz($1.t1)
      + isnz($1.b1) + isnz($1.b2) + isnz($1.c150) + isnz($1.c300)
      + isnz($1.t1n) + isnz($1.t2n) + isnz($1.ot);
    IF $2 IS NOT NULL THEN
      IF s <$2 THEN RETURN 0; END IF;
    END IF;
    IF $3 IS NOT NULL THEN
      IF s > $3 THEN RETURN 0; END IF;
    END IF;
    RETURN s;
  END;
' LANGUAGE 'plpgsql'; 

Použití těchto funkcí zásadně zjednoduší návrh. Funkce isnz vrací předanou hodnotu nebo nulu v případě, že je argument NULL. Funkce c_prodej_seg sečte prodej ve všech tarifech. Pokud je objem prodeje v intervalu určeným druhým a třetím argumentem, pak vrátí tuto hodnotu, jinak vrací nulu. Všimněte si, že (ač trochu nelogicky) první argument funkce je typu tabulka. Ve funkci samotné však s touto hodnotou pracujeme způsobem jako kdyby byla tabulka%ROW.

[Note]Poznámka

Ještě ve verzi 7.3 (stávající verze) nelze volat funkce s parametrem typu tabulka v PL/pgSQL procedůře. Stejně tak nelze volat proceduru v obsluze spouště s parametrem NEW nebo OLD.

Pro data:

INSERT INTO prodejci (rc, jmeno, prijmeni) 
  VALUES ('7307150000','Pavel', 'Stěhule');
INSERT INTO prodejci (rc, jmeno, prijmeni) 
  VALUES ('7807150000','Zdeněk', 'Stěhule');

INSERT INTO prodej (prodejce,c1,c2) VALUES (1,18,11);
INSERT INTO prodej (prodejce,c1,c2) VALUES (1,18,6);
INSERT INTO prodej (prodejce,c3,c2) VALUES (1,50,24);
INSERT INTO prodej (prodejce,t1,c3) VALUES (1,1,1);
INSERT INTO prodej (prodejce,c300)  VALUES (2,10);
INSERT INTO prodej (prodejce,c1,c2) VALUES (2,11,2);
INSERT INTO prodej (prodejce,c1,c2) VALUES (2,1,6);
INSERT INTO prodej (prodejce,c3,c2) VALUES (2,5,12);
INSERT INTO prodej (prodejce,t1,ot) VALUES (2,1,1);
INSERT INTO prodej (prodejce,c300)  VALUES (2,252);

Vrací tento dotaz:

SELECT 
  SUM(c_prodej_seg(prodej,NULL, 4)) AS lt5,
  SUM(c_prodej_seg(prodej,5, 9)) AS be5a9,
  SUM(c_prodej_seg(prodej,10, 19)) AS be10a19,
  SUM(c_prodej_seg(prodej,20, 49)) AS be20a49,
  SUM(c_prodej_seg(prodej,50, NULL)) AS ge50,
  jmeno || ' ' || prijmeni as prodejce  
from 
  prodej join prodejci on prodejce = prodejci.id 
group 
  by jmeno || ' ' || prijmeni ;

tabulku:

 lt5 | be5a9 | be10a19 | be20a49 | ge50 |    prodejce
-----+-------+---------+---------+------+----------------
   2 |     0 |       0 |      53 |   74 | Pavel Stěhule
   2 |     7 |      40 |       0 |  252 | Zdeněk Stěhule

Ke stejnému výsledku se můžeme dostal jedním příkazem SELECT. Pro jednoduchost budu sčítat pouze tarify c1, c2 a c3.

SELECT 
  SUM(
    CASE WHEN (isnz(c1) + isnz(c2) + isnz(c3)) BETWEEN 0 AND 4 
      THEN (isnz(c1) + isnz(c2) + isnz(c3)) ELSE 0 END) AS le4, 
  SUM(
    CASE WHEN (isnz(c1) + isnz(c2) + isnz(c3)) BETWEEN 5 AND 9 
      THEN (isnz(c1) + isnz(c2) + isnz(c3)) ELSE 0 END) AS be5a9, 
  SUM(
    CASE WHEN (isnz(c1) + isnz(c2) + isnz(c3)) BETWEEN 10 AND 19 
      THEN (isnz(c1) + isnz(c2) + isnz(c3)) ELSE 0 END) AS be10a19, 
  SUM(
    CASE WHEN (isnz(c1) + isnz(c2) + isnz(c3)) BETWEEN 20 AND 49 
      THEN (isnz(c1) + isnz(c2) + isnz(c3)) ELSE 0 END) AS be20a49, 
  SUM(
    CASE WHEN (isnz(c1) + isnz(c2) + isnz(c3)) >= 50 
      THEN (isnz(c1) + isnz(c2) + isnz(c3)) ELSE 0 END) AS ge50,
  jmeno || ' ' || prijmeni as prodejce  
from 
  prodej join prodejci on prodejce = prodejci.id 
group 
  by jmeno || ' ' || prijmeni ;

Parametrů typu tabulka může být více, smysluplné použítí je mne napadá pouze u joinovaných tabulek.

9. Funkce vracející tabulky

Počínaje verzí 7.3 mohou funkce vracet tabulky. Jednotlivé řádky vytvoříme opakovaným voláním RETURN NEXT hodnota (tato varianta RETURNu neukončí funkci) a voláním RETURNu bez parametrů, kterým ukončíme provádění funkce. Jako navratový typ můžeme použít skalární typy, typy tabulka nebo složené typy (ty jsou víceméně ekvivalentem typu tabulka (vytvoříme je příkazem CREATE TYPE)).

CREATE TYPE tf AS (f1 varchar(10), f2 varchar(10));

CREATE OR REPLACE FUNCTION makesettf(integer) RETURNS SETOF tf AS '
  DECLARE f tf%ROWTYPE;
  BEGIN
    FOR i IN 1..$1 LOOP
      f.f1 := CAST(i AS varchar(10));
      f.f2 := ''bbbbb ''||CAST(i AS varchar(10));
      RAISE NOTICE ''%'', f.f1;
      RETURN NEXT f;
    END LOOP;
    RETURN;
  END;
' LANGUAGE 'plpgsql';

SELECT a.*, b.* FROM 
  makesettf(10) a JOIN makesettf(5) b ON a.f1 = b.f1;

SELECT * FROM makesettf(3) UNION ALL SELECT * FROM makesettf(8);

Praktičtějším příkladem, inspirován UDF Firebirdu (vracela tabulku konání německých pivních festivalů), je použítí SRF při návrhu funkcí pro určení dne získané přičtením n pracovních dnů k zadanému dni.

SRF funkce se hodí pro generování menších dynamických (parametrizovatelných) tabulek, jelikož jimi vytvořené tabulky nelze indexovat. Toto omezení přeneseme lehce přez srdce při testování. Mnou vytvořené funkce AddWDays a AddWDays2 jsem kontroloval porovnáním hodnot (každá funkce je jinak implementována, musí dávat shodné výsledky) nad fiktivní tabulkou vytvořenou spojením tabulek vytvořených SRF funkcemi.

Funkce DaysInYear vrací tabulku dní v daném roce, fce. Seq vrací celočíselnou posloupnost omezenou parametrem. Funkce AddWDays a AddWDays2 má dva parametry - první den, ke kterému se mají přičítat pracovní dny, počet je určen druhým parametrem). Funkce DiffWDays vrací počet pracovních dnů v intervalu určeného parametry. Pokud jsou všechny funkce správně navržené, pak výsledkem testovacích dotazů musí být prázdná tabulka.

CREATE OR REPLACE FUNCTION DaysInYear(integer) RETURNS SETOF date AS '
DECLARE st date;
BEGIN st := to_date(''0101''||$1,''MMDDYYYY'');
  WHILE EXTRACT(year FROM st) < $1 + 1 LOOP
    RETURN NEXT st;
    st := st + 1;
  END LOOP;
  RETURN;
END' 
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION Seq(integer) RETURNS SETOF integer AS '
DECLARE i integer;
BEGIN i := 1;
  WHILE i <= $1 LOOP
    RETURN NEXT i;
    i := i + 1;
  END LOOP;
  RETURN;
END' LANGUAGE plpgsql;

-- testy
SELECT DaysInYear, Seq FROM DaysInYear(2003) CROSS JOIN Seq(40) 
  WHERE AddWDays(DaysInYear, Seq) <> AddWDays2(DaysInYear, Seq);
SELECT DaysInYear, Seq FROM DaysInYear(2003) CROSS JOIN Seq(40) 
  WHERE DiffWDays(DaysInYear, AddWDays(DaysInYear, Seq)) <> Seq;

Státních svátků nemáme mnoho, nicméně pro každý rok jsou trochu jiné (díky velikonocím), a tak se pro generování tabulky hodí dobře SRF. Pokud by nastali problémy s výkonem, mohu tabulku materializovat, tj. příkazem SELECT INTO uložit do klasické tabulky. Funkce StátníSvátky vrací tabulku se státními svátky, včetně názvu pro rozsah daný prvním a druhým parametrem. Funkce PočetStatníchSvátků spočítá počet, vyřadí dny předané v poli jako třetí parametr (prakticky vždy se bude jednat o sobotu a neděli - {6, 7}). Za povšimnutí stojí snad jen způsob zápisu porovnání hodnoty s obsahem pole = ANY(). Test <>ALL() může způsobit záludnou chybu - v případě, že je pole prázdné, pak je cokoliv <> ALL() vždy splněno.

CREATE TYPE StatniSvatek AS (termin date, nazev varchar(50));

CREATE OR REPLACE FUNCTION StatniSvatky(date, date) RETURNS SETOF StatniSvatek  AS '
DECLARE 
  svatek VARCHAR [][] DEFAULT ARRAY
   [[''Nový rok'', ''0101''], 
    [''Neděle velikonoční'', ''xxxx''],
    [''Pondělí velikonoční'',''xxx1''], 
    [''Svátek práce'',''0501''],
    [''Den osvobození'',''0508''], 
    [''Den slovanských věrozvěstů Cyrila a Metoděje'',''0705''],
    [''Den upálení mistra Jana Husa'',''0706''], 
    [''Den české státnosti'',''0928''],
    [''Den vzniku samostatného československého státu'', ''1028''],
    [''Den boje za svobodu a demokracii'',''1117''],
    [''Štědrý den'',''1224''],
    [''1. svátek vánoční'',''1225''],
    [''2. svátek vánoční'',''1226'']];
  rok integer; stepd date; d varchar;
  sv StatniSvatek%ROWTYPE;
BEGIN stepd := $1;
  WHILE stepd <= $2 LOOP
    rok := CAST(EXTRACT(year FROM stepd) AS INTEGER);
    FOR i IN array_lower(svatek,1)..array_upper(svatek,1) LOOP
      d := svatek[i][2];
      IF d = ''xxxx'' THEN
        sv.termin := velnedele(rok);
      ELSIF d = ''xxx1'' THEN
        sv.termin := sv.termin +1;
      ELSE
        sv.termin := to_date(d||rok,''MMDDYYYY'');
      END IF;
      IF sv.termin BETWEEN stepd AND $2 THEN
        sv.nazev := svatek[i][1]; RETURN NEXT sv;
      END IF;
    END LOOP;
    stepd := date_trunc(''year'', stepd) + interval ''1 year'';
  END LOOP;
  RETURN;
END' LANGUAGE plpgsql STRICT;

CREATE OR REPLACE FUNCTION PocetStatnichSvatku(date, date, INTEGER[]) RETURNS INTEGER AS '
DECLARE 
  svatek VARCHAR [] DEFAULT ARRAY
   [''0101'', ''xxxx'', ''xxx1'', ''0501'', ''0508'', ''0705'', ''0706'', 
    ''0928'', ''1028'', ''1117'', ''1224'', ''1225'', ''1226''];
  rok INTEGER; stepd date; d varchar; sv date; pss INTEGER DEFAULT 0;  OM INTEGER;
BEGIN stepd := $1;
  WHILE stepd <= $2 LOOP
    rok := CAST(EXTRACT(year FROM stepd) AS INTEGER);
    FOR i IN array_lower(svatek,1)..array_upper(svatek,1) LOOP
      d := svatek[i];
      IF d = ''xxxx'' THEN
        sv := velnedele(rok);
      ELSIF d = ''xxx1'' THEN
        sv := sv +1;
      ELSE
        sv := to_date(d||rok,''MMDDYYYY'');
      END IF;
      IF NOT EXTRACT(dow FROM sv) = ANY ($3) THEN
        IF sv BETWEEN stepd AND $2 THEN
          pss := pss + 1;
        END IF;
      END IF;
    END LOOP;
    stepd := date_trunc(''year'', stepd) + interval ''1 year'';
  END LOOP;
  RETURN pss;
END' LANGUAGE plpgsql STRICT;

Funkce AddWDays2 je jednodušší, pro menší počet dnů pravděpodobně i rychlejší. Do proměnné svatky uložím pole svátků na rok od dne, ke kterému přičítám pracovní dny. Funkce StatniSvatky vrací tabulku a tak je nutné převést tabulku na pole konstrukcí ARRAY(SELECT FROM). Pak pro každý pracovní den (není sobotou, nedělí a svátkem) snižuji počet pracovních dnů a posouvám se dopředu.

CREATE OR REPLACE FUNCTION AddWDaysSlow2(date, integer) RETURNS date AS '
DECLARE st date; ed date; svatky date[]; d integer; k integer;
BEGIN st := $1; d := $2; ed := st + 365;
  SELECT INTO svatky ARRAY(SELECT termin FROM StatniSvatky(st, ed));
  LOOP k := CAST(EXTRACT(DOW FROM st) AS INTEGER);
    WHILE k = 6 OR k = 0 OR st = ANY(svatky) LOOP
      st := st + 1; k := CAST(EXTRACT(DOW FROM st) AS INTEGER);
      IF st > ed THEN ed = st + 365; 
        SELECT INTO svatky ARRAY(SELECT termin FROM StatniSvatky(st, ed));
      END IF;
    END LOOP;
    EXIT WHEN d = 0 OR d = 1;
    st := st + 1; d := d - 1;
  END LOOP;
  RETURN st;
END; 
' LANGUAGE plpgsql;

Druhá (první) funkce AddWDays je komplikovanější, a pro delší intervaly rozhodně rychlejší. Využívá funkce AddWDay, která k počtu pracovních dní přičte x dní za víkendy. Pak se opakovaně zjišťuje korekce vůči státním svátkům. Pokud je parametrem AddWDay víkendový den, pak se nejdříve posune na pondělí. Pak probíhá jakási normalizace, posouvám se zpět na neděli, o to co se posunu zpět zvýším počet pracovních dní. Pak mohu jednoduše převést pracovní dny na skutečné (předposlední řádek funkce).

CREATE OR REPLACE FUNCTION AddWDays(date, integer) RETURNS date AS '
DECLARE st date; ed DATE; korekce INTEGER; sv INTEGER; d INTEGER;
BEGIN st := $1; ed := AddWDay(st,$2);
  korekce = PocetStatnichSvatku(st, ed, ARRAY[6,0]); 
  WHILE korekce > 0 LOOP
    st := ed + 1; ed := AddWDay(st, korekce); 
    korekce =  PocetStatnichSvatku(st, ed, ARRAY[6,0]);
  END LOOP;
  RETURN FirstWDay(ed);
END' LANGUAGE plpgsql STRICT;

CREATE OR REPLACE FUNCTION AddWDay(date, integer) RETURNS date AS '
DECLARE st DATE; wd INTEGER; k INTEGER;
BEGIN k := CAST(EXTRACT(DOW FROM $1) AS INTEGER);
  IF k = 0 THEN st := $1 + 1; k := 1;
  ELSIF k = 6 THEN st := $1 + 2; k := 1;
  ELSE st := $1; END IF;
  st := st - k; wd := $2 + k - 2;
  st := st + ((wd) / 5) * 7  + ((wd) % 5) + 1;
  RETURN st;
END' LANGUAGE plpgsql STRICT;

Pokud je první parametr funkce DiffWDays sobota nebo neděle, posunu začátek na pondělí, pokud je druhým parametrem sobota nebo neděle, posunu konec na pátek. Pokud je začátek za koncem, musím zvýšit počet týdnů. V daném intervalu ještě odečtu počet státních svátků. Funkce ISODOW vrací ISO číslo dne, tj. sobota 6, neděle 7.

CREATE OR REPLACE FUNCTION ISODOW(date) RETURNS INTEGER AS '
  SELECT CAST(EXTRACT(DOW FROM $1 - 1) + 1 AS INTEGER);
' LANGUAGE sql STRICT;

CREATE OR REPLACE FUNCTION DiffWDays(date, date) RETURNS integer AS '
DECLARE wc INTEGER; st DATE; ED DATE; p INTEGER;
BEGIN 
  p := ISODOW($1); IF p > 5 THEN st := $1 + (8 - p); ELSE st := $1; END IF;
  p := ISODOW($2); IF p > 5 THEN ed := $2 - (p - 5); ELSE ed := $2; END IF;
  wc := ($2 - $1) / 7;
  IF EXTRACT(DOW FROM st - 1) + 1 > EXTRACT(DOW FROM ed - 1) + 1 THEN
    wc := wc + 1;
  END IF;
  RETURN ($2 - $1 - (wc * 2) - PocetStatnichSvatku(st,ed, ARRAY[6,7]) + 1);
END' LANGUAGE plpgsql STRICT;

10. Rekurzivní volání SRF funkcí

Každé volání SRF funkce má svůj vlastní tzv. kontext, do něhož se zapisuje množina vracených hodnot. Tato vlastnost se projeví pouze při rekurzivním volání SRF funkcí, kdy při chybně navržené funkci, budeme mít pocit, že se nám ztrácí výsledky. Příklad takto chybné funkce je:

CREATE OR REPLACE FUNCTION foo (integer) RETURNS SETOF foo AS '
DECLARE pid ALIAS FOR $1; rec RECORD;
BEGIN
  FOR rec IN SELECT * FROM foo WHERE foo.pid=pid LOOP
    RETURN NEXT rec;
    RAISE NOTICE ''uid=% pid=%'',rec.uid,rec.pid;
    SELECT INTO rec * FROM foo (rec.uid);
  END LOOP;
  RETURN;
END; 
' LANGUAGE plpgsql;

Chybná je doměnka, že volání select into rec * from foo() způsobí přenesení hodnot vnořené funkce. Správný tvar funkce foo je tento:

CREATE OR REPLACE FUNCTION foor (integer) RETURNS SETOF foo AS '
DECLARE pid ALIAS FOR $1; rec RECORD;
DECLARE fo bool;
BEGIN
  FOR rec IN SELECT * FROM foo WHERE foo.pid=pid LOOP
    RETURN NEXT rec;
    FOR rec IN SELECT * FROM foor (rec.uid) LOOP
      RETURN NEXT rec;
    END LOOP;
  END LOOP;
  RETURN;
END;
' LANGUAGE plpgsql;

11. Návrh polymorfních funkcí, používání polymorfních typů

Polymorfní funkce jsou ty, které při definici návratového typu nebo argumentů používají některý z následujících polymorfních typů: anyarray, anyelement.

[Note]Poznámka

Pokud funkce vrací polymorfní typ, pak alespoň jeden z argumentů funkce musí být polymorfního typu. Dynamicky se určí návratový typ funkce podle aktuálního typu tohoto argumentu. Pokud funkce obsahuje více parametrů typu anyelement, pak skutečné parametry musí být stejného typu. Stejná podmínka platí pro anyarray.

CREATE OR REPLACE FUNCTION polyfoo1(anyelement) RETURNS anyelement AS '
BEGIN
  RETURN  $1 + 1;
END;
' LANGUAGE plpgsql;

testdb011=> select polyfoo1(date '1.1.2004'), polyfoo1(10);
  polyfoo1  | polyfoo1
------------+----------
 2004-01-02 |       11
(1 řádka)


CREATE OR REPLACE FUNCTION polyfoo2(anyelement, anyelement) RETURNS anyelement AS '
BEGIN
  RETURN  $1 - $2;
END;
' LANGUAGE plpgsql;

testdb011=> select polyfoo2(date '1.1.2004',date '1.2.2003');
 polyfoo2
----------
        0
(1 řádka)

testdb011=> select polyfoo2(date '1.1.2004',date '1.2.2003');
  polyfoo2
------------
 2003-12-31
(1 řádka)

Příkladem použití polymorfní funkce je tělo agregační funkce jejíž výsledkem je pole hodnot podmnožiny.

CREATE OR REPLACE FUNCTION aggregate_array(ANYARRAY,ANYELEMENT) RETURNS ANYARRAY AS '
   SELECT CASE
     WHEN $1 IS NULL THEN ARRAY[$2]
     WHEN $2 IS NULL THEN $1
     ELSE array_append($1,$2)
 END;
' LANGUAGE 'SQL';

testdb011=> SELECT count(*), aggarray(prijmeni) FROM lide GROUP BY prijmeni ~ '.*á';

 count |                  aggarray
-------+---------------------------------------------
     1 | {Gregorová}
     6 | {Kolík,Čurda,Hojka,Chytlík,Stěhule,Novotný}
(2 řádek)

12. Změny ve verzi 8.x.

Snad nejpodstatnější změnou je možnost zápisu těla funkce mezi dvojici symbolů $$. Pokud použijeme tuto variantu zápisu, pak nemusíme zdvojovat apostrofy. Že se zpřehlední zápis je bez diskuze. Ve všech příkladech používám tento zápis.

V dřívějších verzích jsme mohli určit výchozí hodnoty proměnných, nicméně zápis povoloval pouze konstanty. Počínaje 8.0. můžeme výchozí hodnotu dopočítat. Můžeme tak např. lépe simulovat parametry předávané hodnotou (alias i pojmenované parametry jsou stále read only). PL/pgSQL podporuje konečně pojmenované parametry. V dřívějších verzích byly parametry funkce přístupné pouze prostřednictvím $index proměnných. Pozor: stávající PhpPgAdmin tuto vlastnost nepodporuje, a pokud se pokusíte editovat funkci s pojmenovanými parametry, tak se dočkáte nemilého překvapení. PostgreSQL stále nepodporuje OUT a INOUT parametry. Výchozí hodnotu proměnné nelze určit pro složené typy.

CREATE OR REPLACE FUNCTION foo1(IN par1 integer) RETURNS bool AS $$
DECLARE
  x ALIAS FOR $1;
  y integer = par1;
BEGIN
  RAISE NOTICE '% % %', par1, x, y;
  -- par1 := 11; ERROR: "$1" is declared CONSTANT
  -- x := 10;  ERROR: "$1" is declared CONSTANT
  y := 10;
  RETURN 't';
END; $$ LANGUAGE plpgsql;

Touto verzí PostgreSQL zásadně rozšiřuje možnosti kompozitních (složených) typů (terminologie: composite types, row types). Kromě jiného můžeme lépe zacházet s proměnnými NEW a OLD v funkcích triggerů.

DROP TYPE footype CASCADE;
CREATE TYPE footype AS (x integer, y integer, z varchar);

CREATE OR REPLACE FUNCTION foo2(footype) RETURNS bool AS $$
DECLARE 
  x integer = $1.x;
  y footype;
BEGIN
  RAISE NOTICE '% %', x, $1.y;
  y := $1; y := (10, 10, 'Pavel');
  RETURN 't';
END; $$ LANGUAGE plpgsql;

SELECT foo2((10, 10, 'Pavel'));

CREATE OR REPLACE FUNCTION foo3() RETURNS footype AS $$
DECLARE
  x footype;
BEGIN
  x := row(10, 12, 'Jirka');
  x := (10, 12, 'Jirka');
  RETURN x;
END; $$ LANGUAGE plpgsql;

SELECT foo3(); -- NELZE
SELECT (foo3()).z;

V předchozích verzích nebylo možné předat proměnné OLD a NEW mimo tělo funkce triggeru. Nyní, díky větší podpoře složených typů to není problém.

DROP TABLE testxx CASCADE;
CREATE TABLE testxx(x integer, y integer);

CREATE OR REPLACE FUNCTION trig01(anyelement) RETURNS integer AS $$
BEGIN RAISE NOTICE 'trig01: %', $1.x;
END; $$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION trig02(testxx) RETURNS integer AS $$
BEGIN RAISE NOTICE 'trig02: %', $1.y;
END; $$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION footrig() RETURNS TRIGGER AS $$
DECLARE
  s1 integer; s2 integer;
BEGIN
  s1 := trig01(NEW); s2 := trig02(NEW);
  RAISE NOTICE '% %', s1, s2;
  RETURN NEW;
END; $$ LANGUAGE plpgsql;

CREATE TRIGGER xxx BEFORE INSERT ON testxx 
  FOR EACH ROW EXECUTE PROCEDURE footrig();

PostgreSQL 8.0 umožňuje kopírování (přiřazení) hodnot složených typů (včetně typu RECORD). Přiřazení je dost volné. Originál se od kopie může lišit počtem sloupců, názvy položek, podstatná je jen možnost konvertibility relevantních sloupců (důležité je pořadí sloupců v záznamu). Přebytečné sloupce jsou ignorovány, v případě, že má originál víc sloupců než kopie. Pokud má originál méně sloupců než kopie, pak chybějící hodnoty budou mít hodnotu null.

PL/pgSQL v PostgreSQL 8.0 obsahuje konstrukci umožňující ošetření chyb (vyjímek). Tuto konstrukci bychom měli používat opravdu v případě, že chybě nemůžeme předejít. Interně je tato konstrukce implementována jako vnořená transakce, a zahájení transakce má určitou režii. Bez ohledu na to, zda-li k vyjímce dojde nebo nedojde.

CREATE OR REPLACE FUNCTION foo(integer) RETURNS integer AS $$
BEGIN
  INSERT INTO footab VALUES($1);
  RETURN 0;
EXCEPTION WHEN OTHERS THEN
  RETURN -1;
END; $$ LANGUAGE plpgsql;

PL/pgSQL v PostgreSQL 8.1 podporuje funkce s OUT, INOUT parametry. Tento způsob předávání parametrů použijeme v případě, že funkce vrací více hodnot. V předchozích verzích bylo nutné explicitně deklarovat složený datový typ, který plpgsql funkce vracela. Funkce s OUT, INOUT parametry můžeme volat jak v normálním, tak SRF kontextu. V prvním případě vrací sloupec typu RECORD nebo skalár (pokud byl pouze jeden OUT parametr), v druhém tabulku (názvy sloupců odpovídají sloupcům proměnných, pokud existují alespoň dvě OUT proměnné).

CREATE OR REPLACE FUNCTION foo(OUT x integer, OUT y integer, INOUT z integer) AS $$
BEGIN
  x := z + 1; y := z + 2;
END; $$ LANGUAGE plpgsql;
SELECT foo(10);

Typ funkce musíme deklarovat pouze v případě, že se jedná o SRF funkci, jinak systém určí odpovídající typ - RECORD nebo skalár podle parametrů funkce. Díky tomu, že hodnota funkce je dána určenou proměnnou, sadou proměnných nemá smysl a také nelze v příkazu RETURN specifikovat návrat. hodnotu (ta je určena obsahem OUT proměnných). Příkaz RETURN je nyní nepovinným. Výsledkem funkce je obsah proměnných v okamžiku ukončení funkce.

CREATE OR REPLACE FUNCTION foo(OUT x integer, OUT y integer, IN z integer) 
RETURNS SETOF RECORD AS $$
BEGIN
  FOR _i IN 1 .. z LOOP
    x := _i; y := _i + 1;
    RETURN NEXT;
  END LOOP;
END; $$ LANGUAGE plpgsql;
SELECT * FROM foo5(2);
 x | y
---+---
 1 | 2
 2 | 3
(2 rows)

Při volání funkce je třeba si uvědomit, že OUT proměnné se nepřenáší a tudíž se ani nezapisují do seznamu parametrů volané funkce.

V předchozích verzích bylo velice obtížné získat query plan SQL dotazů v PL/pgSQL funkcích. Počínaje 8.1 to není žádný problém.

CREATE OR REPLACE FUNCTION foox() RETURNS void AS $$
DECLARE _r RECORD;
BEGIN
  FOR _r IN EXPLAIN SELECT * FROM footab LOOP
    RAISE NOTICE '%', _r."QUERY PLAN";
  END LOOP;
END; $$ LANGUAGE plpgsql;
SELECT foox();
NOTICE:  Seq Scan on footab  (cost=0.00..31.40 rows=2140 width=4)
 foox
------

(1 row)

13. Kdy nepoužívat PL/pgSQL

Ne vždy lze použít uložené procedury. Podmínkou pro přenesení kódu do uložených procedur je absolutní neinteraktivita a bezestavovost. Jelikož většina aplikací jistý stupeň interakce vyžaduje, je nepravděpodobné a jak zkušenosti uživatelů napovídají i neefektivní psát aplikace čistě v uložených procedurách. SQL server nemůže nahradit aplikační server, pouze částečně, a je na úvaze uživatele aby zvážil všechna pro a proti.

Stejně tak jako mají uložené procedury svá omezení ma svá omezení jazyk PL/pgSQL. V několika určitých případech nelze PL/pgSQL použít buďto z nedostatečnosti jazyka (PL je příliš statický) nebo z důvodu neefektivity.

PL/pgSQL není vhodné používat k inicializaci velkých polí - důvodem je pomalý zápis do pole. Pokud není zbytí je mnohem rychlejší prvky pole vytvořit pomocí konverze tabulky, kterou vytvoříme SRF funkcí, na pole. Například nejrychlejší způsob vytvoření pole vzestupných hodnot je SELECT ARRAY(SELECT * FROM generate_series(1,100));. Funkce generate_series je SRF funkce generujcí posloupnost hodnot ze zadaného intervalu.

Rychlostí nevyniká ani iterační sestavení řetězce. Pokud nejste spokojeni s rychlostí funkce, zkuste ještě následující trik. Svou funkci převeďte na SRF funkci - část, kde slučujete řetězce nahraďte příkazem RETURN NEXT část_řetězce. Konečné sloučení provedete mimo tělo funkce pomocí funkce array_to_string.

CREATE OR REPLACE FUNCTION generate_string(integer) RETURNS SETOF varchar AS $$
BEGIN
  FOR _i IN 1 .. $1 LOOP 
    RETURN NEXT '<item>'||_i||'</item>';
  END LOOP;
  RETURN;
END; $$ LANGUAGE plpgsql;

SELECT array_to_string(
  ARRAY(SELECT * FROM generate_string(1000)), '');

PL/pgSQL nepodporuje žádné I/O operace, a ani se nepředpokládá, že by je v blizké budoucnosti byly podporovány - PL/pgSQL existuje pouze v trusted variantě. Řešením je použít některý z untrusted jazyků jako je plperlu nebo plpython.

PL/pgSQL není vhodný k návrhnu univerzálních triggerů. V PL/pgSQL neexistuje možnost jak převést hodnotu typu RECORD na pole (Položka typu RECORD je přístupná pouze prostřednictvím statického identifikáturu známého v době kompilace, pole je přístupné prostřednictvím dynamického indexu). Tudíž neexistuje žádná možnost jak dynamicky iterovat skrz všechny položky typu RECORD, nebo dynamicky přistupovat k typu RECORD. Řešením je opět využít jiný programovací jazyk, tentokrát však postačí trusted varianty.