Procedures should be packed as efficient statements together, not degenerate into complicated logic.
- Business processes
- Avoid cursors
- Use
insert...selectand complexupdates
- Sequence of change operations
- Single business process
- Avoid
begin transaction/commitinside
commitis a slow operation (it usually means writing to a file) and it may hurt if the procedure is called repeatedly.
May spent a lot of time in the network if running a business procedure in a remote server.
So we should store the procedure directly on the server. All we need is just one call.
Advantages:
- A single version to maintain
- Execute directly on the DBMS server
- Security, all changes follow the rules
create procedure myproc
execute myproc
PostgreSQL is the only product for which, probably under the influence of languages such as C or Java, procedures are just functions that return nothing.
--
-- A procedure to register a film
--
create function movie_registraction
( p_title varchar,
p_country_name varchar,
p_year varchar,
p_director_fn varchar,
p_director_sn varchar,
p_actor1_fn varchar,
p_actor1_sn varchar,
p_actor2_fn varchar,
p_actor2_sn varchar)
returns void
as $$
declare
n_rowcount int;
n_movieid int;
n_people int;
begin
insert into movies(title, country, year_released)
select p_title, country_code, p_year
from countries
where country_name = p_country_name;
get diagnostics n_rowcount = row_count;
if n_rowcount = 0
then
raise exception 'country not found in table COUNTRIES'
end if
n_movieid := lastval();
--
-- Count how many people were "provided" in the parameters
-- (some of them could be NULL)
--
select count(surname)
into n_people
from ( select p_director_sn as surname
union all
select p_actor1_sn as surname
union all
select p_actor2_sn as surname) specified_people
where surname is not null;
--
-- Get people identifiers and insert into table credits
--
insert into credits(movieid, peopleid, credited_as)
select n_movieid, people.peopleid, provided.credited_as
from (select coalesce(p_director_fn, '*') as first_name,
p_director_sn as surname,
'D' as credited_as
union all
select coalesce(p_actor1_fn, '*') as first_name,
p_actor1_sn as surname,
'A' as credited_as
union all
select coalesce(p_actor2_fn, '*') as first_name,
p_actor2_sn as surname,
'A' as credited_as) provided
inner join people
on people.surname = provided.surname
and coalesce(people.first_name, '*') = provided.first_name
where provided.surname is not null;
get diagnostics n_rowcount = ROW_COUNT;
if n_rowcount != n_people
then
-- My choice is to cancel everything ("raise" will
-- generate a rollback of the whole procedure, the
-- successful insert into movies will be cancelled)
raise exception 'Some people couldn''t be found';
end if;
end;
$$ language plpgsql;select movie_registration('The Adventures of Robin Hood',
'United States', 1938,
'Michael', 'Curtiz',
'Errol', 'Flynn',
null, null);Use perform movie_registration(...) inside another procedure.
begin -- same as'try'
...
exception -- same as as 'catch'
when ... -- exception name here
end;unique_violation
declare
c cursor for select ...;
begin
for row_var in c
-- Inside the loop, you can refer to row_var.col_name
loop
...
end loop;
...
end;DDL operations are usually unsupported by stored procedures.
A
CREATE TABLE(orALTER TABLE) in a procedure will fail.
BUT you can cheat.
begin
...
cmd := 'create table T ...';
execute cmd;
...
end;INFORMATION_SCHEMAcontains "System Views" that describe the database (tables, columns, constraints).CREATE TABLE ... AS SELECTlet you copy structure and data. However, the copy is imperfect.- all columns are created nullable
- constraints and other features we'll see later are "forgotten"
-- An example where using a cursor is fully justified.
-- The procedure queries the catalogue to check the tables
-- in the current schema, and creates a copy (with the date in the
-- name) of every table for which no copy was created today and
-- that isn't itself a copy.
-- There is no way to do it without a cursor.
create or replace function save_tables()
returns void
as $$
declare
v_suffix varchar(50);
v_create_cmd varchar(100);
c cursor for select replace(table_name, v_suffix, '') as table_name
from information_schema.tables
where table_schema = current_schema()
group by replace(table_name, v_suffix, '')
having count(*) = 1
and replace(table_name, v_suffix, '') not like '%_save_%';
begin
select '_save_' || to_char(current_date, 'YYMMDD')
into v_suffix;
for fetched_row in c
loop
v_create_cmd := 'create table ' || fetched_row.table_name || v_suffix
|| ' as select * from ' || fetched_row.table_name;
execute v_create_cmd;
end loop;
end;
$$ language plpgsql;For executing stored procedures automatically.
Fired by data changes (never by a SELECT). [See below]
-
Modify input on the fly
Example: Force input in lowercase.
-
Check complex rules
Example: Complex business rules cannot be checked through declarative constraints. You can abort a transaction in a trigger, and return an error.
-
Manage data redundancy
Example: Split a newly inserted movie title into words.
Depending on what the trigger is designed to achieve, it may be fired by various events and at various possible precise moments.
- Activate the trigger before/after
insertfor the who procedure - Activate the trigger before/after
insertfor each row
insertupdatedelete
Several possible triggers.
Several possible events can fire one trigger
Some products let you have several different events that fire the same trigger (timing must be identical).
create trigger trigger_name
before insert or update or delete
on table_name
for each row
as
begin
...
endOther products allow only one trigger per event/timing, and one event per trigger.
create trigger trigger_name
before delete
on table_name
for each row
as begin
...
endSQL Server is a bit special. Triggers are always after the statement, and syntax is different from other products. But several events can fire one trigger.
create trigger trigger_name
on table_name
after insert, update, delete as
begin
...
endFor instance, you want to make sure that data is always in lowercase but the (bought) data entry program doesn't enforce it.
before insert / update
for each rowSQL Server: modify by joining on inserted
before insert / update / delete
for each rowSQL Server: check by joining on inserted and deleted. Roll back if something wrong.
after insert / update / delete
for each rowSQL Server: deleted/inserted
A third case is managing some data redundancy (which means some duplication of data). A trigger can write in your back to another table.
In the film database, this is done for titles: words are automatically isolated and added to MOVIE_TITLE_FT_INDEX2 whenever you add a row to MOVIES or ALT_TITLES.
create or replace function people_audit_fn() returns trigger
as
$$
begin
if tg_op = 'UPDATE'
then
insert into people_audit(...)
...
elsif tg_op = 'INSERT' then
insert into people_audit(...)
...
else
insert into people_audit(...)
...
end if;
return null;
end;
$$ language plpgsql;Notice that the initial "returns trigger" is completely dummy. We can return anything, null is OK.
create trigger people_trg
after insert or update or delete on people
for each row
execute procedure people_audit_fn();Beware of FOR EACH ROW triggers, you cannot do anything in them.
PostgreSQL
create table test
(id int, label varchar(20),
unique(id) deferrable initially deferred);Consistency and constraints are checked AFTER the update, not DURING. During the update, the state is undefined.
DON'T look at other rows of the modified table in for each row triggers.
If you can, avoid triggers.
- Don't use triggers to fix design issues.
- Use stored procedures preferably to triggers.
- Use triggers if there are multiple access points (other than your programs).
Two columns often queried together can be indexed together; what is indexed is concatenated values (NOT separate values)
Whenever you declare a PRIMARY KEY or UNIQUE constraint, an index is created behind your back.
Additionally, indexes use a lot of storage, sometimes more than data! It has a huge impact on operations (regular activities such as backups).
You can also declare an index to be unique.
create unique index <index name>
on <table name>(<col1>, ... <coln>)Use Unique constraint instead of Unique index.
...
