☰
MySQL ONLY_FULL_GROUP_BY报错深度解析:从原理到五种解决方案
2026/10/10 17:03:29 网站建设 项目流程

每次在 MySQL 里执行带GROUP BY的聚合查询,十有八九会被ONLY_FULL_GROUP_BY劈头盖脸教育一顿。尤其在 MySQL 5.7 之后,这个问题几乎成为每个开发者必踩的坑:明明 SQL 逻辑看起来很顺,结果 MySQL 甩过来一个ERROR 1055,告诉你某个字段不在GROUP BY子句里。

这篇文章就把这个模式彻底讲透。你会看到它是怎么来的、为什么存在、报错的本质是什么,以及五种从“省事”到“规范”的解决方案。我会带上完整的建表、跑数和对比演示,包含我在生产环境里踩过的几个坑,适合刚遇到报错的新手,也适合想搞懂底层原理、准备优化老项目的开发者参考。

1. 先理解这个报错到底在说什么

1.1 一个让无数人崩溃的报错

假设有张成绩表,你想查一下每个班级的最高分,顺便把这个最高分对应的学生名字也带出来。很多人下意识就会写:

SELECT class_id, student_name, MAX(score) FROM exam_score GROUP BY class_id;

然后 MySQL 直接给你表演经典报错:

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

翻译成人话:MAX(score)是聚合函数,MySQL 知道每个班级算出来是一个值,没毛病。但student_name呢?一个班级里明明有几十个学生,你让它按班级分组之后,到底取哪个学生的名字?MySQL 自己判断不出来,于是拒绝执行。

很多人的第一反应是:这 MySQL 是不是有毛病?我就想取一条,随便哪条都行,至于这么大反应吗?

问题就出在这个“随便哪条都行”上。数据库不是人,它不知道你的“随便”是哪种随便。在没有明确规则的情况下,它如果自作主张给你挑一条,在不同版本、不同索引、不同数据量下,结果可能完全不一样。这种“不确定性”是关系型数据库最不能容忍的,于是 SQL 标准从一开始就立了规矩。

1.2 ONLY_FULL_GROUP_BY 的来头

ONLY_FULL_GROUP_BY是 MySQL 的sql_mode里的一项,它代表的是 SQL-92 标准中关于GROUP BY的规范:SELECT 列表中出现的每一个非聚合列,都必须出现在 GROUP BY 子句中,或者被聚合函数包裹。

这条规矩其实很老,但 MySQL 在 5.7.5 之前并没有严格执行。早期版本默认关闭这个模式,你写上面的 SQL 也能跑,MySQL 会从每个组里“随机”取一个student_name返回。问题是执行计划一变化,今天返回小明,明天可能返回小红,线上排查数据对不上,非常难受。所以从 MySQL 5.7.5 开始,官方把它纳入了默认sql_mode,MySQL 8.0 更是直接继承,默认开启。

理解了这个来龙去脉,你就会明白:报错不是 MySQL 在刁难你,而是它在帮你兜底,避免你写出一堆结果不可控的 SQL。

2. 为什么 MySQL 要对这条 SQL 较真

2.1 GROUP BY 的语义本质

要真正绕过或利用这个限制,你得先想清楚GROUP BY干了什么。

GROUP BY class_id的意思是:把表里的数据按班级分成若干个组,每个组压缩成一行。那么问题来了——一个组里几十行数据,最后只留下一行,这一行里的class_id是确定的,MAX(score)也是确定的,唯独不是聚合得到的student_name,它该取哪个值?

在数学上,student_name在这个查询场景里压根没有一个确定性答案。SQL 标准为了不让你写出这种“看心情执行”的查询,直接一刀切:非聚合列不写进GROUP BY,就不让你过。

用一个生活化的类比:你去餐厅点“例汤”,服务员问你要什么汤,你说“随便”。合格的服务员会继续追问“你有忌口吗”,不负责的服务员可能直接给你上他最想卖的那碗。ONLY_FULL_GROUP_BY就是那个较真的服务员,它宁可多问你几句,也不愿意给你上错汤。

2.2 功能依赖:MySQL 5.7 引入的“宽容机制”

看到这里你可能会问:那为什么我这么写又不报错?

