Taller PL-PGSQL
Taller PL-PGSQL
Introducción
Ya hemos visto que SQL (DML) es un lenguaje simple y poderoso para manipular datos en una
base de datos. PL/pgSQL es un lenguaje procedimental que permite crear funciones,
procedimientos y triggers con el fin de realizar operaciones y computaciones más complejas
dentro de la base de datos. Realizar dichas operaciones dentro de la base de datos, y no en las
aplicaciones, puede mejorar el redimiento del sistema puesto que se disminuye la comunicación
entre la aplicación y el SGBD.
Antes de iniciar
Para los ejemplos descritos en este taller se requieren las siguientes tablas y datos:
CREATE TABLE CLIENTES (ID SERIAL PRIMARY KEY,
CC VARCHAR(10) NOT NULL,
NOMBRE VARCHAR(50) NOT NULL);
Primer Ejemplo
La estructura general de un bloque PL/PgSQL es la siguiente:
[ <<label>> ]
[ DECLARE
declarations ]
BEGIN
statements
END [ label ];
Las variables, en caso de ser necesarias, deben ser declaradas en la cláusula DECLARE:
CREATE OR REPLACE FUNCTION CONCAT_NOMBRE(NOMBRE VARCHAR, APELLIDO VARCHAR)
RETURNS VARCHAR AS $$
DECLARE
TEMP VARCHAR;
BEGIN
TEMP:= NOMBRE || ' ' || APELLIDO;
RETURN INITCAP(TEMP);
END;
$$
LANGUAGE plpgsql;
Un ejemplo de uso:
select concat_nombre('carlos','olarte');
concat_nombre
---------------
Carlos Olarte
Un ejemplo de uso:
SELECT INS_CLIENTES('44444','CLIENTE 4');
Note que en este caso, los argumentos de la función no tienen nombre y se refieren a ellos como
$1 y $2 dentro del cuerpo de la función. Además, como solo lanzamos sentencias SQL, el
lenguaje de la función es SQL.
Ejemplo de uso:
postgres=# select transfer(3,2,1000);
transfer
----------
-1000
(1 row)
Es posible realizar algunas validaciones. Por ejemplo, se debe verificar que las dos cuentas
existan y que el monto de la segunda sea suficiente para realizar la transferencia.
/* Versión2: Validacion de los datos de entrada */
La siguiente función actualiza los intereses de mora en las cuentas que están sobregiradas. Los
intereses que se cobran son proporcionales al saldo de la cuenta y se utiliza el porcentaje que se
pasa como parámetro.
CREATE FUNCTION COBRAR_INTERESES(PORCENTAJE REAL) RETURNS VOID AS $$
BEGIN
UPDATE CUENTAS
SET INTERESES = SALDO * PORCENTAJE *
EXTRACT('DAYS'FROM ( NOW()-FECHA_SOBREGIRO))
WHERE FECHA_SOBREGIRO IS NOT NULL;
END;
$$ LANGUAGE plpgsql;
Ejemplo de uso:
bdi00=> SELECT COBRAR_INTERESES(0.01);
Ejemplo de uso:
bdi00=> Select INF_CUENTA(1);
inf_cuenta
----------------------------------------------------
La cuenta pertenece a CLIENTE 1. El saldo es 5000
(1 row)
Note el uso de IF NOT FOUND THEN … para verificar si el SELECT trajo o no algún resultado.
Obviamente la función anterior se hubiera podido escribir de manera mucho más simple
ejecutando una única consulta que traiga la información del cliente y de la cuenta:
CREATE FUNCTION INF_CUENTA2(NUMCUENTA INT) RETURNS VARCHAR AS $$
DECLARE
SALDO_CTA CUENTAS.SALDO%TYPE;
NOMBRE_CLI VARCHAR;
BEGIN
SELECT CLI.NOMBRE, CTA.SALDO INTO NOMBRE_CLI,SALDO_CTA FROM CUENTAS CTA
INNER JOIN CLIENTES CLI ON (CLI.ID = CTA.CLI_ID)
WHERE ID = NUMCUENTA;
En este caso, note que la cláusula SELECT … INTO puede asignar varias variables al tiempo.
Cursores
Un cursor es una estructura que permite recuperar los datos de una consulta fila por fila.
Asuma una tabla con una serie de transacciones bancarias “pendientes”, i.e., transacciones que
deben ser realizadas y afectar las cuentas de los clientes:
CREATE TABLE PENDIENTES (
ID SERIAL PRIMARY KEY,
MONTO REAL NOT NULL,
CTA_ID INT NOT NULL REFERENCES CUENTAS(ID),
FECHA DATE DEFAULT NOW(),
OPERACION CHAR(1),
REALIZADA BOOL DEFAULT FALSE);
La cláusula FOR UPDATE evita que otras transacciones estén modificando las filas seleccionadas.
Ejemplo de uso:
bdi00=> select * from CUENTAS;
id | saldo | cli_id | fecha_sobregiro | intereses
----+-------+--------+-----------------+-----------
1 | 5000 | 1| | 0
2 | -2000 | 2| | 0
3 | 3000 | 3| | 0
(3 rows)
(1 row)
bdi00=> select * from CUENTAS;
id | saldo | cli_id | fecha_sobregiro | intereses
----+-------+--------+-----------------+-----------
1 | 5300 | 1| | 0
2 | -2400 | 2| | 0
3 | 3233 | 3| | 0
Para este caso particular, un lector atento hubiera podido descubrir que el ejercicio se podía
realizar con una sola sentencia del DML:
UPDATE CUENTAS SET INACTIVA = FALSE WHERE CLI_ID IN (SELECT CLI_ID FROM
USUARIOS_FRAUDE);
Triggers
Un trigger es una acción que se lanza cuando se inserta, elimina o actualiza una o varios
registros de una tabla. En PL/pgSQL, los triggers son funciones sin argumentos en las cuales se
crean las siguientes variables:
■ NEW: Es un Record con los datos del registro que se está insertando (actualizando).
■ OLD: Datos del registro que se está eliminando (o actualizando).
■ TG_OP: Operación que se está realizando, puede ser INSERT, UPDATE, DELETE, o
TRUNCATE.
Un ejemplo de Auditoria
Suponga que cada que se modifica una cuenta, se debe registrar la acción en la siguiente tabla:
CREATE TABLE AUDITORIA(
OPERACION CHAR(1),
FECHAUPD TIMESTAMP,
USUARIO TEXT,
ID INT,
SALDO REAL,
CLI_ID INT,
FECHA_SOBREGIRO DATE,
INTERESES REAL,
INACTIVA BOOL);
La siguiente función deja un rastro en la tabla AUDITORIA cada que se realiza una modificación
en la tabla CUENTAS:
CREATE OR REPLACE FUNCTION process_cue_audit() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO AUDITORIA SELECT 'D', now(), user, OLD.*;
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO AUDITORIA SELECT 'U', now(), user, NEW.*;
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO AUDITORIA SELECT 'I', now(), user, NEW.*;
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
Ahora se crea el trigger sobre la tabla CUENTAS que se activa cada que se inserta, actualiza o
elimina una (o varias) fila(s) en CUENTAS:
CREATE TRIGGER cue_audit
AFTER INSERT OR UPDATE OR DELETE ON CUENTAS
FOR EACH ROW EXECUTE PROCEDURE process_cue_audit();
Algunos ejemplos:
bdi00=> insert into cuentas (saldo,cli_id) values (3500,2);
INSERT 0 1
Valores Calculados
Los triggers también pueden ser utilizados para mantener valores calculados. Por ejemplo,
asumamos que en la tabla CLIENTES se desea mantener el acumulado en dinero que tiene el
cliente en sus distintas cuentas. Para esto, modificamos la tabla:
ALTER TABLE CLIENTES ADD COLUMN SALDO REAL DEFAULT 0.0;
Como no habíamos implementado el trigger que se encargara de llevar este saldo, por la primera
vez lo calculamos “manualmente”:
CREATE OR REPLACE FUNCTION UPDATE_SALDOS() RETURNS VOID AS $$
DECLARE
CLIENTE_SALDO REAL;
CLIENTE_ID REAL;
BEGIN
FOR CLIENTE_ID, CLIENTE_SALDO IN SELECT CLI_ID, SUM(SALDO) FROM CUENTAS GROUP
BY CLI_ID LOOP
UPDATE CLIENTES SET SALDO=CLIENTE_SALDO WHERE ID = CLIENTE_ID;
END LOOP;
RETURN;
END;$$
LANGUAGE plpgsql;
Ahora, por cada modificación del saldo en una cuenta, actualizamos el saldo en el cliente:
CREATE OR REPLACE FUNCTION SET_SALDO_CLIENTE() RETURNS TRIGGER AS $$
BEGIN
IF(TG_OP = 'INSERT') THEN
UPDATE CLIENTES SET SALDO = SALDO + NEW.SALDO WHERE ID = NEW.CLI_ID;
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
IF NEW.CLI_ID = OLD.CLI_ID THEN
UPDATE CLIENTES SET SALDO = SALDO + NEW.SALDO - OLD.SALDO WHERE ID =
NEW.CLI_ID;
RETURN NEW;
ELSE /* LA CUENTA CAMBIO DE TITULAR */
UPDATE CLIENTES SET SALDO = SALDO - OLD.SALDO WHERE ID = OLD.CLI_ID;
UPDATE CLIENTES SET SALDO = SALDO + NEW.SALDO WHERE ID = NEW.CLI_ID;
RETURN NEW;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Realice algunas operaciones sobre la tabla CUENTAS (cambiando el titular y/o el saldo) para
verificar que la cuenta se lleva correctamente.
Manejo de Errores
Es posible lanzar y capturar errores de la siguiente manera:
CREATE OR REPLACE FUNCTION TEST(int, int) RETURNS INT AS $$
DECLARE
X INT;
BEGIN
X := $1/$2;
RETURN X;
EXCEPTION
WHEN division_by_zero THEN
RAISE EXCEPTION 'Division por cero';
RETURN 0;
WHEN others THEN
RAISE NOTICE 'Error';
RETURN 0;
END;
$$ LANGUAGE plpgsql;
Ejemplo de uso:
bdi00=> select test(6,2);
test
------
3
(1 row)
bdi00=> select test(6,0);
NOTICE: Division por cero
test
------
0
(1 row)
Red Social
Asuma el siguiente esquema de bases de datos:
usuarios(*id,nombre,email, num_amigos)
amigos(*id_usr1, *id_usr2)
invitaciones(*id, fecha, *id_usr1, *id_usr2, mensaje, estado)
Cuando un usuario quiere ser amigo de otro, debe enviar una invitación. Los estados de la
invitación pueden ser “pendiente”, “aceptado”, “rechazado”.
1. Cree el script de tablas
2. Implemente un procedimiento que cada que se acepte una invitación, se adicione la la
relación de amistad en la tabla amigos.
3. Por cada nuevo amigo se debe actualizar el número de amigos en el campo
USUARIOS.NUM_AMIGOS
4. La relación de amigos se asume que es simétrica, es decir, si A es amigo de B
entonces B es amigo de A. Realice un trigger que evite la inserción de (A,B) en la tabla
AMIGOS si la tuple (B,A) ya se encuentra. Además, se deben rechazar invitaciones de
A,B si A,B ya son amigos (o B,A ya son amigos)
5. Adicione un trigger que rechace nuevas invitaciones de A a B si B ha rechazado
previamente la invitación.
Solucion
Ejercicio
Asuma el siguiente esquema de bases de datos:
cargos(id,nombre,salario);
empleados(id,cc,nombre,car_id);
deducciones(id,nombre, ran_inf,ran_sup, porcentaje);
nomina(id,fecha, emp_id,basico, neto);
novedades(id,observacion,fecha,monto, emp_ID, nom_id);
descuentos(ded_id,nom_id,valor);
Se asume que cada empleado tiene un único cargo y por cada cargo se conoce el salario básico.
La tabla deducciones tiene la información de los impuestos/deducciones que se deben cobrar a
cada empleado si su salario se encuentra en el rango ran_inf - ran_sup.
La tabla novedades tiene la información de las pagos adicionales que se deben realizar a los
empleados. El atributo nom_id es nulo al principio. Una vez se genera la nomina y la novedad es
tenida en cuenta, el valor de este campo debe ser el id de la nomina donde se adicionó la
novedad.
La tabla nomina debe tener el salario básico del empleado así como el neto (después de
deducciones y novedades).
La tabla descuentos es el detalle de las deducciones realizadas a cada empleado en el periodo
actual.
Realice un procedimiento que calcule la nómina con sus deducciones y novedades. Más
precisamente, se debe:
■ Crear los registros en la tabla nomina para cada uno de los empleados.
■ Adicionar en la tabla descuentos las deducciones para cada empleado.
■ Calcular el salario neto teniendo en cuento las deducciones y las novedades.
■ Actualizar el campo nom_id en novedades cuando la novedad se tenga en cuenta en la
nomina.