☰
MySQL ONLY_FULL_GROUP_BY报错详解:从原理到排查与解决
2026/10/6 9:16:28 网站建设 项目流程

凌晨两点接到值班电话,开发说线上 MySQL 突然开始报错:this is incompatible with sql_mode=only_full_group_by。我一边开电脑,一边脑子里已经快速过了一遍接下来的动作:查当前实例的 sql_mode、定位报错 SQL、判断是改配置还是改语句。这个问题在 MySQL 5.7 之后非常常见,尤其是从 5.6 升级上来的业务,或者习惯把 GROUP BY 写得很随意的团队,几乎都会撞上。这篇文章就把这个报错讲透,从原理到解决思路,再到具体的操作顺序,一次说清楚。

1. 先搞清楚:这个报错到底是什么

1.1 一条会炸的 SQL 长什么样

先说结论:这个报错不是你语句写错了,而是数据库开启的ONLY_FULL_GROUP_BY模式不认你的写法。它要求SELECT后面出现的列,要么出现在GROUP BY后面,要么被包在聚合函数里,比如MAX()、MIN()、SUM()、COUNT()。

一个典型例子:

SELECT department_id, last_name, MAX(salary) FROM employee GROUP BY department_id;

这条语句在 MySQL 5.6 默认配置下能跑,但在 MySQL 5.7 及之后的版本里会直接报错,因为last_name既不在GROUP BY里,也不是聚合函数包住的列。完整报错信息一般长这样:

ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'employee.last_name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

很多同学只看懂了最后一句this is incompatible with sql_mode=only_full_group_by,就开始到处找“关掉这个模式的办法”。其实更合理的思路是先弄明白:为什么 MySQL 要这么检查。

1.2 为什么 5.6 时代没事,5.7 之后才开始“发难”

GROUP BY的本质是把多行数据压缩成一行。比如按部门分组,一个部门里有几十个员工,最后输出一行,那这行的last_name到底取谁的?严格从语义上讲,MySQL 不知道,也不该随便取。ONLY_FULL_GROUP_BY做的就是这件事:它把这种“语义不明确”的 SQL 挡在门外,要求开发者自己想清楚每一列怎么处理。

MySQL 5.6 之前,这个模式默认不开启,所以上面那条 SQL 能执行。数据库只是从每组里随机选一条经验值展示,每次执行可能拿到不同的行,这就是“不确定查询”。业务数据量一大,或者索引变了,结果就可能变,线上问题就是这样一点一点埋下的。

5.7 起,ONLY_FULL_GROUP_BY被放入默认的sql_mode中,8.0、8.4 LTS 同样如此。也就是说,这不是某一次升级碰巧搞出来的 Bug,而是 MySQL 在向标准 SQL 靠拢。它宁可报错,也不允许你糊里糊涂地拿一个不确定的结果。

还有一点要注意:云数据库厂商的默认配置可能不一样。有些云 RDS 为了兼容旧业务,默认把ONLY_FULL_GROUP_BY去掉了。所以同一套代码,本地 5.7 一跑就报错,部署到 RDS 反而正常,这也是很常见的现象。排查之前,先确认当前实例到底开没开这个模式,再谈后续。

2. 解决方案怎么选:四类思路逐一拆解

2.1 改 SQL 写法:从根上解决

这是我最推荐的方式,也是所有方案里唯一的“正解”。它的核心原则是:让 SELECT 里的每一列都有明确的语义。

针对最开始的报错 SQL,有三种改法:

-- 方式1:把 last_name 加进 GROUP BY SELECT department_id, last_name, MAX(salary) FROM employee GROUP BY department_id, last_name; -- 方式2:如果不需要 last_name,直接删掉 SELECT department_id, MAX(salary) FROM employee GROUP BY department_id; -- 方式3:如果只是想要部门里某个员工的姓名,用分组合并思路 SELECT department_id, MAX(last_name), MAX(salary) FROM employee GROUP BY department_id;

方式3需要谨慎。MAX(last_name)取的是字符串排序最大的那个姓名,跟MAX(salary)对应的员工不一定是同一个人。如果业务要的是“工资最高员工的姓名”,这三种写法全都不对。

正确的写法是子查询关联,或者用窗口函数。以 MySQL 8.0 为例:

SELECT department_id, employee_name, salary FROM ( SELECT department_id, employee_name, salary, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn FROM employee ) t WHERE rn = 1;