SELECT id, student_name, MAX(score) FROM exam_score GROUP BY id;

这就要说到 MySQL 5.7 引入的一个很有意思的概念:功能依赖。

假设id是主键。那么当两行数据的id相同时,这两行必然是同一行,因为主键唯一。既然是一行,它的student_name自然也跟着确定。这种“id的值能唯一决定student_name的值”的关系,就叫功能依赖。

MySQL 5.7 以后会做这种推导:如果 GROUP BY 的列已经能唯一确定某一行,那么该行的其他字段在逻辑上就是确定的,可以被 SELECT 出来。这就是为什么你按主键分组时,SELECT 其他非聚合列不会报错。

同理,如果你有这样一个唯一索引:

ALTER TABLE exam_score ADD UNIQUE uk_class_student (class_id, student_name);

然后写:

SELECT class_id, student_name, MAX(score) FROM exam_score GROUP BY class_id, student_name;

也不会报错,因为class_id + student_name这个组合已经唯一。MySQL 的优化器足够聪明,它看得出来你组的这个维度本身就能锁定这些字段。

理解功能依赖非常重要。因为后面很多“为什么这样写能过、那样写不能过”的问题,根源都在这里。它不是死板地要求字段名必须一模一样地出现在 GROUP BY 里,而是看逻辑上的确定性。

3. 五种可行方案,从省事到规范

真正动手解决问题之前,先给你一个全貌。这张表列清楚了五种方案的定位,后面我会逐个展开。

方案核心思路适用场景风险等级
关闭 ONLY_FULL_GROUP_BY回到 MySQL 5.6 的宽松行为老项目迁移过渡、SQL 一时改不完高,治标不治本
ANY_VALUE() 包装字段显式告诉 MySQL“取任意值”字段在组内本来就相同,只是 MySQL 认不出来中,结果确定则安全
把字段塞进 GROUP BY扩大分组维度你确实想按多个维度汇总低,但要理解语义变化
子查询先聚合再回表先算出每组的极值,再关联原表取“组内符合条件的那一行”低,是最正统的思路
窗口函数分组排序编号后再过滤MySQL 8.0+ 环境低,写法最清晰

3.1 方案一:关掉 ONLY_FULL_GROUP_BY

最粗暴、也是网上搜到最多的方法:修改sql_mode,把这个模式去掉。

先看看当前模式配置:

SELECT @@global.sql_mode; SELECT @@SESSION.sql_mode;

默认大概率是下面这一长串:

ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

你可以临时在当前会话里改:

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,但保留了其他模式,千万别把sql_mode直接设成空字符串。有些教程让人写SET SESSION sql_mode='',那是把严格模式、日期校验、除零保护全关了,副作用比你想的大得多。数据插不进、零日期混进来、除零不报错,线上迟早出大问题。

如果要全局永久生效,改配置文件。MySQL 的 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_ENGINE_SUBSTITUTION

保存后重启 MySQL 服务。

但我的建议是:这个方案只适合过渡,不适合长期依赖。我见过不止一个团队为了兼容遗留 SQL 把它关掉,结果新同事接着写那些语义不明确的查询,数据对不上、结果抖动,问题从 SQL 层面转移到了业务排查层面。关闭它不等于问题消失,只是把 MySQL 的保护盾收了回去。如果条件允许,能用方案二到方案五改写 SQL 的,尽量改写。

3.2 方案二:用 ANY_VALUE() 告诉 MySQL“随便取一个”

ANY_VALUE()是 MySQL 5.7 提供的函数,作用就是专门应付这种报错:你声明“这个字段我不关心具体值,组内任取一个就行”。

拿开头那个问题举例:

SELECT class_id, ANY_VALUE(student_name), MAX(score) FROM exam_score GROUP BY class_id;

这样不会报错,也能跑出结果。但这里必须泼一盆冷水:如果 student_name 在组内并不是同一个,那么 ANY_VALUE 返回的值是不确定的,和关闭 ONLY_FULL_GROUP_BY 没有本质区别。

那它在什么场景下真正安全?

