Chapter 10 · Stored Programs, Views, Triggers, Events, and Server-Side Logic

Stored Procedures, Functions, Parameters, Condition Handlers, and Diagnostics

Build observable MySQL stored routines without confusing client delimiters with server syntax: use parameters, variables, handlers, SIGNAL/RESIGNAL, GET DIAGNOSTICS, execution context, and side-effect verification deliberately.

Beginner → Intermediate135–175 minstored-routine diagnostics labMySQL Community Server 8.4.10 LTS · InnoDB · free local labprocedures/functions + diagnosticsLast reviewed: August 2026

Learning outcomes

A stored procedure is not “SQL in a magic box,” and DELIMITER is not part of the stored procedure language. This lesson separates client parsing from server-side routine syntax, then builds a procedure whose success and failure paths are observable and testable.

01

Distinguish stored procedures from stored functions and distinguish the mysql client DELIMITER command from SQL sent to the server.

02

Use IN, OUT, and INOUT parameters, local variables, IF/control flow, and explicit transaction boundaries appropriately.

03

Handle expected errors with DECLARE HANDLER and surface application-level errors with SIGNAL or RESIGNAL.

04

Capture SQLSTATE, MySQL error number, and message text with GET DIAGNOSTICS/GET STACKED DIAGNOSTICS.

05

Inspect routine metadata and privileges, verify side effects, and recognize when a routine creates more coupling than value.

A realistic problem: one business operation, several client steps

Closing a work order should validate its current state, write an audit record, set closed_at, and either commit all of that or commit none of it. If every application implements those steps differently, correctness drifts. A stored procedure can provide one server-side operation—but it must make failure semantics and execution context explicit.

Procedure versus function

A procedure is invoked with CALL and can expose OUT/INOUT parameters and result sets. A stored function is invoked inside an expression and returns one scalar value. Functions have additional restrictions because they can run as part of a larger statement; do not treat them as procedures with different spelling.

Client delimiter versus server syntax

The mysql command-line client normally sends a statement when it sees ;. A routine body contains many semicolons, so the client needs a temporary terminator while you type the whole CREATE PROCEDURE. DELIMITER is therefore a mysql-client command. It is not stored in the routine and should not be sent through application drivers that already send complete statement strings.

sql · mysql-client creation script with DELIMITER
USE servicehub_logic_lab;DELIMITER $$CREATE PROCEDURE p_demo(IN p_value INT, OUT p_double INT)BEGIN  SET p_double = p_value * 2;END$$DELIMITER ;CALL p_demo(7,@answer);SELECT @answer AS doubled;SHOW CREATE PROCEDURE p_demo\GDROP PROCEDURE p_demo;

Expected result: @answer is 14. The server never needed to understand the word DELIMITER; the client used it to decide when the CREATE PROCEDURE text was complete.

Build a transactional close-work-order procedure

Create this routine while connected as logic_owner. The default routine security mode is DEFINER, so the routine executes underlying statements using its definer privileges unless you explicitly choose SQL SECURITY INVOKER.

sql · procedure with validation, transaction, handler, and diagnostics
USE servicehub_logic_lab;DELIMITER $$CREATE PROCEDURE close_work_order(  IN p_work_order_id BIGINT UNSIGNED,  IN p_note VARCHAR(180),  OUT p_result VARCHAR(255))SQL SECURITY DEFINERMODIFIES SQL DATABEGIN  DECLARE v_status VARCHAR(20);  DECLARE v_errno INT DEFAULT 0;  DECLARE v_sqlstate CHAR(5) DEFAULT '00000';  DECLARE v_message TEXT DEFAULT '';  DECLARE EXIT HANDLER FOR SQLEXCEPTION  BEGIN    GET STACKED DIAGNOSTICS CONDITION 1      v_sqlstate = RETURNED_SQLSTATE,      v_errno = MYSQL_ERRNO,      v_message = MESSAGE_TEXT;    ROLLBACK;    SET p_result = CONCAT('ERROR ',v_errno,' [',v_sqlstate,'] ',v_message);  END;  START TRANSACTION;  SELECT status INTO v_status  FROM work_orders  WHERE work_order_id = p_work_order_id  FOR UPDATE;  IF v_status IS NULL THEN    SIGNAL SQLSTATE '45000'      SET MYSQL_ERRNO = 31001,          MESSAGE_TEXT = 'Work order does not exist';  END IF;  IF v_status = 'closed' THEN    SIGNAL SQLSTATE '45000'      SET MYSQL_ERRNO = 31002,          MESSAGE_TEXT = 'Work order is already closed';  END IF;  UPDATE work_orders  SET status='closed', closed_at=CURRENT_TIMESTAMP(6)  WHERE work_order_id=p_work_order_id;  INSERT INTO work_order_audit    (work_order_id,old_status,new_status,change_source,changed_by)  VALUES    (p_work_order_id,v_status,'closed',     CONCAT('procedure:',COALESCE(p_note,'')),CURRENT_USER());  COMMIT;  SET p_result = 'OK';END$$DELIMITER ;

