MySQL ONLY_FULL_GROUP_BY报错深度解析:从原理到五种解决方案 每次在 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_modeonly_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_modeMySQL 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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中期必须逐条改写 SQLANY_VALUE() 在 5.6 版本里不存在该函数是 5.7 引入升级 MySQL或用 MIN/MAX(字段) 代替同一 SQL 在不同环境一个报错一个不报错两个环境的 sql_mode 配置不一致检查SELECT GLOBAL.sql_mode;对比差异用窗口函数也报 sql_mode 相关错误窗口函数不涉及 GROUP BY通常不是这个问题检查是否在子查询里混用了 GROUP BY5.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”的老 SQLSELECT 里带了很多关联表的字段根本没在 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 就执行一次验证确认结果符合业务预期再合入。多花在这上面的时间最后都会变成上线后的省心。