📘 PL/SQL Practice Guide: Functions, Procedures, Triggers

📌 Functions in PL/SQL

🔹 Syntax

CREATE OR REPLACE FUNCTION function_name (
    param1 IN data_type,
    param2 IN data_type
) RETURN return_data_type IS
BEGIN
    -- logic
    RETURN some_value;
END;
/

✅ Practice Questions with Solutions

1. Check Even or Odd

CREATE OR REPLACE FUNCTION check_even_odd(n IN NUMBER) RETURN VARCHAR2 IS
BEGIN
    IF MOD(n, 2) = 0 THEN
        RETURN 'EVEN';
    ELSE
        RETURN 'ODD';
    END IF;
END;
/

2. Get Maximum of Two Numbers

CREATE OR REPLACE FUNCTION get_max(a IN NUMBER, b IN NUMBER) RETURN NUMBER IS
BEGIN
    IF a > b THEN
        RETURN a;
    ELSE
        RETURN b;
    END IF;
END;
/

3. Count Vowels in a String

CREATE OR REPLACE FUNCTION count_vowels(str IN VARCHAR2) RETURN NUMBER IS
    count NUMBER := 0;
    ch CHAR;
BEGIN
    FOR i IN 1..LENGTH(str) LOOP
        ch := LOWER(SUBSTR(str, i, 1));
        IF ch IN ('a', 'e', 'i', 'o', 'u') THEN
            count := count + 1;
        END IF;
    END LOOP;
    RETURN count;
END;
/

4. Check Palindrome

CREATE OR REPLACE FUNCTION is_palindrome(str IN VARCHAR2) RETURN VARCHAR2 IS
    reversed_str VARCHAR2(1000) := '';
BEGIN
    FOR i IN REVERSE 1..LENGTH(str) LOOP
        reversed_str := reversed_str || SUBSTR(str, i, 1);
    END LOOP;

    IF LOWER(str) = LOWER(reversed_str) THEN
        RETURN 'YES';
    ELSE
        RETURN 'NO';
    END IF;
END;
/

5. Sum of Digits

CREATE OR REPLACE FUNCTION sum_of_digits(n IN NUMBER) RETURN NUMBER IS
    sum NUMBER := 0;
    digit NUMBER;
    num NUMBER := n;
BEGIN
    WHILE num > 0 LOOP
        digit := MOD(num, 10);
        sum := sum + digit;
        num := FLOOR(num / 10);
    END LOOP;
    RETURN sum;
END;
/

🔧 Procedures in PL/SQL

🔹 Syntax

CREATE OR REPLACE PROCEDURE procedure_name (param1 IN/OUT data_type) IS
BEGIN
    -- logic
END;
/

✅ Practice Questions with Solutions

1. Procedure to Print Square

CREATE OR REPLACE PROCEDURE print_square(n IN NUMBER) IS
BEGIN
    DBMS_OUTPUT.PUT_LINE('Square: ' || (n * n));
END;
/

2. Procedure to Update Employee Salary

CREATE OR REPLACE PROCEDURE increase_salary(empId IN NUMBER) IS
BEGIN
    UPDATE employees
    SET salary = salary * 1.10
    WHERE emp_id = empId;
    COMMIT;
END;
/

3. Procedure with OUT Parameter for Factorial

CREATE OR REPLACE PROCEDURE find_factorial(n IN NUMBER, result OUT NUMBER) IS
    fact NUMBER := 1;
BEGIN
    FOR i IN 1..n LOOP
        fact := fact * i;
    END LOOP;
    result := fact;
END;
/

🔥 Triggers in PL/SQL

🔹 Syntax

CREATE OR REPLACE TRIGGER trigger_name
BEFORE/AFTER INSERT/UPDATE/DELETE ON table_name
FOR EACH ROW
BEGIN
    -- logic
END;
/

✅ Practice Questions with Solutions

1. BEFORE INSERT Trigger

CREATE OR REPLACE TRIGGER trg_set_created_at
BEFORE INSERT ON students
FOR EACH ROW
BEGIN
    :NEW.created_at := SYSDATE;
