Salta el contingut

Procediments emmagatzemats (Stored Procedures)

Què és un procediment emmagatzemat?

Un procediment emmagatzemat (stored procedure) és un conjunt de sentències SQL i lògica procedimental (variables, condicions, bucles, gestió d'errors) que es compila i s'emmagatzema al servidor de base de dades amb un nom, de manera que pot ser cridat repetidament per les aplicacions o per altres objectes de la BD.

A diferència d'una consulta enviada pel client cada vegada, el procediment ja existeix preparsejat i precompilat al servidor. El client simplement diu: "executa el procediment X amb aquests paràmetres" i el servidor s'encarrega de tot.

graph LR
    APP["Aplicació client\n(Python, Java, PHP...)"]
    SP["Procediment\nemmagatzemat\nprocess_order()"]
    T1[("Taula\ncomandes")]
    T2[("Taula\nstock")]
    T3[("Taula\nfactures")]

    APP -- "CALL process_order(42)" --> SP
    SP -- "UPDATE" --> T1
    SP -- "UPDATE" --> T2
    SP -- "INSERT" --> T3
    SP -- "Resultat / codi d'error" --> APP

Sense el procediment, l'aplicació hauria d'enviar tres o més consultes separades, gestionar les transaccions al costat del client i tractar cada error individualment. Amb el procediment, tot passa al servidor en una sola crida.


Avantatges dels procediments emmagatzemats

Avantatge Descripció
Rendiment El codi es precompila i el pla d'execució es pot reutilitzar. Menys parseig en cada crida.
Menys tràfic de xarxa El client envia una sola crida; el servidor executa internament N operacions sense enviar-les pel cable.
Seguretat Es pot donar permís d'EXECUTE sobre un procediment sense donar permisos directes sobre les taules. La lògica queda encapsulada i protegida.
Encapsulació La lògica de negoci resideix al SGBD, no dispersa en múltiples aplicacions. Un canvi al procediment afecta tots els consumidors.
Reutilització Qualsevol aplicació, usuari o altre procediment pot cridar el mateix codi.
Manteniment Es pot modificar el procediment sense tocar el codi de les aplicacions, sempre que la interfície (paràmetres) no canviï.
Transaccions Fàcil de gestionar transaccions complexes (BEGIN/COMMIT/ROLLBACK) dins del procediment.

Procediments i portabilitat

Cada SGBD té el seu propi llenguatge procedural: PL/pgSQL (PostgreSQL), SQL/PSM (MySQL), T-SQL (SQL Server), PL/SQL (Oracle). El codi dels procediments no és portable entre motors. Planifiqueu bé quines parts de la lògica poseu al SGBD si preveieu canviar de motor en el futur.


Sintaxi bàsica de creació

-- En PostgreSQL, els procediments es creen amb PL/pgSQL
-- La funció DO $$ ... $$ s'usa per a blocs anònims (sense nom)
-- PROCEDURE és per a codi que no retorna valor (des de PG 11)

CREATE OR REPLACE PROCEDURE saluda_usuari(p_nom TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
    RAISE NOTICE 'Hola, %!', p_nom;
END;
$$;

-- Cridar-lo:
CALL saluda_usuari('Maria');
-- MySQL usa DELIMITER per canviar el terminador de sentències
-- mentre es defineix el procediment (evita confusió amb els ;)

DELIMITER //

CREATE PROCEDURE saluda_usuari(IN p_nom VARCHAR(100))
BEGIN
    SELECT CONCAT('Hola, ', p_nom, '!') AS missatge;
END //

DELIMITER ;

-- Cridar-lo:
CALL saluda_usuari('Maria');
-- SQL Server usa T-SQL. CREATE OR ALTER permet crear o modificar
-- sense haver de fer DROP prèviament (des de SQL Server 2016 SP1)

CREATE OR ALTER PROCEDURE saluda_usuari
    @p_nom NVARCHAR(100)
AS
BEGIN
    SET NOCOUNT ON;
    SELECT 'Hola, ' + @p_nom + '!' AS missatge;
END;
GO

-- Cridar-lo:
EXEC saluda_usuari @p_nom = 'Maria';
-- Oracle usa PL/SQL. La secció IS/AS separa la capçalera del cos.
-- Les variables locals es declaren entre IS i BEGIN.

CREATE OR REPLACE PROCEDURE saluda_usuari(
    p_nom IN VARCHAR2
)
AS
BEGIN
    DBMS_OUTPUT.PUT_LINE('Hola, ' || p_nom || '!');
END saluda_usuari;
/

-- Cridar-lo (des de SQL*Plus o SQL Developer):
BEGIN
    saluda_usuari('Maria');
END;
/

Paràmetres: IN, OUT, INOUT

Els procediments accepten tres tipus de paràmetres:

  • IN — el client passa un valor al procediment (per defecte). El procediment el llegeix però no el pot modificar fora.
  • OUT — el procediment retorna un valor al client. L'aplicació declara una variable i el procediment l'omple.
  • INOUT — el client passa un valor, el procediment el pot modificar i el valor modificat torna al client.
CREATE OR REPLACE PROCEDURE calcular_descompte(
    IN  p_preu       NUMERIC,
    IN  p_percentatge NUMERIC,
    OUT p_preu_final  NUMERIC
)
LANGUAGE plpgsql
AS $$
BEGIN
    p_preu_final := p_preu - (p_preu * p_percentatge / 100);
END;
$$;

-- Ús (els paràmetres OUT es recuperen amb INTO):
DO $$
DECLARE
    v_resultat NUMERIC;
BEGIN
    CALL calcular_descompte(100.00, 15, v_resultat);
    RAISE NOTICE 'Preu amb descompte: %', v_resultat;
END;
$$;
DELIMITER //

CREATE PROCEDURE calcular_descompte(
    IN  p_preu        DECIMAL(10,2),
    IN  p_percentatge DECIMAL(5,2),
    OUT p_preu_final  DECIMAL(10,2)
)
BEGIN
    SET p_preu_final = p_preu - (p_preu * p_percentatge / 100);
END //

DELIMITER ;

-- Ús:
CALL calcular_descompte(100.00, 15, @resultat);
SELECT @resultat AS preu_final;
CREATE OR ALTER PROCEDURE calcular_descompte
    @p_preu        DECIMAL(10,2),
    @p_percentatge DECIMAL(5,2),
    @p_preu_final  DECIMAL(10,2) OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET @p_preu_final = @p_preu - (@p_preu * @p_percentatge / 100);
END;
GO

-- Ús:
DECLARE @resultat DECIMAL(10,2);
EXEC calcular_descompte
    @p_preu = 100.00,
    @p_percentatge = 15,
    @p_preu_final = @resultat OUTPUT;
SELECT @resultat AS preu_final;
CREATE OR REPLACE PROCEDURE calcular_descompte(
    p_preu        IN  NUMBER,
    p_percentatge IN  NUMBER,
    p_preu_final  OUT NUMBER
)
AS
BEGIN
    p_preu_final := p_preu - (p_preu * p_percentatge / 100);
END calcular_descompte;
/

-- Ús (bloc anònim PL/SQL):
DECLARE
    v_resultat NUMBER;
BEGIN
    calcular_descompte(100, 15, v_resultat);
    DBMS_OUTPUT.PUT_LINE('Preu final: ' || v_resultat);
END;
/

Control de flux

IF / ELSIF / ELSE

CREATE OR REPLACE PROCEDURE classificar_nota(p_nota NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    IF p_nota >= 9 THEN
        RAISE NOTICE 'Excel·lent';
    ELSIF p_nota >= 7 THEN
        RAISE NOTICE 'Notable';
    ELSIF p_nota >= 5 THEN
        RAISE NOTICE 'Aprovat';
    ELSE
        RAISE NOTICE 'Suspès';
    END IF;
END;
$$;
DELIMITER //

CREATE PROCEDURE classificar_nota(IN p_nota DECIMAL(4,1))
BEGIN
    IF p_nota >= 9 THEN
        SELECT 'Excel·lent' AS qualificació;
    ELSEIF p_nota >= 7 THEN
        SELECT 'Notable' AS qualificació;
    ELSEIF p_nota >= 5 THEN
        SELECT 'Aprovat' AS qualificació;
    ELSE
        SELECT 'Suspès' AS qualificació;
    END IF;
END //

DELIMITER ;
CREATE OR ALTER PROCEDURE classificar_nota
    @p_nota DECIMAL(4,1)
AS
BEGIN
    SET NOCOUNT ON;
    IF @p_nota >= 9
        PRINT 'Excel·lent';
    ELSE IF @p_nota >= 7
        PRINT 'Notable';
    ELSE IF @p_nota >= 5
        PRINT 'Aprovat';
    ELSE
        PRINT 'Suspès';
END;
GO
CREATE OR REPLACE PROCEDURE classificar_nota(p_nota NUMBER)
AS
BEGIN
    IF p_nota >= 9 THEN
        DBMS_OUTPUT.PUT_LINE('Excel·lent');
    ELSIF p_nota >= 7 THEN
        DBMS_OUTPUT.PUT_LINE('Notable');
    ELSIF p_nota >= 5 THEN
        DBMS_OUTPUT.PUT_LINE('Aprovat');
    ELSE
        DBMS_OUTPUT.PUT_LINE('Suspès');
    END IF;
END classificar_nota;
/

Bucles: WHILE i FOR

CREATE OR REPLACE PROCEDURE demo_bucles()
LANGUAGE plpgsql
AS $$
DECLARE
    v_i INTEGER := 1;
BEGIN
    -- Bucle WHILE
    WHILE v_i <= 5 LOOP
        RAISE NOTICE 'WHILE: iteració %', v_i;
        v_i := v_i + 1;
    END LOOP;

    -- Bucle FOR (rang numèric)
    FOR v_i IN 1..5 LOOP
        RAISE NOTICE 'FOR: iteració %', v_i;
    END LOOP;

    -- Bucle FOR sobre una consulta
    FOR rec IN SELECT nom FROM clients LIMIT 5 LOOP
        RAISE NOTICE 'Client: %', rec.nom;
    END LOOP;
END;
$$;
DELIMITER //

CREATE PROCEDURE demo_bucles()
BEGIN
    DECLARE v_i INT DEFAULT 1;

    -- Bucle WHILE
    WHILE v_i <= 5 DO
        SELECT CONCAT('WHILE: iteració ', v_i);
        SET v_i = v_i + 1;
    END WHILE;

    -- Bucle REPEAT (com do-while)
    SET v_i = 1;
    REPEAT
        SELECT CONCAT('REPEAT: iteració ', v_i);
        SET v_i = v_i + 1;
    UNTIL v_i > 5
    END REPEAT;

    -- Bucle LOOP amb LEAVE (equivalent a break)
    SET v_i = 1;
    my_loop: LOOP
        IF v_i > 5 THEN
            LEAVE my_loop;
        END IF;
        SELECT CONCAT('LOOP: iteració ', v_i);
        SET v_i = v_i + 1;
    END LOOP;
END //

DELIMITER ;
CREATE OR ALTER PROCEDURE demo_bucles
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @i INT = 1;

    -- Bucle WHILE (únic tipus natiu en T-SQL)
    WHILE @i <= 5
    BEGIN
        PRINT 'Iteració: ' + CAST(@i AS NVARCHAR);
        SET @i = @i + 1;
    END;

    -- Iterar sobre un cursor
    DECLARE @nom NVARCHAR(100);
    DECLARE cur CURSOR FOR SELECT nom FROM clients;
    OPEN cur;
    FETCH NEXT FROM cur INTO @nom;
    WHILE @@FETCH_STATUS = 0
    BEGIN
        PRINT 'Client: ' + @nom;
        FETCH NEXT FROM cur INTO @nom;
    END;
    CLOSE cur;
    DEALLOCATE cur;
END;
GO
CREATE OR REPLACE PROCEDURE demo_bucles
AS
    v_i NUMBER := 1;
BEGIN
    -- Bucle WHILE
    WHILE v_i <= 5 LOOP
        DBMS_OUTPUT.PUT_LINE('WHILE: iteració ' || v_i);
        v_i := v_i + 1;
    END LOOP;

    -- Bucle FOR numèric
    FOR v_j IN 1..5 LOOP
        DBMS_OUTPUT.PUT_LINE('FOR: iteració ' || v_j);
    END LOOP;

    -- Bucle FOR sobre cursor implícit
    FOR rec IN (SELECT nom FROM clients WHERE ROWNUM <= 5) LOOP
        DBMS_OUTPUT.PUT_LINE('Client: ' || rec.nom);
    END LOOP;
END demo_bucles;
/

CASE

CREATE OR REPLACE PROCEDURE descriure_estació(p_mes INT)
LANGUAGE plpgsql
AS $$
DECLARE
    v_estació TEXT;
BEGIN
    v_estació := CASE p_mes
        WHEN 12, 1, 2  THEN 'Hivern'
        WHEN 3, 4, 5   THEN 'Primavera'
        WHEN 6, 7, 8   THEN 'Estiu'
        WHEN 9, 10, 11 THEN 'Tardor'
        ELSE 'Mes invàlid'
    END;
    RAISE NOTICE 'Estació: %', v_estació;
END;
$$;
DELIMITER //

CREATE PROCEDURE descriure_estació(IN p_mes INT)
BEGIN
    DECLARE v_estació VARCHAR(20);
    CASE p_mes
        WHEN 12 THEN SET v_estació = 'Hivern';
        WHEN 1  THEN SET v_estació = 'Hivern';
        WHEN 2  THEN SET v_estació = 'Hivern';
        WHEN 3  THEN SET v_estació = 'Primavera';
        WHEN 4  THEN SET v_estació = 'Primavera';
        WHEN 5  THEN SET v_estació = 'Primavera';
        WHEN 6  THEN SET v_estació = 'Estiu';
        WHEN 7  THEN SET v_estació = 'Estiu';
        WHEN 8  THEN SET v_estació = 'Estiu';
        ELSE SET v_estació = 'Tardor / Invàlid';
    END CASE;
    SELECT v_estació AS estació;
END //

DELIMITER ;
CREATE OR ALTER PROCEDURE descriure_estació
    @p_mes INT
AS
BEGIN
    DECLARE @estació NVARCHAR(20);
    SET @estació = CASE @p_mes
        WHEN 12 THEN 'Hivern'
        WHEN 1  THEN 'Hivern'
        WHEN 2  THEN 'Hivern'
        WHEN 3  THEN 'Primavera'
        WHEN 4  THEN 'Primavera'
        WHEN 5  THEN 'Primavera'
        WHEN 6  THEN 'Estiu'
        WHEN 7  THEN 'Estiu'
        WHEN 8  THEN 'Estiu'
        ELSE 'Tardor'
    END;
    PRINT @estació;
END;
GO
CREATE OR REPLACE PROCEDURE descriure_estació(p_mes NUMBER)
AS
    v_estació VARCHAR2(20);
BEGIN
    v_estació := CASE p_mes
        WHEN 12 THEN 'Hivern'
        WHEN 1  THEN 'Hivern'
        WHEN 2  THEN 'Hivern'
        WHEN 3  THEN 'Primavera'
        WHEN 4  THEN 'Primavera'
        WHEN 5  THEN 'Primavera'
        WHEN 6  THEN 'Estiu'
        WHEN 7  THEN 'Estiu'
        WHEN 8  THEN 'Estiu'
        ELSE 'Tardor'
    END;
    DBMS_OUTPUT.PUT_LINE('Estació: ' || v_estació);
END descriure_estació;
/

Gestió d'excepcions

La gestió d'errors és essencial en procediments que fan operacions crítiques. Cada motor té el seu mecanisme:

CREATE OR REPLACE PROCEDURE inserir_client(
    p_id  INT,
    p_nom TEXT
)
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO clients(id, nom) VALUES (p_id, p_nom);
    RAISE NOTICE 'Client inserit correctament.';
EXCEPTION
    WHEN unique_violation THEN
        RAISE WARNING 'Error: ja existeix un client amb id = %', p_id;
    WHEN not_null_violation THEN
        RAISE WARNING 'Error: el nom no pot ser nul.';
    WHEN OTHERS THEN
        RAISE WARNING 'Error inesperat: % - %', SQLERRM, SQLSTATE;
END;
$$;
DELIMITER //

CREATE PROCEDURE inserir_client(
    IN p_id  INT,
    IN p_nom VARCHAR(100)
)
BEGIN
    -- Declarar handlers ABANS de qualsevol altra instrucció
    DECLARE EXIT HANDLER FOR 1062
    BEGIN
        SELECT 'Error: ja existeix un client amb aquest id.' AS error;
    END;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        GET DIAGNOSTICS CONDITION 1
            @msg = MESSAGE_TEXT, @codi = MYSQL_ERRNO;
        SELECT CONCAT('Error ', @codi, ': ', @msg) AS error;
    END;

    INSERT INTO clients(id, nom) VALUES (p_id, p_nom);
    SELECT 'Client inserit correctament.' AS resultat;
END //

DELIMITER ;
CREATE OR ALTER PROCEDURE inserir_client
    @p_id  INT,
    @p_nom NVARCHAR(100)
AS
BEGIN
    SET NOCOUNT ON;
    BEGIN TRY
        INSERT INTO clients(id, nom) VALUES (@p_id, @p_nom);
        PRINT 'Client inserit correctament.';
    END TRY
    BEGIN CATCH
        PRINT 'Error ' + CAST(ERROR_NUMBER() AS NVARCHAR)
              + ': ' + ERROR_MESSAGE();
        -- Opcional: re-llençar l'error
        -- THROW;
    END CATCH;
END;
GO
CREATE OR REPLACE PROCEDURE inserir_client(
    p_id  IN NUMBER,
    p_nom IN VARCHAR2
)
AS
BEGIN
    INSERT INTO clients(id, nom) VALUES (p_id, p_nom);
    DBMS_OUTPUT.PUT_LINE('Client inserit correctament.');
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        DBMS_OUTPUT.PUT_LINE('Error: ja existeix un client amb id = ' || p_id);
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error inesperat: ' || SQLERRM);
        RAISE; -- Re-llença l'excepció
END inserir_client;
/

Cridar i eliminar procediments

Crida amb CALL / EXEC

-- Crida bàsica
CALL calcular_descompte(100.00, 15, NULL);

-- Crida amb paràmetre OUT capturat
DO $$
DECLARE v_resultat NUMERIC;
BEGIN
    CALL calcular_descompte(100.00, 15, v_resultat);
    RAISE NOTICE 'Resultat: %', v_resultat;
END;
$$;
-- Crida directa
CALL calcular_descompte(100.00, 15, @resultat);
SELECT @resultat;

-- Crida des d'un altre procediment
CALL altre_procediment();
-- Crida amb paràmetres posicionals
EXEC calcular_descompte 100.00, 15, @resultat OUTPUT;

-- Crida amb paràmetres per nom (recomanat)
EXEC calcular_descompte
    @p_preu = 100.00,
    @p_percentatge = 15,
    @p_preu_final = @resultat OUTPUT;
-- Crida des d'un bloc PL/SQL anònim
BEGIN
    calcular_descompte(100, 15, :resultat);
END;
/

-- Des de SQL*Plus amb variable de substitució
VARIABLE v_res NUMBER;
EXEC calcular_descompte(100, 15, :v_res);
PRINT v_res;

Eliminar procediments (DROP)

DROP PROCEDURE IF EXISTS calcular_descompte(NUMERIC, NUMERIC, NUMERIC);
-- Cal especificar els tipus si hi ha sobrecàrrega
DROP PROCEDURE IF EXISTS calcular_descompte;
DROP PROCEDURE IF EXISTS calcular_descompte;
-- Alternativa clàssica:
IF OBJECT_ID('calcular_descompte', 'P') IS NOT NULL
    DROP PROCEDURE calcular_descompte;
DROP PROCEDURE calcular_descompte;
-- Oracle no té IF EXISTS; cal capturar l'error si no existeix

Exemple pràctic: arxivar comandes antigues

Imagina una taula comandes que creix sense parar. Volem arxivar les comandes completades fa més d'un any a una taula comandes_arxiu i eliminar-les de la taula principal.

CREATE OR REPLACE PROCEDURE arxivar_comandes_antigues(
    OUT p_files_arxivades INT
)
LANGUAGE plpgsql
AS $$
DECLARE
    v_data_limit DATE := CURRENT_DATE - INTERVAL '1 year';
BEGIN
    -- Copiar a la taula d'arxiu
    INSERT INTO comandes_arxiu
    SELECT * FROM comandes
    WHERE estat = 'completada'
      AND data_comanda < v_data_limit;

    GET DIAGNOSTICS p_files_arxivades = ROW_COUNT;

    -- Eliminar les arxivades de la taula principal
    DELETE FROM comandes
    WHERE estat = 'completada'
      AND data_comanda < v_data_limit;

    RAISE NOTICE 'Arxivades % comandes anteriors a %',
                 p_files_arxivades, v_data_limit;

    -- La transacció es confirma automàticament en sortir del procediment
EXCEPTION
    WHEN OTHERS THEN
        RAISE WARNING 'Error durant l''arxivat: %', SQLERRM;
        RAISE;
END;
$$;

-- Executar:
DO $$
DECLARE v_n INT;
BEGIN
    CALL arxivar_comandes_antigues(v_n);
    RAISE NOTICE 'Total arxivades: %', v_n;
END;
$$;
DELIMITER //

CREATE PROCEDURE arxivar_comandes_antigues(
    OUT p_files_arxivades INT
)
BEGIN
    DECLARE v_data_limit DATE;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;

    SET v_data_limit = DATE_SUB(CURDATE(), INTERVAL 1 YEAR);

    START TRANSACTION;

    INSERT INTO comandes_arxiu
    SELECT * FROM comandes
    WHERE estat = 'completada'
      AND data_comanda < v_data_limit;

    SET p_files_arxivades = ROW_COUNT();

    DELETE FROM comandes
    WHERE estat = 'completada'
      AND data_comanda < v_data_limit;

    COMMIT;
END //

DELIMITER ;

-- Executar:
CALL arxivar_comandes_antigues(@n);
SELECT @n AS files_arxivades;
CREATE OR ALTER PROCEDURE arxivar_comandes_antigues
    @p_files_arxivades INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @data_limit DATE = DATEADD(YEAR, -1, GETDATE());

    BEGIN TRY
        BEGIN TRANSACTION;

        INSERT INTO comandes_arxiu
        SELECT * FROM comandes
        WHERE estat = 'completada'
          AND data_comanda < @data_limit;

        SET @p_files_arxivades = @@ROWCOUNT;

        DELETE FROM comandes
        WHERE estat = 'completada'
          AND data_comanda < @data_limit;

        COMMIT TRANSACTION;
        PRINT 'Arxivades ' + CAST(@p_files_arxivades AS NVARCHAR) + ' comandes.';
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
        THROW;
    END CATCH;
END;
GO

-- Executar:
DECLARE @n INT;
EXEC arxivar_comandes_antigues @p_files_arxivades = @n OUTPUT;
SELECT @n AS files_arxivades;
CREATE OR REPLACE PROCEDURE arxivar_comandes_antigues(
    p_files_arxivades OUT NUMBER
)
AS
    v_data_limit DATE := ADD_MONTHS(SYSDATE, -12);
BEGIN
    INSERT INTO comandes_arxiu
    SELECT * FROM comandes
    WHERE estat = 'completada'
      AND data_comanda < v_data_limit;

    p_files_arxivades := SQL%ROWCOUNT;

    DELETE FROM comandes
    WHERE estat = 'completada'
      AND data_comanda < v_data_limit;

    COMMIT;
    DBMS_OUTPUT.PUT_LINE('Arxivades ' || p_files_arxivades || ' comandes.');
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
        RAISE;
END arxivar_comandes_antigues;
/

-- Executar:
DECLARE
    v_n NUMBER;
BEGIN
    arxivar_comandes_antigues(v_n);
    DBMS_OUTPUT.PUT_LINE('Total: ' || v_n);
END;
/

Miniactivitat — AC0402 · AC0403

Miniactivitat — AC0402 · AC0403 · Procediment de vendes mensuals

Teniu la taula vendes(id, producte_id, quantitat, preu_unitari, data_venda, comercial_id).

Tasca 1: Creeu un procediment resum_vendes_mes que accepti com a paràmetres l'any i el mes, i retorni (via OUT o SELECT) les dades següents:

  • Nombre total de vendes d'aquell mes
  • Import total facturat
  • Import mitjà per venda
  • Producte més venut (nom)
  • Comercial amb més vendes (nom)

Tasca 2: Afegiu gestió d'errors: si el mes o l'any no són vàlids (mes fora del rang 1-12, any negatiu), el procediment ha de retornar un error informatiu sense executar la consulta.

Tasca 3: Creeu un segon procediment resum_vendes_trimestre(p_any, p_trimestre) que cridi internament resum_vendes_mes per als tres mesos del trimestre i consolidi els resultats.

Entrega: Arxiu SQL amb els procediments i un joc de proves amb CALL (o equivalent) que demostri que funcionen correctament. Escolliu el motor que preferiu per a la implementació principal i afegiu-ne la versió equivalent en un segon motor com a annex.