news 2026/10/10 3:52:10

MySQL 1055报错与ONLY_FULL_GROUP_BY机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL 1055报错与ONLY_FULL_GROUP_BY机制详解

1. 认识ONLY_FULL_GROUP_BY:这不是你的SQL有问题这么简单

1.1 先看一次真实的1055报错现场

先上一段你大概率见过的场景。登录MySQL 5.7或者8.0,建一张简单的学生表,然后执行下面这条SQL:

CREATE TABLE student ( id INT PRIMARY KEY, name VARCHAR(50), class_id INT, age INT ); INSERT INTO student VALUES (1, '张三', 101, 18), (2, '李四', 101, 19), (3, '王五', 102, 20); -- 你以为没问题的查询 SELECT class_id, name, COUNT(*) FROM student GROUP BY class_id;

啪,报错直接甩到脸上:

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

先说结论:这条SQL并不是“完全不能跑”,而是MySQL从5.7.5版本开始默认开启了ONLY_FULL_GROUP_BY这个SQL模式,把你这种行为给拦住了。以前在5.6及更早版本里,同样的SQL能正常执行,但返回的name是组内随机一条记录,换句话说,跑了这次是张三,下次可能就变成李四,结果完全不可复现。

这就是一个很典型的历史包袱。老系统一直跑在宽松模式下,代码里到处是“按班级分组,顺手把姓名查出来”这种写法。搬上5.7或者8.0之后,全部暴露成1055。这篇文章就把这个报错彻底讲透:它到底是什么、什么时候触发、三条官方判定红线、功能依赖是怎么回事,以及改SQL、改模式、用ANY_VALUE这三条出路各自怎么选。不管你是刚碰MySQL的新人,还是被历史遗留SQL坑过无数次的老开发,照着排查就行。

1.2 为什么MySQL 5.7之后默认开这个模式

很多人不理解,MySQL为什么非要“多管闲事”。要回答这个问题,得先明白GROUP BY的语义。GROUP BY class_id的意思是,把班级相同的学生归成一组,每组只输出一行结果。问题来了:一组里明明有张三、李四好几条记录,你却在SELECT列表里写name,MySQL怎么知道该输出谁的名字?

以前的做法是:随便拿一条,这严格来说违反了SQL标准。SQL标准要求,SELECT列表里没被聚合函数包裹的列,必须出现在GROUP BY子句中,否则查询结果就没有确定语义。ONLY_FULL_GROUP_BY这个模式就是为了强制这一条标准而存在的。

它的价值主要体现在两个地方。第一,结果可复现。同一句SQL跑到任何时刻、任何节点,返回的数据都应该是一致的,而不是随执行计划、索引选择变化而飘忽不定。第二,避免统计数据失真。很多人以为“我顺便查个name而已”,但业务上如果出现一个班级有两个班级名称的历史脏数据,你的统计结果就会和明细对不上。与其让错误悄悄发生,不如让数据库帮你把问题暴露出来。这也是我后来做数据库评审时,坚持新库默认保留这个模式的原因。

2. 判定红线与功能依赖:搞清楚哪些字段会触发1055

2.1 SELECT、HAVING、ORDER BY三条红线

ONLY_FULL_GROUP_BY的官方判定范围随着版本有过变化。MySQL 5.7.5之前,它只检查SELECT列表和HAVING子句;5.7.5之后,ORDER BY子句也被纳入检查范围。绝大多数人只知道第一条,结果经常栽在第三条上。

先看SELECT列表违规:

SELECT name, MAX(score) FROM score GROUP BY student_id;

这条等价于问:“每个学生成绩最高的那条记录是哪个?”但你SELECT了name,name既不在GROUP BY里,也不是聚合函数的结果,报1055没商量。正确做法是把name也放进GROUP BY,或者对name做聚合处理。

再看HAVING违规:

SELECT class_id, COUNT(*) FROM student GROUP BY class_id HAVING age > 18;

HAVING是用来对分组结果做过滤的,你这里却拿组内的age和常量比大小,MySQL无法判定“该组的age”到底是哪个值,同样报错。age必须出现在GROUP BY中,或者写成聚合条件,比如HAVING MAX(age) > 18。

最后是很多人忽略的ORDER BY违规:

SELECT class_id, COUNT(*) FROM student GROUP BY class_id ORDER BY age;

age没有参与分组,也没有被聚合,放到ORDER BY里一样报错。这里还有一个细节:老项目从5.6升级到5.7/8.0时,这类ORDER BY写法最容易炸,因为它以前根本不检查。

我把三条红线整理成一张速查表,方便你日常自检:

