Stored Procedures
A stored procedure in MySQL groups SQL into a named routine you call with CALL. Procedures can take IN/OUT/INOUT parameters, use local variables, control flow (IF/CASE/WHILE/LOOP), and handle errors with DECLARE HANDLER. Use them sparingly — they hide logic from version control and unit tests — but they are the right tool for performance-critical batches and admin chores.
Procedure with parameters, transactions, and error handling
EXAMPLE
-- 1) A simple procedure with input + output params
DELIMITER //
CREATE PROCEDURE place_order(
IN p_customer_id BIGINT,
IN p_total_cents BIGINT,
OUT p_order_id BIGINT
)
SQL SECURITY INVOKER
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
INSERT INTO orders (customer_id, total_cents, status, created_at)
VALUES (p_customer_id, p_total_cents, 'new', NOW());
SET p_order_id = LAST_INSERT_ID();
UPDATE customers SET orders_count = orders_count + 1 WHERE id = p_customer_id;
COMMIT;
END//
DELIMITER ;
-- Call it from SQL
CALL place_order(42, 4995, @oid);
SELECT @oid;
-- Call it from PHP (PDO)
-- $stmt = $pdo->prepare('CALL place_order(:cid, :total, @oid)');
-- $stmt->execute(['cid' => 42, 'total' => 4995]);
-- $oid = $pdo->query('SELECT @oid')->fetchColumn();
-- 2) Loops, conditional logic, and a cursor — batch backfill example
DELIMITER //
CREATE PROCEDURE backfill_slugs()
BEGIN
DECLARE v_id BIGINT;
DECLARE v_name VARCHAR(255);
DECLARE done INT DEFAULT 0;
DECLARE cur CURSOR FOR SELECT id, name FROM products WHERE slug IS NULL;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_id, v_name;
IF done THEN LEAVE read_loop; END IF;
UPDATE products
SET slug = LOWER(REGEXP_REPLACE(v_name, '[^a-zA-Z0-9]+', '-'))
WHERE id = v_id;
END LOOP;
CLOSE cur;
END//
DELIMITER ;
CALL backfill_slugs();
-- 3) Error handling: trap a specific SQLSTATE and respond gracefully
DELIMITER //
CREATE PROCEDURE add_unique_customer(IN p_email VARCHAR(255))
BEGIN
DECLARE CONTINUE HANDLER FOR 1062 -- 1062 = duplicate key
BEGIN
SELECT id FROM customers WHERE email = p_email;
END;
INSERT INTO customers (email, created_at) VALUES (p_email, NOW());
SELECT LAST_INSERT_ID() AS id;
END//
DELIMITER ;
-- 4) Permissions — grant EXECUTE only, no direct DML
GRANT EXECUTE ON PROCEDURE shop.place_order TO 'app'@'%';
REVOKE INSERT, UPDATE, DELETE ON shop.orders FROM 'app'@'%';
FLUSH PRIVILEGES;
-- 5) Inspect and maintain
SHOW PROCEDURE STATUS WHERE Db = 'shop';
SHOW CREATE PROCEDURE place_order;
DROP PROCEDURE IF EXISTS place_order;
Why it matters
Source procedures into your repo as .sql files and apply them via migrations — never let "the database" be the source of truth. The moment a stored proc lives only in production, you have a config file that no code review touches and no diff tool tracks, which is how prod and staging quietly diverge.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
DELIMITER //
CREATE PROCEDURE archive_old_orders()
BEGIN
INSERT INTO orders_archive SELECT * FROM orders WHERE created_at < NOW() - INTERVAL 1 YEAR;
DELETE FROM orders WHERE created_at < NOW() - INTERVAL 1 YEAR;
END//
DELIMITER ;
Try it Yourself »
Discussion
Loading…