Úvod do PL/pgSQL - SQL Procedural Language

Ciele

  1. Vytvoriť vlastnú PL/pgSQL funkciu a storovanú procedúru.
  2. Použiť lokálne premenné a vyskúšať si ich naplnenie použitím SELECT ... INTO ...
  3. Použitie cyklu, podmienok.
  4. Práca s kurzormi.

Úvod

Dnešné cvičenie je venované rozšíreniu jazyka SQL o možnosť tvorby imperatívnych procedúr a funkcií. V PostgreSQL je takéto rozšírenie implementované ako jazyk PL/pgSQL.

PL/pgSQL procedurálny jazyk rozširuje štandardný SQL o mnohé procedurálne prvky ako napríklad: riadiace štruktúry, cykly, alebo možnosť komplexných výpočtov. Umožňuje vyvíjať aj komplexné funkcie a storované procedúry v PostgreSQL, ktoré nie je možné napísať v čistom SQL.

Hlavným rozdielom medzi funkciou a storovanou procedúrov v PostgreSQL je, že funkcia vracia výsledok, pričom storovaná procedúra nie. Zámerom takejto implementácie je vykonať určitý druh činnosti, dokončiť činnosť a vrátiť volajúcemu kontrolu. Do verzie PostgreSQL 11, boli storované procedúry implementované ako funkcie, ktoré nevracali údaje. V aktuálnej verzii existuje spôsob, ako priamo deklarovať storované procedúry, ktorých výhodou je tiež to, že sú schopné vo svojom kóde začať novú transakciu.

Poznámka

V PostgreSQL je možné využiť viaceré jazyky (PL/Tcl, PL/Perl a PL/Python) na definovanie funkcií a procedúr avšak je potrebné ich nainštalovať. V rámci cvičení budeme pracovať z jazykom PL/pgSQL, ktorý je v PostgreSQL predinštalovaný.

Postup

Krok 1

Pracujeme na databáze Movies s nasledovnou schémou.

Relačný model databázy Movies
Obr. 1: Relačný model databázy Movies

Syntax funkcií v jazyku PL/pgSQL je nasledovná:

CREATE [OR REPLACE] FUNCTION functionName(someParameter 'parameterType') 
RETURNS 'DATATYPE'
AS $_block_name_$
DECLARE
    --declare variables
BEGIN
    --do something
    --return something
END;
$_block_name_$
LANGUAGE plpgsql;

Volanie funkcie vykonáte pomocou príkazu:

SELECT functionName(someParameter);

Poznámka

V nástroji DBeaver vytovríte funkciu pravým tačidlom myši kliknutím na Functions -> Create New Function v záložke Database Navigator nasledovane:

Vytvorenie funkcie v DBeaver
Obr. 2: Vytvorenie funkcie v DBeaver

Úloha 1.1

Vytvorte funkciu, ktorá vypíše 1 riadok a 1 stĺpec s názvom greet a hodnotou "hello world!".

Riešenie

CREATE OR REPLACE FUNCTION greet()
	RETURNS text
	LANGUAGE plpgsql
AS $function$
	BEGIN
		RETURN 'hello world!';
	END;
$function$
;

Úloha 1.2

Vytvorte funkciu, ktorá vypíše 1 riadok a 1 stĺpec s názvom text a hodnotou "hello meno!". Meno nech je vstupný parameter.

Riešenie

CREATE OR REPLACE FUNCTION greet_name(name text)
	RETURNS text
	LANGUAGE plpgsql
AS $function$
	begin
		return 'hello ' || name || '!';
	END;
$function$
;

Úloha 1.3

Napíšte dopyt, ktorý vypíše celú tabuľku c_city. Posledný stĺpec vo výsledku nech je stĺpec s hodnotou "hello mesto!". Využite funkciu vytvorenú v predchádzajúcej úlohe. Názvy miest nech sú z tabuľky c_city (stĺpec cit_name).

Riešenie

select *, greet_name(cit_name) from c_city cc;

Krok 2

Úloha 2.1

