LearnFrenzy
Sign in Take a free test

Transactions, Procedures and Interviews · Lesson 29 of 30

Stored Procedures, Functions and Triggers

5 min read Updated Nov 7, 2026 Saurabh Samir

Databases can run code too. A stored procedure is a named block of SQL you call with parameters; a function returns a value you can use inside queries; a trigger runs automatically when rows change. They're common in enterprise systems, banking and reporting – and frequently asked about in interviews.

In this lesson

  • Creating and calling stored procedures
  • IN and OUT parameters, variables and IF
  • User-defined functions
  • Triggers for auditing and validation

DELIMITER: a client detail

A procedure body contains semicolons, but the mysql client normally ends a statement at the first ;. DELIMITER $$ tells the client to use $$ instead while you define the routine. (Workbench and most GUI tools handle this for you.)

A stored procedure

DELIMITER $$
CREATE PROCEDURE customer_orders(IN p_customer_id INT)
BEGIN
  SELECT o.order_id, o.order_date, o.status, SUM(i.quantity * i.unit_price) AS total
  FROM orders o
  JOIN order_items i ON i.order_id = o.order_id
  WHERE o.customer_id = p_customer_id
  GROUP BY o.order_id, o.order_date, o.status
  ORDER BY o.order_date;
END $$
DELIMITER ;

CALL customer_orders(1);

Output

+----------+------------+-----------+--------+
| order_id | order_date | status    | total  |
+----------+------------+-----------+--------+
|      101 | 2025-08-02 | delivered | 839.00 |
|      103 | 2025-08-19 | delivered | 800.00 |
|      109 | 2025-10-21 | shipped   | 540.00 |
+----------+------------+-----------+--------+

OUT parameters, variables and IF

DELIMITER $$
CREATE PROCEDURE restock(IN p_product_id INT, IN p_qty INT, OUT p_new_stock INT, OUT p_message VARCHAR(60))
BEGIN
  DECLARE v_exists INT;
  SELECT COUNT(*) INTO v_exists FROM products WHERE product_id = p_product_id;

  IF v_exists = 0 THEN
    SET p_new_stock = NULL, p_message = 'No such product';
  ELSEIF p_qty <= 0 THEN
    SET p_new_stock = NULL, p_message = 'Quantity must be positive';
  ELSE
    UPDATE products SET stock = stock + p_qty WHERE product_id = p_product_id;
    SELECT stock INTO p_new_stock FROM products WHERE product_id = p_product_id;
    SET p_message = 'Restocked';
  END IF;
END $$
DELIMITER ;

CALL restock(3, 20, @stock, @msg);
SELECT @stock AS new_stock, @msg AS message;
CALL restock(99, 5, @stock, @msg);
SELECT @stock AS new_stock, @msg AS message;

Output

+-----------+-----------+
| new_stock | message   |
+-----------+-----------+
|        20 | Restocked |
+-----------+-----------+
+-----------+-----------------+
| new_stock | message         |
+-----------+-----------------+
|      NULL | No such product |
+-----------+-----------------+

@stock and @msg are session variables that receive the OUT values. Inside the procedure, DECLARE creates local variables and SELECT … INTO fills them.

User-defined functions

A function returns one value and can be used anywhere an expression can:

DELIMITER $$
CREATE FUNCTION price_with_gst(p_price DECIMAL(10,2), p_rate DECIMAL(4,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
  RETURN ROUND(p_price * (1 + p_rate / 100), 2);
END $$
DELIMITER ;

SELECT name, price, price_with_gst(price, 18) AS with_gst
FROM products
WHERE category_id = 1
ORDER BY price;

Output

+---------------------+---------+----------+
| name                | price   | with_gst |
+---------------------+---------+----------+
| Wireless Mouse      |  599.00 |   706.82 |
| USB-C Hub           | 1299.00 |  1532.82 |
| Laptop Stand        | 1499.00 |  1768.82 |
| Mechanical Keyboard | 2499.00 |  2948.82 |
+---------------------+---------+----------+

DETERMINISTIC promises the same inputs always give the same output; MySQL requires such a characteristic when binary logging is on.

Triggers

A trigger runs automatically BEFORE or AFTER an INSERT, UPDATE or DELETE on a table, once per affected row. OLD and NEW refer to the row before and after the change.

CREATE TABLE price_audit (
  product_id INT, old_price DECIMAL(10,2), new_price DECIMAL(10,2), changed_at DATETIME
);

DELIMITER $$
CREATE TRIGGER trg_price_audit
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
  IF NEW.price <> OLD.price THEN
    INSERT INTO price_audit VALUES (OLD.product_id, OLD.price, NEW.price, '2025-11-07 10:00:00');
  END IF;
END $$
DELIMITER ;

UPDATE products SET price = 649.00 WHERE product_id = 1;
UPDATE products SET stock = stock - 1 WHERE product_id = 2;     -- price unchanged: no audit row
SELECT * FROM price_audit;

Output

+------------+-----------+-----------+---------------------+
| product_id | old_price | new_price | changed_at          |
+------------+-----------+-----------+---------------------+
|          1 |    599.00 |    649.00 | 2025-11-07 10:00:00 |
+------------+-----------+-----------+---------------------+

(A real audit trigger would use NOW(); a fixed time keeps this example's output stable.)

BEFORE or AFTER?

Trigger timing

  • BEFOREvalidate or adjust NEW values before they're saved (e.g. lower-case emails)
  • AFTERreact to a completed change (audit logs, summary tables)

Should logic live in the database?

ProsCons
Runs next to the data – fewer round tripsHarder to version, test and debug than application code
Enforced for every app and tool that writes dataTies you to one database's dialect
Can restrict access to procedures onlyHidden triggers surprise developers ("why did this row change?")

A balanced approach

Keep core data rules in constraints (they're simple and always on), use triggers sparingly for audit trails, and keep business workflows in application code unless there's a clear performance or security reason.

Practice

Try it yourself

  1. Write a function full_years(d DATE) that returns the whole years from d to 2025-11-07, and use it on employees' hire dates.

    Show solution
    DELIMITER $$
    CREATE FUNCTION full_years(d DATE) RETURNS INT DETERMINISTIC
    BEGIN
      RETURN TIMESTAMPDIFF(YEAR, d, '2025-11-07');
    END $$
    DELIMITER ;
    
    SELECT name, hire_date, full_years(hire_date) AS years
    FROM employees
    WHERE department = 'HR';

    Output

    +--------------+------------+-------+
    | name         | hire_date  | years |
    +--------------+------------+-------+
    | Kavya Reddy  | 2020-02-17 |     5 |
    | Imran Shaikh | 2024-08-01 |     1 |
    +--------------+------------+-------+

Quick check

Procedure vs function?

A function returns a single value and can be used inside SQL expressions; a procedure is called with CALL, can return result sets and OUT parameters, and can change data freely.

What do OLD and NEW mean in a trigger?

The row's values before and after the change (OLD in UPDATE/DELETE, NEW in INSERT/UPDATE).

Why use DELIMITER?

So the client doesn't end the CREATE statement at the first semicolon inside the routine body.

Summary

  • Stored procedures bundle SQL with parameters; call them with CALL.
  • Functions return a value usable in queries; mark them DETERMINISTIC when true.
  • Triggers run on row changes – great for audits, but use them sparingly.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links