在MySQL的语法体系里,@符号承担着变量声明与引用的职责,但它的形态并不单一:一个@开头的写法指向用户变量,两个@@开头的写法指向系统变量,而在存储过程内部用DECLARE声明的局部变量却完全不带任何前缀。三套变量各有各的作用域和生命周期,混用或者误用往往会埋下难以排查的逻辑错误。本文把@符号相关的知识点拆开揉碎,从基本语义、赋值方式、系统变量、存储程序限制到实战技巧逐一展开。

一、@符号的基本语义:会话级用户变量
在MySQL中,以一个@符号开头的变量叫做用户变量。它最重要的特性是会话隔离:你在某个客户端连接里创建的用户变量,只在这个连接内可见,其他连接既看不到也改不了,连接断开时变量随会话一起销毁。这决定了它的定位是存放临时计算结果,而不是做跨连接的数据共享。
用户变量还是弱类型的。声明时不需要指定数据类型,第一次赋值时MySQL根据值自动推断类型,后续赋值甚至可以换成另一种类型。变量名允许由字母、数字、下划线、点号和美元符号组成,赋值用SET语句完成,等号和冒号等号两种操作符都合法。
-- 赋值即声明,无需提前定义类型 SET @name = '张三'; SET @age := 28; -- 也可以把表达式结果赋给变量 SET @next_id = 100 + 1; -- 直接查看变量值 SELECT @name, @age, @next_id;
需要提醒的是,用户变量并不属于SQL标准,它是MySQL的扩展语法。如果项目未来可能迁移到PostgreSQL、Oracle等其他数据库,这部分写法需要提前规划替代方案,比如改用临时表或WITH子句来承载中间结果。
二、两种赋值方式:SET与SELECT INTO的差异
用户变量的赋值途径主要有两条。第一条是用SET语句直接赋常量或表达式,最直观也最常用;第二条是用SELECT ... INTO把查询结果装进变量,适合从表里取值参与后续计算,比如把某个员工的工资取出来做奖金换算。
-- 方式一:SET 直接赋值 SET @base = 8000; -- 方式二:从查询结果取值 SELECT MAX(salary) INTO @max_salary FROM employee; SELECT salary INTO @base FROM employee WHERE emp_id = 101; -- 查询结果为空时变量保持原值,返回多行时会直接报错
这里有一个高频踩坑点:在SELECT语句的查询列表里给变量赋值时,必须用冒号等号而不能用等号。因为SELECT语境下的等号是比较运算符,写成@rownum = @rownum + 1会被当成布尔比较表达式,返回0或1而不是完成赋值。冒号等号才是SELECT语句里唯一的赋值操作符,这也是老版本MySQL实现行号的经典写法。
-- 经典行号写法:先在派生表里初始化变量 SELECT @rownum := @rownum + 1 AS seq, name, salary FROM employee, (SELECT @rownum := 0) AS t ORDER BY salary DESC; -- 注意:这里如果误写成 @rownum = @rownum + 1, -- 整个表达式会变成比较运算,结果完全不对
SELECT INTO还有一条隐含规则:查询必须且只能返回一行。结果为空时变量保持原值并伴随警告,返回多行则直接报错。使用前最好确认过滤条件能锁定唯一记录,或者干脆用MAX、MIN等聚合函数包一层,保证结果集恒为一行。
三、@@双符号:系统变量的读取与修改
两个@@开头的是系统变量,它和用户变量完全是两码事。系统变量由MySQL服务器自身维护,控制着服务器的运行行为,比如最大连接数、自动提交开关、排序缓冲区大小等。每个系统变量都存在全局级别和会话级别两个值:全局值影响整个服务器实例,会话值只影响当前连接,且会话值在连接建立时从全局值复制而来。
-- 读取全局级别的最大连接数 SELECT @@global.max_connections; -- 读取会话级别的自动提交开关,@@session. 前缀可以省略 SELECT @@session.autocommit; SELECT @@autocommit; -- 修改全局变量,需要较高权限 SET GLOBAL max_connections = 500; -- 只修改当前会话的行为 SET SESSION sql_mode = 'STRICT_TRANS_TABLES'; -- 排查问题时最常用的查看方式 SHOW VARIABLES LIKE 'max_connections'; SHOW VARIABLES LIKE 'character%';
修改系统变量有一个必须记住的细节:SET GLOBAL只对当前运行的服务器实例生效,重启后会回退到配置文件里的设定值。想让修改永久生效,必须把参数同步写进my.cnf或my.ini配置文件。另外部分系统变量是只读的,比如版本号、数据目录路径这类,尝试修改会直接报错。
顺带一提,查看当前MySQL版本时常用的SELECT @@version,本质上就是读取了一个只读系统变量。理解了这一点,就不会把@@version和@变量混为一谈。
四、存储过程里的变量:DECLARE与@的分界线
进入存储过程、函数、触发器这些存储程序内部,变量体系会多出一个成员:用DECLARE声明的局部变量。局部变量不带任何@前缀,必须显式指定数据类型,作用域仅限于所在的BEGIN到END块,程序执行出块就失效。而@用户变量在存储程序内部依然可用,生命周期跟着会话走,不随程序结束而销毁。
DELIMITER //
CREATE PROCEDURE calc_bonus(IN p_emp_id INT, OUT p_bonus DECIMAL(10,2))
BEGIN
-- 局部变量:不带@,必须声明类型,作用域仅限本块
DECLARE base_salary DECIMAL(10,2);
DECLARE rate DECIMAL(4,2) DEFAULT 0.10;
SELECT salary INTO base_salary
FROM employee WHERE emp_id = p_emp_id;
SET p_bonus = base_salary * rate;
END //
DELIMITER ;
-- 调用存储过程,用@用户变量接收OUT参数的输出
CALL calc_bonus(101, @my_bonus);
SELECT @my_bonus;
两者的选择有清晰的实践准则:只在程序内部流转的中间结果,优先用DECLARE局部变量,它有明确类型约束,可读性也更好;需要在程序内外传递数据时才动用@用户变量,比如上面用@my_bonus接住OUT参数的值。混着写虽然语法上大多能跑通,但后续维护的人很难分清哪个变量来自哪里,排查问题时会非常痛苦。
还有一个隐蔽的坑:由于@变量的生命周期贯穿整个会话,如果存储过程内部依赖某个@变量做计数或排名,而调用前忘了重置它,第二次调用会接着上一次的残值继续算,结果悄悄出错。稳妥的做法是在每次调用前显式清零,或者在过程内部先初始化再使用。
五、常见陷阱与实战应用场景
第一个陷阱是NULL传染。未初始化的用户变量默认值是NULL,而NULL参与任何算术运算的结果仍然是NULL。不少人写SET @total = @total + 1时忘了先赋初值,算出来一整列NULL还以为代码有bug。解决办法很简单:使用前显式初始化,或者用IFNULL函数兜底。
SET @counter = NULL; SELECT @counter + 1; -- 结果是NULL -- 正确做法:先初始化 SET @counter = 0; SELECT @counter + 1; -- 结果是1 -- 或者用IFNULL兜底 SELECT IFNULL(@counter, 0) + 1;
第二个陷阱是求值顺序不确定。MySQL官方文档明确说明,在HAVING、GROUP BY、ORDER BY子句里引用SELECT列表中赋值的变量,其行为没有保证,依赖变量在特定顺序下求值的写法可能在某些版本或某些数据分布下出错。MySQL 8.0引入窗口函数后,排名、累加这类需求已经有了标准解法,ROW_NUMBER、RANK、SUM OVER既安全又直观,新项目应优先采用,@变量写法主要在维护老代码时还会遇到。
实战价值最高的场景是动态SQL。PREPARE语句只接受字符串形式的SQL,而拼接好的语句需要通过用户变量传入,EXECUTE USING子句配合@变量完成参数化执行。这是@变量少数不可替代的用途之一,动态排序、动态表名查询都靠它实现。
-- 动态拼接SQL并参数化执行 SET @sql_text = 'SELECT emp_id, name FROM employee WHERE dept = ? LIMIT ?'; SET @dept = '技术部'; SET @limit_num = 10; PREPARE stmt FROM @sql_text; EXECUTE stmt USING @dept, @limit_num; DEALLOCATE PREPARE stmt;
再来看累加统计。MySQL 8.0之前的版本没有窗口函数,做累计求和全靠@变量逐行累加,虽然现在有了SUM OVER写法,但读懂这类存量代码依然是日常工作的组成部分。
-- 老写法:用@变量做累计求和
SELECT pay_date, amount,
@cum := @cum + amount AS cumulative
FROM payment, (SELECT @cum := 0) AS t
ORDER BY pay_date;
-- 新写法:窗口函数一步到位
SELECT pay_date, amount,
SUM(amount) OVER (ORDER BY pay_date) AS cumulative
FROM payment;
总结一下这套符号体系:一个@标志用户变量,会话级生命周期、弱类型、随用随声明;两个@@标志系统变量,分全局和会话两级,控制服务器行为;存储程序内的DECLARE声明局部变量,块级作用域、强类型。分清三者的边界,掌握冒号等号赋值和变量初始化这些细节,再了解求值顺序、连接残留这些陷阱,无论是写脚本、做排查还是维护老代码,都能做到心里有数。