END;
/

2. AFTER UPDATE Trigger for Salary Audit

CREATE OR REPLACE TRIGGER trg_salary_audit
AFTER UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
    INSERT INTO salary_audit(emp_id, old_salary, new_salary, change_date)
    VALUES(:OLD.emp_id, :OLD.salary, :NEW.salary, SYSDATE);
END;
/

3. BEFORE DELETE Trigger to Prevent Deletion

CREATE OR REPLACE TRIGGER trg_prevent_delete
BEFORE DELETE ON departments
FOR EACH ROW
BEGIN
    RAISE_APPLICATION_ERROR(-20001, 'Deletion is not allowed on departments table.');
END;
/

📚 Additional Practice Questions

🔧 Functions

🔹 6. Reverse a String

Question: Write a function to reverse a given string.

CREATE OR REPLACE FUNCTION reverse_string(str IN VARCHAR2) RETURN VARCHAR2 IS
    reversed_str VARCHAR2(1000) := '';
BEGIN
    FOR i IN REVERSE 1..LENGTH(str) LOOP
        reversed_str := reversed_str || SUBSTR(str, i, 1);
    END LOOP;
    RETURN reversed_str;
END;
/

🔹 7. Prime Number Check

Question: Write a function to check if a number is prime. Return ‘YES’ or ‘NO’.

CREATE OR REPLACE FUNCTION is_prime(n IN NUMBER) RETURN VARCHAR2 IS
    i NUMBER;
BEGIN
    IF n <= 1 THEN
        RETURN 'NO';
    END IF;
    FOR i IN 2..FLOOR(SQRT(n)) LOOP
        IF MOD(n, i) = 0 THEN
            RETURN 'NO';
        END IF;
    END LOOP;
    RETURN 'YES';
END;
/

🔧 Procedures

🔹 4. Display Employee Details

Question: Write a procedure that takes employee ID and displays employee details.

CREATE OR REPLACE PROCEDURE show_employee(empId IN NUMBER) IS
    emp_name employees.emp_name%TYPE;
    emp_salary employees.salary%TYPE;
BEGIN
    SELECT emp_name, salary INTO emp_name, emp_salary
    FROM employees
    WHERE emp_id = empId;

    DBMS_OUTPUT.PUT_LINE('Name: ' || emp_name || ', Salary: ' || emp_salary);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Employee not found.');
END;
/

🔹 5. Delete Department by ID

Question: Write a procedure that deletes a department by ID.

CREATE OR REPLACE PROCEDURE delete_department(deptId IN NUMBER) IS
BEGIN
    DELETE FROM departments WHERE dept_id = deptId;
    COMMIT;
END;
/

🔥 Triggers

🔹 4. AFTER INSERT Trigger to Log Insertions

Question: Create a trigger that logs new employee entries into a table employee_log.

CREATE TABLE employee_log (
    emp_id NUMBER,
    emp_name VARCHAR2(100),
    log_time DATE
);

Trigger:

CREATE OR REPLACE TRIGGER trg_log_employee_insert
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
    INSERT INTO employee_log(emp_id, emp_name, log_time)
    VALUES(:NEW.emp_id, :NEW.emp_name, SYSDATE);
END;
/

🚀 Intermediate and Hard PL/SQL Questions

🔧 Functions

🔹 8. Count Words in a String

Question: Write a function to count the number of words in a given string (words are separated by spaces).

CREATE OR REPLACE FUNCTION count_words(str IN VARCHAR2) RETURN NUMBER IS
    word_count NUMBER := 0;
    i NUMBER;
BEGIN
    FOR i IN 1..LENGTH(str) LOOP
        IF SUBSTR(str, i, 1) = ' ' THEN
            word_count := word_count + 1;
        END IF;
    END LOOP;
    RETURN word_count + 1;
END;
/

🔹 9. Find GCD of Two Numbers

Question: Write a function to return the greatest common divisor (GCD) of two integers.

