资讯动态

MySQL存储过程实战指南:从基础语法到性能优化

发布时间:2026/10/1 3:57:39 来源:尧图企业网站定制
1. 存储过程到底解决什么问题一个懒DBA的真实诉求我最早接触存储过程印象并不好。大学时Oracle数据库课上老师用PL/SQL写了一大段包嵌套游标加异常处理看着像天书。后来工作中真刀真枪处理MySQL线上数据才发现存储过程这种东西能不能用好直接决定了你凌晨三点被电话叫醒的次数。先说个最常见的场景每个月一号业务表里的流水要滚动归档。如果你在公司里做这件事有两种选择。第一种写个定时脚本在应用服务器上跑连数据库执行几条INSERT INTO ... SELECT再DELETE。第二种把整个逻辑写进一个存储过程让数据库调度器或者外部任务系统只调用一个CALL。我强烈建议你选第二种理由特别朴素脚本可能被运维清理、服务器可能迁移、环境变量可能丢失但只要数据库还在存储过程就在。而且存储过程把数据操作封闭在数据这一层应用侧只负责触发权限控制也干净——业务账号只需要EXECUTE权限根本不用碰底层表结构。再一个场景多个应用系统需要执行同一套业务规则。比如电商订单状态流转无论你从订单系统、售后系统还是管理后台触发最终落库的判定逻辑必须完全一致。如果每个系统各写各的SQL早晚会出现一个系统判断通过、另一个系统拒绝的冲突。把规则沉淀成存储过程相当于把唯一事实源放在了数据库里所有入口都走同一段代码逻辑分裂的问题从根上消除。但我也必须泼一盆冷水存储过程不是银弹。MySQL的存储过程相比Oracle、SQL Server能力上弱一截比如没有原生包机制、调试手段有限、优化器对存储过程内部的复杂逻辑可能做不出很好的执行计划。所以我的判断标准很简单——第一逻辑涉及多步数据操作且必须保证顺序和安全第二同一规则要被多个入口复用第三数据量可控单次调用处理的数据规模不要到百万级以上还指望存储过程快速跑完。前两条满足就值得用第三条不满足时先想想是不是该用ETL工具而不是存储过程。这篇学习笔记我打算从真实工程视角把MySQL存储过程完整过一遍语法、变量、流程控制、游标、异常处理、调试技巧、实战案例。我建议你别跳着看因为后面很多坑都是前面语法细节埋下的。2. 从零搭起一个可运行的存储过程声明、赋值、参数与CALL2.1 最简结构DELIMITER与CREATE PROCEDURE先明确一个概念存储过程就是一段预编译的SQL命令集合它存储在数据库服务端名字唯一调用时由服务端执行。MySQL 8.0默认的存储引擎InnoDB支持事务所以存储过程里可以放心使用事务控制这是很多老教程没提到的——InnoDB已经是默认引擎不必再像MyISAM年代那样担心事务无效。看一个最简单的完整示例-- 先把结束符从分号临时改成别的否则MySQL客户端会把存储过程体内的分号当作整个语句的结束 DELIMITER // CREATE PROCEDURE sp_hello() BEGIN SELECT Hello, MySQL Stored Procedure! AS msg; END // DELIMITER ;这里有个关键点必须讲透为什么非要改DELIMITER因为MySQL客户端解析SQL语句时按分号断句。CREATE PROCEDURE的BEGIN...END内部自带多个分号如果不把结束符临时改成//客户端看到第一行内部的分号就提前发送语句了服务端收到的就是不完整的CREATE指令必然报语法错误。DELIMITER这条命令是客户端指令不是SQL语法只在客户端会话里生效。你在Navicat这类图形工具里写存储过程时工具往往自动处理了但在命令行mysql客户端里必须自己加。创建成功后调用用CALLCALL sp_hello();如果要删除用DROP PROCEDURE IF EXISTS sp_hello;。我习惯在每次创建前先执行一次DROP IF EXISTS方便反复调试但你如果用版本管理工具管理这些脚本就别在生产库上这么干否则同事会顺着网线来找你。2.2 参数三兄弟IN、OUT与INOUT存储过程参数有三种模式这是新手最容易混淆的地方参数类型语义使用场景IN传入参数过程内只读不能修改最终传回条件类输入如订单ID、时间范围OUT输出参数过程内赋值调用方获取结果返回状态码、结果行数、错误信息INOUT既能传入也能被过程修改后返回需要原地更新的变量如累计值、状态机流转写个例子说明OUT的用法DELIMITER // CREATE PROCEDURE sp_count_users_by_status(IN p_status TINYINT, OUT p_count INT) BEGIN SELECT COUNT(*) INTO p_count FROM users WHERE status p_status; END // DELIMITER ; -- 调用方先声明一个自定义变量再用CALL触发最后SELECT出来看 SET cnt 0; CALL sp_count_users_by_status(1, cnt); SELECT cnt;注意细节OUT参数在过程体内不能当输入用MySQL对OUT读取可能返回NULL如果既要读又要写就用INOUT。调用方的接收变量要加前缀这个var是会话级用户自定义变量和存储过程内部的DECLARE变量不是一回事。2.3 变量体系DECLARE局部变量与SET赋值存储过程内部的变量必须先用DECLARE声明而且DECLARE必须放在BEGIN块的最开头不能穿插在语句中间。这是MySQL的硬性规定好多人第一次写就栽在这——先写个SELECT再想声明一个变量来存结果直接报语法错误。DELIMITER // CREATE PROCEDURE sp_demo_vars() BEGIN DECLARE v_total INT DEFAULT 0; DECLARE v_mid INT; SET v_mid 100; SET v_total v_mid 5; SELECT v_total AS total_value; END // DELIMITER ;声明时可以用DEFAULT给初值不给定默认值则初始为NULL。变量名建议带v_前缀和列名区分。我当时接手的一个老项目里有人声明了一个变量叫status表里也有个status列结果SELECT status时MySQL的解析规则把列名覆盖了变量名查出来的值完全不对。最佳实践是变量名一律v_开头永远不要和列名重名。赋值方式两种SET和SELECT INTO。SELECT INTO从结果集中取单行单列必须保证只返回一行否则报错。日常写法是SELECT COUNT(*) INTO v_cnt FROM orders WHERE created_at v_start;这里还有个隐式坑如果SELECT INTO查不到任何数据变量不会被赋值保留原值。这和Oracle的NO_DATA_FOUND异常行为不同MySQL不报错。所以判断是否查到不能看变量是不是变了要依赖接下来要讲的异常处理或先COUNT一次。3. 存储过程的灵魂部分条件判断、循环、游标与异常处理3.1 IF与CASE让SQL学会做决定存储过程和普通SQL批处理最大的区别就在于控制流。IF语句语法如下IF v_score 90 THEN SET v_level A; ELSEIF v_score 60 THEN SET v_level B; ELSE SET v_level C; END IF;注意这里是ELSEIF不是ELSIF也不是ELSE IF连在一起写。另一个更紧凑的判断方式是CASESET v_level CASE WHEN v_score 90 THEN A WHEN v_score 60 THEN B ELSE C END;CASE在MySQL里两种用法一种是上面这种表达式形式的CASE可以嵌进SET、SELECT里另一种是语句形式的CASE WHEN ... THEN ... END CASE用在过程体内控制流程。实话说我90%的场景只用IF只有计算字段时才用CASE表达式代码可读性好得多。3.2 LOOP、WHILE与REPEAT三种循环的取舍循环是存储过程的招牌。我个人经验事务型业务逻辑里循环出现频率最高的是逐行处理比如把一张表的每一行改完后插入另外的表。三种循环常用的是WHILE和REPEATLOOP比较底层。-- WHILE先判断后执行 WHILE v_i 10 DO SET v_i v_i 1; END WHILE; -- REPEAT先执行后判断至少执行一次 REPEAT SET v_i v_i - 1; UNTIL v_i 0 END REPEAT; -- LOOP需要借助LEAVE手动退出 SET v_i 0; loop_label: LOOP SET v_i v_i 1; IF v_i 10 THEN LEAVE loop_label; END IF; END LOOP loop_label;注意REPEAT的UNTIL后面没有分号而且条件成立才退出。LEAVE相当于其他语言里的breakITERATE相当于continue必须配合循环标签使用。循环写多了容易踩死循环我的防御性习惯是每次循环体里加一个最大次数计数器超过预期值就LEAVE宁可报错也不能让存储过程跑死。3.3 游标逐行处理数据的正确方式与声明顺序陷阱游标本质是从结果集中一行一行取数据的机制。MySQL的游标有三个受限制的特性只进、只读、不支持滚动。也就是说你不能往前翻记录也不能修改当前游标指向的数据。如果想边遍历边更新常见做法是把要改的主键先读进临时表再基于主键去做UPDATE。一个典型的游标加循环处理示例DELIMITER // CREATE PROCEDURE sp_process_cursor() BEGIN -- 声明必须在最前面 DECLARE v_id INT; DECLARE v_name VARCHAR(50); DECLARE v_done INT DEFAULT 0; -- 游标必须声明在变量之后 DECLARE cur_emp CURSOR FOR SELECT id, name FROM employees WHERE status 1; -- 声明NOT FOUND处理器必须在游标声明之后 DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done 1; OPEN cur_emp; read_loop: LOOP FETCH cur_emp INTO v_id, v_name; IF v_done THEN LEAVE read_loop; END IF; -- 这里做业务处理 UPDATE employees SET last_check_at NOW() WHERE id v_id; END LOOP; CLOSE cur_emp; END // DELIMITER ;声明顺序是个大坑MySQL要求DECLARE变量必须先于游标游标必须先于HANDLER。顺序错了直接报You have an error in your SQL syntax这类让人摸不着头脑的错误。更合理的解释是HANDLER依赖游标游标依赖变量所以它们必须按这个依赖链声明。我第一次写反了顺序整整排查了二十分钟才弄清是声明顺序问题。CONTIUNE HANDLER FOR NOT FOUND的实际作用当FETCH读到结果集末尾时MySQL会触发NOT FOUND条件执行SET v_done 1。注意这个处理器的作用域是整个存储过程块一旦触发本次调用的后续任何找不到数据的情况都会被这个处理器拦截。所以如果你在循环之后还有一条SELECT INTO想判断是否存在而之前游标已经触发过NOT FOUNDv_done可能还是1逻辑就会出错。解决办法是循环结束后把v_done手动重置为0。3.4 异常处理捕获SQLException并优雅回滚真实线上环境里存储过程绝不能裸奔。我见过最离谱的情况一段存储过程执行到一半报错前面成功插入的数据没有回滚导致业务数据出现半成品状态。原因就是没加异常处理。DELIMITER // CREATE PROCEDURE sp_transfer_money(IN p_from_acct INT, IN p_to_acct INT, IN p_amount DECIMAL(10,2)) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; UPDATE accounts SET balance balance - p_amount WHERE acct_id p_from_acct; UPDATE accounts SET balance balance p_amount WHERE acct_id p_to_acct; COMMIT; END // DELIMITER ;这个例子把核心思想说清楚了用DECLARE EXIT HANDLER FOR SQLEXCEPTION捕获所有SQL异常异常发生时回滚事务再通过RESIGNAL把原始错误信息重新抛出给调用方。RESIGNAL是MySQL 5.5之后才有的语法它保留原始错误码和错误消息这样应用层能拿到准确的报错信息而不是被存储过程吞掉变成调用失败。异常处理分三类条件处理类型触发时机行为EXIT HANDLER条件触发后立即退出当前BEGIN块CONTINUE HANDLER条件触发后继续执行后续语句UNDO HANDLER条件触发后回滚到BEGIN前MySQL不支持别用SQLEXCEPTION捕获所有SQL异常。还有更精确的写法比如捕获指定错误码或SQLSTATE比如常见的1062唯一键冲突可以单独处理。具体是写EXIT还是CONTINUE取决于业务语义需要取消后续操作就EXIT需要跳过坏数据继续循环就CONTINUE。3.5 条件处理器声明的嵌套与作用域细节有人会问存储过程里多个游标并排时多个NOT FOUND handler会不会冲突MySQL的规则是HANDLER按声明的先后顺序匹配一旦某个条件被某个HANDLER捕获本次触发就算处理完了不会继续匹配后面的HANDLER。所以在多个游标共用时我给每个游标准备一个独立的BEGIN...END块每个块里各自声明自己的游标和NOT FOUND handler隔离性最好。CREATE PROCEDURE sp_multi_cursor() BEGIN -- 第一个游标独立块 BEGIN DECLARE v_done1 INT DEFAULT 0; DECLARE cur1 CURSOR FOR SELECT id FROM table_a; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done1 1; OPEN cur1; read_loop1: LOOP FETCH cur1 INTO id_a; IF v_done1 THEN LEAVE read_loop1; END IF; END LOOP; CLOSE cur1; END; -- 第二个游标独立块 BEGIN DECLARE v_done2 INT DEFAULT 0; DECLARE cur2 CURSOR FOR SELECT id FROM table_b; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done2 1; OPEN cur2; read_loop2: LOOP FETCH cur2 INTO id_b; IF v_done2 THEN LEAVE read_loop2; END IF; END LOOP; CLOSE cur2; END; END;这个写法能避免两个NOT FOUND handler互相污染。每次块退出时各自的handler状态也就自动失效了干净利落。4. 调试才是真正的分水岭让存储过程把话说清楚4.1 就地取材的日志表谁说MySQL存储过程不能打日志MySQL存储过程没有内置的调试日志机制不像Java有Log4j。但我们可以自己造一个。我的惯用方案是建一张日志表在过程关键节点INSERT日志行过程结束后再SELECT出来看。CREATE TABLE proc_log ( id INT AUTO_INCREMENT PRIMARY KEY, proc_name VARCHAR(64), log_level VARCHAR(10), log_time DATETIME DEFAULT CURRENT_TIMESTAMP, message VARCHAR(255) );然后在存储过程里插入日志INSERT INTO proc_log(proc_name, log_level, message) VALUES (sp_demo, INFO, CONCAT(当前变量v_id, v_id));关键节点打上日志出问题一眼就能看出跑到哪一步了。我一直觉得这是MySQL存储过程调试最实用的手段比任何第三方工具都可靠。有时需要用SELECT直接输出中间值我会在开发阶段临时保留几行SELECT v_id;验证完再删掉。注意如果存储过程末尾还有一个最终的SELECT结果集过程中间有临时SELECT那你的结果集顺序会乱——比如CALL时Navicat里出现两个结果集。所以带返回结果集的存储过程调试SELECT记得临时注释掉。4.2 GET DIAGNOSTICS官方提供的错误信息获取除了RESIGNALMySQL还提供了GET DIAGNOSTICS语句可以获取详细错误上下文。这个语法知道的人不多但排查复杂错误时极好用。DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN GET DIAGNOSTICS CONDITION 1 err_no MYSQL_ERRNO, err_msg MESSAGE_TEXT; SELECT err_no, err_msg; ROLLBACK; END;这条语句把当前异常的错误码和错误消息取到会话变量里你可以写进日志表也可以直接SELECT出来。相比RESIGNALGET DIAGNOSTICS更适合你自己记录错误而把控制权留在存储过程内比如决定继续还是回滚。我的经验调试阶段用GET DIAGNOSTICS配上日志表线上稳定后只保留RESIGNAL或直接让EXIT HANDLER处理性能更干净。4.3 权限检查与创建时的常见报错清单存储过程创建失败很大概率是权限问题。MySQL 8.0默认不允许普通用户创建存储过程需要显式授权GRANT CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON db_name.* TO app_user%;如果只是调用别人写的存储过程至少要有EXECUTE权限。这里提醒一句如果你用root建的存储过程但业务账号没有EXECUTE权限调用时会报PROCEDURE X does not exist而不是permission denied很容易误导人。另一个常见报错是Prepared statement needs to be re-prepared这个在存储过程里偶尔出现通常和表结构变更有关——存储过程内部SQL引用了某张表而你在创建后被改了结构。原因是表定义版本号变了服务端预编译缓存失效。解决办法就是删掉存储过程重新创建。别问为什么我遇到过两次一次是同事改字段长度一次是跑了个ALTER TABLE加索引症状一模一样。5. 两个实战场景把知识点串起来分页查询与批量归档5.1 封装分页返回数据集与总行数一次搞定很多老的Java Web项目分页就是复制粘贴一段SQL每个Mapper写一遍LIMIT和COUNT。用存储过程可以把这逻辑统一收口。要求是既要返回当前页数据又要返回总条数。MySQL存储过程可以返回结果集同时用OUT参数把总条数捎出来。DELIMITER // CREATE PROCEDURE sp_page_query( IN p_table_name VARCHAR(64), -- 表名注意不能直接拼接表名做动态SQL这里仅示意 IN p_page_no INT, IN p_page_size INT, OUT p_total INT ) BEGIN DECLARE v_offset INT; SET v_offset (p_page_no - 1) * p_page_size; SELECT COUNT(*) INTO p_total FROM orders; SELECT order_id, order_no, total_amount, created_at FROM orders ORDER BY created_at DESC LIMIT v_offset, p_page_size; END // DELIMITER ;这里有个令很多人困惑的点为什么表名不能用变量因为MySQL存储过程里表名、列名这类标识符不能直接用参数替代只有值可以。真要做动态表名得用PREPARE/EXECUTE拼SQL但拼接SQL有注入风险和注释bug风险我强烈建议业务上别这么干。字段名和表名在存储过程层面必须是静态的如果你一定要做通用分页器考虑用视图或者代码层拼接。LIMIT的两种写法值得记一下LIMIT v_offset, p_page_size; -- 旧式两个参数 LIMIT p_page_size OFFSET v_offset; -- 可读性更好两者的执行计划没有区别后一种读起来清楚是先偏移再取数。若p_page_no从1开始v_offset正确。注意前端传来页码为0或者负数时这里v_offset可能为负MySQL会报错。我在调用层做了SQL_MODE校验存储过程里也做防御IF p_page_no 1 OR p_page_size 1 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT page_no and page_size must be positive; END IF;SIGNAL是手动抛出异常SQLSTATE 45000是用户自定义错误专用状态码。这在参数校验场景下非常管用你可以让存储过程主动报错而不是返回一个莫名字段给应用层。5.2 批量归档事务控制下的旧数据搬家假设订单表三个月前的数据要搬到orders_history保证单次调用可重复、可追溯、不出半成品。完整实现如下DELIMITER // CREATE PROCEDURE sp_archive_old_orders(IN p_before_date DATE, IN p_batch_size INT) BEGIN DECLARE v_done INT DEFAULT 0; DECLARE v_batch_count INT DEFAULT 0; DECLARE cur_old CURSOR FOR SELECT order_id FROM orders WHERE created_at p_before_date LIMIT p_batch_size; DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done 1; START TRANSACTION; OPEN cur_old; archive_loop: LOOP FETCH cur_old INTO order_id; IF v_done THEN LEAVE archive_loop; END IF; INSERT INTO orders_history(order_id, order_no, total_amount, created_at, archived_at) SELECT order_id, order_no, total_amount, created_at, NOW() FROM orders WHERE order_id order_id; DELETE FROM orders WHERE order_id order_id; SET v_batch_count v_batch_count 1; END LOOP; CLOSE cur_old; COMMIT; SELECT v_batch_count AS archived_count; END // DELIMITER ;这段代码包含了本篇文章的所有核心知识点变量声明、游标、NOT FOUND handler、循环、事务控制、INSERT INTO ... SELECT、输出结果。执行完如果报错EXIT HANDLER会回滚整个事务归档和删除同时回滚不会出现复制了但没有删掉或者删掉了却没复制的数据不一致状态。注意一个微妙设计我是先INSERT再DELETE而不是直接INSERT INTO ... SELECT加DELETE连写。为什么不写成一句INSERT加一次DELETE把全部数据搬走因为要先试insert成功再delete对应一行逐行操作可以把异常范围控制在单行级别。如果第100行insert失败前99行归档数据全部回滚业务是安全的不变。如果你用一条INSERT ... SELECT全量数据再一条DELETE任何一个步骤失败工作量大很多排查也麻烦。LIMIT p_batch_size放在游标SQL里就能控制每次调用最多处理多少行避免一次性锁太多行拖垮线上。实践里让这个归档过程配合数据库调度器每天凌晨跑一次每次处理5000行线上几乎无感。把批处理控制在一个可控范围比一次性闷头执行完更稳妥。5.3 动态SQL的正确打开方式PREPARE与EXECUTE刚才说了动态SQL有风险但有些场景确实没法静态写SQL。比如统计报表的字段列是动态的或者每天要校验两张结构一致但名称不同的表。MySQL存储过程支持PREPARE/EXECUTE/DEALLOCATE三步走DELIMITER // CREATE PROCEDURE sp_dynamic_query(IN p_start_date DATE, IN p_end_date DATE) BEGIN SET sql CONCAT( SELECT order_id, total_amount FROM orders , WHERE created_at BETWEEN ? AND ? ORDER BY created_at DESC ); PREPARE stmt FROM sql; EXECUTE stmt USING p_start_date, p_end_date; DEALLOCATE PREPARE stmt; END // DELIMITER ;PREPARE的优点动态字符串拼装时可以用?占位符代替值避免注入风险表名和列名不能用占位符必须拼字符串。我自己的原则值全部用占位符表名列名如果确实要动态拼白名单校验后再拼。比如IF p_table NOT IN (orders, orders_history, users) THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT invalid table name; END IF;白名单校验后用CONCAT拼进去把风险挡在入口处。显然PREPARE/EXECUTE在MySQL存储过程里算高级操作新手阶段可以理解原理但建议先把静态SQL写成习惯动态操作确实能不用就不用。遇到复杂动态报表我更倾向把条件放SQL里用复杂CASE配合动态排序把动态范围压缩到极限这样既保留灵活性又可维护。6. 性能与可维护性存储过程的边界意识和工程习惯讲了大半篇语法最后这部分是我实际项目里踩过坑之后总结出的几条活下来的法则每条都是血泪教训换来的。6.1 存储过程内的SQL同样要优化执行计划很多人误以为把SQL包在存储过程里就不用关心执行计划了。大错特错。存储过程内的每一行SQL依然要单独做优化索引有没有命中、是不是全表扫描、排序是否走文件这些优化手段全部照常适用。MySQL存储过程没有Oracle那样的包级缓存机制不会替你自动优化复杂流程。我之前优化过一个慢存储过程把单条SQL拆开后发现问题出在WHERE条件的写法上-- 慢函数套在字段上导致索引失效 WHERE DATE(created_at) CURDATE(); -- 快范围查询走索引 WHERE created_at CURDATE() AND created_at DATE_ADD(CURDATE(), INTERVAL 1 DAY);把全表扫改成索引范围扫过程整体从8秒钟降到0.2秒效果立竿见影。所以存储过程优化第一原则过程是代码组织方式SQL本身才是性能核心。6.2 避免长事务与锁范围失控存储过程启动事务后如果内部的SELECT带锁如FOR UPDATE锁会一直维持到COMMIT或者ROLLBACK。如果存储过程里循环几万行每行都锁住直到最后才提交那这些行在事务期间全都被锁应用侧走并发更新时会大量阻塞严重的直接拖垮业务。解决思路很简单分片提交。比如归档5000行数据不要一次开一个大事务而是每500行提交一次。虽然失去了整体原子性但每个片内部还是完整的而且锁时间大大缩短。这个取舍要结合业务容忍度来定。我的经验是数据归档、日志清理这类批量操作的原子性要求不高完全可以分片提交涉及金额、库存等强一致场景必须整体事务宁可锁时间稍长也不能半截状态。6.3 代码管理视角版本化、注释与命名规范存储过程也是一种代码以文本方式存在数据库里如果完全没有版本管理生产库的存储过程出了问题根本没法追溯是谁改的、为什么改。我的建议是所有存储过程脚本纳入Git管理目录按项目分层脚本头部写清楚功能描述、作者、创建日期、修改历史命名规范化模块前缀业务动作比如sp_orders_archive、sp_orders_query_by_status创建脚本用IF NOT EXISTS或先DROP再CREATE方便幂等重建。我见过生产库里面出现sp_1、sp_aa、proc_rod这种名字维护起来真的是灾难。别嫌这些规范琐碎等你要在凌晨三点快速定位一个存储过程时你会感谢当年写规范的自己。6.4 监控与告警存储过程跑挂了如何感知最后补充一点运维视角。存储过程如果由MySQL自带的EVENT调度器执行出错了默认只写错误日志应用侧完全没有感知。我上线归档任务时刻意在存储过程末尾往一张任务执行记录表写一行状态包括开始时间、影响行数、结束状态。调度器每次跑完应用侧报表查一下这张表就知道成功与否。如果连续三天没有记录监控系统自动告警。CREATE TABLE proc_run_log ( id INT AUTO_INCREMENT PRIMARY KEY, proc_name VARCHAR(64), status VARCHAR(20), message VARCHAR(255), affect_rows INT DEFAULT 0, start_time DATETIME, end_time DATETIME DEFAULT CURRENT_TIMESTAMP );在存储过程入口INSERT一条起始记录并拿到自增ID结束时UPDATE状态为SUCCESS异常时在EXIT HANDLER里UPDATE为FAILED并写入MESSAGE_TEXT。这样即使存储过程本身挂了日志表里留下的FAILED状态也是线索。运维省心程度完全不一样。MySQL存储过程这道题学起来其实不难难的是把它用对场合。语法只是表面背后是把数据规则沉淀在最合适的位置这种设计思想。如果你手头的项目里SQL逻辑散落在各个业务系统里多套规则对同一份数据各说各话那确实该考虑用存储过程收口了如果只是为了在数据库里炫技那我劝你老老实实写应用的Service层。工具没有高低用得合适才是真正的工程师。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