Napíšte funkciu slúžiacu na INSERT dát do tabuľky c_city. Funkcia na vkladanie údajov nech má 2 vstupné a 1 výstupný parameter. Vstupné parametre sú: názov mesta (cit_name) a krajina (xcit_cou_id), v ktorej sa mesto nachádza. Výstupný parameter nech je id novovytvoreného mesta (čiže integer).

Riešenie

CREATE OR REPLACE FUNCTION movies.fn_c_city_ins(cit_name text, xcit_cou_id text, out cit_id integer)
	RETURNS int4
	LANGUAGE plpgsql
AS $function$
DECLARE
	BEGIN
		WITH vlozene AS (
			INSERT INTO c_city(
				cit_name, xcit_cou_id
				)
			VALUES (
				fn_c_city_ins.cit_name, fn_c_city_ins.xcit_cou_id
				)
				RETURNING
					c_city.cit_id
		)
		SELECT
			vlozene.cit_id
		INTO
			fn_c_city_ins.cit_id
		FROM
			vlozene
		;
		RETURN;
	END;
$function$
;

Úloha 2.2

Napíšte procedúru slúžiacu na UPDATE dát v tabuľke c_city. Procedúra na zmenu údajov nech má 4 vstupné parametre. Vstupné parametre sú: id mesta, ktorého atribúty chceme zmeniť (cit_id), nový názov mesta, nový názov krajiny a meno používateľa, ktorý záznam modifikuje. Využitím COALESCE ošetrite prípad, aby stĺpec cit_modified_by nemal hodnotu NULL, ale aby sa v takom prípade doplnila hodnota systémovej funkcie session_user. Takisto doplnite do stĺpca cit_modified aktuálny čas a dátum príslušnou funkciou.

Riešenie

CREATE OR REPLACE PROCEDURE movies.pr_c_city_upd(cit_id integer, cit_name text, xcit_cou_id text, cit_modified_by text DEFAULT NULL::text)
 LANGUAGE plpgsql
AS $procedure$
	begin
		update c_city
		set cit_name = pr_c_city_upd.cit_name, 
			xcit_cou_id = pr_c_city_upd.xcit_cou_id, 
			cit_modified_by = coalesce(pr_c_city_upd.cit_modified_by, "session_user"()::text), 
			cit_modified = now()
		where c_city.cit_id = pr_c_city_upd.cit_id
		;
    
	END;
$procedure$
;

Úloha 2.3

Napíšte procedúru slúžiacu na DELETE dát v tabuľke c_city. Procedúra na zmazanie údajov nech má 1 vstupný parameter. Vstupný parameter je: id mesta, ktoré chceme vymazať (cit_id).

Riešenie

CREATE OR REPLACE PROCEDURE movies.pr_c_city_del(cit_id integer)
	LANGUAGE plpgsql
AS $procedure$
	begin
		delete from c_city 
		where c_city.cit_id = pr_c_city_del.cit_id ;
	END;
$procedure$
;

Úloha 2.4

Napíšte funkciu slúžiacu na SELECT dát z tabuľky c_city. Funkcia na select údajov nech má 1 vstupný parameter. Vstupný parameter je: podmienka, podľa ktorej chceme dáta vyselectovať. Funkcia nech vráti tabuľku c_city.

Riešenie

CREATE OR REPLACE FUNCTION movies.fn_c_city_sel(podmienka text)
 RETURNS TABLE(cit_id integer, cit_name character varying, cit_inserted_by text, cit_inserted timestamp without time zone, cit_modified_by text, cit_modified timestamp without time zone, xcit_cou_id char)
 LANGUAGE plpgsql
AS $function$
	begin
		RETURN QUERY
		execute
		'SELECT
				cit_id
			    , cit_name
				, cit_inserted_by
				, cit_inserted
				, cit_modified_by
				, cit_modified
				, xcit_cou_id
		FROM
			c_city '::text
			|| coalesce(' WHERE ' || fn_c_city_sel.podmienka , ''::text)
			|| ' order by cit_name asc'::text
		;
	END;
$function$
;

Poznámka