CREATE OR REPLACE FUNCTION find_gcd(a IN NUMBER, b IN NUMBER) RETURN NUMBER IS
BEGIN
    WHILE b != 0 LOOP
        DECLARE
            temp NUMBER := b;
        BEGIN
            b := MOD(a, b);
            a := temp;
        END;
    END LOOP;
    RETURN a;
END;
/

🔧 Procedures

🔹 6. Transfer Funds Between Accounts

Question: Write a procedure that transfers money from one account to another.

CREATE TABLE accounts (
    acc_id NUMBER PRIMARY KEY,
    acc_name VARCHAR2(100),
    balance NUMBER
);

CREATE OR REPLACE PROCEDURE transfer_funds(
    from_acc IN NUMBER,
    to_acc IN NUMBER,
    amount IN NUMBER
) IS
BEGIN
    UPDATE accounts SET balance = balance - amount WHERE acc_id = from_acc;
    UPDATE accounts SET balance = balance + amount WHERE acc_id = to_acc;
    COMMIT;
END;
/

🔹 7. Generate Fibonacci Series

Question: Write a procedure to print the first N terms of the Fibonacci series.

CREATE OR REPLACE PROCEDURE print_fibonacci(n IN NUMBER) IS
    a NUMBER := 0;
    b NUMBER := 1;
    temp NUMBER;
BEGIN
    DBMS_OUTPUT.PUT_LINE(a);
    DBMS_OUTPUT.PUT_LINE(b);
    FOR i IN 3..n LOOP
        temp := a + b;
        DBMS_OUTPUT.PUT_LINE(temp);
        a := b;
        b := temp;
    END LOOP;
END;
/

🔥 Triggers

🔹 5. Restrict High Salary Update

Question: Create a trigger that prevents updating salary to more than 100000.

CREATE OR REPLACE TRIGGER trg_salary_limit
BEFORE UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
    IF :NEW.salary > 100000 THEN
        RAISE_APPLICATION_ERROR(-20002, 'Salary cannot exceed 100000');
    END IF;
END;
/

🔹 6. Audit All DML Actions

Question: Write a trigger to log all INSERT, UPDATE, and DELETE operations on the employees table.

CREATE TABLE employee_dml_log (
    action_type VARCHAR2(10),
    emp_id NUMBER,
    action_date DATE
);

CREATE OR REPLACE TRIGGER trg_employee_dml_log
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
BEGIN
    IF INSERTING THEN
        INSERT INTO employee_dml_log VALUES ('INSERT', :NEW.emp_id, SYSDATE);
    ELSIF UPDATING THEN
        INSERT INTO employee_dml_log VALUES ('UPDATE', :NEW.emp_id, SYSDATE);
    ELSIF DELETING THEN
        INSERT INTO employee_dml_log VALUES ('DELETE', :OLD.emp_id, SYSDATE);
    END IF;
END;
/

FUNCTIONS

Write a PL/pgSQL function named get_product_price that returns the price of a product based on its product ID.

CREATE TABLE products (
    product_id INTEGER PRIMARY KEY,
    product_name VARCHAR(100),
    price INTEGER
);

    
    
INSERT INTO products (product_id, product_name, price) VALUES
(1, 'Laptop', 80000),
(2, 'Smartphone', 40000),
(3, 'Tablet', 25000),
(4, 'Headphones', 5000);

CREATE OR REPLACE FUNCTION GET_PRODUCT_PRICE(P_ID INTEGER)
	RETURNS INTEGER
    LANGUAGE PLPGSQL
    AS $$
    DECLARE
		PRODUCT_PRICE INTEGER;
    BEGIN
    	SELECT PRICE INTO PRODUCT_PRICE
        FROM PRODUCTS
        WHERE PRODUCT_ID = P_ID;
    RETURN PRODUCT_PRICE;
END;
$$;
    
SELECT GET_PRODUCT_PRICE(1)

Write a PL/pgSQL function named update_product_stock that updates the stock quantity of a product given its product ID.

