讲解

存储过程(PROCEDURE)是保存在数据库里的一段 SQL 程序,用 CALL 调用,可以有输入(IN)、输出(OUT)、输入输出(INOUT)参数;存储函数(FUNCTION)返回单个值,可以像内置函数一样用在 SQL 表达式里。它们把多条 SQL 和流程控制(IF、WHILE、游标)封装在数据库端,适合数据加工、批量迁移、需要减少应用与数据库往返的场景。

写多语句的存储过程会碰到一个经典问题:过程体里每条 SQL 以分号结尾,客户端读到第一个分号就以为定义结束了。解法是 DELIMITER——注意它是 mysql 客户端的命令,不是 SQL:DELIMITER // 把语句结束符临时改成 //,定义写完再改回分号。

函数和过程有个和复制相关的坑:开了 binlog 的实例上,创建函数时必须声明 DETERMINISTIC(确定性)、NO SQL 或 READS SQL DATA 之一,否则报 1418——因为不确定性的函数在 STATEMENT 格式 binlog 下会导致主从数据不一致。声明 DETERMINISTIC 是承诺「同样输入永远同样输出」。最后泼点冷水:现代工程实践对存储过程很克制——业务逻辑藏进数据库后难测试、难版本管理、难 review,团队协作的应用系统一般把逻辑放在应用层,存储过程留给数据密集的内部任务。

示例

建表造数,然后定义并调用一个带 IN/OUT 参数的存储过程(DELIMITER 是客户端指令,不是 SQL):

CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), city VARCHAR(20));

INSERT INTO users (name, city) VALUES ('李明', '北京'), ('王芳', '上海'), ('张伟', '北京'), ('赵磊', '北京');

DELIMITER //

CREATE PROCEDURE CountUsersByCity(IN city_name VARCHAR(50), OUT cnt INT)
BEGIN
  SELECT COUNT(*) INTO cnt FROM users WHERE city = city_name;
END //

DELIMITER ;

CALL CountUsersByCity('北京', @cnt);

SELECT @cnt AS beijing_users;

定义一个确定性函数并用在查询表达式里(开了 binlog 的实例必须声明 DETERMINISTIC,否则报 1418):

DELIMITER //

CREATE FUNCTION AfterTax(price DECIMAL(10,2)) RETURNS DECIMAL(10,2) DETERMINISTIC
BEGIN
  RETURN ROUND(price * 1.13, 2);
END //

DELIMITER ;

SELECT AfterTax(100.00) AS price_with_tax;

SHOW FUNCTION STATUS WHERE Db = DATABASE();

清理演示对象:

DROP PROCEDURE IF EXISTS CountUsersByCity;

DROP FUNCTION IF EXISTS AfterTax;

常见坑

  • 忘了 DELIMITER:多语句过程体里的第一个分号就让客户端提前提交定义,报语法错误。mysql 客户端和大多数 GUI 都支持 DELIMITER,但它只是客户端约定,不是服务器语法。
  • 创建函数报 1418:binlog 开启时函数必须声明 DETERMINISTIC / NO SQL / READS SQL DATA。如实声明,别图省事设 log_bin_trust_function_creators 全局放行。
  • 把业务核心逻辑塞进库里:存储过程难调试、难单测、难进代码评审,几年后没人敢动。应用系统逻辑放应用层,库里只留数据密集任务。
  • DEFINER 账号被删导致对象失效:存储过程默认以定义者权限执行,定义者账号删除后调用报错。迁移账号时留意 DEFINER,必要时用 ALTER ... DEFINER 或重建。

小结

存储过程用 CALL、参数分 IN/OUT/INOUT,函数返回值可用于表达式;多语句定义先切 DELIMITER;binlog 下函数要声明 DETERMINISTIC;业务逻辑慎入数据库。下一章看另外两种库内自动化:触发器与事件调度器。