news 2026/10/19 12:35:23

MySQL 1055报错解析:ONLY_FULL_GROUP_BY与GROUP BY查询规范

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL 1055报错解析:ONLY_FULL_GROUP_BY与GROUP BY查询规范

1. 先说结论:这个报错到底在说什么

如果你是一个经常跟 MySQL 打交道的人,SELECT 语句里带 GROUP BY 查询时,大概率见过下面这行报错:

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

我第一次踩到这个坑的时候,第一反应是“MySQL 怎么突然变得这么严格”,后来仔细查了才发现,不是 MySQL 变严格了,而是 sql_mode 里多了一个叫ONLY_FULL_GROUP_BY的东西在起作用。这个报错本质上就是在告诉你:你写的 SQL 里,SELECT 出来的列不满足 GROUP BY 的约束规则,数据库拒绝执行。

这篇文章不是给你念官方文档,而是把我实际排查和解决这个问题的过程、思路、坑点完整记录下来,希望能帮你下次遇到时少走弯路。

1.1 这个报错影响了哪些人

  • 从 MySQL 5.6 升级到 5.7/8.0 的开发者
  • 在项目里使用 ORM 框架(如 MyBatis、Hibernate、Entity Framework)自动生成 SQL 的人
  • 写复杂统计报表、数据分析查询的数据工程师
  • 刚接触 SQL 分组查询的初学者

凡是你的 SQL 涉及GROUP BY,而且 SELECT 列表里有“不是分组字段、也不是聚合函数”的普通列,就有机会触发这个报错。

1.2 为什么值得花时间搞懂它

很多人的第一反应是“把 sql_mode 里的 ONLY_FULL_GROUP_BY 去掉不就完了”,确实能绕过去,但这不是长久之计。因为这背后涉及 SQL 标准里一个非常重要的概念:分组查询的语义完整性。搞懂它不仅能解决当前的报错,还能让你以后写分组查询时少犯很多逻辑错误,避免查出“看起来对、实际上错”的数据。

2. 深入理解 ONLY_FULL_GROUP_BY 的前因后果

要真正解决 1055 报错,不能只停留在“怎么消掉这个报错”的层面,得先搞清楚它为什么存在,以及它到底在保护什么。

2.1 一个经典的分组查询例子

我拿一个最常见的场景来说明。假设有一张用户订单表orders,里面有这些字段:

字段名含义
id订单ID
user_id下单用户ID
product_name商品名称
amount订单金额
created_at下单时间

现在我想查“每个用户的订单总金额”,正常的 SQL 是:

SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id;

这个 SQL 没问题,因为 SELECT 列表里只有两个东西:一个是分组字段user_id,另一个是聚合函数SUM(amount),完全符合分组查询的规则。

但如果我这样写:

SELECT user_id, product_name, SUM(amount) AS total_amount FROM orders GROUP BY user_id;

问题就来了:product_name既不是分组字段,也不是聚合函数。同一个user_id下面可能有多条不同product_name的记录,那到底该显示哪一个商品名?MySQL 在ONLY_FULL_GROUP_BY模式下直接拒绝执行,因为结果存在歧义。

2.2 报错信息逐字拆解

我们来把报错信息拆开看,实际上它就说了三件事:

Expression #1 of SELECT list is not in GROUP BY clause

SELECT 列表中的第 1 个表达式没有出现在 GROUP BY 子句中。

contains nonaggregated column 'db.table.col'

这个表达式里包含了一个非聚合列,也就是没有用 MAX、MIN、SUM、COUNT 等函数包裹的列。

which is not functionally dependent on columns in GROUP BY clause

这个非聚合列在功能上不依赖于 GROUP BY 后面的字段——这半句是很多人忽略的重点。意思是:如果这个列在功能上依赖分组字段,那即使它没写在 GROUP BY 里也是允许的。

什么情况叫“功能上依赖于分组字段”?比如user_id是主键,你查user_id, user_name,而user_name在业务上由user_id唯一确定,那么user_name就是功能依赖于user_id。MySQL 在 5.7 及以后版本里,如果检测到这种依赖关系,即使没把user_name放进 GROUP BY,也不会报 1055。

2.3 ONLY_FULL_GROUP_BY 为什么要存在

在 MySQL 5.6 及更早版本,默认 sql_mode 里没有这个选项,上面的“错误 SQL”能正常执行。但结果可能很随机——MySQL 会随便从分组里挑一条记录的product_name返回。