CREATE TABLE products (
    product_id NUMERIC PRIMARY KEY,
    product_name VARCHAR(100),
    price NUMERIC,
    stock_quantity NUMERIC
);
INSERT INTO products (product_id, product_name, price, stock_quantity) VALUES
(1, 'Laptop', 80000, 20),
(2, 'Smartphone', 40000, 50),
(3, 'Tablet', 25000, 30);

CREATE OR REPLACE FUNCTION UPDATE_PRODUCT_STOCK(P_ID NUMERIC, UPDATED_STOCK NUMERIC)
	RETURNS VOID
    LANGUAGE PLPGSQL
    AS $$
    BEGIN
    	UPDATE PRODUCTS
        SET STOCK_QUANTITY = UPDATED_STOCK
        WHERE PRODUCT_ID = P_ID;
    END;
    $$;
    
SELECT UPDATE_PRODUCT_STOCK(1,60);
SELECT * FROM PRODUCTS;

Write a PL/pgSQL function named get_total_stock_value that returns the total monetary value of all products in stock.

CREATE TABLE products (
    product_id NUMERIC PRIMARY KEY,
    product_name VARCHAR(100),
    price DECIMAL,
    stock_quantity NUMERIC
);
INSERT INTO products (product_id, product_name, price, stock_quantity) VALUES
(1, 'Laptop', 80000, 10),
(2, 'Smartphone', 40000, 20),
(3, 'Tablet', 25000, 15);

CREATE OR REPLACE FUNCTION GET_TOTAL_STOCK_VALUE()
	RETURNS DECIMAL
	LANGUAGE PLPGSQL 
	AS $$
	DECLARE
    	TOTAL DECIMAL;
    BEGIN
    	SELECT SUM(PRICE * STOCK_QUANTITY) INTO TOTAL
        FROM PRODUCTS;
    	RETURN TOTAL;
    END;
    $$;

SELECT GET_TOTAL_STOCK_VALUE();
        

Write a PL/pgSQL function named count_products_by_category that returns the number of products in a specific category.

CREATE TABLE products (
    product_id NUMERIC PRIMARY KEY,
    product_name VARCHAR(100),
    category_id NUMERIC,
    price DECIMAL,
    stock_quantity NUMERIC
);

INSERT INTO products (product_id, product_name, category_id, price, stock_quantity) VALUES
(1, 'Laptop', 101, 80000, 10),
(2, 'Smartphone', 101, 40000, 20),
(3, 'Tablet', 102, 25000, 15),
(4, 'Monitor', 103, 15000, 5),
(5, 'Keyboard', 103, 2000, 50);