比如按user_id分组查用户表,SELECT 里出现user_name。一个用户可能有多条订单记录,但user_name对同一个user_id来说永远是同一个值。这时候ANY_VALUE(user_name)的结果就是确定且正确的,只是 MySQL 的优化器没法从索引上证明这一点,用ANY_VALUE()包装一下,等于你替它做了担保。

场景再确认一遍:逻辑上确定,但 MySQL 推导不出来,用 ANY_VALUE;逻辑上本来就不确定,用了 ANY_VALUE 等于自己骗自己,结果照样是随机的。

3.3 方案三:把字段塞进 GROUP BY

既然 SELECT 里的非聚合列必须出现在 GROUP BY 里,那我把它加进去不就行了?

SELECT class_id, student_name, MAX(score) FROM exam_score GROUP BY class_id, student_name;

这条路能走,但你要清楚它带来的变化:分组维度变细了。以前按班级分组,一个班一条记录;现在按班级加学生分组,一个班有多少个学生,就有多少条记录。严格来说,它的语义变成了“查询每个学生在自己班级里的最高分”,和“查询每个班级的最高分及其学生”已经是两个问题了。

如果你本来就想要这种多维度汇总,当然没问题。但如果你只是想消掉报错,千万别无脑加字段。我见过有人把 SELECT 里的字段全部塞进 GROUP BY,结果原来一条一个组的数据变成了几十条,统计报表数字全变了,比报错更让人头大。

所以这个方案的适用范围很明确:你确实需要按多个维度做汇总统计时使用,它不是用来“骗过”校验的工具。

3.4 方案四:子查询先聚合,再回原表取明细

现在回到那个经典需求:查每个班级最高分对应的学生完整信息。

真正规范的思路不是“选了字段然后按班级分组”,而是分两步走:

第一步,先算每个班级的最高分:

SELECT class_id, MAX(score) AS max_score FROM exam_score GROUP BY class_id;

第二步,拿着最高分去关联原表,把对应学生找出来:

SELECT s.class_id, s.student_name, s.score FROM exam_score s INNER JOIN ( SELECT class_id, MAX(score) AS max_score FROM exam_score GROUP BY class_id ) m ON s.class_id = m.class_id AND s.score = m.max_score;

这个写法不会触发 ONLY_FULL_GROUP_BY,因为子查询里 SELECT 的只有class_id和MAX(score),非聚合列class_id正好在 GROUP BY 里;外层查询根本没有 GROUP BY,只是普通 JOIN,自然不涉及这个约束。

但有一个细节必须提醒:如果同一个班级有两个人考了一样的最高分,这个 SQL 会把两个人都查出来。业务上这可能是对的,也可能是错的。如果你只要其中一个人,就得再加一个去重条件,比如要求s.id最小之类。这个“万一有并列怎么办”的思考,才是实际开发里真正体现水平的地方。

3.5 方案五:窗口函数,MySQL 8.0 的最优解

如果你们数据库已经是 MySQL 8.0,那么面对“取每组符合条件的某一行的完整信息”,窗口函数是写法最清晰、性能也靠谱的方案。

用ROW_NUMBER()对每个班级内的学生按分数倒序编号,编号为 1 的就是最高分:

SELECT class_id, student_name, score FROM ( SELECT class_id, student_name, score, ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS rn FROM exam_score ) t WHERE rn = 1;

这段 SQL 即使有ONLY_FULL_GROUP_BY也不会报错,因为根本没有 GROUP BY。窗口函数解决的是排序和编号问题,跟分组聚合完全不是一个赛道。

它还有一个好处:ORDER BY score DESC这一句顺带帮你定了“如果取不到,就取哪个”的逻辑。比如你可以追加ORDER BY score DESC, id ASC,指定并列时取学号更靠前的那个。这在子查询方案里需要多花心思处理,在窗口函数里就是排序键加一个字段的事。

如果 MySQL 版本还是 5.7,就老老实实用方案四;架构升级到 8.0 之后,新写的取明细 SQL 我基本都会优先用窗口函数。

4. 实操演练:一个完整的“取每组最高分学生”案例

4.1 建表和准备测试数据

空口理论半天,不如直接建个表实战一下。我们把前面的exam_score表完整建出来,顺便加一条“并列第一”的数据,专门用来验证前面说的问题。

