
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_modeonly_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订单IDuser_id下单用户IDproduct_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 clauseSELECT 列表中的第 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 BYSELECT 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 连接串里追加sessionVariablessql_mode...来指定会话级别的 sql_mode。比如原来连接串是jdbc:mysql://localhost:3306/mydb?useSSLfalseserverTimezoneAsia/Shanghai改成jdbc:mysql://localhost:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghaisessionVariablessql_modeSTRICT_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_modeJOIN 查询时关联表的字段报 1055关联字段不满足功能依赖加入 GROUP BY或改为子查询取数聚合函数嵌套时报 1055SELECT 里有表达式同时包含聚合与非聚合部分拆开表达式或把嵌套部分也纳入 GROUP BY使用 DISTINCT 与 GROUP BY 混用时报错逻辑冗余导致检查冲突去掉 DISTINCT分组查询本身已去重ORM 框架生成的 SQL 报 1055框架自动生成的 SELECT 列与 GROUP BY 不匹配用框架提供的聚合查询 API或改写为原生 SQL改了 sql_mode 后重启失效没有写入配置文件修改 my.cnf/my.ini 后重启 MySQL5.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 报错基本按这个流程走基本能在几分钟内定位复制完整报错信息锁定 Expression #N 的位置。查看当前 SQL 的 SELECT 列表和 GROUP BY 列表标出不在 GROUP BY 里、也没被聚合函数包裹的列。确认这些列是否有唯一性依赖的依据主键、唯一索引、JOIN 关系。按优先级选择改 SQL 加入 GROUP BY - 用聚合函数包裹 - 用 ANY_VALUE - 子查询取数 - 最后才考虑动 sql_mode。改完在测试环境跑一遍对比修改前后的结果集行数和关键指标确认结果无变化。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 会导致大量业务报错。建议分阶段处理先在测试环境开启 ONLY_FULL_GROUP_BY找出所有有问题的 SQL。逐一分析这些 SQL能改则改不能改的登记在案。在维护窗口期修改生产环境 sql_mode观察业务报错情况。优先保证存量 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永远是最稳妥的路径。