CREATE OR REPLACE FUNCTION count_products_by_category(p_category_id NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE 
    c_count NUMERIC;
BEGIN
    SELECT COUNT(*) INTO c_count
    FROM products
    WHERE category_id = p_category_id;

    RETURN c_count;
END;
$$;


SELECT count_products_by_category(101);  

Write a PL/pgSQL function get_products_below_price(p INTEGER) that returns a comma-separated string of product names whose price is less than or equal to the given price.

CREATE TABLE products (
    product_id INTEGER PRIMARY KEY,
    product_name VARCHAR(100),
    price INTEGER,
    stock_quantity INTEGER
);
INSERT INTO products (product_id, product_name, price, stock_quantity) VALUES
(1, 'Laptop', 80000, 10),
(2, 'Smartphone', 40000, 20),
(3, 'Tablet', 25000, 15),
(4, 'Monitor', 15000, 8),
(5, 'Keyboard', 2000, 30),
(6, 'Mouse', 1000, 40);

--SELECT * FROM PRODUCTS;

CREATE OR REPLACE FUNCTION get_products_below_price(P INTEGER)
RETURNS TEXT
LANGUAGE plpgsql
AS $$ 
DECLARE
	P_NAMES TEXT;
BEGIN
	SELECT STRING_AGG(PRODUCT_NAME,',' ORDER BY PRODUCT_NAME ASC) INTO P_NAMES
    FROM PRODUCTS
    WHERE PRICE <= P;
    
    RETURN P_NAMES;
END;
$$;
    
    
SELECT get_products_below_price(8000);

Procedure

Write a PL/pgSQL procedure add_new_book(p_title, p_author, p_published_year, p_genre) that inserts a new book into the books table using the provided parameters.

CREATE TABLE books (
    book_id SERIAL PRIMARY KEY,
    title TEXT,
    author TEXT,
    published_year INTEGER,
    genre TEXT
);

SELECT * FROM BOOKS;

CREATE OR REPLACE PROCEDURE ADD_NEW_BOOK(
  P_TITLE TEXT,
  P_AUTHOR TEXT,	
  P_PUBLISHED_YEAR INTEGER,
  P_GENRE TEXT
)
LANGUAGE PLPGSQL
AS
$$
BEGIN
	INSERT INTO BOOKS(TITLE,AUTHOR,PUBLISHED_YEAR,GENRE) 
    VALUES(P_TITLE,P_AUTHOR, P_PUBLISHED_YEAR,P_GENRE);
END;
$$;

CALL ADD_NEW_BOOK('GAME OF THRONES','ADITYA',2002,'STRATAGY');
SELECT * FROM BOOKS;

Write a PL/pgSQL procedure loan_book(p_book_id INTEGER, p_member_id INTEGER) that records a book being loaned to a member by inserting a row into a loans table. The current date should be recorded as the loan date, and the return date should initially be NULL.

CREATE TABLE books (
    book_id SERIAL PRIMARY KEY,
    title TEXT,
    author TEXT
);
CREATE TABLE members (
    member_id SERIAL PRIMARY KEY,
    name TEXT
);
CREATE TABLE loans (
    loan_id SERIAL PRIMARY KEY,
    book_id INTEGER REFERENCES books(book_id),
    member_id INTEGER REFERENCES members(member_id),
    loan_date DATE,
    return_date DATE
);
CREATE OR REPLACE PROCEDURE loan_book(
    p_book_id INTEGER,
    p_member_id INTEGER
)
LANGUAGE plpgsql
AS
$$
BEGIN
  INSERT INTO loans(book_id, member_id, loan_date, return_date)
  VALUES (p_book_id, p_member_id, now()::date, NULL);
END;
$$;

-- Insert sample books
INSERT INTO books (title, author) VALUES
('1984', 'George Orwell'),
('The Alchemist', 'Paulo Coelho');

-- Insert sample members
INSERT INTO members (name) VALUES
('Alice'),
('Bob');

-- Call the procedure to loan a book
CALL loan_book(1, 1);  -- Loans book_id=1 to member_id=1

-- Check loan record
SELECT * FROM loans;

Write a PL/pgSQL procedure return_book(p_loan_id INTEGER) that updates the return_date of a loan to a fixed date (‘2025-03-18’).

CREATE TABLE loans (
    loan_id SERIAL PRIMARY KEY,
    book_id INTEGER REFERENCES books(book_id),
    member_id INTEGER REFERENCES members(member_id),
    loan_date DATE,
    return_date DATE
);
CREATE OR REPLACE PROCEDURE return_book(p_loan_id INTEGER)
LANGUAGE plpgsql
AS
$$
BEGIN
  UPDATE loans
  SET return_date = '2025-03-18'
  WHERE loan_id = p_loan_id;
END;
$$;
CALL return_book(1);  -- Set return_date for loan_id = 1

SELECT * FROM loans;  -- Check result

Write a PL/pgSQL function get_member_loans(p_member_id INTEGER) that returns a list of books (title and loan date) borrowed by a member.

CREATE OR REPLACE FUNCTION get_member_loans(p_member_id INTEGER)
RETURNS TABLE(title TEXT, loan_date DATE)
LANGUAGE plpgsql
AS
$$
BEGIN
  RETURN QUERY
  SELECT books.title, loans.loan_date
  FROM books
  JOIN loans ON loans.book_id = books.book_id
  WHERE loans.member_id = p_member_id;
END;
$$;
SELECT * FROM get_member_loans(1);  -- Show books loaned by member_id = 1

TRIGGERS


1. Prevent insertion of negative marks using a trigger

CREATE TABLE students (
    student_id INT PRIMARY KEY,
    student_name TEXT,
    marks INTEGER
);

CREATE OR REPLACE FUNCTION check_negative_marks()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.marks < 0 THEN
        RAISE EXCEPTION 'Marks cannot be negative';
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER prevent_negative_marks
BEFORE INSERT OR UPDATE ON students
FOR EACH ROW
EXECUTE FUNCTION check_negative_marks();

-- Example:
-- INSERT INTO students(student_id, student_name, marks) VALUES (1, 'ABC', 80); -- Works
-- INSERT INTO students(student_id, student_name, marks) VALUES (2, 'DEF', -10); -- Fails

2. Automatically apply 10% discount after a customer makes a purchase

CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    customer_name TEXT,
    purchase_amount DECIMAL,
    discount DECIMAL
);

