Збережена процедура - іменований блок SQL з параметрами, що виконується на сервері через CALL. Збережена функція повертає одне значення і використовується у виразах.
DELIMITER //
CREATE PROCEDURE close_month(IN p_month DATE)
BEGIN
START TRANSACTION;
INSERT INTO monthly_totals (month, total)
SELECT p_month, SUM(amount) FROM payments
WHERE created_at >= p_month AND created_at < p_month + INTERVAL 1 MONTH;
UPDATE payments SET closed = 1
WHERE created_at >= p_month AND created_at < p_month + INTERVAL 1 MONTH;
COMMIT;
END //
CREATE FUNCTION vat(amount DECIMAL(12,2)) RETURNS DECIMAL(12,2)
DETERMINISTIC NO SQL
RETURN amount * 0.2 //
DELIMITER ;
CALL close_month('2026-09-01');
SELECT id, vat(total) FROM orders;
DELIMITER - команда клієнта mysql, а не сервера: тіло процедури містить ;, тож клієнту потрібен інший роздільник. Через PDO (і DB::unprepared() у Laravel) він не потрібен - запит надсилається цілком.
Характеристики функції - DETERMINISTIC, NO SQL, READS SQL DATA. Якщо ввімкнено бінарний журнал, MySQL відмовиться створити функцію без цих позначок (або без log_bin_trust_function_creators): недетермінована функція могла б дати різні результати на джерелі й репліці. Позначка - обіцянка: неправдиве DETERMINISTIC MySQL не перевіряє.
Коли процедури доречні:
- обробка великих обсягів даних без передачі їх у застосунок;
- спільна логіка для кількох застосунків на різних мовах, що працюють з однією базою;
- адміністративні операції, які запускає DBA.
Чому в застосунках на Laravel їх зазвичай уникають:
- тестування й налагодження складніші: немає Pest, покриття, нормального налагоджувача;
- деплой і версіонування: код живе в міграціях, рев'ю й відкат незручні, легко розійтися між середовищами;
- масштабування: база - найважче для масштабування місце, а логіка в ній додає навантаження;
- обмеження: функції й тригери не можуть змінювати таблицю, яку вже читає оператор, що їх викликав; рекурсивні функції заборонені;
- знання в команді: критична логіка на мові, якою команда пише рідко.
Виклик з Laravel: DB::select('CALL report_for(?)', [$month]) для процедури з результатом, DB::statement('CALL close_month(?)', [...]) - без нього. Помилка з SIGNAL SQLSTATE '45000' приходить як QueryException.
Докладніше в документації: MySQL: CREATE PROCEDURE і CREATE FUNCTION