如果是 MySQL 5.7,没有窗口函数,就用关联子查询:

SELECT e.department_id, e.employee_name, e.salary FROM employee e JOIN ( SELECT department_id, MAX(salary) AS max_salary FROM employee GROUP BY department_id ) t ON e.department_id = t.department_id AND e.salary = t.max_salary;

注意这里如果同一部门存在相同最高工资,会返回多行。业务上如果不接受,还要再补一个唯一排序条件。

我见过太多人为了省事直接加ANY_VALUE(),或者干脆改全局配置,结果 SQL 跑出来的数据是错的,还不好排查。改 SQL 虽然前期成本高,但每个字段的语义都是确定的,以后无论换版本、换数据库、做迁移,都不会再在这个问题上栽跟头。

2.2 ANY_VALUE():代价最小的临时补丁

ANY_VALUE()是 MySQL 5.7 给出的一个“绕过校验”的函数,字面意思就是:你随便给个值吧,我不在乎。

SELECT department_id, ANY_VALUE(last_name), MAX(salary) FROM employee GROUP BY department_id;

这样写能过校验,也能出结果。但你要清楚:ANY_VALUE(last_name)返回的分组内哪个姓名,是完全不可预测的。MySQL 会在分组内挑一个它认为最方便的值,在数据分布均匀、索引不同时,结果都可能不一样。

所以这个函数我只建议用在两种场景:

  • 分组后这个字段的值本来就确定一样。比如按订单号分组,顺便取用户ID,一个订单只属于一个用户,那ANY_VALUE(user_id)没问题。
  • 临时救急,线上已经挂了,开发来不及改 SQL,先让业务恢复,事后马上排期改。

从长期维护角度看,ANY_VALUE()是给“技术债”打补丁,不是还债。项目里如果大量出现这个函数,要警惕——它不是聚合函数,不保证任何确定性,后续排查数据问题时很容易翻车。

2.3 修改 sql_mode:会话级和全局级怎么选

修改sql_mode是网上铺天盖地的“标准答案”,但我要先说一句:能不动全局,尽量别动全局。

先看会话级,只对当前连接有效:

SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';

注意,我上面这一长串是“去掉ONLY_FULL_GROUP_BY后的典型默认值”。千万不要直接写SET SESSION sql_mode = ''。把sql_mode设成空会连带关闭STRICT_TRANS_TABLES等保护项,插入超长字符串会静默截断,插入非法日期也不再报错,等于把 MySQL 的“安全护栏”全拆了,这是高风险操作。

会话级改动的特点是:不需要重启,不影响其他连接,当前连接马上生效。适合排查问题、验证假设,或者给某个专门的运维账号用。

再看全局级,它影响之后新建的所有连接:

SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';

注意两个细节:

  • 已经存在的连接不会刷新,必须重连才生效。很多同学改了 GLOBAL 之后,用原来的 Navicat 窗口再执行一遍报错 SQL,发现还是报错,就以为没改成功,其实只是没重连。
  • SET GLOBAL在部分云数据库上会被禁止,或者重启后被参数模板覆盖。云 RDS 需要在控制台的“参数组”里改sql_mode,再提交生效。

全局改动能快速止血,但代价是:它让整个实例的 SQL 校验降级了。所有不规范查询都会重新放行,短期看是“恢复正常”,长期看是“慢性病复发”。

2.4 四类方案对比与落地优先级

做一个简单对照,方便你根据现场情况选方案:

方案改造成本数据风险生效速度推荐场景
修改 SQL 语法高低慢,需要开发改代码新功能上线、永久修复
增加 ANY_VALUE()低中快,改动一行少量语句快速绕过
修改会话级 sql_mode低低最快,无需重启排查问题、临时验证
修改全局/配置文件 sql_mode低高中等,需重启或重连存量系统短期过渡

我的建议是,处理顺序按上面表格从下往上来:如果线上正在报错,先用会话级或全局级把系统拉起来,保证业务可用;然后立即建立“问题 SQL 清单”,排期让开发按标准写法修复;最后等修复完,再把sql_mode恢复成默认值。整个过程不能只做一半,只关校验不修 SQL,等于把雷留在生产环境里,不知道哪一天被谁踩爆。

3. 完整走一遍排查与处理流程

3.1 第一步:确认当前环境的 sql_mode

不管线上发生了什么,第一件事永远是确认现状。我习惯执行两条命令:

SELECT @@GLOBAL.sql_mode; SELECT @@SESSION.sql_mode;

@@GLOBAL.sql_mode是实例级配置,@@SESSION.sql_mode是当前连接继承下来的值。如果全局没有ONLY_FULL_GROUP_BY但会话里有,那可能是某个中间件或 ORM 框架在建立连接时手动设置了会话级sql_mode,这种情况改配置文件也没用。

也可以用这条命令快速检查:

SHOW VARIABLES LIKE 'sql_mode';

它默认展示会话级的值。排查时这两条都执行一下,能帮你判断问题出在哪个层级。

3.2 第二步:把报错 SQL 完整捞出来

很多时候,你手里只有一句“MySQL 报错:this is incompatible with sql_mode=only_full_group_by”,但不知道是哪条 SQL 触发的。生产环境开general_log是很重的操作,一般不建议直接搞。我常用的路径有这几条:

如果应用层有完整报错日志,直接搜关键字ERROR 1055或only_full_group_by,往往能拿到完整语句。如果日志里只打印了部分 SQL,配合应用代码里的 MyBatis 或 ORM 日志去补全。

数据库侧可以查performance_schema里最近执行过的语句,比如:

SELECT DIGEST_TEXT, COUNT(*) AS cnt FROM performance_schema.events_statements_history_long WHERE DIGEST_TEXT LIKE '%GROUP BY%' GROUP BY DIGEST_TEXT ORDER BY cnt DESC LIMIT 20;

但要注意,events_statements_history_long默认记录条数很少,生产库的实时负载又很高,这条路径通常只能抓到最近一小段时间的语句。要覆盖更大范围,建议在测试环境或低峰期开一段时间的general_log,记录到表里:

SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';

等抓够了语句,立刻执行SET GLOBAL general_log = 'OFF'。然后把mysql.general_log表里包含GROUP BY且和报错特征匹配的语句导出来,按次数统计,就能知道哪些 SQL 受影响最严重。

不管用哪种方式,最终都要落到一个“SQL 责任清单”上:哪条 SQL 不规范、由哪个系统发出、当前负责人是谁、准备怎么改。没有这个清单,后面的修复就是盲人摸象。

3.3 第三步:按业务影响选择方案并落地

具体落地分两种情况。

第一种,问题影响面小,只有一两个查询在报错。那就走正路:直接修 SQL。开发在本地复现,按第 2 节讲的方式改写,测试通过之后正常发版。改的时候注意三点:先确认GROUP BY字段有没有覆盖业务想表达的分组粒度;SELECT里每个非聚合字段的取值语义是否确定;如果需要“分组内取出最大/最小对应的整行”,用窗口函数或关联子查询,不要图省事用ANY_VALUE凑。

第二种,问题影响面大,几十条 SQL 同时报错,业务已经挂了。那就先止血:在云数据库控制台或者直接用SET GLOBAL去掉ONLY_FULL_GROUP_BY,等业务恢复后再排期修 SQL。止血过程中记录好改动时间、修改前后配置值,并通知所有开发同学“当前全局校验已临时放宽,请自查近期上线 SQL,避免引入更多不规范写法”。同时建一个定时任务,比如两周后检查问题 SQL 的修复进度,全修完之后再把ONLY_FULL_GROUP_BY加回去。

这里要特别强调:临时放宽配置不是目的,是手段。如果团队没有后续修复计划,我建议宁可顶着报错,也不要把全局配置长期放在弱校验状态。

3.4 第四步:验证结果与观察影响

改完之后不能只看那条 SQL 不报错就完事。我会做这几件事:

  • 重新执行刚才报错的 SQL,确认不再报错,且结果符合预期。如果是通过修改 SQL 修复的,还要用EXPLAIN看一眼执行计划,确认没有因为新加的字段顺序、排序规则导致索引失效。
  • 观察连接池行为。全局配置改了之后,连接池里的旧连接不会自动刷新,需要等连接超时重建,或者主动重启应用。如果改了配置仍然报错,优先怀疑这里。
  • 监控数据库变更。放宽ONLY_FULL_GROUP_BY之后,原本被拦截的不规范 SQL 会重新被执行,慢查询数可能短暂上升。留意慢查询日志和 CPU 使用率。
  • 关注主从一致性。如果启用了主从复制,主库的sql_mode改了,从库也要同步修改,否则从库执行相同 SQL 时可能因为模式不一致产生同步中断。