CREATE OR REPLACE FUNCTION calculate_discount()
RETURNS TRIGGER AS $$
DECLARE
    disc DECIMAL;
BEGIN
    disc := NEW.purchase_amount * 0.1;
    UPDATE customers SET discount = disc WHERE customer_id = NEW.customer_id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER apply_discount
AFTER INSERT ON customers
FOR EACH ROW
EXECUTE FUNCTION calculate_discount();

3. Log salary updates using a trigger

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name TEXT,
    salary INT
);

CREATE TABLE log_table (
    log_message TEXT,
    log_timestamp TIMESTAMP
);

CREATE OR REPLACE FUNCTION log_salary_update()
RETURNS TRIGGER AS $$
DECLARE
    message TEXT;
BEGIN
    message := 'Salary updated for employee ID ' || NEW.emp_id || ': from ' || OLD.salary || ' to ' || NEW.salary;
    INSERT INTO log_table(log_message, log_timestamp) VALUES (message, current_timestamp);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER salary_update_trigger
AFTER UPDATE ON employees
FOR EACH ROW
WHEN (NEW.salary <> OLD.salary)
EXECUTE FUNCTION log_salary_update();

4. Prevent price updates on shopping table

CREATE TABLE shopping (
    item_id INT PRIMARY KEY,
    item_name TEXT,
    price DECIMAL
);

CREATE OR REPLACE FUNCTION prevent_price_update()
RETURNS TRIGGER AS $$
BEGIN
    RAISE EXCEPTION 'Price updates are not allowed.';
    RETURN OLD;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER prevent_price_update_trigger
AFTER UPDATE ON shopping
FOR EACH ROW
WHEN (NEW.price <> OLD.price)
EXECUTE FUNCTION prevent_price_update();

5. Log booking deletion events using a trigger

CREATE TABLE bookings (
    booking_id INT PRIMARY KEY,
    customer_name TEXT,
    booking_date DATE,
    booking_status TEXT
);

CREATE TABLE log_table (
    log_message TEXT,
    log_timestamp TIMESTAMP DEFAULT current_timestamp
);

CREATE OR REPLACE FUNCTION log_booking_deletion()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO log_table (log_message)
    VALUES (
        CONCAT('Booking deleted: ID ', OLD.booking_id, 
               ', Customer: ', OLD.customer_name, 
               ', Date: ', OLD.booking_date, 
               ', Status: ', OLD.booking_status)
    );
    RETURN OLD;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER booking_deletion_trigger
AFTER DELETE ON bookings
FOR EACH ROW
EXECUTE FUNCTION log_booking_deletion();

6. Create a function to get employee salary

CREATE TABLE employees (
    employee_id INT PRIMARY KEY,
    employee_name TEXT,
    salary INTEGER
);

CREATE OR REPLACE FUNCTION get_employee_salary(emp_id INTEGER)
RETURNS INTEGER
LANGUAGE plpgsql
AS
$$
DECLARE
    emp_salary INTEGER;
BEGIN
    SELECT salary INTO emp_salary FROM employees WHERE employee_id = emp_id;
    RETURN emp_salary;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
END;
$$;

-- Example usage:
SELECT get_employee_salary(101);