根本原因是局部变量、用户变量@、系统变量@@混用及begin end块组织不当;declare必须紧接begin后声明,嵌套块中同名变量会静默遮蔽,prepare不识别declare变量,仅支持@变量或字面量。

MySQL存储函数里变量作用域混乱,根本原因不是语法写错,而是**局部变量、用户变量 @、系统变量 @@ 三者混用 + BEGIN END 块组织不当**,导致运行时读的不是你以为的那个值。
DECLARE 变量必须紧接 BEGIN 后声明,否则直接报错
MySQL强制要求所有 DECLARE(含变量、游标、处理器)必须出现在 BEGIN 之后、任何可执行语句之前。这不是风格建议,是硬性语法限制。
-
ERROR 1337 (42000)或ERROR 1064 (42000)就是典型信号 - 错误写法:
BEGIN SELECT 1; DECLARE x INT DEFAULT 0; END;→ 先执行了SELECT,DECLARE就非法 - 正确顺序只能是:
BEGIN DECLARE x INT DEFAULT 0; SET x = 1; ... - 嵌套块也一样:每个
BEGIN ... END内部都得重走一遍这个顺序
嵌套块中同名变量会静默遮蔽,而非报错或警告
内层 BEGIN END 块里 DECLARE v INT,不会和外层同名变量冲突,而是自动遮蔽(shadow)——运行时读写的是当前块的副本,但你完全感知不到。
- 外层
DECLARE v INT DEFAULT 10;,内层DECLARE v INT DEFAULT 20; - 内层
SELECT v;输出20,外层SELECT v;输出10 - 调试时只看最终结果,极易忽略中间已被替换的事实
- 解决办法:统一加前缀,比如局部变量用
l_counter,参数用p_id,用户变量坚持用@batch_start
局部变量无法用于 PREPARE,且与 @ 变量同名时优先级陷阱极隐蔽
PREPARE 动态 SQL 不认 DECLARE 变量,只接受字面量或 @ 用户变量。更危险的是,当 DECLARE v 和 SET @v = 5 同时存在,后续 SET v = 100 修改的只是局部变量,@v 仍为 5 —— 但你可能以为它们联动。
- 错误尝试:
PREPARE stmt FROM 'SELECT * FROM t WHERE id = ?'; EXECUTE stmt USING v_id;→v_id是局部变量,不被支持 - 可行方案:先
SET @v_id = v_id;,再EXECUTE stmt USING @v_id; - 字符串拼接要用
QUOTE(v_str)防注入,且注意@sql总长受max_allowed_packet限制 - 最易踩坑点:
SELECT v := 100这种写法默认操作局部变量,不是赋值给@v
没有 BEGIN END 就没有真正的作用域隔离
缩进、注释、IF 分支、WHILE 循环,都不构成作用域边界。只有显式的 BEGIN ... END 块才能隔离变量生命周期和异常处理器。
- 两个游标共用一个
CONTINUE HANDLER FOR NOT FOUND,子游标触发后,上层的done标志也会被置 1,导致提前退出 - 正确做法:每个游标逻辑包一层独立块,如
BLOCK1: BEGIN ... BLOCK2: BEGIN ... END BLOCK2; END BLOCK1; - 命名块(如
BLOCK1:)非必需,但能避免LEAVE目标歧义,调试时更清晰 - 局部变量生命周期只到对应
END就销毁,但遮蔽效应和@变量残留常在多层嵌套 + 动态 SQL 混合时才暴露
真正难调试的从来不是单个 DECLARE 的位置,而是顺着块结构一层层画出作用域树后,才发现某处变量早被静默覆盖、某条 SET 实际没改到预期目标、某个 PREPARE 因为空值悄悄跳过了逻辑分支。











