Esta publicación es la continuación de «Taller SQL: Consultas SQL en Oracle Database 19C usando Oracle Live SQL». El objetivo de esta publicación es abordar los conceptos y sintaxis de PL/SQL.
Como se explica en la publicación anterior, no es necesario que instales el motor de base de datos Oracle 19C en tu computador, ya que vamos a utilizar la plataforma de entrenamiento de Oracle Live SQL, para trabajar los ejercicios propuestos.
¿Qué es PL/SQL?
A diferencia de SQL (Structure Query Language), PL/SQL es un lenguaje de programación de tipo procedimental. Las siglas PL y SQL de su nombre significan: «Procedural Language / Structure Query Language«.
Debes tener en cuenta que, PL/SQL fue desarrollado por la empresa Oracle Inc para dotar de mayores características y herramientas al SQL convencional, convirtiéndolo en un potente lenguaje de consulta y manipulación de base de datos Oracle.
Podemos decir que, el lenguaje PL/SQL viene incorporado en el servidor de base de datos Oracle y forma parte de la suite de herramientas que ofrece Oracle a sus millones de clientes.
Es precisamente con el lenguaje PL/SQL que podemos crear estructuras para consultar y manipular base de datos Oracle, como por ejemplo:
- Bloques anónimos
- Procedimientos Almacenados
- Funciones
- Triggers
Antes de continuar, sugerimos tener en cuenta lo siguiente:
- Tener una cuenta de usuario en Oracle Live.
- Tener conocimientos básicos sobre diseño de base de datos y sentencias SQL. Para este punto y el primero, puedes revisar esta publicación «Taller SQL: Consultas SQL en Oracle Database 19C usando Oracle Live SQL»
- Y como última sugerencia, tener un conocimiento básico sobre programación.
¿Qué es un procedimiento almacenado?
Un procedimiento almacenado, es un bloque de código declarativo de SQL o PL/SQL que se almacena en el catálogo de la base de datos y puede ser invocado posteriormente por un programa, un trigger o incluso otro procedimiento almacenado.
La ventaja de trabajar con procedimientos almacenados es que la definición y sintaxis del SQL o PL/SQL ya se encuentran interpretados por la instancia de una base de datos, mejorando los tiempos de respuesta al consumidor (aplicación o agente que solicita información)
¿Qué es un trigger?
Un trigger o disparador (en español) son bloques de código que se ejecutan de manera automática cuando ocurre un evento en la base de datos o en algún objeto de la base de datos. Por ejemplo, un trigger puede ejecutarse antes o después que ocurra un evento en la base de datos (inserción, actualización o eliminación de registros de una tabla).
Los ejercicios abordarán los siguientes temas:
- Declaración de variables
- Estructuras de control (IF, LOOP, WHILE, FOR)
- Manejo de cursores
- Manipulación de Excepciones
- Creación de Bloques Anónimos
- Creación de Procedimientos Almacenados
- Creación de Funciones
- Creación de Triggers
- Código PL/SQL
¿Deseas recibir contenido como este a tu correo electrónico?
A continuación, se detalla el enunciado para cada ejercicio y el script de la solución. Iniciaremos con ejercicios básicos (que no necesitan una base de datos), hasta ejercicios que devuelvan informes y/o se ejecuten cuando ocurra un evento en la base de datos.
Ejercicio 01 : Funciones Matemáticas
Crear un programa que calcule la distancia de dos puntos de un plano cartesiano (x,y). Dado los valores, x = (3, 2); y = (5, 8). Mostrar en pantalla la distancia de los puntos x, y.
DECLARE
v_point_x1 NUMBER := 3;
v_point_x2 NUMBER := 2;
v_point_y1 NUMBER := 5;
v_point_y2 NUMBER := 8;
v_distance DECIMAL(6,2);
BEGIN
v_distance := SQRT(POWER((v_point_x1-v_point_x2),2)+POWER((v_point_y1-v_point_y2),2));
DBMS_OUTPUT.PUT_LINE('CALCULAR LA DISTANCIA DE DOS PUNTOS');
DBMS_OUTPUT.PUT_LINE('************************************');
DBMS_OUTPUT.PUT_LINE('Punto X : (' || v_point_x1||','||v_point_x2||')');
DBMS_OUTPUT.PUT_LINE('Punto Y : (' || v_point_y1||','||v_point_y2||')');
DBMS_OUTPUT.PUT_LINE('Distancia XY : ' || v_distance);
END;
Ejercicio 02 : Sentencia Loop While
Desarrollar un programa que sume «n» veces un número aleatorio.
Imprimir en pantalla los números aleatorios generados y la suma obtenida.
La cantidad de iteraciones deberá generarse de manera aleatoria, considerando los valores del 1 al 10. Los números aleatorios generados en cada iteración deberán ser del 10 al 100.
DECLARE
v_sum NUMBER := 0;
v_x NUMBER :=0;
v_num NUMBER;
v_limit NUMBER;
BEGIN
v_limit := FLOOR(DBMS_RANDOM.value(1,10));
LOOP
v_x := v_x + 1;
v_num := FLOOR(DBMS_RANDOM.value(10,100));
DBMS_OUTPUT.PUT_LINE('Número aleatorio '||v_x||' -> '|| v_num);
v_sum := v_sum + v_num;
EXIT WHEN v_x = v_limit;
END LOOP;
DBMS_OUTPUT.PUT_LINE('Suma Total = '||v_sum);
END;
Ejercicio 03 : Sentencia FOR
Desarrollar un programa que devuelva la cantidad de números múltiplos de 3 que existen del 1 al 100.
DECLARE
v_multiple_3 NUMBER := 0;
BEGIN
FOR v_num IN 1..100 LOOP
IF (MOD(v_num,3)=0) THEN
v_multiple_3 := v_multiple_3 + 1;
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE('Números multiplos de 3 del 1 al 100: '||v_multiple_3);
END;
-- Output:
-- Números multiplos de 3 del 1 al 100: 3
Ejercicio 04: Cursores
Elabore un bloque anónimo PL/SQL que almacene en un objeto cursor la lista de empleados (código, nombres, apellidos y salario) que tiene un sueldo menor e igual $ 2,800. Mostrar el resultado por cada registro. Utilizar el esquema de base de datos HR (Human Resources) que viene por defecto en Live SQL.
DECLARE
CURSOR employees_cursor IS
SELECT employee_id,first_name,last_name,salary FROM hr.employees WHERE salary<=2800;
v_id hr.employees.employee_id%TYPE;
v_name hr.employees.first_name%TYPE;
v_surname hr.employees.last_name%TYPE;
v_salary hr.employees.salary%TYPE;
v_num NUMBER := 0;
BEGIN
DBMS_OUTPUT.PUT_LINE(LPAD('-',50,'-'));
DBMS_OUTPUT.PUT_LINE('INFORME DE EMPLEADOS');
DBMS_OUTPUT.PUT_LINE(LPAD('-',50,'-'));
OPEN employees_cursor;
LOOP
FETCH employees_cursor INTO v_id,v_name,v_surname,v_salary;
EXIT WHEN employees_cursor%NOTFOUND;
v_num := v_num+1;
DBMS_OUTPUT.PUT_LINE('[' || v_num || '] Empleado N° '|| v_id || ' - ' || v_name || ' ' || v_surname || ' tiene un sueldo de' ||TO_CHAR(v_salary,'$99,999.00'));
END LOOP;
CLOSE employees_cursor;
END;
Ejercicio 05: Sentencia IF
Elaborar un bloque anónimo PL/SQL que devuelva un reporte de los empleados que tienen un salario menor e igual a $ 2,800.00. El reporte debe mostrarse agrupado por departamentos. Los valores de la columna deben ser por ancho fijo.
DECLARE
CURSOR dpto_employees_cursor IS
SELECT e.department_id,d.department_name,e.employee_id,e.first_name,e.last_name,e.salary
FROM hr.employees e
INNER JOIN hr.departments d ON e.department_id=d.department_id
WHERE salary<2800
ORDER BY d.department_name DESC, salary DESC;
v_last_department_id hr.departments.department_id%TYPE:=0;
v_num NUMBER := 0;
BEGIN
DBMS_OUTPUT.PUT_LINE(LPAD('-',70,'-'));
DBMS_OUTPUT.PUT_LINE('INFORME DE EMPLEADOS POR DEPARTAMENTOS');
DBMS_OUTPUT.PUT_LINE(LPAD('-',70,'-'));
FOR emp IN dpto_employees_cursor
LOOP
IF v_last_department_id<>emp.department_id THEN
DBMS_OUTPUT.PUT_LINE(chr(13)||'DEPARTAMENTO: ' || UPPER(emp.department_name));
DBMS_OUTPUT.PUT_LINE(LPAD('-',70,'-'));
ELSE
DBMS_OUTPUT.PUT_LINE(rpad(emp.employee_id, 10) || rpad(emp.first_name|| ' ' || emp.last_name, 20) ||TO_CHAR(emp.salary,'$999,999.00'));
END IF;
v_last_department_id:=emp.department_id;
END LOOP;
END;
¿Deseas recibir contenido como este a tu correo electrónico?
Base de datos de ventas
Para los siguientes ejercicios utilizaremos una propia base de datos de ventas, debido a que no contamos con los permisos de alterar la información de los esquemas de base de datos que nos proporciona Oracle Live SQL.
Copia las siguientes sentencias SQL y pégalo en la ventana de trabajo de la herramienta para finalmente ejecutar las sentencias.
-- Ventas Database SQL DUMP
-- version 1.1.2
-- source: www.solocodigoweb.com
-- author: Gonzalo Chacaltana Buleje
-- Servidor: Oracle Database 19c Enterprise Edition Release
-- Versión del servidor: 19.4.0.0.0
-- CREANDO TABLA CLIENTES
CREATE TABLE clientes (
cod_cliente CHAR(5) NOT NULL PRIMARY KEY,
nombres VARCHAR2(30) NOT NULL,
distrito VARCHAR2(30) NULL,
telefono VARCHAR2(10) NULL
);
-- CREANDO TABLA PRODUCTOS
CREATE TABLE productos (
cod_producto CHAR(5) NOT NULL PRIMARY KEY,
descripcion VARCHAR2(30) NULL,
precio_unitario NUMBER(9,2) NULL
);
-- CREANDO TABLA FACTURAS
CREATE TABLE facturas (
cod_factura CHAR(5) NOT NULL PRIMARY KEY,
cod_cliente CHAR(5) NOT NULL,
fecha_emision DATE NULL,
importe_total DECIMAL(9,2) NULL
);
-- CREANDO TABLA DETALLE_FACTURAS
CREATE TABLE detalle_facturas (
cod_factura CHAR(5) NOT NULL,
cod_producto CHAR(5) NOT NULL,
cantidad SMALLINT NOT NULL,
subtotal DECIMAL(9,2) NOT NULL
);
-- AGREGANDO RELACIONES Y CLAVES PRIMARIAS
ALTER TABLE facturas
ADD FOREIGN KEY (cod_cliente)
REFERENCES clientes(cod_cliente);
ALTER TABLE detalle_facturas
ADD PRIMARY KEY (cod_factura,cod_producto);
ALTER TABLE detalle_facturas
ADD FOREIGN KEY (cod_factura)
REFERENCES facturas(cod_factura);
ALTER TABLE detalle_facturas
ADD FOREIGN KEY (cod_producto)
REFERENCES productos(cod_producto);
-- INSERTANDO REGISTROS A LA TABLA CLIENTES
INSERT INTO clientes
VALUES('C0001','Julián Pérez','Lince','3214568');
INSERT INTO clientes
VALUES('C0002','Maria Chavez','Jesus Maria','4215678');
INSERT INTO clientes
VALUES('C0003','Juan Chacaltana','Comas','5416321');
INSERT INTO clientes
VALUES('C0004','Guadalupe Fernández','Cercado','5216364');
INSERT INTO clientes
VALUES('C0005','José Gómez','Lince','3458974');
INSERT INTO clientes
VALUES('C0006','Carlos Segura','Rimac','4562312');
INSERT INTO clientes
VALUES('C0007','Javier Buleje','Lince','3218974');
INSERT INTO clientes
VALUES('C0008','Mary Huaman','Cercado','5631278');
INSERT INTO clientes
VALUES('C0009','Cristian Sánchez','Cercado','5410591');
INSERT INTO clientes
VALUES('C0010','Flor Paz','Lince','3564165');
-- INSERTANDO REGISTROS A LA TABLA PRODUCTOS
INSERT INTO productos
VALUES('P0001','Memoria DDR 256 Mb',120.00);
INSERT INTO productos
VALUES('P0002','Memoria DDR 500 Mb',190.00);
INSERT INTO productos
VALUES('P0003','Mainboard PcChip v25',280.00);
INSERT INTO productos
VALUES('P0004','Mainboard Intel V101',320.00);
INSERT INTO productos
VALUES('P0005','Procesador Intel 3.6',390.00);
INSERT INTO productos
VALUES('P0006','Procesador Intel 2.6',340.00);
INSERT INTO productos
VALUES('P0007','Procesador Celeron 3.2',320.00);
INSERT INTO productos
VALUES('P0008','Disco Duro SATA 80 GB',180.00);
INSERT INTO productos
VALUES('P0009','Disco Duro SATA 120 GB',220.00);
INSERT INTO productos
VALUES('P0010','Case Prezcom 500 Wats',120.00);
-- INSERTANDO REGISTROS A TABLA FACTURAS
INSERT INTO facturas
VALUES('F0001','C0004',SYSDATE,0);
INSERT INTO facturas
VALUES('F0002','C0006',SYSDATE,0);
INSERT INTO facturas
VALUES('F0003','C0007',SYSDATE,0);
INSERT INTO facturas
VALUES('F0004','C0004',SYSDATE,0);
INSERT INTO facturas
VALUES('F0005','C0008',SYSDATE,0);
INSERT INTO facturas
VALUES('F0006','C0002',SYSDATE,0);
INSERT INTO facturas
VALUES('F0007','C0006',SYSDATE,0);
INSERT INTO facturas
VALUES('F0008','C0009',SYSDATE,0);
INSERT INTO facturas
VALUES('F0009','C0010',SYSDATE,0);
INSERT INTO facturas
VALUES('F0010','C0004',SYSDATE,0);
-- INSERTANDO REGISTROS A LA TABLA DETALLE FACTURAS
INSERT INTO detalle_facturas
VALUES('F0001','P0003',3,840.00);
INSERT INTO detalle_facturas
VALUES('F0001','P0005',5,1950.00);
INSERT INTO detalle_facturas
VALUES('F0001','P0006',2,680.00);
INSERT INTO detalle_facturas
VALUES('F0001','P0009',1,220.00);
INSERT INTO detalle_facturas
VALUES('F0002','P0009',3,660.00);
INSERT INTO detalle_facturas
VALUES('F0002','P0005',8,3120.00);
INSERT INTO detalle_facturas
VALUES('F0002','P0002',2,380.00);
INSERT INTO detalle_facturas
VALUES('F0002','P0007',3,960.00);
INSERT INTO detalle_facturas
VALUES('F0002','P0010',1,120.00);
INSERT INTO detalle_facturas
VALUES('F0003','P0010',4,480.00);
INSERT INTO detalle_facturas
VALUES('F0003','P0003',7,1960.00);
INSERT INTO detalle_facturas
VALUES('F0003','P0006',2,680.00);
INSERT INTO detalle_facturas
VALUES('F0003','P0009',1,220.00);
INSERT INTO detalle_facturas
VALUES('F0004','P0005',3,1170.00);
INSERT INTO detalle_facturas
VALUES('F0004','P0006',6,2040.00);
INSERT INTO detalle_facturas
VALUES('F0004','P0007',8,2560.00);
INSERT INTO detalle_facturas
VALUES('F0005','P0003',3,840.00);
INSERT INTO detalle_facturas
VALUES('F0005','P0005',5,1950.00);
INSERT INTO detalle_facturas
VALUES('F0005','P0002',1,190.00);
INSERT INTO detalle_facturas
VALUES('F0006','P0007',3,960.00);
INSERT INTO detalle_facturas
VALUES('F0006','P0002',2,380.00);
INSERT INTO detalle_facturas
VALUES('F0007','P0003',3,840.00);
INSERT INTO detalle_facturas
VALUES('F0007','P0008',4,720.00);
INSERT INTO detalle_facturas
VALUES('F0008','P0003',3,840.00);
INSERT INTO detalle_facturas
VALUES('F0008','P0006',1,340.00);
INSERT INTO detalle_facturas
VALUES('F0009','P0003',3,840.00);
INSERT INTO detalle_facturas
VALUES('F0010','P0009',5,1100.00);
Ejercicio 06: Procedimiento Almacenado
Crear un procedimiento almacenado para actualizar el precio de un producto que se envíe como parámetro de entrada.
CREATE OR REPLACE PROCEDURE actualizar_precio_producto
(v_cod_producto IN CHAR, v_nuevo_precio IN NUMBER)
IS
BEGIN
UPDATE productos SET precio_unitario = v_nuevo_precio
WHERE cod_producto = v_cod_producto;
COMMIT;
DBMS_OUTPUT.PUT_LINE('Precio actualizado con éxito');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END actualizar_precio_producto;
-- EXECUTION
CALL actualizar_precio_producto('P0001',130);
Ejercicio 07: Iterar un cursor
Crear un procedimiento almacenado que devuelva la relación de clientes que no tienen una factura emitida a su nombre.
CREATE OR REPLACE PROCEDURE clientes_sin_facturas
IS
CURSOR report_cursor IS
SELECT cod_cliente,nombres FROM clientes WHERE cod_cliente NOT IN (SELECT distinct(cod_cliente) FROM facturas);
BEGIN
DBMS_OUTPUT.PUT_LINE('Clientes sin factura');
FOR rep IN report_cursor
LOOP
DBMS_OUTPUT.PUT_LINE(RPAD(rep.cod_cliente,8)||rep.nombres);
END LOOP;
END clientes_sin_facturas;
-- EXECUTION
CALL clientes_sin_facturas()
Ejercicio 08: Funciones
Crear una función que devuelva el nombre del producto más vendido.
CREATE OR REPLACE FUNCTION producto_mas_vendido
RETURN VARCHAR2
IS
product_name VARCHAR2(30);
CURSOR report IS
SELECT p.descripcion "producto"
FROM detalle_facturas fd
INNER JOIN productos p ON fd.cod_producto=p.cod_producto
GROUP BY p.descripcion
ORDER BY SUM(fd.cantidad) DESC
FETCH NEXT 1 ROWS ONLY;
BEGIN
OPEN report;
FETCH report INTO product_name;
RETURN product_name;
END producto_mas_vendido;
-- PROBANDO
SELECT producto_mas_vendido FROM dual;
Ejercicio 09: Función de agrupación SUM
Escribir un procedimiento almacenado que permita actualizar el importe total de una factura, según el detalle que tenga asignado. El código de factura debe enviarse como parámetro de entrada.
CREATE OR REPLACE PROCEDURE actualiza_total_factura
(v_cod_factura IN CHAR)
IS
BEGIN
UPDATE facturas SET importe_total=(
SELECT SUM(subtotal)
FROM detalle_facturas WHERE cod_factura=v_cod_factura)
WHERE cod_factura=v_cod_factura;
DBMS_OUTPUT.PUT_LINE('Operación realizada con éxito');
END actualiza_total_factura;
-- EXECUTE
-- SELECT importe_total FROM facturas WHERE cod_factura='F0002'
CALL actualiza_total_factura('F0002');
Ejercicio 10: Subconsultas
Crear un procedimiento que permita insertar información al detalle de una factura.
CREATE OR REPLACE PROCEDURE insertar_detalle_factura(
v_cod_factura IN CHAR,
v_cod_producto IN CHAR,
v_cantidad IN NUMBER
)
IS
v_precio_producto NUMBER(9,2);
v_subtotal NUMBER(9,2);
BEGIN
SELECT precio_unitario INTO v_precio_producto FROM productos WHERE cod_producto=v_cod_producto;
v_subtotal:=v_precio_producto*v_cantidad;
INSERT INTO detalle_facturas (cod_factura,cod_producto,cantidad,subtotal) VALUES (v_cod_factura,v_cod_producto,v_cantidad,v_subtotal);
DBMS_OUTPUT.PUT_LINE('Detalle de factura registrado con éxito.');
END insertar_detalle_factura;
-- Probando
CALL insertar_detalle_factura('F0010','P0001',12);
-- Validando resultado
SELECT * FROM detalle_facturas where cod_factura='F0010';
Ejercicio 11: Reporte de ventas
Desarrollar un procedimiento almacenado que devuelva un reporte con la lista y sumatoria total de todas facturas agrupadas por cliente.
CREATE OR REPLACE PROCEDURE reporte_cliente_ventas
IS
v_total NUMBER(9,2):=0;
CURSOR clientes_cursor IS
SELECT cod_cliente,nombres FROM clientes WHERE cod_cliente IN (SELECT DISTINCT(cod_cliente) FROM facturas);
CURSOR facturas_cursor IS
SELECT cod_factura,importe_total,cod_cliente FROM facturas;
BEGIN
DBMS_OUTPUT.PUT_LINE(LPAD('-',70,'-'));
DBMS_OUTPUT.PUT_LINE('INFORME DE VENTAS');
DBMS_OUTPUT.PUT_LINE(LPAD('-',70,'-'));
FOR cli IN clientes_cursor
LOOP
DBMS_OUTPUT.PUT_LINE(chr(13)||RPAD(UPPER(cli.cod_cliente),6)||UPPER(cli.nombres));
DBMS_OUTPUT.PUT_LINE(LPAD('-',70,'-'));
v_total:=0;
FOR fa IN facturas_cursor
LOOP
IF fa.cod_cliente=cli.cod_cliente THEN
v_total:=v_total+fa.importe_total;
DBMS_OUTPUT.PUT_LINE(RPAD(fa.cod_factura, 6) || TO_CHAR(ROUND(fa.importe_total,2),'$999,999.00'));
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE(RPAD('Total:',6) || TO_CHAR(ROUND(v_total,2),'$999,999.00'));
END LOOP;
END reporte_cliente_ventas;
-- Probando
CALL reporte_cliente_ventas;
Ejercicio 12: Triggers
Crear un trigger que actualice el importe total de una factura cuando se inserte o se elimine un registro de la tabla detalle facturas.
CREATE OR REPLACE TRIGGER actualiza_factura
BEFORE INSERT OR DELETE ON detalle_facturas
FOR EACH ROW
BEGIN
IF INSERTING THEN
UPDATE facturas SET importe_total = importe_total + :new.subtotal WHERE cod_factura = :new.cod_factura;
END IF;
IF DELETING THEN
UPDATE facturas SET importe_total = importe_total - :old.subtotal WHERE cod_factura = :old.cod_factura;
END IF;
END actualiza_factura;
Publicaciones que pueden interesarte:
- Buenas prácticas para administrar la base de datos de producción
- Taller SQL: Consultas SQL en Oracle Database 19C usando Oracle Live SQL