查询位置允许出现的内容违规条件
SELECT列表GROUP BY中的列、聚合函数包裹的列、常量出现非聚合列且不在GROUP BY中
HAVING条件GROUP BY中的列、聚合函数包裹的列出现非聚合列且不在GROUP BY中
ORDER BY排序GROUP BY中的列、聚合函数包裹的列出现非聚合列且不在GROUP BY中

2.2 功能依赖:为什么GROUP BY主键就能SELECT所有列

有心的朋友到这里肯定会产生一个疑问:那下面这条SQL呢?

SELECT id, name, age, class_id FROM student GROUP BY id;

它不报错。原因就是报错信息里反复提到的那句话:which is not functionally dependent on columns in GROUP BY clause,也就是“功能依赖”(Functional Dependency)。

id是主键,主键唯一标识一行记录。既然按id分组,每组里天然只有一行数据,那么这一行的name、age、class_id都是确定的,不存在“组内多行取哪个”的歧义。所以MySQL放行。这个检测逻辑从5.7.5开始引入,支持主键和NOT NULL的唯一索引两种场景。

这里要提醒一句:MySQL的“功能依赖”只认数据库层面的约束,不认你业务上的“逻辑唯一”。比如你在代码里一直保证“每个班级只有一条班级名称”,但这种约束没有建立唯一索引,MySQL不会买账,照样报1055。正确做法是把约束落到数据库上,建一个唯一索引。这样一来,功能依赖检测自然通过,二来也防止未来有人往表里写脏数据,一举两得。

2.3 GROUP BY多个字段时更容易踩的雷

分组粒度的理解,是GROUP BY多字段场景里最核心的事。看这条SQL:

SELECT class_id, gender, name, COUNT(*) FROM student GROUP BY class_id, gender;

GROUP BY class_id, gender的分组粒度是“一个班级内的一种性别”,每组里可能有多个学生。所以name照样不属于分组列,也不被聚合函数包裹,报错。想让它不报错,有两条路:

一条是把name也加进GROUP BY:

SELECT class_id, gender, name, COUNT(*) FROM student GROUP BY class_id, gender, name;

能跑,但语义完全变了。分组粒度变成了“一个班级内的一种性别下的一个学生姓名”,每条学生记录都各自成组,COUNT(*)基本都等于1,这个统计就没意义了。所以加GROUP BY列之前,一定要想清楚:你是想把显示粒度调细,还是只是想让SQL能跑通。

另一条路是对name做聚合,典型的是GROUP_CONCAT(name),把组内所有学生姓名拼接成一个字符串。这是处理“分组后还想看到组内明细”的标准姿势。需要注意的是,GROUP_CONCAT默认最大长度只有1024字节,名字一多就会被静默截断。遇到这个坑时,可以临时调大会话变量group_concat_max_len,或者把需求改成聚合统计。

3. 三套解决方案:改SQL、调模式、ANY_VALUE怎么选

3.1 首选方案:改写SQL,用聚合函数包住非分组列

我处理过的生产事故里,最稳妥的长期方案永远是改SQL,而不是动全局配置。因为改SQL是让查询符合标准,改配置是让标准迁就查询。前者一劳永逸,后者会埋下新的债。

先看最基础的改写。原来的写法:

SELECT class_id, name, COUNT(*) FROM student GROUP BY class_id;

改成聚合写法:

SELECT class_id, GROUP_CONCAT(name), COUNT(*) FROM student GROUP BY class_id;

如果你需要的只是一个样例值,不关心取到哪一条,可以保留原结构但用ANY_VALUE(name);如果你真正想要的是“组内某个字段最大/最小对应的完整记录”,那上面的写法就不够了,需要用子查询或窗口函数。

我遇到最多、也最有代表性的一个需求是:按学生分组取每科成绩最高的那条记录。很多人会自然写成:

SELECT student_id, name, MAX(score) FROM score GROUP BY student_id;

这条SQL即使不报错,结果也是不对的——它只能查出每个学生的最高分,但查不到“考出这个最高分时对应的科目”。要拿完整行,推荐两种写法。8.0以上用窗口函数:

SELECT * FROM ( SELECT s.id, s.name, sc.subject, sc.score, ROW_NUMBER() OVER (PARTITION BY s.id ORDER BY sc.score DESC) rn FROM student s JOIN score sc ON sc.student_id = s.id ) t WHERE rn = 1;

5.7或兼容性要求高,用关联子查询:

SELECT s.id, s.name, sc.subject, sc.score FROM student s JOIN score sc ON sc.student_id = s.id JOIN ( SELECT student_id, MAX(score) max_score FROM score GROUP BY student_id ) t ON t.student_id = sc.student_id AND t.max_score = sc.score;