4. 改完还报错?常见坑与排障清单

4.1 配置已改却仍然报错的四个原因

这是我在运维里最常听到的一句话:“我明明把 only_full_group_by 去掉了,怎么还报错?”

逐个排查这四个位置,基本都能找到问题:

第一,配置文件改了,但 MySQL 没重启。sql_mode写在my.cnf的[mysqld]段,绝大多数情况下需要重启才生效。我遇到过有人把配置文件改好了,却因为systemctl restart mysqld出现权限或启动失败的问题,服务其实没重启成功,自然还是旧配置。用SHOW VARIABLES LIKE 'sql_mode'一看就知道。

第二,连接没重连。这个问题前面提过,改的是全局配置,但当前连接是旧的。Navicat、DBeaver 里连着的查询窗口如果不重开,会一直用旧会话配置。判断方法是执行SELECT @@SESSION.sql_mode,看里面的ONLY_FULL_GROUP_BY是否还在。

第三,多个实例,只改了一个。很多公司的一套业务连着多个 MySQL 实例,读写分离的场景下,报错可能来自从库或某个分片。你改了主库,业务走的却是另一个节点。解决方法是登录所有相关实例,批量执行查询,确认每个节点的sql_mode。

第四,ORM 或中间件在连接初始化时覆盖了配置。比如 MyBatis 里配置了sql_mode,或者在连接池初始化 SQL 中执行了SET SESSION sql_mode = ...,那你在数据库层怎么改,都会被应用层覆盖回去。这种情况要改的是应用配置,不是数据库配置。

4.2 去掉 only_full_group_by 后,查询结果会变乱吗

这是业务方最关心的问题,也是 DBA 最难解释清楚的问题。我的回答是:结果不一定乱,但不可预测。

ONLY_FULL_GROUP_BY只负责“拦截语义不明确的 SQL”,它不负责“决定取哪一行”。当你把模式关掉后,MySQL 会退回到 5.6 时代的行为:对于SELECT last_name ... GROUP BY department_id,它会在每个部门分组里随意挑一条记录的last_name返回。注意“随意”不是“随机”,它可能遵循某种内部顺序,也可能因为数据插入、索引变更、执行计划变化而改变。

所以如果你问“会不会出错”,答案是:在当前数据分布下可能不会出错,但只要数据一变,就可能翻车。比如分组内有两条记录,一条是active状态,一条是cancelled状态,这次执行返回active,下次执行可能返回cancelled,业务逻辑如果因为看到cancelled而走了退款流程,问题就大了。

这是我反复强调“配置可以临时改,SQL 必须修”的原因。数据库层放行不代表业务语义正确,它是一种“技术性豁免”。

4.3 函数依赖:为什么 GROUP BY 主键时不炸

MySQL 5.7 的ONLY_FULL_GROUP_BY并不是无脑拦截所有不满足“分组列 + 聚合列”的查询。它对主键分组有一个例外,叫“函数依赖”。

举个例子:

SELECT user_id, user_name, COUNT(*) FROM user GROUP BY user_id;

如果user_id是主键,user_name和其他字段都由user_id唯一确定,那么即使这些字段没被聚合,也没出现在GROUP BY里,MySQL 也不会报错。因为主键唯一性保证了每个分组只有一行,取值是确定的,不存在歧义。

这个设计很聪明,让很多合理查询免于重写。但要注意两个坑:

  • GROUP BY联合主键的一部分不适用。比如主键是(order_id, item_id),你只GROUP BY order_id,这时item_id无法确定,SELECT里如果出现了其他非聚合字段,照样报错。
  • 部分旧版本对函数依赖的判断存在边界场景,比如通过DISTINCT、别名、函数表达式产生的列,识别逻辑可能不同。遇到拿不准的情况,别钻牛角尖,直接把列加进GROUP BY最稳妥。

4.4 版本差异:5.7.44、8.0、8.4 LTS 默认值都是什么

MySQL 5.7 和 8.0 的默认sql_mode都包含ONLY_FULL_GROUP_BY。8.4 LTS 也一样。具体默认配置是:

ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE, NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

有些同学会纠结网上流传的“5.7.44 之后官方怎么变成 5.7.43 了”,其实这类版本号变化是官方发布节奏的细节,和sql_mode行为没有任何关系。5.7 系列从 5.7.44 起不再有功能更新,安全和行为层面的检查规则早就在 5.7 生命周期里稳定了。换句话说,只要你的实例是 5.7,无论小版本是 5.7.26 还是 5.7.44,这个报错的处理方式完全一致。

