MySQL Stored Procedures
A stored procedure is a named block of SQL statements saved in the database and executed on demand with a CALL statement. Procedures encapsulate business logic on the database server, reduce network round-trips, and can be granted execution rights independently of direct table access.
Why Use Stored Procedures?
Reduce network traffic — one CALL replaces dozens of individual queries
Centralize business logic — changes apply immediately for all applications
Security — grant EXECUTE on a procedure without exposing underlying tables
Performance — procedure plans can be cached by the server
Encapsulation — hide schema complexity behind a simple interface
The DELIMITER Command
MySQL uses ; to end statements. Inside a procedure body you also need ; to end each SQL statement. To avoid the MySQL client treating each inner ; as the end of the whole CREATE PROCEDURE, you temporarily change the delimiter:
-- In the MySQL CLI or Workbench, change delimiter before CREATE PROCEDURE DELIMITER // CREATE PROCEDURE my_proc() BEGIN SELECT 'hello'; END // DELIMITER ;
Basic Stored Procedure
DELIMITER //
CREATE PROCEDURE get_active_customers()
BEGIN
SELECT
customer_id,
first_name,
last_name,
email
FROM customers
WHERE status = 'active'
ORDER BY last_name;
END //
DELIMITER ;
-- Execute the procedure
CALL get_active_customers();IN, OUT, and INOUT Parameters
Mode | Direction | Use Case |
|---|---|---|
IN | Caller passes value in; procedure cannot change the caller's variable | Passing filter criteria, IDs, config values |
OUT | Procedure sets the value; caller reads it after CALL | Returning a single computed result |
INOUT | Caller passes value in; procedure can modify it; caller reads back | Accumulating totals, modifying a running value |
DELIMITER // -- IN parameter: filter orders by customer CREATE PROCEDURE get_customer_orders(IN p_customer_id INT) BEGIN SELECT order_id, order_date, total_amount, status FROM orders WHERE customer_id = p_customer_id ORDER BY order_date DESC; END // -- OUT parameter: return order count CREATE PROCEDURE count_orders( IN p_customer_id INT, OUT p_order_count INT ) BEGIN SELECT COUNT(*) INTO p_order_count FROM orders WHERE customer_id = p_customer_id; END // -- INOUT parameter: apply a discount rate and return the new price CREATE PROCEDURE apply_discount( IN p_discount_pct DECIMAL(5,2), INOUT p_price DECIMAL(10,2) ) BEGIN SET p_price = p_price * (1 - p_discount_pct / 100); END // DELIMITER ; -- Calling with IN CALL get_customer_orders(42); -- Calling with OUT — use a user variable to capture result CALL count_orders(42, @order_count); SELECT @order_count; -- Calling with INOUT SET @price = 99.99; CALL apply_discount(10, @price); SELECT @price; -- returns 89.99
Variables with DECLARE
DELIMITER //
CREATE PROCEDURE calculate_order_total(IN p_order_id INT)
BEGIN
-- Declare local variables (must come first in BEGIN block)
DECLARE v_subtotal DECIMAL(10,2) DEFAULT 0;
DECLARE v_tax_rate DECIMAL(5,4) DEFAULT 0.0875;
DECLARE v_tax DECIMAL(10,2);
DECLARE v_total DECIMAL(10,2);
-- Calculate subtotal from order items
SELECT SUM(quantity * unit_price)
INTO v_subtotal
FROM order_items
WHERE order_id = p_order_id;
-- Calculate tax and total
SET v_tax = v_subtotal * v_tax_rate;
SET v_total = v_subtotal + v_tax;
-- Return result
SELECT
p_order_id AS order_id,
v_subtotal AS subtotal,
v_tax AS tax,
v_total AS total;
END //
DELIMITER ;IF / ELSEIF / ELSE
DELIMITER //
CREATE PROCEDURE categorize_customer(
IN p_customer_id INT,
OUT p_category VARCHAR(20)
)
BEGIN
DECLARE v_total_spent DECIMAL(10,2);
SELECT SUM(total_amount)
INTO v_total_spent
FROM orders
WHERE customer_id = p_customer_id
AND status = 'completed';
IF v_total_spent IS NULL THEN
SET p_category = 'New';
ELSEIF v_total_spent < 500 THEN
SET p_category = 'Bronze';
ELSEIF v_total_spent < 2000 THEN
SET p_category = 'Silver';
ELSEIF v_total_spent < 10000 THEN
SET p_category = 'Gold';
ELSE
SET p_category = 'Platinum';
END IF;
END //
DELIMITER ;
CALL categorize_customer(42, @cat);
SELECT @cat;WHILE Loop
DELIMITER //
CREATE PROCEDURE generate_date_series(
IN p_start DATE,
IN p_end DATE
)
BEGIN
DECLARE v_current DATE;
SET v_current = p_start;
CREATE TEMPORARY TABLE IF NOT EXISTS date_series (dt DATE);
TRUNCATE TABLE date_series;
WHILE v_current <= p_end DO
INSERT INTO date_series VALUES (v_current);
SET v_current = DATE_ADD(v_current, INTERVAL 1 DAY);
END WHILE;
SELECT dt FROM date_series ORDER BY dt;
END //
DELIMITER ;
CALL generate_date_series('2024-01-01', '2024-01-07');REPEAT Loop
DELIMITER //
CREATE PROCEDURE repeat_example()
BEGIN
DECLARE v_counter INT DEFAULT 1;
REPEAT
INSERT INTO log_table (message, created_at)
VALUES (CONCAT('Iteration ', v_counter), NOW());
SET v_counter = v_counter + 1;
UNTIL v_counter > 5
END REPEAT;
END //
DELIMITER ;LOOP with LEAVE
DELIMITER //
CREATE PROCEDURE loop_example()
BEGIN
DECLARE v_i INT DEFAULT 0;
my_loop: LOOP
SET v_i = v_i + 1;
IF v_i > 10 THEN
LEAVE my_loop; -- Exit the named loop
END IF;
IF MOD(v_i, 2) = 0 THEN
ITERATE my_loop; -- Skip even numbers (like 'continue')
END IF;
INSERT INTO odd_numbers VALUES (v_i);
END LOOP my_loop;
END //
DELIMITER ;Error Handling with DECLARE HANDLER
Stored procedures can catch SQL exceptions and warnings using DECLARE ... HANDLER:
DELIMITER //
CREATE PROCEDURE safe_insert_customer(
IN p_email VARCHAR(255),
IN p_first VARCHAR(100),
IN p_last VARCHAR(100),
OUT p_result VARCHAR(100)
)
BEGIN
-- Declare a handler for duplicate key violations (SQLSTATE '23000')
DECLARE EXIT HANDLER FOR SQLSTATE '23000'
BEGIN
SET p_result = 'ERROR: Email already exists';
END;
-- Declare a general handler for any SQL exception
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET p_result = 'ERROR: Unexpected database error';
ROLLBACK;
END;
START TRANSACTION;
INSERT INTO customers (email, first_name, last_name, status, created_at)
VALUES (p_email, p_first, p_last, 'active', NOW());
COMMIT;
SET p_result = 'SUCCESS';
END //
DELIMITER ;
CALL safe_insert_customer('jane@example.com', 'Jane', 'Doe', @res);
SELECT @res;SHOW and Manage Procedures
-- List all stored procedures in the current database SHOW PROCEDURE STATUS WHERE Db = DATABASE()G -- Show the CREATE PROCEDURE source SHOW CREATE PROCEDURE calculate_order_totalG -- Query information_schema SELECT ROUTINE_NAME, ROUTINE_TYPE, CREATED, LAST_ALTERED FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE() AND ROUTINE_TYPE = 'PROCEDURE'; -- Drop a procedure DROP PROCEDURE IF EXISTS get_active_customers;
Practical Example: Order Placement Procedure
DELIMITER //
CREATE PROCEDURE place_order(
IN p_customer_id INT,
IN p_product_id INT,
IN p_quantity INT,
OUT p_order_id INT,
OUT p_message VARCHAR(200)
)
BEGIN
DECLARE v_stock INT;
DECLARE v_price DECIMAL(10,2);
DECLARE v_total DECIMAL(10,2);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_message = 'Order failed due to a database error';
END;
-- Check stock availability
SELECT stock_qty, unit_price
INTO v_stock, v_price
FROM products
WHERE product_id = p_product_id
FOR UPDATE;
IF v_stock < p_quantity THEN
SET p_message = 'Insufficient stock';
LEAVE place_order; -- Exit the procedure (named via BEGIN label)
END IF;
SET v_total = v_price * p_quantity;
START TRANSACTION;
-- Create order
INSERT INTO orders (customer_id, order_date, total_amount, status)
VALUES (p_customer_id, NOW(), v_total, 'pending');
SET p_order_id = LAST_INSERT_ID();
-- Add order item
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (p_order_id, p_product_id, p_quantity, v_price);
-- Deduct stock
UPDATE products
SET stock_qty = stock_qty - p_quantity
WHERE product_id = p_product_id;
COMMIT;
SET p_message = CONCAT('Order ', p_order_id, ' placed successfully');
END //
DELIMITER ;
-- Usage
CALL place_order(42, 7, 3, @oid, @msg);
SELECT @oid AS order_id, @msg AS message;Best Practices
Prefix parameter names (e.g. p_) and local variables (e.g. v_) to avoid column name collisions
Always declare EXIT HANDLERs for SQLEXCEPTION inside transactions to prevent partial commits
Keep procedures focused — one procedure per logical business operation
Version-control your procedures in .sql migration files alongside schema changes
Use SHOW PROCEDURE STATUS regularly to audit procedures that may be orphaned or outdated
Avoid heavy loops inside procedures — prefer set-based SQL whenever possible