比如说,某个用户买了两样东西,第一次查可能返回“手机”,第二次查可能返回“电脑”,完全不确定。这种不确定行为非常危险,特别是在做财务统计、订单报表时,数据错了很难排查出来。

ONLY_FULL_GROUP_BY就是 MySQL 为了纠正这种不严谨的行为而引入的约束。它是 SQL 标准的一部分,让分组查询的结果变得确定、可预期。

从 MySQL 5.7 开始,ONLY_FULL_GROUP_BY默认是开启的,这也是为什么很多人从 5.6 升级后突然发现一堆原本能跑的 SQL 全部报 1055 的原因。

3. 五种解决方案逐一拆解

解决 1055 报错不是只有一种方法,关键看你所处的场景和能动的范围。我按推荐程度从高到低排列,分别讲解。

3.1 方案一:修改 SQL 语句,从根源上符合规范

这最推荐的方式,也是最彻底的。核心思路:要么把非聚合列加入 GROUP BY,要么把它用聚合函数包起来,要么干脆去掉。

还是用前面那个例子。如果你确实需要查“每个用户的每个商品的订单总金额”,那 SQL 应该改成:

SELECT user_id, product_name, SUM(amount) AS total_amount FROM orders GROUP BY user_id, product_name;

把product_name加进 GROUP BY,语义就清晰了:按用户和商品两个维度分组统计。

如果你只是需要每个用户的订单总金额,那直接删掉product_name就好。如果你需要每个用户订单总金额,同时还想看一眼这个用户随便某个订单的商品名,可以用聚合函数:

SELECT user_id, MAX(product_name) AS any_product_name, SUM(amount) AS total_amount FROM orders GROUP BY user_id;

这个写法表示:我不在乎具体是哪个商品名,随便取一个就行。用MAX或者MIN就明确表达了这个意图。

补充一个实用技巧:MySQL 5.7.5 及更高版本还支持ANY_VALUE()函数,专门用于这种场景:

SELECT user_id, ANY_VALUE(product_name) AS any_product_name, SUM(amount) AS total_amount FROM orders GROUP BY user_id;

ANY_VALUE()的意思比MAX()更直白:“我不关心取哪个值,只要是非 NULL 的一个值就行”。不过注意,ANY_VALUE()在 MySQL 8.0 里依然可用,但在某些复制场景下可能有一定行为差异,生产环境建议先测试。

3.2 方案二:调整 GROUP BY 与 SELECT 的对应逻辑

有一种情况很典型:你写了一个 subquery 或者 JOIN 查询,子查询里已经做了分组,外层又要关联其他表字段,这时候容易触发 1055。

举个例子:

SELECT o.user_id, u.user_name, SUM(o.amount) AS total FROM orders o LEFT JOIN users u ON o.user_id = u.user_id GROUP BY o.user_id;

这个 SQL 报 1055 的原因:u.user_name既不在 GROUP BY 里,也没被聚合函数包裹。虽然直觉上user_name由user_id唯一确定,但如果users表里user_id不是主键或唯一键,MySQL 不会认可这种依赖关系。

解决方式有两种。第一种是把u.user_name也加进 GROUP BY:

SELECT o.user_id, u.user_name, SUM(o.amount) AS total FROM orders o LEFT JOIN users u ON o.user_id = u.user_id GROUP BY o.user_id, u.user_name;

第二种是把用户信息拿出来单独查,避免在分组查询里关联其他表的非分组字段:

SELECT o.user_id, (SELECT u.user_name FROM users u WHERE u.user_id = o.user_id LIMIT 1) AS user_name, SUM(o.amount) AS total FROM orders o GROUP BY o.user_id;

这种情况下,子查询返回的user_name是独立的聚合结果,MySQL 不会把它当成非聚合列来检查,自然也不会报 1055。

3.3 方案三:修改 sql_mode 解除限制

如果 SQL 实在改不动,或者项目里的旧查询太多、一时半会改不完,可以考虑从数据库层面去掉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

去掉ONLY_FULL_GROUP_BY的方式有两种。第一种是会话级别临时改,只影响当前连接:

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

但要提示一句:SET GLOBAL只对新连接生效,当前已有的连接不受影响。而且 MySQL 服务重启后,如果没写进配置文件,这个设置会丢失。要持久化的话,需要修改配置文件(如my.cnf或my.ini):

[mysqld] sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

改完之后重启 MySQL 服务即可。

3.4 方案四:在 JDBC 连接串里指定 sql_mode

如果是 Java 项目,还可以在 JDBC 连接串里追加sessionVariables=sql_mode=...来指定会话级别的 sql_mode。

比如原来连接串是:

jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai

改成:

jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&sessionVariables=sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

这个方式的好处是:不用动数据库全局配置,只对当前应用的所有连接生效,对别的应用无影响。适合那种“数据库是共用的,不能随便改配置”的场景。

但有几点要注意:URL 里的逗号需要转义吗?不用,XML 配置里才需要。如果你是在 Spring Boot 的application.yml里配置数据源,注意 YAML 对特殊字符的处理,建议用引号包起来。

3.5 方案五:升级 SQL 写法,使用窗口函数

MySQL 8.0 引入了窗口函数,这给分组查询带来了新的写法。某些场景下,可以用窗口函数替代 GROUP BY,不仅解决 1055 报错,还能实现更复杂的统计分析。

比如“查每个用户的最大金额订单”这种需求,用 GROUP BY 写很容易踩坑:

SELECT user_id, product_name, MAX(amount) AS max_amount FROM orders GROUP BY user_id;

这条 SQL 在ONLY_FULL_GROUP_BY下直接报 1055,因为product_name没进 GROUP BY、也没被聚合。但如果你要的其实就是“金额最大的那笔订单的商品名”,那应该用窗口函数:

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

这个写法语义非常清晰:按用户分组,按金额倒序排序,取每组第一条。不存在什么“非聚合列未分组”的问题,因为子查询里根本没有 GROUP BY。

如果你的需求恰好是这种“每组取一条特定记录”的类型,窗口函数是比 GROUP BY 优雅得多的方案。如果你的数据库是 MySQL 5.6/5.7,不支持窗口函数,可以退而求其次用“先 JOIN 聚合结果再关联明细”的方式实现。

4. 实操复盘:一次真实项目的排查与修复记录

前面把原理和方案都过了一遍,这一部分我完整记录一次实际排查过程,包括思路、命令、踩坑点,给你一个可以照着做的实操参考。

4.1 背景与现场

某天项目群里反馈,一个统计报表接口突然报 500 错误。看了一下日志,核心错误就是 1055。这个报表在升级数据库之前一直正常,MySQL 从 5.6 升级到 5.7 之后就出现了这个问题。

这个报表的 SQL 大概长这样:

SELECT DATE(created_at) AS day, user_id, user_name, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE created_at >= '2024-01-01' GROUP BY DATE(created_at), user_id;

报错信息指向user_name这一列。原因是user_id在orders表里不是主键,虽然业务上 user_id 对应唯一的 user_name,但 MySQL 无法确认这种依赖关系,所以判定为“未分组的非聚合列”。

4.2 排查步骤

第一步:确认 sql_mode。

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

结果里确实有ONLY_FULL_GROUP_BY。确认是这个报错来源。

第二步:在测试库复现 SQL。

把 SQL 原封不动地在测试库跑一遍,确认报 1055。再把user_name从 SELECT 中临时移除,查询可以正常执行。进一步确认就是user_name引发的问题。

第三步:确认字段依赖关系。

查一下orders表结构和索引:

SHOW CREATE TABLE orders;

发现user_id有普通索引,但不是唯一索引。users表里id是主键,user_name属于普通字段。MySQL 的 ONLY_FULL_GROUP_BY 检测功能依赖,是基于表上已有的主键/唯一键推断的,orders.user_id不是唯一键,users.user_name也不是唯一键,所以推断不出来。

4.3 方案选型

当时我评估了几个方案:

  • 改 SQL:把user_name加进 GROUP BY。改动很小,报表层的逻辑不变。
  • JOIN users 表:让 user_name 从 users 表里实时取,但这需要改 SQL 结构,关联的字段还是可能触发问题。
  • 直接去掉 sql_mode 里的 ONLY_FULL_GROUP_BY:太粗暴,影响全局所有查询的严谨性。
  • 用 ANY_VALUE:也是个办法,但 MySQL 5.6 不支持。

最终选了第一种:把user_name加进 GROUP BY,即:

SELECT DATE(created_at) AS day, user_id, user_name, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE created_at >= '2024-01-01' GROUP BY DATE(created_at), user_id, user_name;

虽然多了一个分组字段,但因为 user_name 本来就和 user_id 一一对应,不会放大行数,查询结果与预期一致,改完上线再没出过问题。

4.4 过程中的额外发现

排查时还发现了一个隐藏问题:报表 SQL 用了DATE(created_at)作为分组字段,导致created_at上的索引完全用不上。数据量大了以后,这个查询走了全表扫描。

