
先说结论如果你在 MySQL 5.7 及以上的版本里碰到ERROR 1055 (42000)先别急着骂数据库更别一上来就找配置文件把ONLY_FULL_GROUP_BY关掉。这条错误本质上是 MySQL 在提醒你分组查询写得不够严谨。前几天我刚好处理过这么一件事。项目里负责统计功能的同事提交了一个查询本地跑得好好的结果一部署到测试环境整个接口直接报错。他把日志截图甩过来内容就是下面这条ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column mall_order.user_name which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_modeonly_full_group_by报错信息写得其实挺直白SELECT列表里的第 1 个字段user_name既不在GROUP BY子句里也不是聚合函数COUNT、SUM、MAX这些的入参。在启用了ONLY_FULL_GROUP_BY这种 SQL 模式的情况下MySQL 直接把这条 SQL 拒了。当时同事的代码大概长这样SELECT user_name, COUNT(*) AS order_count, SUM(order_amount) AS total_amount FROM mall_order WHERE order_status 1 GROUP BY user_id;他当时的想法很典型我按user_id分组然后顺手把对应的user_name查出来反正一个user_id只对应一个user_name取哪条都一样。这个业务逻辑单看没问题但 MySQL 没法证明user_id和user_name一定是一对一。除非你有明确的唯一键约束并且查询里能推导出这种依赖关系否则它只能按“不确定字段”处理然后给你一个 1055。这个错误名字听起来挺唬人但绝大多数情况不是 SQL 语法错了而是 SQL 语义在“分组之后我能访问哪些列”这个问题上不符合标准。MySQL 5.7 之后把ONLY_FULL_GROUP_BY放进了默认 SQL 模式大量从 5.6 及更早版本迁过来的应用第一次跑就翻车基本都在这个点上。这篇随记想把 1055 的根因、排查路径和长期应对办法一次性说透。如果你刚接触 MySQL或者正被线上分组查询报错折磨看完应该能省不少时间。1. 一场凌晨的发布事故1055报错长什么样先说回同事遇到的那次发布。现象是接口在本地开发环境一切正常到了测试环境就挂。日志里没有别的异常就一条 1055。由于是统计查询SQL 本身不复杂问题出在哪里其实一眼就能定位。但真正让我觉得值得写下来的不是这条报错本身而是团队里没人意识到“不同版本的 MySQL 对分组查询的容忍度不一样”。补充一条更完整的报错信息方便搜索引擎能命中、也方便你对着排查ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column mall_order.user_name which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_modeonly_full_group_by有个细节值得注意报错信息里写了Expression #1意思是SELECT列表的第 1 个位置出了问题。如果你的查询有 10 个字段可能第 5 个字段才是不合规的那个。排查的时候从#N这个序号往前数 SELECT 列表就行准确率很高。这种报错的“味道”和平时常见的语法错误完全不一样。语法错误是一眼假的少了逗号、多了括号、关键字拼错MySQL 在解析阶段就会告诉你。但 1055 是语义层面的审查它出现在查询分析阶段。也就是说你的 SQL 语法没问题、表存在、字段也存在执行计划都快要生成了MySQL 突然踩了刹车。1055 常见于三类场景老项目从 MySQL 5.6 或更早版本迁移到 5.7新项目使用了 5.7 版本但团队成员的习惯还停留在“宽松模式”原本没开ONLY_FULL_GROUP_BY的实例因为配置基线的调整被打开了。这三类场景我都踩过。所以后面的内容我尽量按实际项目里最可能的出现顺序来讲。2. 别急着关SQL模式理解ONLY_FULL_GROUP_BY的来龙去脉sql_mode是 MySQL 提供的一组行为开关每种模式代表一种约束或者兼容性策略。它不是一整个开关而是一串模式名组合在一起。正常情况下你执行下面这条语句就能看到当前生效的组合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而 1055 的报错就是其中ONLY_FULL_GROUP_BY这一项在起作用。很多人不理解的是为什么 MySQL 5.7 之后非要把这个模式默认打开过去不是跑得好好的吗这里要说一个背景。在 MySQL 5.6 及更早的版本里ONLY_FULL_GROUP_BY默认是不开启的。这意味着你可以写出下面这种非常随意的查询SELECT user_id, user_name, user_phone, COUNT(*) AS order_count FROM mall_order GROUP BY user_id;user_name和user_phone既没出现在GROUP BY里也不是聚合函数的结果。MySQL 不报错而是直接从分组里“随便挑一行”给你返回。关键是它挑哪一行不受你控制。拿现实生活打个比方全班按小组分组做项目汇报评委要求每组选一个人上台。正常流程是小组内部确定人选再由这个人上台。但宽松模式下的 MySQL 不这样它直接在组里随便抓一个人推上台。这个人是谁取决于数据页里谁先被扫到、索引怎么走、连接顺序怎么定。稍后同一条 SQL 再跑一次可能上台的又是另一个人。这种灵活性表面上是方便实际上埋了两个雷第一结果不稳定。索引选择、数据分布一变同一个分组内返回的“那一条”可能就变了报表前后对不上测试环境与生产环境结果不一致。第二没有任何提示。开发者会误以为自己的写法是被支持的直到某一天数据量变大执行计划发生变化突然出现诡异的数据波动排查成本极高。所以 MySQL 从 5.7.5 开始把ONLY_FULL_GROUP_BY纳入默认 SQL 模式。它的要求很明确所有在SELECT、HAVING、ORDER BY中出现的列要么出现在GROUP BY子句里要么被聚合函数包裹要么能证明与分组列存在函数依赖关系。这里“函数依赖”是个关键概念。比如user_id是用户表的主键user_name就函数依赖于user_id。既然一个user_id决定唯一一行数据那么在这一行里取user_name是确定的、不会有歧义。MySQL 允许在这种情况下省略user_name的聚合处理。举个合法例子SELECT user_id, user_name, COUNT(*) AS order_count FROM user_info GROUP BY user_id;因为user_id是主键user_name函数依赖于它这条 SQL 在ONLY_FULL_GROUP_BY下是可以通过的。这一点很多人不知道也不愿意查文档一遇到报错就跑去改全局配置错过了 MySQL 本来允许的合法路径。理解到这个层面你就明白1055 不是 MySQL 故意刁难开发者而是它想让你把“分组后取哪个值”这个意图表达得足够清楚。场景MySQL 5.6默认MySQL 5.7默认SELECT 非分组列直接返回任意一行不报错不通过报 1055SELECT 函数依赖列允许允许SELECT 聚合函数允许允许结果稳定性可能不稳定严格约束稳定可预期3. 三条主攻路线改SQL、改配置、改查询逻辑到底怎么选1055 的解决方案网上能搜到的基本集中在三类。但很多帖子只讲操作不讲取舍导致不少人跟着抄完了才发现坑更深。我这里按实用优先级逐一拆开讲。3.1 方案一重写 SQL让每个非聚合列都有明确归属这是我最推荐的第一步也是根治路径。如果你能改业务代码里的 SQL优先在这里解决。核心思路就一句话让每个非聚合列要么进GROUP BY要么套聚合函数要么通过子查询把分组和明细拆开。还是拿刚才那个例子业务诉求是“按用户分组统计订单数、订单总额同时展示用户名”。如果确认用户名在同一用户下是不变的可以直接套聚合函数SELECT MAX(user_name) AS user_name, COUNT(*) AS order_count, SUM(order_amount) AS total_amount FROM mall_order WHERE order_status 1 GROUP BY user_id;MAX(user_name)在业务上等价于“从该用户所有订单里取最大用户名”。如果用户名确实固定结果就和直接取user_name一样。这种写法改变了字段的语义但仍然清晰、可预期、MySQL 也接受。如果嫌MAX语义绕MySQL 5.7.5 之后还提供了ANY_VALUE()SELECT ANY_VALUE(user_name) AS user_name, COUNT(*) AS order_count, SUM(order_amount) AS total_amount FROM mall_order WHERE order_status 1 GROUP BY user_id;ANY_VALUE(user_name)的意思是“这个分组里任意一条的用户名都行”。它比MAX(user_name)更诚实——我不关心是哪一个我只想消除语法报错。代价同样是结果不确定。如果只在同一分组内取一个业务上完全相等的字段问题不大如果这个字段在不同行里有不同值那么ANY_VALUE就是在掩盖数据质量问题。更严谨的做法是通过子查询把“先分组算汇总再回表取明细”两步分开SELECT u.user_name, t.order_count, t.total_amount FROM ( SELECT user_id, COUNT(*) AS order_count, SUM(order_amount) AS total_amount FROM mall_order WHERE order_status 1 GROUP BY user_id ) t JOIN user_info u ON u.user_id t.user_id;这一步的逻辑就非常标准了子查询只汇总user_id维度的数据外层再通过关联拿用户名。用户名的来源是user_info表的主键关联不存在任何歧义。代价是多了一次关联但因为user_id上有索引性能通常可以接受。如果后续要加筛选条件也更不容易被ONLY_FULL_GROUP_BY规则绊住。3.2 方案二调整 sql_mode把报错临时压下去这种做法的核心是去掉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;但全局修改同样不能持久化MySQL 重启后又回到配置文件里的值。真正持久化的办法是改配置文件。Linux 下通常是/etc/my.cnfWindows 下一般是my.ini在[mysqld]段里写[mysqld] sql_modeSTRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION保存后用systemctl restart mysqld重启实例。这玩意儿的代价是什么你只是把报错藏了起来并没有把 SQL 写对。一旦分组查询里出现了“随机挑一行”的用法线上数据就可能出现难以解释的跳动。我之前接触过一家公司的报表系统隔三差五有人反馈“同一个 SQL 上午查和下午查结果不一样”。最后定位到根因就是有人在迁移时为了兼容第三方系统的历史 SQL把ONLY_FULL_GROUP_BY关掉了结果分组查询里几十个裸字段开始随机取值。数据从哪一行来的谁都不知道。所以我的建议非常明确ONLY_FULL_GROUP_BY只适合在迁移过渡期临时关闭或者作为历史遗留 SQL 改造完成之前的“缓刑期”手段。它不该是一个长期存在的配置。3.3 方案三利用函数依赖让查询合法化前面提过MySQL 允许查询里出现函数依赖于分组列的字段。这个特性在日常查询里很有用尤其在联合查询时可以省掉很多多余的聚合函数。一个我会经常使用的示例SELECT o.user_id, u.user_name, u.user_phone, COUNT(*) AS order_count FROM mall_order o JOIN user_info u ON u.user_id o.user_id GROUP BY o.user_id;这里user_name、user_phone都不在 GROUP BY 中但它们函数依赖于user_info表的主键user_id。因为连接条件让u.user_id o.user_id而o.user_id又是分组列MySQL 能推导出这些字段是确定的于是放行。这个方案对“明细表 维度表”的经典模型特别有效。你要按事实表某个维度分组同时显示维度的描述性字段只要维度表那个字段对应的键是主键或唯一键就能直接写。不需要MAX不需要ANY_VALUE结果也完全确定。但要注意函数依赖推导在跨表场景下是有前提的连接条件必须是等值连接且参与推导的列必须是目标表的唯一键。如果连接条件里用了函数包裹比如ON DATE(u.create_time) o.create_date推导大概率失败。三种方案对比一下给个选型参考判断维度重写 SQL调整 sql_mode利用函数依赖是否根治是否是是否改变结果语义可能需要 MAX/ANY_VALUE不改变但结果可能随机不改变结果确定对性能影响需看具体写法无直接影响低连接走索引即可适合场景业务代码可改历史遗留 SQL 过渡期维度表关联场景4. 本地能跑、生产报错的排查链路版本差异和现场复现现在回头说那天的排障过程。我当时没有直接帮他把 SQL 改掉而是先带着他把整个排查链路走了一遍因为这种“本地好、线上炸”的问题以后一定会再遇到。第一步确认两边 MySQL 版本。本地一查SELECT VERSION(); -- 5.6.51-log测试环境再查SELECT VERSION(); -- 5.7.30-log到这里事情已经清楚了八成。MySQL 5.6 默认没开ONLY_FULL_GROUP_BY5.7 默认开了。同一个 SQL一个睁一只眼闭一只眼一个严格审查。第二步确认生效中的 sql_modeSELECT GLOBAL.sql_mode\G SELECT SESSION.sql_mode\G本地输出的字符串里没有ONLY_FULL_GROUP_BY测试环境输出的字符串里第一项就是它。这个差异是决定性的。第三步在本地复现问题。最简单的方式不需要装一个 5.7 实例直接在本地会话里临时手动打开这个模式SET 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;然后再跑同一条 SQL1055 立刻冒出来。这就证明问题不在数据而是模式差异导致的行为差异。能当场复现对后面说服开发同事非常有用也省去了反复上线验证的折腾。第四步做应急处理。测试环境已经有人阻塞了先把服务恢复才重要。因为当前连接执行SET SESSION只对当前会话有效而中间件连接池建立的连接并不会听话所以我最终还是建议用最稳妥的方式允许他在测试环境的全局配置里临时去掉ONLY_FULL_GROUP_BY重启 MySQL 恢复服务。同时明确记录时间点约定一个 deadline要求三天内所有问题 SQL 全部改完第四天把配置改回来。这中间有个很容易犯的错有人认为直接在my.cnf里删掉这一项就完事但其实 5.7 版本如果配置里没有显式写sql_mode默认值反而会带上ONLY_FULL_GROUP_BY。也就是说你以为删干净了重启后又回来了。正确做法是像我上面那样显式写出一整串模式想要哪几个就只能写哪几个。排障链路走完之后得到的结论其实就一句话本地与生产环境在sql_mode上出现了配置漂移。这种漂移比代码漂移更隐蔽因为它不报错、不警告只会让同一段代码在不同环境里跑出不同行为。版本的统一和配置的统一必须同时做。5. 最容易漏的边角情况ORDER BY、HAVING和函数包裹1055 并不会只出现在最简单的SELECT ... GROUP BY里。实际业务 SQL 一旦复杂起来报错的地方往往在你想不到的位置。5.1 ORDER BY 裸字段很多开发者以为只要 SELECT 列表合法就完事了忘了 ORDER BY 同样受ONLY_FULL_GROUP_BY约束。比如SELECT user_id, COUNT(*) AS order_count FROM mall_order GROUP BY user_id ORDER BY user_name;user_name既不在分组里也不是聚合结果这条 SQL 照样报 1055。解决办法要么把user_name也放进分组要么用MIN(user_name)、MAX(user_name)排序ORDER BY MAX(user_name);这种边角情况在“按维度分组然后按描述字段排序”的需求里特别常见。5.2 HAVING 里引用未分组的列HAVING 是对分组结果做过滤但很多人会顺手写错SELECT user_id, COUNT(*) AS order_count FROM mall_order GROUP BY user_id HAVING user_name 张三;过滤条件user_name 张三里的user_name不是聚合列也不是分组列MySQL 立即报 1055。正确写法是把过滤条件放到 WHERE 里如果确实要从分组结果里判断也必须使用MAX(user_name) 张三这种聚合表达。5.3 SELECT 列表里用了函数但没做聚合包裹这个是重灾区。比如按用户分组后想顺便展示下单日期的年份SELECT user_id, DATE_FORMAT(create_time, %Y-%m-%d) AS create_date, COUNT(*) AS order_count FROM mall_order GROUP BY user_id;DATE_FORMAT(create_time, %Y-%m-%d)是对裸列create_time做函数操作不是聚合操作所以依然违规。正确做法是聚合后在子查询外再处理或者直接用MAX(create_time)这类写法。类似的情况还有CASE WHEN、IF、字符串拼接等一切针对裸列做处理的表达式。你只要记住它们都没被聚合函数包住就都算裸列。5.4 SELECT * 与 GROUP BY 的组合开发里还有一种很常见的写法SELECT * FROM mall_order GROUP BY user_id;这条 SQL 的意图可能是“按用户去重查最近的订单”但它把整张表的所有列都放进了 SELECT 列表绝大多数是裸列直接 1055。遇到这种情况不要试图用SELECT *去绕过先把需求拆清楚你到底是想要每个用户的一条完整订单还是想要用户维度的汇总前者应该用窗口函数或者子查询后者必须明确列出聚合列。5.5 派生表绕行也要小心很多时候把问题 SQL 包一层子查询确实能让外层查询合法。我见过不少人被 1055 逼急了直接在原始 SQL 外面加了一层SELECT * FROM (原SQL) t然后就不报错了。原因是子查询内部确实做了分组外层的*引用的都是已经聚合后的结果不再是裸列。但这种方式容易养成坏习惯。它在语义上经常不对如果你原来就想在子查询里返回多个“非分组列”MySQL 会在子查询内部直接报 1055包几层都一样。而且多套一层子查询执行计划可能走全表扫描性能和可读性都下降。派生表是某些特定场景的合法解法不是通用逃生舱。6. 根本解法把“防1055”写进团队规范里处理完这次发布事故我没有止步于把 SQL 改对。因为只要团队整体意识不改变1055 一定还会以另外的面孔回来。那天之后我在项目组里定了三条规则。第一条统一 MySQL 版本与 sql_mode 基线。开发环境、测试环境、生产环境尽量用同一个大版本。数据库初始化脚本里必须显式声明sql_mode不依赖发行版默认值。把ONLY_FULL_GROUP_BY保留下来让开发者在本地就能碰到严格模式而不是等上了测试环境才发现问题。初始化命令可以直接加进部署脚本mysql -uroot -p -e SET GLOBAL sql_modeONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION;改配置文件更稳反正目的就一个让所有环境的sql_mode肉眼可见地一致。每次环境构建时顺手跑一下SELECT GLOBAL.sql_mode;放进交付检查单配置漂移问题基本就绝迹了。第二条把分组查询的评审点写进代码审查流程。凡是改动涉及GROUP BY的 SQL审查人必须确认三件事非聚合列是否真正依赖分组键聚合函数是否选得对查询结果是否在多次执行下保持稳定。有ANY_VALUE的地方必须额外注释说明这里为什么不在乎具体值。别小看这一条它可以把 1055 从“上线后故障”变成“评审时指出”的低成本问题。第三条对历史遗留的复杂 SQL 做渐进式改造。我检查过一些老项目的慢查询日志发现里面有大量“裸列 GROUP BY”的写法。这些查询在旧版本上跑了几年换版本后全成了合规风险。逐一手工改不现实但也不能永久禁用ONLY_FULL_GROUP_BY。我的做法是分批处理先用EXPLAIN和日志找出高频分组查询优先改报表和核心接口剩下低频查询在季度维护窗口内改完。改完一批就把sql_mode的严格程度提一档直到所有历史 SQL 都在严格模式下能跑。对了还有个小技巧值得分享。排查阶段如果不想手写一长串sql_mode可以直接用SELECT GLOBAL.sql_mode拿到当前完整值然后手工去掉ONLY_FULL_GROUP_BY之后复制回去。这样可以少打很多字也不会意外丢其他模式。回过头看MySQL 1055 这个报错其实是件好事。它逼着开发者在写分组查询时想清楚每一列的语义。只要环境配置统一、开发习惯规范、审查流程严格这个错误就基本不会再出现在你的发布日志里。最后再补一句如果你现在正被这个问题卡着优先改 SQL关模式只是缓兵之计。这既是技术选择也是团队习惯的取舍。