FOR UPDATE locks the selected work-order row within the transaction so a concurrent close operation cannot casually race past the state check. The exit handler captures the condition from the handler stack, rolls the transaction back, and returns a deterministic diagnostic string for this teaching lab.

Production API design

Returning an error string through an OUT parameter is useful for seeing the handler in a lesson, but production clients often benefit from a documented SQLSTATE/error contract and a real exception. You can log context in a handler and RESIGNAL rather than converting every failure into “success plus text.”

Invoke, observe, and verify

sql · successful invocation and side-effect verification
SET @result=NULL;CALL close_work_order(1,'dispatcher close',@result);SELECT @result AS procedure_result;SELECT work_order_id,status,closed_atFROM work_orders WHERE work_order_id=1;SELECT audit_id,work_order_id,old_status,new_status,change_source,changed_byFROM work_order_audit WHERE work_order_id=1 ORDER BY audit_id;

The work order and audit row should change together. Now call the routine again for the already-closed order:

sql · intentional handled failure
SET @result=NULL;CALL close_work_order(1,'duplicate close attempt',@result);SELECT @result AS procedure_result;SELECT COUNT(*) AS audit_rowsFROM work_order_audit WHERE work_order_id=1;

The second call should report the custom 31002 condition and should not add another audit row. This proves more than “the procedure printed an error”: the rollback preserved the expected database state.

SIGNAL, RESIGNAL, and GET DIAGNOSTICS

SIGNAL deliberately raises a condition. GET DIAGNOSTICS reads the diagnostics area; GET STACKED DIAGNOSTICS is available inside a handler to inspect the condition that activated it. RESIGNAL rethrows the current condition, optionally changing selected condition information.

sql · handler that logs context, then rethrows
DROP PROCEDURE IF EXISTS demonstrate_resignal;DELIMITER $$CREATE PROCEDURE demonstrate_resignal()BEGIN  DECLARE v_message TEXT;  DECLARE EXIT HANDLER FOR SQLEXCEPTION  BEGIN    GET STACKED DIAGNOSTICS CONDITION 1 v_message = MESSAGE_TEXT;    SET v_message = CONCAT('demonstrate_resignal: ',v_message);    RESIGNAL SET MESSAGE_TEXT = v_message;  END;  SIGNAL SQLSTATE '45000'    SET MESSAGE_TEXT='Intentional Chapter 10 diagnostic failure';END$$DELIMITER ;CALL demonstrate_resignal();-- The CALL returns the same SQLSTATE with the handler-enriched message.

This example deliberately avoids a logging side effect: it demonstrates stacked diagnostics and rethrow semantics without creating a second transaction question. If you need durable error telemetry, design that channel explicitly—application logs, server logs, or a separately owned persistence path—rather than assuming a handler insert will survive every rollback boundary.

IN, OUT, and INOUT parameters

Parameter modeDirectionTypical use
INCaller → routineIdentifiers, options, input values; this is the default mode for procedure parameters
OUTRoutine → callerOne scalar status/count/value returned through a caller variable
INOUTCaller → routine → callerA value that is both supplied and modified; useful occasionally but can make APIs harder to read
Function parameterInput onlyStored functions take input parameters and return one scalar with RETURN
sql · small INOUT example
DELIMITER $$CREATE PROCEDURE add_retry(INOUT p_attempts INT)BEGIN  SET p_attempts = COALESCE(p_attempts,0) + 1;END$$DELIMITER ;SET @attempts=2;CALL add_retry(@attempts);SELECT @attempts AS attempts_after_call;DROP PROCEDURE add_retry;

Stored functions: useful, but a narrower contract

A stored function can be useful for deterministic scalar logic that is genuinely better located with the data. It can also become dangerous when it hides expensive queries inside row-by-row expressions. Keep the example side-effect-free. Before creating it, inspect binary-log policy:

