Chapter 20 — Database Programmability: Stored Procedures & Control Flow
1. What is it?
A Stored Procedure is a pre-compiled subroutine consisting of one or more SQL statements and procedural control-flow logic (IF, CASE, WHILE, LOOP) stored directly inside the database data dictionary.
Applications execute a stored procedure by issuing a single CALL procedure_name(...) command. Because the routine lives on the database server, the query statements are parsed, validated, and optimized in advance.
The DELIMITER Command: Why Is It Mandatory?
By default, the MySQL client interprets a semicolon (;) as the termination delimiter of an SQL statement. However, a stored procedure contains multiple internal semicolons to separate its statements. If you do not change the client delimiter, the MySQL parser attempts to execute the first internal line of the procedure before the definition is complete, triggering syntax errors!
To define a procedure in the MySQL CLI, you temporarily reassign the statement delimiter to an alternative string (commonly // or $$), complete the procedure definition, and then reset the delimiter back to ;:
DELIMITER //
CREATE PROCEDURE my_procedure()
BEGIN
-- Internal statements end with standard semicolon ;
SELECT * FROM employees;
END //
DELIMITER ;2. Why do we use it?
- Reduced Network Traffic: Complex business transactions requiring 15 separate queries can be bundled inside a single stored procedure. Instead of sending 15 round-trip network requests between the application server and the database, the application issues a single
CALLstatement, drastically reducing network latency. - Encapsulation & Security: You can grant users
EXECUTEprivileges on a stored procedure while revoking directSELECT,UPDATE, andDELETEpermissions on the underlying base tables. The stored procedure acts as a secure API gateway controlling access to sensitive data. - Centralized Business Logic: When multiple disparate applications (e.g., a Python backend, a legacy Java service, and a mobile reporting tool) interact with the same database, embedding shared business rules (such as checkout validation or payroll calculation) inside a stored procedure ensures identical logic across all platforms.
- Pre-Compiled Execution Efficiency: Stored procedures reduce query parsing overhead on repeated executions.
3. Parameter Modes: IN, OUT, and INOUT
Stored procedures communicate with calling applications through three distinct parameter modes:
| Parameter Mode | Direction | Behavior Description |
|---|---|---|
IN (Default) | Caller $\rightarrow$ Procedure | Passes a value into the procedure. The procedure can read and modify the variable internally, but changes are not visible to the caller. |
OUT | Procedure $\rightarrow$ Caller | Passes an uninitialized variable into the procedure. The procedure computes a result and assigns it to the variable, returning it to the caller. |
INOUT | Caller $\leftrightarrow$ Procedure | Passes an initial value into the procedure; the procedure can read it, modify it, and return the modified value back to the caller. |
4. Syntax
Procedural Control-Flow Constructs
DELIMITER //
CREATE PROCEDURE sp_demo_flow(
IN p_employee_id INT,
OUT p_bonus_amount DECIMAL(10,2)
)
BEGIN
-- 1. Local Variable Declarations (Must appear first in BEGIN block)
DECLARE v_salary DECIMAL(10,2);
DECLARE v_tenure_years INT;
-- 2. Populate variables from table queries using INTO
SELECT salary, TIMESTAMPDIFF(YEAR, hire_date, CURDATE())
INTO v_salary, v_tenure_years
FROM employees
WHERE employee_id = p_employee_id;
-- 3. Conditional Flow: IF - ELSEIF - ELSE
IF v_tenure_years >= 5 THEN
SET p_bonus_amount = v_salary * 0.20;
ELSEIF v_tenure_years >= 2 THEN
SET p_bonus_amount = v_salary * 0.10;
ELSE
SET p_bonus_amount = v_salary * 0.05;
END IF;
END //
DELIMITER ;Managing Stored Procedures
-- Execute a Stored Procedure
CALL procedure_name(arg1, @out_var);
-- View Procedure Definition
SHOW CREATE PROCEDURE procedure_name;
-- Drop Procedure
DROP PROCEDURE IF EXISTS procedure_name;5. Basic Example
Creating and calling a simple stored procedure:
USE sql_mastery;
DELIMITER //
CREATE PROCEDURE sp_get_department_employees(IN p_dept_id INT)
BEGIN
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = p_dept_id
ORDER BY salary DESC;
END //
DELIMITER ;
-- Call the procedure for Department 1 (Engineering)
CALL sp_get_department_employees(1);
-- Clean up
DROP PROCEDURE sp_get_department_employees;6. Real-World Example: An Enterprise Checkout Order Processor
Let us design an enterprise stored procedure sp_process_order_checkout that coordinates:
- Validating that the product exists and has sufficient stock.
- Generating a new order record inside a managed transaction.
- Inserting the line item and calculating totals.
- Decrementing stock inventory.
- Returning the newly created
order_idas anOUTparameter, along with an informative status message.
USE sql_mastery;
DELIMITER //
CREATE PROCEDURE sp_process_order_checkout(
IN p_customer_id INT,
IN p_product_id INT,
IN p_quantity INT,
OUT p_new_order_id INT,
OUT p_status_message VARCHAR(100)
)
BEGIN
-- Local variables
DECLARE v_current_stock INT;
DECLARE v_unit_price DECIMAL(10, 2);
DECLARE v_total_cost DECIMAL(12, 2);
-- Declare an exception handler for SQLEXCEPTION: rollback on any error
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_new_order_id = NULL;
SET p_status_message = 'ERROR: Transaction rolled back due to internal database failure.';
END;
-- Step 1: Check product stock and price
SELECT stock_quantity, unit_price
INTO v_current_stock, v_unit_price
FROM products
WHERE product_id = p_product_id;
-- Validate product existence
IF v_unit_price IS NULL THEN
SET p_new_order_id = NULL;
SET p_status_message = 'REJECTED: Product does not exist.';
-- Validate stock quantity
ELSEIF v_current_stock < p_quantity THEN
SET p_new_order_id = NULL;
SET p_status_message = CONCAT('REJECTED: Insufficient stock. Only ', v_current_stock, ' available.');
ELSE
-- Step 2: Begin Transaction
START TRANSACTION;
SET v_total_cost = v_unit_price * p_quantity;
-- Insert Order
INSERT INTO orders (customer_id, order_date, status, shipping_fee, total_amount)
VALUES (p_customer_id, CURDATE(), 'Pending', 15.00, v_total_cost + 15.00);
-- Capture auto-generated order_id
SET p_new_order_id = LAST_INSERT_ID();
-- Insert Line Item
INSERT INTO order_items (order_id, product_id, quantity, unit_price, discount)
VALUES (p_new_order_id, p_product_id, p_quantity, v_unit_price, 0.00);
-- Decrement stock
UPDATE products
SET stock_quantity = stock_quantity - p_quantity
WHERE product_id = p_product_id;
-- Commit changes
COMMIT;
SET p_status_message = 'SUCCESS: Order placed and inventory updated successfully.';
END IF;
END //
DELIMITER ;7. Step-by-Step Explanation & Execution
Let us test the stored procedure:
USE sql_mastery;
-- Test 1: Successful Order (Customer 1 orders 2 units of Product 3 TrueSound Headphones)
CALL sp_process_order_checkout(1, 3, 2, @order_id, @status_msg);
-- Inspect output parameters
SELECT @order_id AS generated_order_id, @status_msg AS execution_result;
-- Test 2: Insufficient Stock Rejection (Customer 1 tries to order 9999 units of Product 3)
CALL sp_process_order_checkout(1, 3, 9999, @failed_order_id, @failed_msg);
-- Inspect rejection output
SELECT @failed_order_id AS failed_order_id, @failed_msg AS rejection_reason;
-- Clean up demo order
DELETE FROM order_items WHERE order_id = @order_id;
DELETE FROM orders WHERE order_id = @order_id;
UPDATE products SET stock_quantity = stock_quantity + 2 WHERE product_id = 3;
DROP PROCEDURE sp_process_order_checkout;8. Expected Result
Terminal output from calling the stored procedure:
mysql> SELECT @order_id AS generated_order_id, @status_msg AS execution_result;
+--------------------+---------------------------------------------------------------+
| generated_order_id | execution_result |
+--------------------+---------------------------------------------------------------+
| 1011 | SUCCESS: Order placed and inventory updated successfully. |
+--------------------+---------------------------------------------------------------+
1 row in set (0.01 sec)
mysql> SELECT @failed_order_id AS failed_order_id, @failed_msg AS rejection_reason;
+-----------------+----------------------------------------------------+
| failed_order_id | rejection_reason |
+-----------------+----------------------------------------------------+
| NULL | REJECTED: Insufficient stock. Only 118 available. |
+-----------------+----------------------------------------------------+
1 row in set (0.00 sec)9. Common Mistakes
- Forgetting to Reset the
DELIMITER:- Mistake: Changing
DELIMITER //, compiling the procedure, and forgetting to runDELIMITER ;. - Consequence: Subsequent standard SQL queries like
SELECT * FROM employees;will appear to hang, waiting for//to terminate the statement.
- Mistake: Changing
- Variable Shadowing (Name Collisions):
- The Nightmare Trap:sql
CREATE PROCEDURE get_emp(IN employee_id INT) BEGIN SELECT * FROM employees WHERE employee_id = employee_id; -- AMBIGUOUS! END; - Consequence: The parser evaluates
employee_id = employee_idas a tautology (WHERE 1 = 1), returning every employee in the entire company instead of the requested one! - Rule: Always prefix parameters with
p_(e.g.,p_employee_id) and local variables withv_(e.g.,v_salary).
- The Nightmare Trap:
- Placing
DECLAREStatements Out of Order:- In MySQL stored procedures, all
DECLAREstatements for local variables, conditions, cursors, and handlers must be placed at the very beginning of theBEGIN ... ENDblock, before any executable statements. Placing aDECLAREafter an assignment triggers a syntax error.
- In MySQL stored procedures, all
10. Best Practices
- Adopt Strict Naming Conventions:
- Prefix procedures with
sp_orusp_(User Stored Procedure). - Prefix parameters with
p_and local variables withv_.
- Prefix procedures with
- Always Implement Exception Handlers with Transactions:
- Include
DECLARE EXIT HANDLER FOR SQLEXCEPTIONwhen writing multi-statement transactions to ensure that errors automatically trigger aROLLBACK, preventing orphaned uncommitted locks.
- Include
- Avoid Procedural Loops When Set-Based SQL Can Do the Job:
- Beginners often write procedural
WHILEloops andCURSORSto process rows one by one. In relational databases, set-based operations (UPDATE table SET col = ... WHERE ...) are typically 100x faster than cursor loops. Use loops only when procedural logic cannot be expressed declaratively.
- Beginners often write procedural
11. Practice Questions
Easy
- Why must you change the client
DELIMITERwhen creating a stored procedure in MySQL? - What is the difference between an
INparameter and anOUTparameter? - Which SQL command executes a stored procedure?
Medium
- Write a stored procedure named
sp_update_product_pricethat accepts ap_product_id INTand ap_percentage_increase DECIMAL(4,2)asINparameters and increases the product'sunit_priceby that percentage. - Write a procedure
sp_get_customer_metricsthat takes acustomer_idas anINparameter and returns their total order count as anOUTparameter and total spend as anOUTparameter. - Write a stored procedure using a
WHILEloop to insert 5 test records into a table.
Difficult
- Write a stored procedure
sp_transfer_departmentthat moves an employee from their current department to a new department, updates their salary based on the new department's average, and logs the transfer in an audit table. Wrap all operations in a transaction with anEXIT HANDLER FOR SQLEXCEPTION. - Explain the performance implications of placing heavy business logic inside MySQL stored procedures versus executing that logic within an application service layer (e.g., Go, Java, or Python microservices). What are the scaling tradeoffs?
12. Interview Questions
Q1: What are the primary advantages and disadvantages of using Stored Procedures?
Answer:
- Advantages:
- Reduced Network Overhead: Consolidates multi-query interactions into a single network round-trip.
- Security & Data Abstraction: Applications can be granted
EXECUTErights on procedures without direct table access. - Centralized Business Rules: Shared logic is maintained in one place across multiple client applications.
- Disadvantages:
- Database Server CPU Contention: Intensive procedural computations consume database CPU, which is typically harder and more expensive to scale horizontally than stateless application servers.
- Version Control & CI/CD Complexity: Managing database migration scripts, branching, and automated testing for stored routines is more difficult than standard application code.
- Vendor Lock-in: Procedural dialects (MySQL PL/SQL, Oracle PL/SQL, SQL Server T-SQL) are proprietary and non-portable.
Q2: What is the difference between an IN, OUT, and INOUT parameter?
Answer:
IN: Passes data into the procedure. The procedure can read it, but cannot overwrite the caller's original variable outside the procedure.OUT: Passes an empty variable into the procedure. The procedure computes a value and assigns it to the variable, making the result available to the caller after the procedure finishes.INOUT: Passes an initialized variable into the procedure; the procedure reads the initial value, modifies it, and returns the updated value back to the caller.
Q3: What is variable shadowing in MySQL stored procedures, and how do you prevent it?
Answer: Variable shadowing occurs when a parameter or local variable shares the exact same identifier name as a column in a referenced table (e.g., WHERE customer_id = customer_id). The MySQL parser cannot distinguish between the column reference and the variable reference, typically resolving the clause to true for every row. This results in unintended updates or mass data exposure. It is prevented by strictly adopting prefix naming conventions: prefixing all procedure parameters with p_ (e.g., p_customer_id) and all local variables with v_ (e.g., v_customer_id).
13. Quick Revision
- Use
DELIMITER //when creating procedures to prevent early statement termination. INpasses data in;OUTreturns data to the caller;INOUTdoes both.- Place all
DECLAREstatements at the very beginning of theBEGIN ... ENDblock. - Avoid name collisions by prefixing parameters with
p_and local variables withv_. - Use
DECLARE EXIT HANDLER FOR SQLEXCEPTIONto automatically roll back transactions upon error.