CREATE TABLE exam_score ( id INT PRIMARY KEY AUTO_INCREMENT, class_id INT NOT NULL, student_name VARCHAR(50) NOT NULL, score INT NOT NULL, exam_date DATE NOT NULL, KEY idx_class_score (class_id, score) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO exam_score (class_id, student_name, score, exam_date) VALUES (1, 'A同学', 90, '2024-04-01'), (1, 'B同学', 92, '2024-04-01'), (1, 'C同学', 85, '2024-04-01'), (2, 'D同学', 88, '2024-04-01'), (2, 'E同学', 95, '2024-04-01'), (2, 'F同学', 78, '2024-04-01'), (3, 'G同学', 91, '2024-04-01'), (3, 'H同学', 91, '2024-04-01'), (3, 'I同学', 89, '2024-04-01');

注意我特意让 3 班的 G 同学和 H 同学同分 91,这样后面可以直观看到不同处理方式的区别。

4.2 错误写法 vs 正确写法对比

先执行那个必报错的写法:

SELECT class_id, student_name, MAX(score) FROM exam_score GROUP BY class_id;

结果如预期,报ERROR 1055。接下来分别用三种方式解决问题。

方式一:ANY_VALUE()直接顶上

SELECT class_id, ANY_VALUE(student_name), MAX(score) FROM exam_score GROUP BY class_id;

执行后能出结果:

1 B同学 92 2 E同学 95 3 H同学 91

注意看,3 班最高分明明是 91,而 G、H 都是 91,这里返回 H 同学其实是“碰巧”。ANY_VALUE()对你的业务断言毫无帮助,它只是不报错。

方式二:子查询先聚合再回表

SELECT s.class_id, s.student_name, s.score FROM exam_score s INNER JOIN ( SELECT class_id, MAX(score) AS max_score FROM exam_score GROUP BY class_id ) m ON s.class_id = m.class_id AND s.score = m.max_score ORDER BY s.class_id;

执行结果:

1 B同学 92 2 E同学 95 3 G同学 91 3 H同学 91

看到了吧,3 班因为两个人同分,返回了两行。这不算错误,而是体现了数据真相:最高分学生确实有两个。业务上如果只想要一个代表,就要手动加条件,比如加上s.id = (SELECT MIN(id) FROM exam_score WHERE class_id = s.class_id AND score = s.score)之类的去重。

方式三:窗口函数

SELECT class_id, student_name, score FROM ( SELECT class_id, student_name, score, ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC, id ASC) AS rn FROM exam_score ) t WHERE rn = 1 ORDER BY class_id;

执行结果:

1 B同学 92 2 E同学 95 3 G同学 91

这里在ORDER BY score DESC后面追加了id ASC,所以 3 班同分的两人里取了 id 更小的 G 同学。如果你想要 H 同学,就把id ASC改成id DESC。窗口函数对“并列时选谁”的控制非常直接。

4.3 三种方案的结果对比

方案3班返回是否能保证确定性适用建议
ANY_VALUE()H同学否,结果随执行计划变化仅用于逻辑上确定、MySQL 推导不出的字段
子查询回表G同学、H同学是,能反映真实并列情况需要完整保留并列记录时使用
窗口函数G同学是,可控需要每组取一条代表时使用,8.0 首选

这个对比实验告诉你一个核心判断:写 SQL 前先问自己,同组出现多个满足条件的行时,你希望保留全部,还是只取一个?这个问题的答案决定你最后选哪种方案,而不是哪个方案“不报错”你就用哪个。

5. 常见问题与排查技巧实录

5.1 问题速查表

把我在各种项目和答疑里遇到的高频问题汇总成一张表,可以直接拿来排查。

