见过太多考生,题目拿到手,存储过程的概念能说一大堆,一到填空题,CREATE填成INSERT,THEN后面忘了条件,END IF不知道在配哪个IF。我自己批试卷、做技术评审、给团队做数据库培训这些年,发现一个很扎心的事实:填空题看起来只考几个关键字,实际上考的是你对整个语法结构有没有形成肌肉记忆。“程序填空题——存储过程”这个题型,表面上是补单词,本质上是逼你把存储过程的骨架完整默写一遍。这篇文章就把这类题背后的命题逻辑、语法骨架、各数据库之间的差异,以及现场怎么自查,一次讲清楚。适合正在备考数据库课程或等级考试的在校生,也适合日常写业务SQL熟练、但没系统写过存储过程的开发同学。
1. 程序填空题的命题逻辑:存储过程到底考什么
1.1 为什么出题人偏爱填空题
选择题考“认得”,填空题考“写得出来”。人脑有个很典型的特点:看到正确答案时觉得眼熟,觉得“我肯定知道”,但让你白手写出来,立刻露馅。存储过程恰恰是被这种“眼熟错觉”坑得最狠的知识点。你经常在项目里看别人写好的存储过程,CREATE PROCEDURE、WHILE、END IF这些词都见过,可一旦试卷上把它们挖掉,你才发现自己根本不确定这个位置到底该填什么。
出题人很清楚这一点。所以存储过程类题目里,程序填空出现的频率远高于简答题和选择题。它不需要你长篇大论解释原理,只需要你在正确的位置写出正确的关键字,这对知识精确度的要求反而更高。凡是靠“大概懂”混日子的人,基本在这里原形毕露。
1.2 存储过程的五层考点地图
从阅卷和面试的反馈来看,存储过程的填空考点高度集中在五个层面上:
| 考点层级 | 具体内容 | 典型填空位置 |
|---|---|---|
| 声明层 | CREATE、PROCEDURE、OR REPLACE、DELIMITER | 过程创建语句的开头 |
| 参数层 | IN、OUT、INOUT,参数类型与顺序 | 过程名后的括号内 |
| 变量与赋值层 | DECLARE、DEFAULT、SET、SELECT INTO、:= | BEGIN后的声明区 |
| 流程控制层 | IF/ELSEIF/ELSE/END IF、WHILE/DO/END WHILE、LOOP/LEAVE、CASE/END CASE | 过程体逻辑中部 |
| 游标与异常层 | CURSOR FOR、OPEN、FETCH INTO、CLOSE、CONTINUE HANDLER、NOT FOUND | 过程体后半段 |
五层其实是递进关系:声明层不过关,后面全废;参数层决定数据怎么进怎么出;变量层决定中间状态怎么存;流程控制层决定业务逻辑怎么写;游标异常层是拉开分差的地方。应对填空题,就是把这五层分别练到“顺手就能写出来”的程度。
2. 先背骨架:MySQL声明存储过程的完整结构
2.1 一份能当模板的完整代码
别急着刷题,先看一个完整的MySQL声明存储过程的模板。填空填不对,九成是因为脑子里没有这幅完整的图:
DELIMITER $$ CREATE PROCEDURE sp_get_user( IN p_uid INT, OUT p_name VARCHAR(50) ) BEGIN DECLARE v_nick VARCHAR(50) DEFAULT 'unknown'; IF p_uid > 0 THEN SELECT nickname INTO v_nick FROM users WHERE uid = p_uid; SET p_name = v_nick; ELSE SET p_name = 'invalid'; END IF; END$$ DELIMITER ;把这个模板看熟,再去看填空题,你会发现每个空都不是孤立的。DELIMITER挖掉就是让你填,CREATE挖掉也是让你填,IN和OUT挖掉还是让你填。模板就是地图,空就是地图上的坐标。
2.2 每个关键词在填空时扮演什么角色
逐个过一遍,顺便说清楚为什么这个位置容易出题:
DELIMITER $$:MySQL客户端默认用分号切分语句,而过程体里全是分号。不改分隔符,客户端会在第一个分号处就认为语句结束,报错。所以这条几乎必考,而且答案经常被写成;或者\G这类错误值。CREATE PROCEDURE 过程名:创建动作只能是CREATE。注意有些环境支持CREATE OR REPLACE,如果在Oracle或openGauss体系里,OR REPLACE本身也是个空位。IN、OUT、INOUT:参数方向。IN是入参,OUT是出参,INOUT是既能进又能出。填空时最经典的错误就是把OUT填到入参位置。BEGIN和END:过程体的起止。MySQL要求必须成对出现,缺了谁过程都无法编译。DECLARE:声明局部变量,必须放在BEGIN之后、可执行语句之前。很多人把DECLARE和SET搞混,前者是声明,后者是赋值。- 流程控制关键词:
IF配END IF,WHILE配END WHILE,LOOP配END LOOP,各配各的,不能串。
提示:填空时养成一个习惯——每填一个关键词,立刻找它的另一半。填了
IF就在心里追问“它的END IF在哪”,填了WHILE就问“END WHILE在哪”。这种成对校验能挡掉一半低级错误。
3. 典型程序填空题的拆解与填法
3.1 入门:参数方向与声明
给出一道基础题,大家感受一下填空的节奏。题目给出一段计算两数之和的存储过程:
CREATE PROCEDURE sp_add( ___ a INT, ___ b INT, ___ result INT ) BEGIN SET result = a + b; END;三个空分别填什么?第一、第二个是入参,填IN;第三个要往外输出结果,填OUT。这道题看着简单,实际考试里失分率不低,因为很多人不看业务含义,凭感觉给三个参数全部填IN。记住判据:数据往过程里进就是IN,从过程里带出来就是OUT,既要带进又要带出才是INOUT。
再扩展一层,把参数类型也考进去。比如Oracle风格下,一个NUMBER类型的输出参数,空位可能出现在这里:
CREATE OR REPLACE PROCEDURE sp_calc( p_total ___ NUMBER )这里填OUT还是IN?看名字p_total是总金额,过程要把它算出来交给调用者,所以填OUT。参数方向在Oracle里是写在参数名和类型之间的,这和MySQL的写法位置不同,填的时候要看清题目的数据库方言。
3.2 进阶:分支与循环的闭合
基础参数过了,中等难度的题开始考流程控制。下面是一个根据分数评定等级的过程,四个空分别考分支结构:
CREATE PROCEDURE sp_grade( IN p_score INT, OUT p_level VARCHAR(10) ) BEGIN IF p_score >= 90 THEN SET p_level = '优'; ___ p_score >= 60 THEN SET p_level = '及格'; ___ SET p_level = '不及格'; ___; END;答案依次是ELSEIF、ELSE、END IF。你看这几个空的设计逻辑:第一个空后面跟着条件和THEN,只有ELSEIF能接续分支;第二个空后面直接是执行语句,说明是兜底分支,填ELSE;第三个空是整个IF语句的收尾,填END IF。
这里最容易被坑的是很多人把END IF写成END。在MySQL里,END是过程体的结尾,而END IF是分支结构的结尾,两者缺一不可。做题时可以数一数:一个过程里出现几个IF,就应该有几个END IF;如果数出来对不上,一定有一处填错了。
循环结构同理,再补一段常见的累加题:
CREATE PROCEDURE sp_sum( IN p_n INT, OUT p_total INT ) BEGIN DECLARE i INT DEFAULT 1; SET p_total = 0; WHILE i <= p_n DO SET p_total = p_total + i; SET i = i + 1; ___; END;最后一个空填END WHILE。注意,MySQL的WHILE循环闭合关键字是完整的END WHILE,不是END LOOP。如果你填了END,MySQL会把WHILE和BEGIN...END的结尾混在一起,语法直接崩。
3.3 拔高:游标与NOT FOUND
能拿高分的题往往落在游标和异常处理上。游标填空题有一个经典套路,下面这个例子是逐行读取员工工资并求和:
CREATE PROCEDURE sp_total_salary(OUT p_total DECIMAL(10,2)) BEGIN DECLARE done INT DEFAULT 0; DECLARE v_salary DECIMAL(10,2); DECLARE cur CURSOR FOR SELECT salary FROM employee; DECLARE CONTINUE HANDLER ___ NOT FOUND SET done = 1; SET p_total = 0; OPEN cur; WHILE done = 0 DO FETCH cur ___ v_salary; IF done = 0 THEN SET p_total = p_total + v_salary; END IF; END WHILE; ___ cur; END;三个空分别填FOR、INTO、CLOSE。逐个说:
DECLARE CONTINUE HANDLER FOR NOT FOUND是MySQL特有的游标结束判断写法,意思是“当游标取不到数据时,把done置为1”。这个FOR经常被写成ON或WHEN,都是错的,因为MySQL异常处理器的固定语法就是HANDLER FOR。
FETCH cur INTO v_salary是从游标当前行取值到变量里。FETCH...INTO是配套动作,很多人受SELECT...FROM的影响,在游标这里填FROM,这就是典型的知识迁移过度。
CLOSE cur是关闭游标,释放资源。漏掉CLOSE在填空题里不一定报错,但在真实生产环境里会造成资源占用,所以出题人特别喜欢把它挖成空,顺便考察你有没有资源回收意识。
4. 换个数据库答案就变:Oracle与openGauss的填空差异
4.1 Oracle的声明与赋值陷阱
存储过程的填空不是MySQL一家的事。在Oracle的PL/SQL体系里,很多写法跟MySQL完全不同,照搬MySQL的肌肉记忆会死得很惨。
Oracle声明存储过程用的是CREATE OR REPLACE PROCEDURE,比MySQL多了OR REPLACE,这是其中一个高频空位。参数方向写在参数名和类型之间:p_id IN NUMBER、p_result OUT NUMBER。变量声明不用DECLARE关键字,直接在IS或AS之后写变量名和类型。赋值不用SET,而是用:=,这是个极具辨识度的填空考点。
另外,Oracle的分支闭合关键字是ELSIF,不是MySQL的ELSEIF。这个差异非常阴险,因为两个词只差一个字母。以下这段就是典型的Oracle填空:
CREATE OR REPLACE PROCEDURE sp_check( p_score IN NUMBER, p_level OUT VARCHAR2 ) IS v_temp VARCHAR2(10); BEGIN IF p_score >= 90 THEN v_temp := '优'; ___ p_score >= 60 THEN v_temp := '及格'; ELSE v_temp := '不及格'; END IF; p_level := v_temp; END sp_check;空位填ELSIF。如果你填ELSEIF,在Oracle里直接报错。PL/SQL还要求END后面可以跟过程名,即END sp_check;,这个位置也可能被挖空。
4.2 openGauss的PL/pgSQL风格
openGauss这几年在国产数据库里出镜率很高,它的存储过程语法整体走的是PL/pgSQL路线,跟Oracle有不少相似之处,但也有自己的脾气。
openGauss的创建语句同样支持CREATE OR REPLACE PROCEDURE,参数方向写在参数名前面,比如IN p_id INT、OUT p_result TEXT,这一点和MySQL更像。过程体用AS引入,变量可以写在DECLARE区,也可以写在AS的声明区。赋值用:=,分支用ELSIF,这些跟Oracle一致。
openGauss游标处理的一个常见填空套路是用EXIT WHEN NOT FOUND来跳出循环:
CREATE OR REPLACE PROCEDURE sp_traverse() AS DECLARE cur CURSOR FOR SELECT id FROM user_tab; v_id INT; BEGIN OPEN cur; LOOP FETCH cur INTO v_id; EXIT ___ NOT FOUND; -- 处理 v_id 的逻辑 END LOOP; CLOSE cur; END; /空位填WHEN。EXIT WHEN NOT FOUND是PL/pgSQL风格的循环退出条件,问的就是你有没有见过这种写法。如果在gsql命令行环境里,过程结束后还需要一个斜杠/提交,这也可能成为填空点。很多从MySQL过来的人不知道这个/的存在,属于经验盲区。
4.3 三库对照速查表
把三个常用数据库的差异列成表,做填空之前先过一遍,比临场瞎猜强得多:
| 对比项 | MySQL | Oracle | openGauss |
|---|---|---|---|
| 创建语法 | CREATE PROCEDURE | CREATE OR REPLACE PROCEDURE | CREATE OR REPLACE PROCEDURE |
| 参数写法 | IN param INT,方向在前 | param IN NUMBER,方向在前 | IN param INT,方向在前 |
| 变量声明位置 | BEGIN内用DECLARE | IS/AS之后直接声明 | DECLARE区或AS声明区 |
| 赋值方式 | SET var = value | var := value | var := value |
| 分支关键字 | ELSEIF | ELSIF | ELSIF |
| 游标结束判断 | HANDLER FOR NOT FOUND | 异常处理或%NOTFOUND | EXIT WHEN NOT FOUND |
| 提交方式 | DELIMITER $$ | / 或 ; | / |
这张表概括了绝大多数填空差异点。读题第一步先判断“这是哪个数据库”,再决定脑内加载哪一套语法模板。数据库认错,后面全盘皆输。
5. SQLSugar调用存储过程:填空之外的实战一环
5.1 为什么要关心调用方式
很多人以为存储过程只要会写就行,实际上在真实项目里,光会CREATE PROCEDURE远远不够,还得知道怎么从代码里把它调起来。最近几年搜索热度持续走高的是“sqlsugar存储过程”,说明大量.NET开发在用SqlSugar这个ORM框架时,卡在了存储过程调用这一步。填空题考你声明,面试和项目考你调用,两个环节必须打通。
SqlSugar本身是ORM,日常增删改查走实体映射香得很,但遇到存储过程,它默认不会“猜”你要调用过程,需要显式声明。这个切换动作,就对应到调用代码里的一个关键方法。
5.2 两种常用调用写法
第一种:简单查询型存储过程,直接返回结果集,比如我们前面写的sp_get_user。在SqlSugar里这样调:
using SqlSugar; var db = new SqlSugarClient(new ConnectionConfig { ConnectionString = "server=localhost;database=test;uid=root;pwd=123456;", DbType = DbType.MySql, IsAutoCloseConnection = true }); DataTable dt = db.Ado.UseStoredProcedure().GetDataTable("sp_get_user", new { p_uid = 1001 });注意UseStoredProcedure()这个方法,它把ADO命令类型切换成存储过程。后面的匿名对象new { p_uid = 1001 },属性名必须和存储过程的入参名一致,否则SqlSugar匹配不上参数。
第二种:带输出参数的过程,比如我们那个求和的sp_sum。输出参数需要显式声明成SugarParameter并设置Direction:
var pTotal = new SugarParameter("@p_total", null, System.Data.DbType.Int32) { Direction = System.Data.ParameterDirection.Output }; db.Ado.UseStoredProcedure() .ExecuteCommand("sp_sum", new SugarParameter("@p_n", 100), pTotal); int total = Convert.ToInt32(pTotal.Value);这里有两个坑:一是@p_total的参数名要和存储过程的OUT p_total INT严格对应;二是必须指定ParameterDirection.Output,如果你不设置,它默认按入参处理,输出永远拿不到值。在Oracle或openGauss场景下,参数名的@前缀规则可能不同,SqlSugar新的DbType枚举会自动适配,但你在填空和真实代码里都要留意这个细节。
注意:SqlSugar调用存储过程时,最常报的错是“找不到参数”和“参数方向错误”。排查思路很简单——先到数据库手动
CALL一遍过程,确认过程本身没问题,再回代码里逐一对参数名和Direction。
6. 高频失分点与现场排查技巧
6.1 十个最容易填错的空
把这些年阅卷时统计出来的高频错误汇总成一张速查表:
| 空位位置 | 易错答案 | 正确答案 | 出错原因 |
|---|---|---|---|
| ___ PROCEDURE | INSERT / ALTER | CREATE | 创建动作和操作动作混淆 |
| DELIMITER ___ | ; | $$ | 不理解分隔符的作用 |
| 参数 ___ id INT | OUT | IN | 入参出参方向颠倒 |
| ___ i INT DEFAULT 0 | SET / INT | DECLARE | 声明和赋值混为一谈 |
| ___ p_score >= 60 THEN | ELSE | ELSEIF | 有条件的中间分支用错关键字 |
| 分支结尾 ___ | END | END IF | 分不清过程体结束和IF结束 |
| 循环结尾 ___ | END / END LOOP | END WHILE | 循环闭合关键字张冠李戴 |
| FETCH cur ___ v_id | FROM | INTO | 与查询取数语法混淆 |
| 结束游标 ___ cur | OPEN / DROP | CLOSE | 资源释放意识缺失 |
| HANDLER ___ NOT FOUND | IS / ON | FOR | 异常处理器语法不熟 |
每一条背后都对应一种理解偏差。比如DECLARE和SET那一行,本质是分不清“创建变量”和“改变变量值”两个阶段;FETCH...INTO那一行,本质是不知道游标取值是“把行数据放入变量”,而不是“从表里筛数据”。认准偏差根源,比死记答案有用得多。
6.2 三个现场自查办法
考试和面试现场没有数据库可以实时运行,但至少有三个不用开库就能做的检查:
第一,数配对。把过程体从头到尾扫一遍,统计IF和END IF、WHILE和END WHILE、LOOP和END LOOP、BEGIN和END的数量是否一一对应。配对数量对不上,答案里必有错。
第二,朗读一遍。填空填完后,把整段代码当作自然语言读出来。“IF分数大于等于90 THEN设优秀,ELSEIF分数大于等于60 THEN设及格,ELSE设不及格,END IF”——读起来逻辑不顺的地方,多半就是填错位置。
第三,文本替换校验。如果是在电脑上练习,可以把每个空位替换成你填的候选答案,然后搜索这个关键字在代码里出现的次数。比如END IF应该出现两次,如果只搜到一次,说明某个IF没有闭合。这个方法在复习阶段特别好用,能快速暴露系统性盲点。
平时训练时,还建议用SHOW CREATE PROCEDURE查看数据库里已有过程的完整定义,然后手动把关键字遮住,自己当出题人重新填空。这个逆向练习做上十道,你对这些空位的敏感度会明显提升。
7. 从填空到独立编写:我的训练路径
最后说点个人经验。我带过的人里,进步最快的一批,从来不是只刷填空题的,而是把填空当成“半成品改写”来练的人。具体路径分为三步,你可以照着试。
第一步,默写骨架。不参考任何资料,在编辑器里写一个最简单的存储过程:
CREATE PROCEDURE demo(IN p_id INT, OUT p_name VARCHAR(50)) BEGIN SELECT name INTO p_name FROM users WHERE uid = p_id; END;写不出来就继续看模板,写到能顺手默写为止。这一步过不了,后面的技巧都是空中楼阁。
第二步,改写变种。把刚才的IF改成CASE,把WHILE改成REPEAT或LOOP,把单游标改成双游标嵌套。每次改写都是一次完整的语法肌肉训练。等到你能同时写出MySQL和Oracle两个版本,再遇到openGauss也不慌,因为它们的差异就是一张表的距离。
第三步,设计题目。把自己当成出题人,在写好的全过程里刻意挖掉五个空,然后合上代码,凭记忆填空并核对。这个过程的本质是主动检索,比被动看十遍答案都管用。我当年备考时就是用这个方法,把存储过程常考的十几个空位练成了条件反射,考试时看到题目几乎不用思考,手自己就写出来了。
在实际生产环境里,存储过程写得好不好,差距不在会不会填几个空,而在有没有“闭合意识”和“资源意识”。BEGIN配END,CURSOR配CLOSE,变量声明配赋值,逻辑分支配条件——这些习惯养成之后,不光填空题,连线上问题排查都会顺手很多。希望这份基于长期踩坑和阅卷经验的梳理,能让你下次再看到“程序填空题——存储过程”时,心里踏实一点。