注意子查询方案在“同分”场景下会返回多行,如果业务要求同分只取一条,还是得靠窗口函数或者再加行号条件。这属于典型的“报错只是表象,业务语义才是核心”的案例。

3.2 应急方案:调整sql_mode,从源头放开限制

有些历史项目SQL量实在太庞大,一条条改不现实。这时候可以先调整sql_mode应急。调整分为三个层级,从临时到永久,各有用处。

第一层,会话级,只对当前连接生效,断开就失效:

-- MySQL 5.7 SET SESSION 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 8.0(已移除NO_AUTO_CREATE_USER) SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';

第二层,全局级,影响之后新建的所有连接:

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';

第三层,永久生效,修改配置文件。Linux下一般是/etc/my.cnf或/etc/mysql/mysql.conf.d/mysqld.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服务即可。8.0也支持SET PERSIST命令,但配置文件方式最通用、最直观。

这里有几个必须强调的雷区。改动前先执行SELECT @@GLOBAL.sql_mode;把原值记下来,方便回滚。设置全局变量只影响新连接,应用连接池里已经建好的旧连接不会自动更新模式,必须让应用重建连接池或重启应用,否则你会看到“明明改了怎么还报错”的灵异现象。另外,主从架构里主库和从库的sql_mode必须保持一致,尤其是使用STATEMENT格式的复制时,主库上能执行的SQL到了从库可能因为模式不同而执行失败,甚至造成主从数据不一致。

3.3 单点处置:用ANY_VALUE显式声明“我不在乎取哪个”

ANY_VALUE()是MySQL 5.7.5新增的函数,专门用来处理这种需求:你知道某个非分组列在组内的值其实无所谓,但你又不想把整条SQL重写成聚合形态。

SELECT class_id, ANY_VALUE(name), COUNT(*) FROM student GROUP BY class_id;

这条SQL不会报错,它等价于告诉MySQL:这个name列你就随便从组里挑一条吧,我认了。适用场景很明确:分组键在业务上其实是唯一的,但数据库里没有建唯一索引;或者你只是需要把一个列“顺带”带出来,完全不参与计算,取谁都一样。

但我要泼一盆冷水:ANY_VALUE让结果变得不可复现。同一句SQL,数据量变化、索引变化、执行计划变化,都可能导致取到不同的那一行。如果这个值后续被用来展示或者参与二次计算,风险是实打实的。我自己的经验是,ANY_VALUE只适合当成“救火队员”,不适合写进新项目的核心查询里。真正需要“确定取哪一行的某个值”时,用窗口函数固定住排序条件,比ANY_VALUE靠谱得多。

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

4.1 从报错信息里快速定位是第几个字段

1055报错信息很长,但真正有用的信息其实就几处。以之前的报错为例:

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

Expression #2表示SELECT列表里第2个表达式(从1开始数)违规。也就是说,直接数你SELECT后面写了几个字段,第2个就是name。当SELECT列表特别长,或者里面混着子查询、CASE WHEN时,靠肉眼数很容易数错。我的习惯是直接把'test.student.name'这段丢到SQL编辑器里全局搜索,一下就能定位到是哪个别名、哪个字段。

另外要注意,如果是HAVING违规,报错会写Expression #N of HAVING clause;如果是ORDER BY违规,会写Expression #N of ORDER BY clause。搞清楚这个前缀,能少走很多弯路。多表JOIN场景下,报错信息里还会带出完整库表字段名,比如test.score.student_id,定位难度会低很多。

4.2 为什么老项目升到5.7/8.0就炸了

这个问题我半年内至少被问过五次。每次都是同一个剧情:项目从MySQL 5.6升到5.7或8.0,启动后应用日志里刷出一堆1055,看起来像数据库坏了。其实不是数据库坏了,而是5.7.5开始默认开启了ONLY_FULL_GROUP_BY,老SQL以前能跑是沾了宽松模式的光。

碰到这类问题,第一步不是急着改配置,而是先盘点影响面。把应用日志里的1055报错SQL全部捞出来,按“涉及表、涉及查询类型”归类,你会发现大部分是同一类写法:分组后取非聚合列。先评估数量,如果只有几十条,直接改写SQL;如果有几百条且短时间改不完,才考虑临时调整sql_mode。

我处理过的老库迁移案例,最终方案基本都是两步走:短期内关闭ONLY_FULL_GROUP_BY保住业务,同时在排期表里把核心模块的SQL改成标准写法,等技术债还完之后再把模式打开。这里有个小建议:在团队开发规范里明确“GROUP BY之后只能SELECT分组列和聚合结果”,从源头上堵住新代码继续制造债务。

