MySQL从5.7版本开始,默认的sql_mode中包含了ONLY_FULL_GROUP_BY选项。很多从5.6或更早版本升级过来的项目,原本运行正常的分组查询突然开始报错,错误信息通常是“Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column ... which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by”。本文将从原理、SQL改写和配置修改三个方面,完整讲解如何应对这个问题。

一、理解ONLY_FULL_GROUP_BY的校验原理
ONLY_FULL_GROUP_BY是MySQL对SQL标准GROUP BY行为的一种严格实现。当这个模式开启时,MySQL会拒绝执行这样的查询:SELECT列表、HAVING子句或ORDER BY子句中出现了非聚合列,而这些列既不在GROUP BY子句中,也不函数依赖于GROUP BY的列。
举个典型例子,假设有一张订单表orders,包含id、user_id、amount、created_at等字段。执行下面的查询在旧版本没问题,但在ONLY_FULL_GROUP_BY模式下会直接报错:
SELECT user_id, amount, COUNT(*) FROM orders GROUP BY user_id;
问题出在amount这一列。当按user_id分组后,一个用户可能有多条订单记录,amount可能有多个值,MySQL无法确定应该返回哪一个。旧版本会“随机”取一条记录的值,这种结果是不确定的,也是SQL标准不允许的。ONLY_FULL_GROUP_BY的本质就是把这种隐式行为变成显式报错,逼迫开发者明确表达自己的意图。
需要注意一种特殊情况:如果GROUP BY的是主键或者唯一索引列,那么表中的其他列都函数依赖于它,此时SELECT任意列都不会报错。例如GROUP BY id时查询所有字段是合法的,这也是很多人疑惑“为什么有的查询能过有的不能过”的原因。
二、方案一:改写SQL语句(推荐做法)
最规范的修复方式是改写SQL,让语句符合标准语义。常见思路有以下几种。
第一种,把需要展示的列补进GROUP BY,或者对它使用聚合函数。如果想看每个用户的订单总金额,应该写成:
SELECT user_id, SUM(amount) AS total_amount, COUNT(*) AS order_count FROM orders GROUP BY user_id;
第二种,使用ANY_VALUE函数。如果业务上确实不关心取哪个值,只想让查询通过,MySQL 5.7及以上提供了ANY_VALUE来显式声明“随便取一个值即可”:
SELECT user_id, ANY_VALUE(amount), COUNT(*) FROM orders GROUP BY user_id;
ANY_VALUE的效果等同于关闭校验,但它只作用于当前这一列,影响范围可控,比全局关闭ONLY_FULL_GROUP_BY安全得多。需要注意ANY_VALUE是MySQL特有函数,如果项目要兼容其他数据库,不建议使用。
第三种,利用子查询或JOIN改写。例如先查出每个用户的最新订单id,再关联原表取完整字段,这样语义清晰且结果确定:
SELECT o.*
FROM orders o
INNER JOIN (
SELECT user_id, MAX(id) AS max_id
FROM orders
GROUP BY user_id
) t ON o.id = t.max_id;这种写法在“分组取最新一条记录”这类高频需求中是最稳妥的方案,性能也通常优于ANY_VALUE。
三、方案二:修改sql_mode配置关闭该校验
如果历史SQL数量庞大,短期内无法逐条改写,可以直接调整sql_mode。先查看当前配置:
SELECT @@GLOBAL.sql_mode; SELECT @@SESSION.sql_mode;
临时生效的方式是在输出结果中去掉ONLY_FULL_GROUP_BY,然后重新设置:
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
SET GLOBAL只影响新连接,SET SESSION只影响当前连接,数据库重启后都会失效。想永久生效必须修改配置文件。Linux下通常是/etc/my.cnf或/etc/mysql/my.cnf,在[mysqld]段中添加或修改:
[mysqld] sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
修改后重启MySQL服务即可,Windows环境则要找到my.ini文件做同样的修改。修改前务必先查询当前的完整sql_mode值,只移除ONLY_FULL_GROUP_BY这一项,保留其他严格模式选项,否则会连带放宽其他校验,引入新的数据质量风险。
四、两种方案的对比与选型建议
修改SQL的优点是根治问题,查询语义明确,结果可预期,且与SQL标准和其他数据库兼容;缺点是工作量大,涉及大量历史代码时改造成本高。修改sql_mode的优点是见效快、一次配置全局生效;缺点是掩盖了SQL本身的不确定性,分组查询可能返回不确定的值,还可能在后续版本升级或主从切换不一致配置时再次踩坑。
实践中的建议是:新项目和核心业务SQL坚持改写语句,使用聚合函数或子查询的规范写法;遗留系统可以先用配置修改的方式快速止血,再分批次排查改造。特别提醒,从MySQL 8.0开始sql_mode默认值中已经去掉了NO_AUTO_CREATE_USER,复制配置时如果照搬5.7的值到8.0会导致启动报错,务必按实际版本的默认值来调整。另外,关闭ONLY_FULL_GROUP_BY之前,建议先在测试环境跑一遍全量SQL回归,确认没有依赖严格模式的隐藏逻辑被破坏。
ONLY_FULL_GROUP_BYsql_modeMySQL报错修改时间:2026-09-01 23:42:33