SQL Stored Procedures
A stored procedure is a named block of SQL that lives in the database and runs with CALL. It can take parameters, use variables, IF and loops, run several statements in a transaction and return result sets. Teams use procedures to keep important business operations, like placing an order, in one tested place that every application calls the same way.
- Create and call a procedure with
DELIMITERandCALL - Pass values in and out with
INandOUTparameters - Use variables,
IFand loops - Handle errors and wrap work in a transaction
Why DELIMITER?
A procedure body contains several statements, each ending in ;. The mysql client would send the CREATE PROCEDURE at the first ;, cutting it off. DELIMITER // temporarily changes the statement terminator so the whole body is sent as one statement:
DELIMITER //CREATE PROCEDURE name(...)BEGIN statement1; statement2;END //DELIMITER ;MySQL Workbench and most drivers need the same trick or send the body as one statement automatically.
A first procedure
DELIMITER //CREATE PROCEDURE list_department(IN p_dept_id INT)BEGIN SELECT emp_id, first_name, job_title, salary FROM employees WHERE dept_id = p_dept_id ORDER BY salary DESC;END //DELIMITER ; CALL list_department(2);| emp_id | first_name | job_title | salary |
|---|---|---|---|
| 105 | Vikram | Sales Manager | 105000 |
| 106 | Neha | Sales Executive | 54000 |
| 107 | Arjun | Sales Executive | 48000 |
IN p_dept_id INTdeclares an input parameter. Ap_prefix keeps parameter names apart from column names.- The body is a normal
SELECTthat uses the parameter. CALL list_department(2)runs it for department 2 and returns its result set.
OUT parameters
An OUT parameter sends a value back to the caller through a session variable:
DELIMITER //CREATE PROCEDURE dept_stats(IN p_dept_id INT, OUT p_staff INT, OUT p_payroll INT)BEGIN SELECT COUNT(*), SUM(salary) INTO p_staff, p_payroll FROM employees WHERE dept_id = p_dept_id;END //DELIMITER ; CALL dept_stats(1, @staff, @payroll);SELECT @staff AS staff, @payroll AS payroll;| staff | payroll |
|---|---|
| 5 | 392000 |
SELECT ... INTO p_staff, p_payrollstores the query’s single row into the OUT parameters.- The caller passes user variables
@staffand@payrollto receive them. - Engineering has 5 people and a payroll of 392,000.
INOUT parameters are both: the caller passes a value in and gets a changed value back.
Variables and IF
DELIMITER //CREATE PROCEDURE stock_status(IN p_product_id INT, OUT p_status VARCHAR(20))BEGIN DECLARE v_stock INT; SELECT stock INTO v_stock FROM products WHERE product_id = p_product_id; IF v_stock IS NULL THEN SET p_status = 'unknown product'; ELSEIF v_stock = 0 THEN SET p_status = 'out of stock'; ELSEIF v_stock < 20 THEN SET p_status = 'low'; ELSE SET p_status = 'ok'; END IF;END //DELIMITER ; CALL stock_status(3, @s1);CALL stock_status(5, @s2);CALL stock_status(1, @s3);CALL stock_status(99, @s4);SELECT @s1, @s2, @s3, @s4;| @s1 | @s2 | @s3 | @s4 |
|---|---|---|---|
| out of stock | low | ok | unknown product |
DECLARE v_stock INTcreates a local variable (only valid inside theBEGIN ... ENDblock).SELECT stock INTO v_stockreads the product’s stock; for an unknown id no row is found and the variable stays NULL.IF ... ELSEIF ... ELSE ... END IFpicks the status.
Loops
Create a table of the next 5 invoice numbers with a WHILE loop:
CREATE TABLE invoice_numbers (n INT);DELIMITER //CREATE PROCEDURE make_invoices(IN p_start INT, IN p_count INT)BEGIN DECLARE i INT DEFAULT 0; WHILE i < p_count DO INSERT INTO invoice_numbers VALUES (p_start + i); SET i = i + 1; END WHILE;END //DELIMITER ; CALL make_invoices(5001, 5);SELECT * FROM invoice_numbers;| n |
|---|
| 5001 |
| 5002 |
| 5003 |
| 5004 |
| 5005 |
MySQL also has REPEAT ... UNTIL ... END REPEAT and LOOP with LEAVE. Remember that one set-based INSERT ... SELECT is usually much faster than a loop.
A business operation with a transaction
Place an order: check stock, insert the order and its line, reduce stock, all or nothing:
DELIMITER //CREATE PROCEDURE place_order(IN p_order_id INT, IN p_customer_id INT, IN p_product_id INT, IN p_qty INT)BEGIN DECLARE v_stock INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; SELECT stock INTO v_stock FROM products WHERE product_id = p_product_id FOR UPDATE; IF v_stock IS NULL OR v_stock < p_qty THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Not enough stock'; END IF; INSERT INTO orders (order_id, customer_id, order_date, status) VALUES (p_order_id, p_customer_id, '2026-01-15', 'Pending'); INSERT INTO order_items (order_id, product_id, quantity) VALUES (p_order_id, p_product_id, p_qty); UPDATE products SET stock = stock - p_qty WHERE product_id = p_product_id; COMMIT;END //DELIMITER ; CALL place_order(2001, 6, 1, 2);SELECT order_id, customer_id, status FROM orders WHERE order_id = 2001;SELECT stock FROM products WHERE product_id = 1;| order_id | customer_id | status |
|---|---|---|
| 2001 | 6 | Pending |
| stock |
|---|
| 118 |
DECLARE EXIT HANDLER FOR SQLEXCEPTIONruns if any statement fails: it rolls back and re-raises the error (RESIGNAL) so the caller sees it.SELECT ... FOR UPDATElocks the product row while we check and change it.SIGNAL SQLSTATE '45000'raises a custom error when there is not enough stock.- Manoj (customer 6) orders 2 mice: the order exists and stock drops from 120 to 118.
Asking for more than is in stock raises the custom error, and nothing is saved:
CALL place_order(2002, 6, 3, 1);ERROR 1644 (45000): Not enough stock
SELECT COUNT(*) AS order_2002_rows FROM orders WHERE order_id = 2002;| order_2002_rows |
|---|
| 0 |
Manage procedures
| Task | Statement |
|---|---|
| List procedures | SHOW PROCEDURE STATUS WHERE Db = 'company'; |
| See the code | SHOW CREATE PROCEDURE place_order; |
| Remove | DROP PROCEDURE IF EXISTS place_order; |
| Change | drop and create again (there is no ALTER for the body) |
Pros and cons
- Pros: one tested implementation of a business rule; fewer round trips; permissions can be granted on the procedure instead of the tables.
- Cons: harder to version, test and debug than application code; logic split between two places; MySQL’s procedural language is limited.
Quick quiz
1. Why is DELIMITER needed when creating a procedure in the mysql client?
The client splits statements at the delimiter.
2. How do you run a stored procedure?
MySQL uses CALL.
3. How does a procedure return a single value to the caller?
Functions use RETURN; procedures use OUT parameters (or result sets).
Stored procedure FAQ
What is a stored procedure in MySQL?
A named, stored set of SQL statements with optional parameters and control flow, executed with CALL.
What is the difference between a stored procedure and a function?
A function returns one value and can be used inside SQL expressions. A procedure is run with CALL, can return result sets and OUT parameters, and can manage transactions. See User-Defined Functions.
How do I handle errors in a MySQL procedure?
Declare a handler: DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; and raise your own errors with SIGNAL SQLSTATE '45000'.