4.3 我踩过的坑和一些善后细节

最后分享几个真实踩过的坑,每一个都付出了实际代价。

第一个坑是改完SET GLOBAL sql_mode后,旧连接不生效。当时我改完全局变量,自己用新开的命令行窗口验证没问题,但应用一直报错。排查半天,发现应用连接池里的连接是在修改之前建立的,连接级别还带着旧的sql_mode。处理办法是通知开发重建连接池,或者重启应用。那次之后我养成了一个习惯:凡是改全局变量,改完立刻提醒业务方重连。

第二个坑是主从架构下sql_mode不一致。主库为了救急关了ONLY_FULL_GROUP_BY,从库没同步改。结果某条不规范的SQL在STATEMENT格式复制下,主库执行成功,传到从库执行时报错,直接中断复制。从此我学乖了,涉及sql_mode的变更,主从必须一起评估、一起改动。

第三个坑是MySQL 8.0上沿用5.7的配置直接报错。8.0已经移除了NO_AUTO_CREATE_USER这个模式,配置里如果还带着它,启动时会报Variable 'sql_mode' can't be set to the value of 'NO_AUTO_CREATE_USER'。所以5.7和8.0的sql_mode配置不能直接互相抄,动手前先确认版本。

第四个坑是视图和存储过程也会受sql_mode影响。视图在创建时会固化当时的sql_mode,有些视图创建于宽松模式下,关闭模式后重新执行视图内部SQL也可能报错。改完模式之后,记得把关键视图和存储过程重新验证一遍,别只盯着业务SQL。我个人在处理这类问题时,最朴素的做法是:先拿到全部报错SQL清单,把其中“分组键其实唯一”的简单案例直接加进GROUP BY或建唯一索引,把真正需要组内多行的案例改成聚合或窗口函数写法,只有实在改不过来的老SQL才用ANY_VALUE或调整模式。希望你也别一上来就关掉ONLY_FULL_GROUP_BY,那相当于把安全气囊拆了——当时是不吵了,真出问题的时候,没人替你兜底。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 3:51:03

MySQL参数优化实战:从瓶颈定位到配置调优,避免常见踩坑

很多朋友一听到“MySQL参数优化”,第一反应就是去打开 my.cnf 或者 my.ini,把 innodb_buffer_pool_size、max_connections 这种一眼就能看懂的参数调大,仿佛数字越大性能就越好。我在实际运维和支持业务的过程中见过太多这种操作:…

作者头像 李华
网站建设 2026/10/10 3:51:03

MySQL索引优化实战:从B+树原理到联合索引避坑指南

1. 为什么一个小小的索引能让查询快上百倍做后端开发这几年,我见过太多因为索引问题把数据库搞垮的案例。最典型的一次是刚接手一个电商项目,订单表两千多万行,运营同学按用户昵称查历史订单,一条SQL跑了14秒,接口超时…

作者头像 李华
网站建设 2026/10/10 3:50:34

彻底看懂MySQL 8.0:新特性、底层重构与升级避坑指南

MySQL 8.0从2018年4月正式GA到现在,其实已经不算是“新面孔”了。但有意思的是,我这些年不管是做技术分享,还是帮朋友排查线上问题,总会碰到围绕“MySQL 8.0新增特性”的讨论。很多人从5.7迁移到8.0之后,第一反应往往是…

作者头像 李华
网站建设 2026/10/10 3:49:33

Python智能租房系统:协同过滤与线性回归的算法实践

1. 项目整体设计与思路拆解1.1 这个项目到底在解决什么问题毕业设计选题这件事,每年都让大量同学头疼。我见过太多人一上来就选什么“基于深度学习的图像识别系统”,结果数据集还没找齐就慌了,更别提要完成训练、调参、部署一整套流程。相比之…

作者头像 李华
网站建设 2026/10/10 3:48:50

基于卡方分布的Bartlett检验:方差齐性分析从原理到实战

1. 为什么我开始盯上方差齐性这件事1.1 从一次分析事故说起先说个我自己的事。有年我做某个过程优化项目,需要对比三批不同工艺参数下产品的均匀性指标,数据收了将近两百条,各组样本量基本均衡。跑完方差分析(ANOVA)一…

作者头像 李华
网站建设 2026/10/10 3:48:42

Android 15冷启动源码解析:从Bootloader到CarService的车载开机优化实践

有段时间,我一直被车载项目的开机时间困扰。测试同事每天早上把车机通电,盯着秒表等主界面出现,从12秒压到10秒再压到9秒,每轮版本都在和自己较劲。后来把Android 15的冷启动流程源码完整啃了一遍,才把“启动慢”这三个…

作者头像 李华