Stored procedures, functions, triggers and events move logic into the database. That can be right, and it can be a trap. These guidelines help you decide and keep them manageable.
The decision
| Need | Best tool |
|---|---|
| A rule about one row’s values | CHECK, NOT NULL, DEFAULT, a generated column |
| A rule about relationships | Foreign key, unique key |
| A query reused in many places | View |
| A small pure calculation shared by many queries | Stored function (or a generated column) |
| A multi-step write that must be atomic and shared by several applications | Stored procedure, or a transaction in a shared application library |
| An audit trail that must capture every change from any source | Trigger |
| Housekeeping on a schedule, pure SQL | Event (or cron calling a procedure) |
| Business workflow, validation messages, anything with external calls | Application code |
When stored programs are a good idea
- Several applications (in different languages) must apply the same rule, and a shared library is not possible.
- A security boundary: an application account may run
CALL place_order(...)but has no right to touch the tables directly. - Fewer round trips for a chain of dependent statements (rare in modern setups with low-latency networks).
- Batch maintenance that is purely data-driven.
- Auditing that must not be bypassed.
When they are a poor fit
- Application logic that changes often, needs unit tests, feature flags, logging and metrics.
- Teams without database specialists: the code is rarely reviewed or tested.
- Scaling: the database server is the most expensive and hardest-to-scale part of the system. CPU spent on string processing and loops in procedures is CPU that queries cannot use.
- Portability: stored program syntax is MySQL-specific.
- Debugging: no real debugger, limited logging, errors that appear far from their cause.
Security
SELECT routine_name, routine_type, security_type, definer IS NOT NULL AS has_definer FROM information_schema.routines WHERE routine_schema = 'learn' ORDER BY routine_name;
Empty set
SQL SECURITY DEFINER(the default) runs the program with the creator’s privileges. It is how a restricted account can run a controlled write. Use a dedicated low-privilege definer, notroot, and not a developer’s personal account.SQL SECURITY INVOKERruns with the caller’s privileges, which is safer when the program only does what the caller is allowed to do anyway.- Grant
EXECUTEon specific routines, and nothing broader (Privileges). - Avoid dynamic SQL (
PREPAREfrom strings). If you must, whitelist identifiers (Prepared Statements). - A trigger or procedure created by a powerful definer can be a privilege escalation path if a lower user can change what it calls.
Deployment and version control
- Keep every routine, trigger and event in a SQL file in Git, with
DROP ... IF EXISTSandCREATE, so a deploy recreates them. - Include them in the migration process, and list them in the schema documentation.
- Back them up:
mysqldump --routines --triggers --events(mysqldump). Triggers are included by default, routines and events are not. - Beware of drift: someone edits a procedure directly in production. Compare
SHOW CREATE PROCEDUREoutput with the repository in CI.
Naming and structure
| Object | Convention |
|---|---|
| Procedures | verb_noun (archive_old_orders, place_order) |
| Functions | noun or get_/calc_ prefix (tax_rate_for) |
| Triggers | trg_table_timing_event (trg_products_after_update) |
| Events | ev_purpose |
| Parameters | Prefix to avoid clashes with columns (p_customer_id), because a parameter named like a column hides the column inside the body |
DELIMITER //
CREATE PROCEDURE rename_customer(IN id INT, IN new_name VARCHAR(30))
BEGIN
UPDATE customers SET name = new_name WHERE id = id;
END//
DELIMITER ;
CREATE TABLE customers (id INT PRIMARY KEY, name VARCHAR(30));
INSERT INTO customers VALUES (1, 'Maya'), (2, 'Rohan');
CALL rename_customer(1, 'Maya Shah');
SELECT id, name FROM customers ORDER BY id;
+----+-----------+
| id | name |
+----+-----------+
| 1 | Maya Shah |
| 2 | Maya Shah |
+----+-----------+
2 rows in set
Both customers were renamed. Inside the procedure, the parameter id hides the column id, so WHERE id = id compares the parameter with itself and is true for every row. This classic bug is avoided by prefixing parameters (p_id, p_new_name), so that they never share a name with a column.
Testing
- Treat routines as code: write SQL test scripts that set up data, call the routine, and compare results (a framework such as
tSQLtequivalents for MySQL exist, or plain scripts run in CI). - Test failure paths: bad input, missing rows, concurrency.
- Test with realistic volume.
Monitoring
- The error log shows event and trigger errors.
performance_schemaandsysshow statement and routine statistics:sys.statement_analysisincludesCALLstatements, andperformance_schema.events_statements_summary_by_programgives per-routine totals.- Log business events into a table from the routine, so that you can see what ran.
Alternatives that keep logic in the application
- A transaction script in one well-tested application service.
- A shared library or microservice that owns all writes.
- An outbox table processed by a worker instead of a trigger calling out (Transaction Patterns).
A balanced rule
Use the database for data rules and data movement, and the application for business rules and workflow. A few well-chosen stored programs (an audit trigger, a purge event, a controlled-write procedure) are a good investment. Hundreds of them make a system that only one person understands.
Checklist before you add a stored program
- [ ] Could a constraint, a view, a generated column or application code do this?
- [ ] Who will test it, and how?
- [ ] Is it in version control and in the deployment process?
- [ ] Who is the definer, and is the privilege level minimal?
- [ ] What happens on error, and who is told?
- [ ] Is it fast for the largest realistic input?
- [ ] Is it documented where the application developers will look?