Views, stored procedures, functions and triggers
Package queries as views, orchestrate multi-step work in routines, and decide honestly whether a trigger belongs in the database or in the application.
Views
CREATE OR REPLACE VIEW v_open_orders AS
SELECT o.id, o.status, o.total, c.name AS customer_name
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE o.status IN ('new', 'paid');
SELECT * FROM v_open_orders WHERE total > 100;
CREATE VIEW v_status_summary AS
SELECT status, COUNT(*) AS n FROM orders GROUP BY status;
-- not updatable: it aggregates
SHOW CREATE VIEW v_open_ordersG- A view is a stored query, not a stored result. It costs nothing extra at read time beyond the query it contains, unless the algorithm forces a temporary table.
- A view is updatable when it maps to a single table with no aggregation,
DISTINCT,UNIONor derived column. Otherwise writes must go to the base tables. - By default a view runs with the privileges of its definer. That is convenient, and it also means a view can grant indirect access to data the caller could not read directly. Use
SQL SECURITY INVOKERwhen that matters. - Views do not make a bad query fast. Check
EXPLAINagainst the view, not just against the statement inside it.
Procedures and functions
DELIMITER //
CREATE PROCEDURE ship_order (IN p_order_id BIGINT UNSIGNED, OUT p_ok TINYINT)
BEGIN
DECLARE v_status VARCHAR(20);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET p_ok = 0;
ROLLBACK;
END;
START TRANSACTION;
SELECT status INTO v_status FROM orders WHERE id = p_order_id FOR UPDATE;
IF v_status = 'paid' THEN
UPDATE orders SET status = 'shipped' WHERE id = p_order_id;
SET p_ok = 1;
COMMIT;
ELSE
SET p_ok = 0;
ROLLBACK;
END IF;
END //
DELIMITER ;
CALL ship_order(1042, @ok);
SELECT @ok;DELIMITER is a client directive, not SQL the server understands. It exists so the client does not stop reading at the first semicolon inside the routine body. Use the same delimiter in scripts that recreate the routine, or the load will fail halfway.
- A procedure returns values through
OUTparameters or result sets; a function returns one value and can be called inside a query. - An
EXIT HANDLERturns an unexpected error into a defined outcome. Without one, a failure halfway through leaves the caller with an open transaction. - Routines are interpreted statement by statement. A loop over rows is orders of magnitude slower than the equivalent set-based statement, so keep procedures for orchestration rather than for data crunching.
Triggers and the maintenance trade-off
DELIMITER //
CREATE TRIGGER trg_orders_audit
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
IF NEW.status <> OLD.status THEN
INSERT INTO order_status_log (order_id, old_status, new_status, changed_at)
VALUES (NEW.id, OLD.status, NEW.status, CURRENT_TIMESTAMP(3));
END IF;
END //
DELIMITER ;
SHOW TRIGGERS;
DROP TRIGGER trg_orders_audit;- A trigger runs inside the transaction of the statement that fired it, so a failing trigger rolls the whole statement back — including the change the caller thought it made.
- Row-level triggers fire once per row. A bulk update of a million rows runs the trigger a million times, which is easy to miss when reviewing the statement.
- Triggers are invisible from application code. Debugging a missing row is far harder when a trigger, rather than the service, removed it.
- MySQL has no
INSTEAD OFtriggers on tables, and a table cannot carry two triggers with the same timing and event. - Keep rules that change often, need tests, or must be observable in the application. Keep in the database the invariants that every writer must respect, including ad-hoc SQL.
⚠️
MySQL refuses direct recursion into the same table, but two triggers over two tables can still hand a change back and forth. Keep trigger logic to logging and derivation, and never let it re-enter the table that fired it.
FAQ
Database or application for business logic?
Application for anything that changes often, needs unit tests or must be observable in logs and traces. Database for invariants that every writer must obey, such as audit trails and derived totals. Choose deliberately — a mixture nobody owns is the expensive outcome.
Why is my stored procedure slow?
Routines are interpreted statement by statement, so a row-by-row loop costs far more than one set-based statement doing the same work. Rewrite the loop as a single
INSERT ... SELECT or UPDATE ... JOIN and keep the procedure for orchestration.Related
Writing data safely: DML and transactions JSON, generated columns and window functions
Last refreshed 2026-09-18.