ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

存储过程实战全解:MySQL、Oracle、openGauss差异与SQLSugar调用

存储过程实战全解:MySQL、Oracle、openGauss差异与SQLSugar调用 存储过程这个老面孔在数据库考试的程序填空题里是常驻选手在实际业务里也是处理复杂逻辑的一把好手。很多同学在填空题里能写对CREATE PROCEDURE的拼写但一碰到DELIMITER、游标、异常处理就露怯很多开发同学说会用但真让他写一个带事务、带循环、带游标的存储过程又容易卡壳。这篇就来把存储过程从头到尾拆一遍覆盖MySQL、Oracle、openGauss三种主流数据库的写法差异还会带上.NET生态里用SQLSugar调存储过程的实战姿势。不管你是准备考试、应付面试还是要上手写生产级存储过程这篇都能给你一套能直接抄的思路。1. 存储过程到底是什么——把它想象成数据库里的“预制菜生产线”1.1 存储过程的本质不是“能放代码”而是把逻辑沉淀在数据层很多人把存储过程理解成“数据库里的函数”这个说法对了一半。函数重在计算和返回存储过程重在“执行一段完整的事务逻辑”。它是一段预先编译好的、保存在数据库服务端的代码块客户端通过一个名字加一组参数就能触发它。我习惯用一个类比来解释程序填空题里让你补全的存储过程就像一条预制菜生产线。原材料是表里的数据生产线的每个工位是一条SQL语句控制流转的是IF、WHILE、CURSOR这些流程控制结构最后出货口是OUT参数或者结果集。调用方不需要关心生产线内部怎么运转只需要把原料IN参数送进去到点来取结果就行。这个设计带来的核心价值有三个第一高性能——存储过程在首次执行时会被数据库优化器编译后续执行直接走缓存计划尤其在循环调用场景下比反复发送SQL文本快得多第二低耦合——业务逻辑对客户端隐藏修改内部实现不需要改应用代码也不需要对客户端暴露表结构第三安全性可控——DBA可以只授予执行权限而不授予底层表的读写权限。不过正因为逻辑沉在数据库里它也带来了调试困难、版本管理不便、跨库迁移成本高等问题。所以现在的架构趋势是“能不放就不放”但遇到复杂报表、批量数据处理、事务一致性要求高的场景存储过程依然是不可替代的选择。1.2 考试、面试和真实业务里存储过程各占什么位置在考试的程序填空题里存储过程主要考三件事语法结构完整性、流程控制逻辑、参数与变量的使用。也就是说你不需要设计一个复杂的业务但要能准确补全DELIMITER、BEGIN、DECLARE、END IF、FETCH这些关键字并且保证逻辑正确。在面试里考察点会偏向“是否理解存储过程的边界”什么时候适合用、什么时候不能用、和事务如何配合、并发下有什么问题。在真实业务里尤其是银行、电商、ERP这类系统存储过程依然是核心交易链路里的常客Oracle存储过程、MySQL存储过程、openGauss存储过程都有大量存量代码在跑。从搜索热词能看出一个趋势——很多人在找“mysql存储过程”和“oracle存储过程”的语法对比还有“opengauss存储过程”这种国产数据库的写法。这说明存储过程不仅没有过时反而因为国产数据库迁移再次成为技能刚需。不管底层数据库怎么换存储过程的思维模型是通用的只是语法细节有差异。2. 声明语法逐行拆解——程序填空题的得分关键2.1 从一段最朴素的MySQL存储过程讲起先看一段最简单的MySQL存储过程也是程序填空题最基础的模板DELIMITER $$ CREATE PROCEDURE sp_hello(IN name VARCHAR(20)) BEGIN SELECT CONCAT(Hello, , name, !) AS greeting; END$$ DELIMITER ;这里面每行都有考点。DELIMITER是MySQL特有的、最容易在填空题里被挖掉的关键字。它的作用是把MySQL客户端默认的分号结束符临时改成别的符号否则客户端会在CREATE PROCEDURE的END处就把整个语句截断导致语法错误。CREATE PROCEDURE后面跟过程名和参数列表参数用括号包裹每个参数由“模式 参数名 数据类型”组成。BEGIN和END之间是过程体过程体里可以写多条SQL语句、变量声明、流程控制、游标等。最后一定要把DELIMITER改回分号否则后续的普通语句都会以$$结尾容易踩坑。Oracle和openGauss的存储过程结构略有不同它们没有DELIMITER这一说直接以AS或IS开始声明部分过程体用BEGIN...END;包裹。比如CREATE OR REPLACE PROCEDURE sp_hello (p_name IN VARCHAR2) IS BEGIN DBMS_OUTPUT.PUT_LINE(Hello, || p_name || !); END sp_hello; /openGauss兼容Oracle语法的存储过程同时也支持类似PL/pgSQL的写法。填空题里如果考Oracle或openGauss多半会挖IN、IS、BEGIN、END这些位置。2.2 IN、OUT、INOUT三种参数模式怎么选才不丢分参数模式是存储过程的核心考点也是新手最容易混淆的地方。MySQL的参数模式有三种IN、OUT、INOUTOracle和openGauss则写成IN、OUT、IN OUT注意中间有空格。IN模式是默认模式也是最常用的。它表示调用方传入一个值存储过程内部可以读取这个值但对它的修改不会传回给调用方。想象成你把钱放进保险箱保险箱处理完钱还是你的不会因为你存过一次它就变成别的面额。OUT模式表示存储过程内部会给这个参数赋值调用方在调用前不需要给它值调用完成后可以读取到结果。这就是“出货口”。比如你送进去一个订单号过程把统计金额写到OUT参数里返回。INOUT模式则是双向通道调用方先传入一个值存储过程内部可以读、可以改最终值传回给调用方。最典型的应用场景是做自增序号或累加器。选择题和填空题最喜欢出这种题问“存储过程内部修改后哪个参数的值能传回调用方”答案是OUT和INOUTIN不行。如果题目问“调用前必须赋初值的是哪个”答案是INOUT因为OUT调用前不需要赋值IN就算赋了也会被忽略。2.3 变量的三个层级搞不清就写不出正确的赋值逻辑存储过程里变量有三个层级局部变量、用户变量、系统变量。程序填空题里常挖的是前两种。局部变量用DECLARE声明必须放在BEGIN...END语句块的开头部分也就是所有可执行语句之前。比如DECLARE total_amount DECIMAL(10,2) DEFAULT 0; DECLARE done INT DEFAULT 0;局部变量只能在声明它的BEGIN...END块内使用块结束变量就销毁。给它赋值有两种方式SET和SELECT...INTO。用户变量用前缀比如dept_count。它不需要DECLARE直接用SET赋值即可创建生命周期是当前会话。用户变量的一个特点是可以在存储过程内部使用也可以在过程返回后继续在客户端查询。这常用于在存储过程之间传递临时值或者在调试时打印中间结果。区分这三个层级的有效记忆方法是DECLARE是“面试前的本地草稿纸”是“会议室的公共白板”系统变量则不要碰一般不用存储过程去修改。填空题里如果出现“声明变量”的关键字答案通常就是DECLARE如果出现“给变量赋值”的语句优先考虑SET。3. 核心实现机制的五个必考模块IF、CASE、循环、游标、异常3.1 条件判断IF和CASE的语法差异填空题的“送分题”与“送命题”存储过程里的IF和普通编程语言里的IF很接近但语法细节不同。MySQL的IF结构是IF 条件 THEN 语句; ELSEIF 条件 THEN 语句; ELSE 语句; END IF;注意ELSEIF是一个单词不是ELSE IF而且整个结构必须以END IF加空格加冒号结尾也就是END IF;。这两个位置是程序填空题的高频挖空点。如果写成ENDIF会直接语法报错。CASE有两个变体。第一个是简单CASE拿一个表达式和多个值比较CASE level WHEN A THEN SET score 100; WHEN B THEN SET score 80; ELSE SET score 60; END CASE;第二个是搜索CASE每个WHEN后面跟完整条件布尔表达式CASE WHEN score 90 THEN SET grade 优; WHEN score 60 THEN SET grade 及格; ELSE SET grade 不及格; END CASE;别漏掉END CASE;结尾。在Oracle和openGauss中IF的写法变成了ELSIF少一个EEND IF同样不可缺少而CASE的结束是END CASE;与MySQL一致。考Oracle时ELSIF就是一个经典的坑很多人想当然写上ELSEIF就丢了分。3.2 三种循环结构WHILE、REPEAT、LOOP什么时候用哪个MySQL存储过程支持三种循环WHILE、REPEAT、LEAVE配合LOOP。WHILE循环是“先判断后执行”循环体可能一次都不执行。语法WHILE 条件 DO 语句; END WHILE;REPEAT循环是“先执行后判断”至少执行一次。语法REPEAT 语句; UNTIL 条件 END REPEAT;LOOP则是一个无限循环必须配合LEAVE语句跳出LOOP 语句; IF 条件 THEN LEAVE loop_label; END IF; END LOOP;括号里这个loop_label就是循环标签标签必须在循环体开头声明格式是标签名加冒号比如loop_label: LOOP。程序填空题里考LOOP循环时会挖LEAVE和标签的位置。实际写复杂逻辑时我比较推荐WHILE因为它最直观不容易出现死循环。REPEAT适合“无论如何处理至少要先做一次”的场景比如遍历生成序列。LOOP则适合需要在循环体中间多条件跳出、又没有统一循环条件的场景。3.3 游标的四步走声明、打开、抓取、关闭一步都不能少游标是程序填空题的压轴题。游标本质上是一个指向查询结果集的指针让你可以一行一行地处理数据而不是一次性把整个结果集塞进内存。一个标准的游标使用流程包含四个步骤正好是填空题的四个空位-- 第一步声明游标绑定查询结果集 DECLARE cur_emp CURSOR FOR SELECT id, salary FROM employee; -- 第二步声明异常处理器用于判断数据是否遍历完 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; -- 第三步打开游标 OPEN cur_emp; -- 第四步循环抓取数据处理完关闭 FETCH cur_emp INTO v_id, v_salary; CLOSE cur_emp;关键点在于done变量的配合使用。在MySQL中当FETCH没有更多数据可抓取时会触发NOT FOUND条件如果之前声明了CONTINUE HANDLER FOR NOT FOUND就会把done变量置为TRUE。于是循环条件可以写成WHILE NOT done DO循环体内第一件事是FETCH第二件事是处理数据。这里有个非常经典的踩坑点FETCH语句和CONTINUE HANDLER的声明顺序。在MySQL里DECLARE语句必须在BEGIN块最前面游标和异常处理器也都属于DECLARE范畴所以必须放在一起声明并且游标必须声明在变量的声明之后、异常处理器的声明之前否则会报语法错误。这个顺序在程序填空题里如果挖空很容易写乱。Oracle和openGauss的游标写法也遵循类似的四步流程不过异常处理的写法变成BEGIN OPEN cur_emp; LOOP FETCH cur_emp INTO v_id, v_salary; EXIT WHEN cur_emp%NOTFOUND; END LOOP; CLOSE cur_emp; END;这里用的是游标属性%NOTFOUND来判断是否取完不再依赖CONTINUE HANDLER。两种风格都要熟因为考试和面试可能一起考。3.4 异常处理不是加分项而是生产环境的保命项MySQL的异常处理核心是DECLARE...HANDLER它有两种级别CONTINUE和EXIT。CONTINUE表示遇到异常继续执行后续代码EXIT表示遇到异常直接退出当前BEGIN块。常见写法DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SELECT 事务回滚 AS msg; END;SQLEXCEPTION是MySQL的异常类别它捕获SQL执行过程中的所有错误。也可以针对具体错误码写比如声明一个专门针对“表不存在”错误码1146的处理器。这种精细化控制在批量任务里很有用能做到“遇到某类错误就跳过遇到致命错误就停止”。Oracle和openGauss则是用EXCEPTION块EXCEPTION WHEN NO_DATA_FOUND THEN v_msg : 数据不存在; WHEN OTHERS THEN v_msg : 未知错误; END;这里OTHERS对应MySQL的SQLEXCEPTIONNO_DATA_FOUND对应“NOT FOUND”条件。在生产级存储过程里异常处理不是可有可无的装饰而是必须在编码阶段就布局好的安全网——否则一条报错就能让整个批量任务的中断状态变得不可控。4. 完整实操从建表到SQLSugar调用一条龙写一个订单统计存储过程4.1 需求设计这个存储过程要解决什么问题光讲语法不落地等于纸上谈兵。这里我设计一个贯穿始终的实战例子一个电商系统的订单表需要按部门统计订单总额并把结果保存到统计表同时返回总额给调用方。这个场景涵盖了建表、变量、游标、事务、OUT参数、异常处理可以一次性全用上。先建两张表CREATE TABLE dept_order_count ( dept_id INT PRIMARY KEY, total_amount DECIMAL(12,2) DEFAULT 0.00, stat_date DATE ); CREATE TABLE order_detail ( order_id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1 COMMENT 1正常 0取消 );order_detail是订单流水dept_order_count是统计结果表。存储过程要做的事情是遍历所有部门统计每个部门在order_detail里未取消订单的总额如果统计表里已有该部门的记录就更新没有就插入最后把所有部门的总汇总金额写到OUT参数。这个需求用一条普通的GROUP BY SQL也能做但一旦牵扯到“逐部门处理还要做UPSERT还要返回总金额给外部”用存储过程就显得更内聚。4.2 完整代码带注释的MySQL存储过程范本先不急着上完整代码要分析一个关键点为什么这里要用游标而不是直接INSERT...SELECT因为需求要求“逐个部门计算并输出中间过程”在实际的报表场景里可能每个部门还要调用独立的算法或者写日志。游标能把“遍历部门”这个流程显式化方便扩展。如果MySQL的窗口函数能完美处理就不必游标“能用集合操作优先用集合”始终是铁律。下面是完整代码也是可以直接背下来的模板DELIMITER $$ CREATE PROCEDURE sp_stat_dept_order( IN p_stat_date DATE, OUT p_total_amount DECIMAL(12,2) ) BEGIN -- 声明局部变量 DECLARE v_dept_id INT; DECLARE v_amount DECIMAL(12,2); DECLARE v_total DECIMAL(12,2) DEFAULT 0.00; DECLARE done INT DEFAULT FALSE; -- 声明游标遍历有效订单的部门 DECLARE cur_dept CURSOR FOR SELECT dept_id, SUM(amount) FROM order_detail WHERE status 1 AND DATE(create_time) p_stat_date GROUP BY dept_id; -- 声明异常处理器 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; OPEN cur_dept; REPEAT FETCH cur_dept INTO v_dept_id, v_amount; IF NOT done THEN -- 如果统计表中已有记录更新否则插入 IF EXISTS(SELECT 1 FROM dept_order_count WHERE dept_id v_dept_id) THEN UPDATE dept_order_count SET total_amount v_amount, stat_date p_stat_date WHERE dept_id v_dept_id; ELSE INSERT INTO dept_order_count(dept_id, total_amount, stat_date) VALUES(v_dept_id, v_amount, p_stat_date); END IF; SET v_total v_total v_amount; END IF; UNTIL done END REPEAT; CLOSE cur_dept; -- 结果写入OUT参数 SET p_total_amount v_total; END$$ DELIMITER ;有几个细节值得留意。第一游标查询里明确只查status1的订单把无效订单在源头过滤掉第二IF EXISTS判断后面子查询SELECT 1比SELECT *性能更好第三v_total累加逻辑放在IF NOT done之内避免done为TRUE时把重复的FETCH结果累加进去。程序填空题如果拿这个当原型挖空点会集中在DELIMITER、DECLARE、CURSOR FOR、CONTINUE HANDLER、REPEAT、UNTIL、CLOSE这些位置。你可以在心里试着把这些词遮住默写一遍就知道自己哪里薄弱了。4.3 在Navicat和命令行里调用调用的姿势也要规范存储过程创建好之后调用本身很简单。命令行里用CALL命令CALL sp_stat_dept_order(2025-01-31, total); SELECT total;第一句执行存储过程传入统计日期第二个参数传一个用户变量用来接收OUT返回值。调用完成后SELECT total;能看到总金额。Navicat里可以直接选中存储过程点击运行按钮在弹出的参数输入界面填参数值。如果你传的是一个普通字符串注意加上单引号否则会被当成列名解析报错。这是很多新手在可视化工具里的第一个坑。4.4 用SQLSugar在.NET里调用存储过程ORM不是只能查表如果项目是.NET技术栈SQLSugar是一个非常好用的ORM框架它对存储过程的支持也很完整。热词里出现“sqlsugar存储过程”说明现在Express应用里调存储过程的需求并不少。用SQLSugar调存储过程核心是SqlSugarClient的Ado属性。最常用的方式是var result db.Ado.UseStoredProcedure() .GetDataTable(sp_stat_dept_order, new SugarParameter(p_stat_date, 2025-01-31), new SugarParameter(p_total_amount, null, System.Data.DbType.Decimal, ParameterDirection.Output)); decimal total result.Rows[0][0] DBNull.Value ? 0 : Convert.ToDecimal(result.Rows[0][0]);如果你更关心OUT参数用UseStoredProcedure配合GetDataTable时可以把输出参数一并传递返回的表数据是过程里最后SELECT出来的结果集。关于OUT参数注意SugarParameter的构造重载第四个参数是ParameterDirection.Output这样调用完成后同一个SugarParameter对象的Value属性里就会有输出值。如果你要的是OUT值而非结果集更清晰的做法是var outParam new SugarParameter(p_total_amount, null, System.Data.DbType.Decimal, ParameterDirection.Output); db.Ado.UseStoredProcedure().ExecuteCommand(sp_stat_dept_order, new SugarParameter(p_stat_date, 2025-01-31), outParam); Console.WriteLine(outParam.Value);建议在项目里统一封装一个StoredProcedureHelper把参数构造、异常处理、日志输出都放到公共方法里避免每个业务点都重复写参数代码。调用完成后务必判断outParam.Value是否为DBNull因为存储过程里没给OUT赋值时客户端收到的值会是DBNull直接转decimal会抛异常。5. 常见报错与踩坑实录——每一个都是真金白银换来的教训5.1 DELIMITER不生效的几种情况第一个坑是生命周期问题。DELIMITER只在当前会话里生效如果你断开连接重连必须再设置一次。用Navicat或DataGrip时如果创建存储过程总是报错先检查编辑器底部的会话是否支持多语句执行有些图形工具默认就把DELIMITER当成普通语句发给服务端需要关闭“将查询用分号分隔”这类选项。第二个坑是最容易忽略的DELIMITER后面必须跟空格。写成DELIMITER$$和DELIMITER $$在多数情况下都能用但老版本MySQL里如果没空格分隔符解析可能出错。更稳妥的写法是DELIMITER $$后面留一个空格。第三个坑是无意中把DELIMITER写进了存储过程内部。分隔符只是客户端指令不是数据库服务端语法所以千万别把DELIMITER写在BEGIN...END块内部那会直接报语法错误。5.2 参数名和列名冲突这是一个隐形的逻辑炸弹很多初学者会给参数起名叫dept_id而表里正好也有dept_id列。这时候在WHERE子句里写WHERE dept_id dept_id数据库会怎么理解MySQL在这种情况下会把两个dept_id都当作列名来解析结果就是条件恒为真查询结果是全表数据。更隐蔽的情况是在SET语句里写SET dept_id dept_id本意是想把参数值赋给变量结果两边都指向列名根本没有赋值效果。避免办法只有两条一是参数命名统一加前缀比如p_dept_id、v_dept_id做区分二是在存储过程开头就养成习惯任何参数和变量都不与列名同名。Oracle和openGauss开发者也有个习惯就是在参数前加p_前缀变量加v_前缀这不是什么编码洁癖而是实打实规避二义性的做法。MySQL同样适用。5.3 游标和循环的性能陷阱游标是逐行处理性能天然不如集合操作。在数据量大的表上跑游标如果每行都执行一个INSERT或UPDATE大量行锁、日志和网络往返很快会把数据库拖垮。建议的做法是能用JOIN、窗口函数、CASE WHEN聚合解决的就坚决不用游标必须用游标时在游标查询里就把条件收紧尽量只捞必要字段。另外在循环里做UPDATE前先仔细检查WHERE条件是否命中索引否则每行一个全表扫描直接变成生产事故。OPEN到CLOSE之间的游标会占用临时表空间用完务必CLOSE释放。程序填空题里的CLOSE就是这个作用它不是形式而是资源管理的一部分。5.4 权限问题创建和执行不是一回事存储过程创建后调用方不一定有权限执行。MySQL里需要单独授权GRANT EXECUTE ON PROCEDURE demo_db.sp_stat_dept_order TO app_userlocalhost;很多开发者在本地用root写好了存储过程部署到测试环境直接报“PROCEDURE sp_stat_dept_order does not exist”排查半天发现其实是账号没有EXECUTE权限导致的不是过程不存在。权限设计上还有个专业建议存储过程内部读哪些表授权时不需要把这些表的SELECT权限也授给调用方。因为存储过程默认以创建者权限执行或明确使用SQL SECURITY INVOKER来按调用者权限。这是存储过程在安全层面的优势也是设计存储过程时一定要考虑清晰的边界。说到我的经验最值得记住的一点是存储过程的代码风格必须比普通SQL更保守每一步都要考虑异常退出后的状态该回滚就回滚该关游标就关游标。程序填空题只考你会不会写而生产环境会考你能不能善后。
返回列表