V praxi sa aj volanie jednoduchých príkazov INSERT, UPDATE, DELETE, SELECT realizuje pomocou funkcií, z dôvodu optimalizácie a diagnostiky práce s databázou. Napríklad slúži na logovanie, kto spustil select alebo na doplnenie dátových kontrol pred samotným vykonaním funkcie. Taktiež na logovanie času trvania vykonávania funkcie. Ďalší dôvod vytvárania takýchto funkcií je potreba jasného ohraničenia (odelenia) medzi GUI aplikácie a databázou (žiadne SQL príkazy nie sú v GUI - v GUI sa nachádzajú iba volania funkcií).

Krok 3

Úloha 3.1

Vytvorte funkciu, ktorá vypíše názov dňa v týždni podľa jeho poradia v rámci týždňa (prvý nech je pondelok). Vstupný parameter je: číslo dňa. Vráťte názov dňa. Ošetrite nesprávny vstup (číslo menšie ako 1 a väčšie ako 7). Zadefinujte lokálnu premennú typu pole reťazcov na uloženie názvov dní, kde v poli reťazcov budú vymenované názvy dní v týždni.

Riešenie

CREATE OR REPLACE FUNCTION movies.fn_den(cislo_dna integer)
 RETURNS text
 LANGUAGE plpgsql
AS $function$
DECLARE
dni varchar[]:= array ['Pondelok','Utorok','Streda','Štvrtok','Piatok','Sobota','Nedeľa'];
i int;
vystup varchar;
	BEGIN
		if (cislo_dna > 0 and cislo_dna < 8) then 
			for i in 1..7 loop 
				if i = cislo_dna then
					vystup:=dni[i];
				end if;
			--raise notice 'den: %', dni[i];
			end loop;
		else
			vystup:='Cislo mimo rozsah';
		end if;
	return vystup;
    
	END;
$function$
;

Poznámka

Polia v PosgreSQL defaultne začínajú od 1.

Poznámka

Pri ladení funkcií pl/pgsql odporúčame používať výpis do konsoly cez RAISE NOTICE.

Poznámka

Ak má niekto z Vás skúsenosť s prácou s databázou Oracle, treba si dať pozor na vracanie hodnôt pomocou výstupných parametrov kvôli odlišnostiam v PostgreSQL. Na túto tému si odporúčam prečítať diskusiu.

Úloha 3.2

Vytvorte tabuľku s názvom t_log na logovanie udalostí v databáze. Nech ma stĺpce: log_id (primárny kľúč typu serial), log_event(text), log_time (timestamp s default hodnotou now()), log_user (text s default hodnotou session_user), log_value (float), log_object (text). Všetky stĺpce majú byť povinné.

Riešenie

-- DROP TABLE t_log;
  
CREATE TABLE t_log (
	log_id serial NOT NULL,
	log_event text NOT NULL,
	log_time timestamp(0) NOT NULL DEFAULT now(),
	log_user text NOT NULL DEFAULT SESSION_USER,
	log_object text NOT NULL,
	log_value float8 NOT NULL,
	CONSTRAINT t_log_pk PRIMARY KEY (log_id)
);

Úloha 3.3

Vytvorte funkciu (napr. s názvom fn_t_log_ins) na logovanie dĺžky trvania vykonávania ľubovoľnej funkcie ( neskôr si vyberiete, na ktorej z už vytvorených funkcií si logovanie odskúšate). To znamená, že chceme vytvoriť funkciu fn_t_log_ins, ktorá bude neskôr volaná inou funkciou. Účelom fn_t_log_ins je spraviť INSERT do tabuľky t_log, s údajom o dĺžke vykonávania funckie, ktorá ju bude volať. Vstupné parametre sú: log_event, log_object a log_value. Výsledok zaznamenajte (insert) do tabuľky t_log. Výstupný parameter je id vytvoreného záznamu v tabuľke t_log.

Riešenie

CREATE OR REPLACE FUNCTION movies.fn_t_log_ins(log_event text, log_object text, log_value real, OUT log_id integer)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
DECLARE
	BEGIN
		WITH vlozene AS (
			INSERT INTO t_log (log_event, log_object , log_value )
			VALUES ( fn_t_log_ins.log_event, fn_t_log_ins.log_object, fn_t_log_ins.log_value)
			RETURNING
					t_log.log_id
		)
		SELECT
			vlozene.log_id
		INTO
			fn_t_log_ins.log_id
		FROM
			vlozene
		;
	
		RETURN;
  
	END;