8.0 以后,MySQL 还新增了窗口函数,这让“分组内取特定行”的问题有了更优雅的解法。如果你的业务已经跑在 8.0 上,遇到ONLY_FULL_GROUP_BY报错时,优先考虑用ROW_NUMBER()这类窗口函数重写,而不是回到 5.7 年代的子查询思路。

5. 两个容易忽略的隐患场景

5.1 5.6 升 5.7 的存量业务,提前排雷

很多公司现在还在跑 MySQL 5.6,但 5.6 已经停止维护,升级是大势所趋。5.6 默认不带ONLY_FULL_GROUP_BY,所以存量系统里积压了大量不规范 SQL。升级到 5.7 或 8.0 的那天,就是集中爆雷日。

我建议升级前做三件事:

第一,搭建一个 5.7 的测试实例,把业务流量灰度切一部分过去,或者用测试环境重放历史流量。观察错误日志里有没有ERROR 1055,有就及时抓出来。

第二,低峰期在 5.6 老实例上开一段时间的general_log,把带GROUP BY的 SQL 全部抓下来,在 5.7 测试库上逐一执行,看哪些会报错,整理成改造清单。

第三,升级窗口内不要“裸奔”。如果开发团队来不及把所有 SQL 修完,可以在升级时临时把ONLY_FULL_GROUP_BY去掉,但必须同步建立改造任务,明确时间表,否则这个“临时”往往就变成永久的。

升级数据库不是换个版本那么简单,它是对过去技术债的一次总清算。早发现,早改,代价最小;等到线上挂了再救火,成本会高很多。

5.2 GROUP BY 搭配 ORDER BY 或 HAVING 时的正确姿势

ONLY_FULL_GROUP_BY不仅管SELECT列表,也管HAVING和ORDER BY。这里有个非常典型的错误:

SELECT user_id, MAX(amount), create_time FROM orders GROUP BY user_id ORDER BY create_time DESC;

这条 SQL 想表达“按用户分组,按最近的订单时间排序”,但create_time没在GROUP BY里也没被聚合,会报错。常见的修复是:

SELECT user_id, MAX(amount), MAX(create_time) FROM orders GROUP BY user_id ORDER BY MAX(create_time) DESC;

这样能通过校验,但语义要小心:`CREATE_TIME 取的是“分组内最大的创建时间”,即最近一单的时间;MAX(amount) 取的是“分组内最大的金额”。这两个值来自不同订单,如果业务想表达“用户最近一单的金额”,这个写法是错的。

正确做法是先找到每组最近一单,再回原表取金额。MySQL 8.0 用窗口函数:

SELECT user_id, amount, create_time FROM ( SELECT user_id, amount, create_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM orders ) t WHERE rn = 1;

MySQL 5.7 用关联子查询:

SELECT o.user_id, o.amount, o.create_time FROM orders o JOIN ( SELECT user_id, MAX(create_time) AS max_time FROM orders GROUP BY user_id ) t ON o.user_id = t.user_id AND o.create_time = t.max_time;

HAVING也一样。比如按用户分组后筛选状态为“已支付”的用户:

SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING status = 'paid';

这个写法会报错,因为status不在分组里,也不是聚合结果。正确做法是把筛选条件放到WHERE里,或者改成HAVING SUM(status = 'paid') > 0这类聚合表达式。

说到底,ONLY_FULL_GROUP_BY逼着你想清楚一个核心问题:每个输出的字段,它代表的到底是一行里的一个值,还是一个组里的一个聚合结果。想清楚了这个,类似报错就都不存在了。

我在实际处理这类问题的经验是:先把问题面摸清楚,再决定动配置还是动代码。线上已经崩了,那就先做最小干预把服务拉起来,但心里要清楚,这只是缓兵之计。SQL 的规范化和语义澄清才是真正需要落地的改进。每次看到有人为了图省事永久删掉ONLY_FULL_GROUP_BY,我都替他们捏把汗——数据库退回到 5.6 的宽松模式,看起来是“解决了报错”,实际上是把一批不确定查询重新放回了生产环境。最后再分享一个小技巧:不管你动了sql_mode还是改了 SQL,验证的时候一定要新开一个连接窗口再执行,别用之前的旧连接,很多“改完还报错”的案例,死就死在没重连这一步上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询