顺手优化了一下:把 WHERE 条件里的时间过滤改写为created_at >= '2024-01-01' AND created_at < '2024-02-01',范围可以命中索引。同时把分组字段改为先按created_at的日期分组的需求,暂时没有更好的索引方案,就维持原样。这一步算是排查 1055 时额外赚到的性能收益。

5. 常见问题与排查技巧速查表

这部分把日常遇到的一些高频问题整理成一张速查表,方便你遇到时快速对照。

问题原因解决方案
报错 Expression #1 is not in GROUP BY clauseSELECT 里的普通列没进 GROUP BY把该列加入 GROUP BY,或用聚合函数包裹
升级到 5.7/8.0 后大量 SQL 报 1055默认启用了 ONLY_FULL_GROUP_BY优先改 SQL;临时应急可改 sql_mode
JOIN 查询时关联表的字段报 1055关联字段不满足功能依赖加入 GROUP BY,或改为子查询取数
聚合函数嵌套时报 1055SELECT 里有表达式同时包含聚合与非聚合部分拆开表达式,或把嵌套部分也纳入 GROUP BY
使用 DISTINCT 与 GROUP BY 混用时报错逻辑冗余导致检查冲突去掉 DISTINCT,分组查询本身已去重
ORM 框架生成的 SQL 报 1055框架自动生成的 SELECT 列与 GROUP BY 不匹配用框架提供的聚合查询 API,或改写为原生 SQL
改了 sql_mode 后重启失效没有写入配置文件修改 my.cnf/my.ini 后重启 MySQL

5.1 容易忽略的几个细节

第一,报错里的 Expression #N 指的是 SELECT 列表中第 N 个表达式,从 1 开始计数。遇到报错先看 N 是多少,直接定位到 SELECT 列表的第 N 列,排查效率会高很多。

第二,MySQL 8.0.13 及以后版本里,ONLY_FULL_GROUP_BY对功能依赖的检测能力更强,能够识别某些通过 JOIN 产生的依赖关系。所以同一个 SQL 在 5.7 报错、在 8.0 不一定报错。反过来,有些在 5.7 能跑的 SQL 在 8.0 反而报错,因为 8.0 对 GROUP BY 的解析更严格了。

第三,如果是在存储过程、视图或触发器中使用了有问题的 SQL,改动时要特别注意:视图在创建时可能不校验 SQL 合法性,但查询视图时会报 1055。遇到这种情况,要重建该视图。

5.2 推荐的排查流程

我自己后来遇到 1055 报错,基本按这个流程走,基本能在几分钟内定位:

  1. 复制完整报错信息,锁定 Expression #N 的位置。
  2. 查看当前 SQL 的 SELECT 列表和 GROUP BY 列表,标出不在 GROUP BY 里、也没被聚合函数包裹的列。
  3. 确认这些列是否有唯一性依赖的依据(主键、唯一索引、JOIN 关系)。
  4. 按优先级选择:改 SQL 加入 GROUP BY -> 用聚合函数包裹 -> 用 ANY_VALUE -> 子查询取数 -> 最后才考虑动 sql_mode。
  5. 改完在测试环境跑一遍,对比修改前后的结果集行数和关键指标,确认结果无变化。

6. 针对不同使用场景的建议

不同身份的人,遇到 1055 报错的应对策略其实不太一样。我把常见角色和对应建议整理一下。

6.1 如果你是后端开发

你写业务 SQL 时,建议从第一步就养成好习惯:

  • 写 GROUP BY 时,SELECT 列表里只放分组字段和聚合函数。
  • 如果确实需要展示非分组字段的值,想清楚自己要的是“任意值”还是“特定条件下的值”。前者用 ANY_VALUE 或 MIN/MAX,后者用窗口函数或子查询。
  • 使用 ORM 时,尽量避免 select 实体对象但 group by 部分字段的操作,这很容易触发 1055。正确做法是用专门的查询对象或自定义 SQL。

6.2 如果你是 DBA 或数据库管理员

如果线上数据库已经大量存在依赖非严格模式的 SQL,直接开启 ONLY_FULL_GROUP_BY 会导致大量业务报错。建议分阶段处理:

  1. 先在测试环境开启 ONLY_FULL_GROUP_BY,找出所有有问题的 SQL。
  2. 逐一分析这些 SQL,能改则改,不能改的登记在案。
  3. 在维护窗口期修改生产环境 sql_mode,观察业务报错情况。
  4. 优先保证存量 SQL 修正后再切换,避免某天突然改配置导致大面积故障。

6.3 如果你是架构师或技术决策者