$function$
;

Úloha 3.4

Upravte už existujúcu funkciu tak, aby vo svojom tele volala funkciu z predchádzajúcej úlohy. Môžte využiť aj pg_sleep(random());. My sme upravili funkciu úlohy 1.1.

Riešenie

CREATE OR REPLACE FUNCTION movies.greet()
 RETURNS text
 LANGUAGE plpgsql
AS $function$
declare 
t timestamptz := clock_timestamp();
	begin
		perform pg_sleep(random());
		perform fn_t_log_ins('EXECUTE','greet()',extract(second from clock_timestamp() - t)::real);	 --perform ked nechceme spracovat vystup z volania
		RETURN 'hello world!';
	END;
$function$
;

Poznámka

Teraz si prezrite tabuľku t_log. Mali Vám do nej pribudnúť záznamy (dĺžka vykonávania) vami upravenej funkcie.

Úloha 3.5

Vytvorte funkciu, ktorá skontroluje správnosť formátu slovenského PSČ. Znaky nech sú iba číslice od 0-9 a dĺžka PSČ je rovná 5. Vstupný parameter je: psc (text). Funkcia nech vráti 1 v prípade chyby PSČ (napríklad dlhšie PSČ, iné znaky ako číslice,...). Inak nech vráti 0. V prípade, že dĺžka PSČ je rovná 5 ale PSČ aj tak nie je platné, využitím raise notice vypíšte, na ktorej pozícii sa nachádza chybný znak.

Riešenie

CREATE OR REPLACE FUNCTION movies.fn_kontrola_psc_sk(psc text)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$
declare 
	dlzka int; 
	i int;
	begin
		dlzka:=char_length(fn_kontrola_psc_sk.psc);	
		if dlzka<>5 then
			raise notice 'Nesedi dlzka zadaneho PSC, musi byt 5';
			return 1;
		end if;
		
		for i in 1..5 loop
			if ascii(substring(fn_kontrola_psc_sk.psc from i for 1)) < 48 or ascii(substring(fn_kontrola_psc_sk.psc from i for 1)) > 57 then
				raise notice 'Na pozicii % nie je cislica',i;
				return 1;
			end if;
		end loop;
	
	return 0;   --0 - ok, 1 - nesedi PSC, 2 - chyba funkcie
	END;
$function$
;

Krok 4

Úloha 4.1

Vytvorte funkciu, v ktorej skontrolujete správnosť formátu PSČ už zadaných záznamov v tabuľke t_cinema. Môžte pritom využiť funkciu z predošlej úlohy. Použite CURSOR, ktorý dotaz "zapuzdrí" a umožní spracovávať výsledky dotazu po jednotlivých riadkoch. Kurzory sa tiež využívajú, ak chceme zabrániť preplneniu pamäte v prípade, že výsledok dotazu obsahuje veľký počet riadkov. Kurzor, ktorý zapuzdrujue všetky riadky z tabuľky t_cinema si môžte deklarovať nasledovne:

cur_cinema CURSOR FOR SELECT * FROM t_cinema;

Riešenie

CREATE OR REPLACE FUNCTION movies.fn_psc_cinema()
	RETURNS int4
	LANGUAGE plpgsql
AS $function$
declare 
--zaznam movies.t_cinema%ROWTYPE;
cur_cinema CURSOR FOR SELECT * FROM t_cinema;
riadok record;
psc_spravne integer;
	begin
		open cur_cinema;
loop
	fetch cur_cinema into riadok;
	exit when not found;
   
	select fn_kontrola_psc_sk(riadok.cin_postal_code) into psc_spravne;
	if (psc_spravne<>0) then
	raise notice 'Riadok s ID: % %' ,riadok.cin_id, riadok.cin_name;
return 1;
	end if;
end loop;	
  
close cur_cinema;	
return 0;
  
END;
$function$
;

Upozornenie

Po vypracovaní úloh z cvičenia prosím o vyplnenie nasledujúceho dotazníka.

Zdroje

  1. Odkaz
  2. Tutoriál
  3. Článok: storované procedúry a funkcie v PostgreSQL