现象原因快速处理
报错 1055,提到“Expression #N of SELECT list”SELECT 列表某个非聚合列不在 GROUP BY把字段用 ANY_VALUE() 包起来或加入 GROUP BY
报错 1055,但我的字段明明在 GROUP BY 里可能 SELECT 里还有别的新字段漏掉了看报错里的 Expression #N 定位具体是第几个字段
老项目从 MySQL 5.6 升到 5.7 后大批 SQL 报错旧环境未开启该模式,新环境默认开启短期可临时放宽 sql_mode,中期必须逐条改写 SQL
ANY_VALUE() 在 5.6 版本里不存在该函数是 5.7 引入升级 MySQL,或用 MIN/MAX(字段) 代替
同一 SQL 在不同环境一个报错一个不报错两个环境的 sql_mode 配置不一致检查SELECT @@GLOBAL.sql_mode;对比差异
用窗口函数也报 sql_mode 相关错误窗口函数不涉及 GROUP BY,通常不是这个问题检查是否在子查询里混用了 GROUP BY

5.2 容易忽略的几个坑

第一个坑:ORDER BY也会被约束。很多人以为 ONLY_FULL_GROUP_BY 只管 SELECT 里的列,其实它同样作用于 ORDER BY。你写SELECT class_id, MAX(score) FROM exam_score GROUP BY class_id ORDER BY student_name;一样会报错,因为student_name没有出现在 GROUP BY 里。要排序,要么把这个字段也加进去,要么排序列也用聚合函数,比如ORDER BY MAX(student_name)。

第二个坑:SET GLOBAL sql_mode不是立即对所有连接生效。已经存在的连接仍然沿用旧的sql_mode,新建立的连接才会用新的全局值。改完发现“怎么还是报错”,十有八九是用了老连接。要么断开重连,要么先SET SESSION sql_mode在当前窗口验证。

第三个坑:把sql_mode设回默认值的时候,别手动抄一串字符串抄错了。最稳妥的做法是先把@@GLOBAL.sql_mode查出来后复制,再去掉ONLY_FULL_GROUP_BY这段。而且做完之后顺手执行一个SELECT @@SESSION.sql_mode;确认一下,不要想当然。

第四个坑:HAVING子里有非聚合列同样受限。HAVING student_name = '某同学'这种条件在分组后根本没法计算,因为学生名不是一个组级值。这种情况下要么改成ANY_VALUE(student_name) = '某同学',要么趁早把这个条件移到WHERE里。

5.3 生产环境排查的真实案例

有一次线上报表接口突然大量报警,日志里全是 1055。查下来发现是某个自动化任务把数据库实例从 5.6 升到了 5.7,而项目里存在一批“先 JOIN 再 GROUP BY”的老 SQL,SELECT 里带了很多关联表的字段,根本没在 GROUP BY 里。当时最理性的处理不是马上关掉ONLY_FULL_GROUP_BY,而是先按接口维度把报错 SQL 分成三类:能加字段升级语义的、能用子查询回表改写的、以及历史遗留需要业务方确认结果的。第一周先把前两类改完,第三类用 ONLY_FULL_GROUP_BY 临时放行,限定期限内全部改掉。最终真正关掉它的时间不超过两周。

这个案例想表达的其实就是一个原则:报错是提示,不是敌人。它的出现帮你暴露了一批结果可能不确定的 SQL,这比数据在线上悄悄出错要划算得多。先理解每条 SQL 想表达的业务意图,再选择改写方案,而不是发现能关闭限制就一关了之。

6. 我在实际开发中的一点体会

和这个错误打了这么多年交道,我的态度已经从“烦躁”变成“感激”。因为只要开着一个严格的模式,它就逼着你把 SQL 的语义想清楚。写聚合查询的时候,多问自己一句“分组之后,这个字段真的确定吗”,能避免大量线上数据对不上的事故。

如果非要给一个选择优先级,我的习惯是:MySQL 8.0 环境下取组内明细首选窗口函数;5.7 环境下首选子查询回表;字段逻辑上确定、只是 MySQL 推导不出来时,用ANY_VALUE()做局部豁免;只有面对改造不动的大量遗留 SQL 时,才考虑临时放宽全局模式,并且要定下清除期限。

最后分享一个小技巧,排查 1055 报错时,先把报错信息里的Expression #N数清楚。#2代表 SELECT 列表第二个表达式,逐个对照,十秒钟就能定位到罪魁祸首。遇到老项目成片报错也别慌,先把sql_mode查出来备份好,每改一条 SQL 就执行一次验证,确认结果符合业务预期再合入。多花在这上面的时间,最后都会变成上线后的省心。

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

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

立即咨询