sql · check the stored-function creation policy
SHOW VARIABLES LIKE 'log_bin';SHOW VARIABLES LIKE 'log_bin_trust_function_creators';

If binary logging is enabled and log_bin_trust_function_creators=OFF, current MySQL requires the creator to have SUPER in addition to the normal routine privilege. That setting is a replication/recovery safety policy. Do not switch it globally to ON just to complete a tutorial. Run the following function creation as your disposable local administrator if the restricted logic_owner account receives error 1419. The function is explicitly DETERMINISTIC and NO SQL, which documents its behavior but does not bypass the creator-privilege rule.

sql · create and use a scalar function
DELIMITER $$CREATE FUNCTION priority_label(p_priority TINYINT UNSIGNED)RETURNS VARCHAR(12)DETERMINISTICNO SQLRETURN CASE p_priority  WHEN 1 THEN 'critical'  WHEN 2 THEN 'high'  WHEN 3 THEN 'normal'  ELSE 'low'END$$DELIMITER ;SELECT work_order_id,priority,priority_label(priority) AS priority_labelFROM work_orders ORDER BY work_order_id;SHOW CREATE FUNCTION priority_label\G

The DETERMINISTIC and NO SQL characteristics document properties of this function; they are not a free performance guarantee. Stored functions also have documented restrictions, including no recursion and constraints on modifying tables involved in the invoking statement.

Routine metadata and least privilege

sql · inspect routine definitions and metadata
SHOW PROCEDURE STATUS WHERE Db='servicehub_logic_lab';SHOW FUNCTION STATUS WHERE Db='servicehub_logic_lab';SHOW CREATE PROCEDURE close_work_order\GSHOW CREATE FUNCTION priority_label\GSELECT ROUTINE_NAME,ROUTINE_TYPE,DATA_ACCESS,SECURITY_TYPE,DEFINERFROM INFORMATION_SCHEMA.ROUTINESWHERE ROUTINE_SCHEMA='servicehub_logic_lab'ORDER BY ROUTINE_TYPE,ROUTINE_NAME;

To use a routine, a caller needs the appropriate EXECUTE privilege. A definer-security routine can intentionally expose a narrow operation without granting broad table privileges, but that makes routine review and definer lifecycle security-critical. Do not casually grant CREATE ROUTINE or broad schema DML to application accounts.

The wrong assumption: “an SQL error rolls back everything”

A stored routine does not suspend MySQL transaction rules. Some errors roll back only the failing statement, and many DDL statements cause implicit commits or cannot be rolled back. Mixing routine-definition DDL with business DML and expecting one outer ROLLBACK to erase all effects is unsafe.

Design rule

Keep transactional business work inside explicit InnoDB transactions. Treat CREATE/ALTER/DROP ROUTINE and other DDL as deployment operations, not as ordinary rollback-able business statements. Test both the database state and the error contract after intentional failures.

Hands-on lab acceptance checklist

  • You can explain why DELIMITER belongs to the mysql client.
  • close_work_order updates one order and writes one audit row in one successful transaction.
  • A second close attempt activates the handler and does not add an extra audit row.
  • SHOW CREATE and INFORMATION_SCHEMA.ROUTINES expose the recorded routine security/definer metadata.
  • The scalar function remains read-only and its data-access characteristic is documented.

Knowledge check

  1. Is DELIMITER part of CREATE PROCEDURE syntax understood by the MySQL server?
  2. When should GET STACKED DIAGNOSTICS be used?
  3. What does RESIGNAL do?
  4. Why can a definer-security procedure support least privilege?
  5. Why should routine DDL not be mixed into a business rollback plan?
Reveal answers
  1. No. It is a client-side parsing command used by clients such as mysql so semicolons can appear inside the routine body.
  2. Inside a condition handler when you need information about the condition that activated the handler.
  3. It rethrows the current handled condition, optionally with changed condition attributes.
  4. A caller can receive EXECUTE on a narrow operation while underlying table privileges are checked through the controlled definer context.
  5. Many DDL statements cannot be rolled back and may cause implicit transaction boundaries.

Production judgment and next step

Stored procedures are most defensible when they encapsulate a stable transactional operation shared by multiple clients, enforce a narrow privilege boundary, or reduce round trips without creating an opaque monolith. Stored functions are best kept small, predictable, and inexpensive when called from set-oriented queries. Version routine definitions, test error paths, expose SQLSTATE/error contracts, and monitor expensive statements executed inside routines.

Lesson 3 examines an even less visible form of server-side behavior: triggers that fire automatically when rows change.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.