从这个报错能看出一个更深层的问题:数据库从 5.6 到 5.7/8.0 的升级,不仅仅是版本号的变化,更是 SQL 语义严谨度的跃迁。在升级前一定要评估所有历史 SQL 的兼容性。

我们当时做版本升级时,专门写了一个扫描脚本,把库里的所有存储过程、视图、定时任务里的 SQL 全部跑一遍,筛选出所有带 GROUP BY 的语句,提前修掉潜在问题。这个脚本后来救了我们很多次,因为有些埋得很深的报表 SQL,平时根本没执行到,真到月底跑的时候才发现问题,那时候再改就很被动了。

强烈建议:任何涉及 MySQL 大版本升级的项目,都安排一个“GROUP BY 兼容性专项排查”环节。这个报错只是其中一小部分,但它是排查 SQL 兼容性的一个很好的入口。

7. 写到最后:一点个人体会

MySQL 1055 报错本身并不复杂,但它牵出的问题往往不小。我在实际项目里见过因为这个问题导致月报出数失败、因为临时去掉了 ONLY_FULL_GROUP_BY 导致报表数据偶发不一致、因为升级后没排查导致第二天业务量上来时大量超时的情况。

最后分享两个小细节。第一个:排查 1055 时,先看 Expression #N 里的 N,能省一半时间。第二个:如果同一台服务器上有多个业务库,改全局 sql_mode 的影响面比你想的要大得多,动之前一定要确认所有库的所有查询都兼容,不然很容易按下葫芦浮起瓢。用SET SESSION先验证,再考虑SET GLOBAL,永远是最稳妥的路径。

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

四大名著NLP实战:情感分析与文本挖掘全流程

我正式接触自然语言处理&#xff0c;是在一门文本挖掘课上&#xff0c;当时的练习语料是某电商平台的商品评论。数据很规整&#xff0c;观点很直白&#xff0c;模型跑完也能出个准确率&#xff0c;但我心里总觉得少了点意思——真实世界里的中文&#xff0c;哪里是那么听话的句…

作者头像 李华
网站建设 2026/10/19 12:33:45

VS2026+Win10下C++数据导出模块开发实战与踩坑记录

1. 先说清楚&#xff1a;这一节到底在做什么我先把这个标题拆开给大家看&#xff0c;“板凳”是我自己常用的昵称&#xff0c;圈里的朋友都这么叫&#xff1b;“枯藤”是手上一个开发了挺久的项目的代号。标题里的vs2026指Visual Studio 2026这代IDE&#xff0c;win10就是我们的…

作者头像 李华
网站建设 2026/10/19 12:33:08

Agent源码拆解:12行代码实现Reaction消息回调绑定

开头先说一个可能有点反直觉的结论&#xff1a;我断断续续读了几个开源Agent项目的源码&#xff0c;发现真正决定Agent“好用不好用”的&#xff0c;往往不是那些动辄上千行的规划器、记忆管理器&#xff0c;反而是十几个小节甚至十几行的“小零件”。OpenClaw源码剖析里的reac…

作者头像 李华
网站建设 2026/10/19 12:25:48

YOLOv8轻量化改造:GhostConv替换C2f卷积的模型压缩实战

前一阵做边缘端目标检测部署&#xff0c;手里一个跑在NUC上的YOLOv8模型&#xff0c;参数量、功耗和延迟三样都卡在设备算力线上&#xff0c;剪枝、蒸馏、TensorRT量化折腾了一圈&#xff0c;效果都不太理想。后来换了个思路&#xff1a;不动训练范式&#xff0c;直接从模型结构…

作者头像 李华
网站建设 2026/10/19 12:22:52

SIGTERM vs SIGKILL:Android自动化测试中如何优雅清理monkey进程

有段时间我经常要处理自动化测试设备的残留进程问题。跑完Monkey压测后&#xff0c;设备上偶尔会留下几个monkey进程&#xff0c;拖慢后续任务。一开始清理方式很粗暴&#xff0c;用kill -9硬杀。结果某次批量跑完测试后&#xff0c;新启动的任务频繁报端口冲突和日志目录被占用…

作者头像 李华
网站建设 2026/10/19 12:22:48

QT多线程实战:QThread、锁与信号槽的踩坑与对策

简介&#xff1a;一套面向Qt/C开发者的多线程学习资源&#xff0c;聚焦QThread线程类、run()重写、moveToThread对象迁移及信号槽跨线程交互等核心机制&#xff0c;适合需要掌握并发编程、优化后台任务处理的中级开发者。压缩包内共十三个文件&#xff0c;整体约2.16MB&#xf…

作者头像 李华