多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MySQL ONLY_FULL_GROUP_BY报错详解:从原理到解决方案

MySQL ONLY_FULL_GROUP_BY报错详解:从原理到解决方案 做开发的同学大概率都跟GROUP BY这个语法打过交道也大概率在某次升级或者新环境部署之后被一条ERROR 1055拍在脸上。报错信息很长什么 Expression #2 of SELECT list is not in GROUP BY clause什么 contains nonaggregated column第一次看到确实头皮发麻。这个报错的幕后推手就是 MySQL 默认开启的ONLY_FULL_GROUP_BY模式。这篇就把它从上到下讲透它是干什么的、为什么会突然开始报错、哪些写法会踩雷、有哪些解决方案以及我在实际运维中踩过的坑和总结的排查方法。无论你是在做报表统计还是维护老系统只要跟 MySQL 打交道这篇文章都能让你少走不少弯路。1. 先搞清楚ONLY_FULL_GROUP_BY 到底是什么1.1 先把报错信息翻译成人话MySQL 的报错信息向来偏法言法语我第一次看到 1055 也懵了。完整信息一般是这样的ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column test.orders.order_time which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_modeonly_full_group_by拆开看其实就是三层意思。第一报错码1055SQLSTATE 是42000这是 MySQL 专门为这个约束场景准备的错误码。第二Expression #2 of SELECT list is not in GROUP BY clause意思是你的 SELECT 列表里第 2 个字段没有出现在 GROUP BY 子句中。第三contains nonaggregated column说的是这个字段既没有被MAX、COUNT、AVG这类聚合函数包住也不是分组字段MySQL 不知道该拿它怎么办。说白了ONLY_FULL_GROUP_BY就是 sql_mode 里的一个强制检查开关当你用 GROUP BY 分组时SELECT 出来的普通字段必须是分组依据之一或者被聚合函数包裹。违反这条规则的 SQL 直接拒收不给任何侥幸空间。它和STRICT_TRANS_TABLES、NO_ZERO_DATE这些模式一样都是 MySQL 的行为约束条款只不过这一条专门管分组查询。1.2 MySQL 历史上的放纵与收紧很多老同学不理解为什么同样一条 SQL在 5.6 上跑得好好的升到 5.7 就炸了因为 5.6 及更早的版本MySQL 根本不检查这个。你写SELECT user_id, order_time, amount FROM orders GROUP BY user_id;它也能跑而且返回结果里 order_time 和 amount 是随机从组内某行取的。这个行为在官方文档里有个专门的词叫扩展了标准 SQL 的行为。听着像夸自己实际上是个大坑。因为返回的到底是哪一条记录的 order_time完全不可预期同一个 SQL 在不同时间跑结果都可能不一样。很多老项目里的数据错乱、对不上账根源就在这种隐性行为上。SQL 标准本身写得明明白白SELECT 列表、HAVING 条件、ORDER BY 排序里出现的非聚合字段都必须包含在 GROUP BY 中。MySQL 从 5.7.5 开始决定把这个口子收紧默认开启ONLY_FULL_GROUP_BY从此这些放纵写法统统报错。所以别怪 MySQL 折腾你它只是终于决定向标准看齐了。从 5.7 到 8.0这个默认行为一路延续版本越新SQL 写法就越需要讲规矩。1.3 默认 sql_mode 里都有谁查看当前模式用一条 SQL 就够SELECT sql_mode;。在 MySQL 8.0 的默认配置里你会看到这么一串ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION5.7 的默认串里还会多一个NO_AUTO_CREATE_USER8.0 已经把它移除。这些模式各管一摊STRICT_TRANS_TABLES控制严格模式下对数据长度、非法值的处理ERROR_FOR_DIVISION_BY_ZERO管除以零NO_ZERO_DATE禁止存 0000-00-00 这种日期。排查 1055 问题时我习惯先把整个 sql_mode 打出来看一眼防止改了半天发现是别的模式在作怪。另外提醒一句网上很多老文章贴的 sql_mode 字符串是五六年前的抄之前先确认版本不然可能包含 8.0 已经不认识的模式名设置直接失败。2. 报错是怎么触发的三处雷区逐一排查2.1 SELECT 列表里的漏网之鱼最典型的触发场景就在 SELECT 列表。比如订单表orders有id、user_id、order_time、amount四个字段你想统计每个用户的订单量顺手把该用户最近的下单时间也带出来写了这么一条SELECT user_id, order_time, COUNT(*) AS cnt FROM orders GROUP BY user_id;这条在 5.6 能跑在 5.7 以上直接 1055。报错里说的 Expression #2对应的就是 order_time。原因很简单分组依据只有 user_idorder_time 不是分组依据也没做聚合MySQL 没法保证这个字段在组内取值唯一自然不许你 SELECT 出来。GROUP BY 多个字段的时候规则同样成立。比如按user_id和DATE(order_time)分组SELECT user_id, DATE(order_time) AS order_day, COUNT(*) AS cnt FROM orders GROUP BY user_id, DATE(order_time);这里 SELECT 里的字段user_id和order_day正好是分组键MySQL 允许在 GROUP BY 里用 SELECT 别名所以合法。但如果你 SELECT 的是原始列order_time而分组键是DATE(order_time)那照样报错因为order_time并不能由user_id, DATE(order_time)唯一决定——同一天可能有多条不同时间的订单。2.2 HAVING 和 ORDER BY 也会踩雷不少人以为只要 SELECT 里的字段合规就没事忽略了 HAVING 和 ORDER BY这两个地方同样受约束。比如SELECT department, COUNT(*) AS cnt FROM employees GROUP BY department HAVING salary 10000;salary 既不在 GROUP BY 里也没被聚合用它做 HAVING 过滤照样触发 1055。ORDER BY 同理SELECT department, COUNT(*) AS cnt FROM employees GROUP BY department ORDER BY salary;这条想表达按薪资排序部门但 salary 不是分组字段也不是聚合结果MySQL 直接拒绝。正确写法应该是ORDER BY MAX(salary)或者ORDER BY cnt先想清楚你到底要按什么排序。所以排查 1055 时不要只盯 SELECT 那几列把 HAVING、ORDER BY 全部过一遍才不容易漏。2.3 功能依赖一个隐藏的免死金牌ONLY_FULL_GROUP_BY不是铁板一块。从 5.7.5 开始MySQL 支持功能依赖检测。简单说如果分组字段是主键或唯一键那么同一行里的其他字段都被这个键唯一确定取哪个都一样逻辑上没有歧义MySQL 会自动放行。比如SELECT id, name, age, MAX(score) AS best_score FROM student GROUP BY id;id 是主键name、age 由 id 唯一决定所以这条 SQL 不报错即使 name 和 age 没在 GROUP BY 里也没被聚合。这个特性在按主键分组取整行的场景很好用。但要注意功能依赖只对主键、唯一键生效如果你拿一个普通字段分组哪怕这个字段在当前表里恰好没有重复值MySQL 也不会做值唯一性推理该报错还是报错。别心存侥幸。2.4 完整案例从报错到修复的全过程用一个真实场景把整个排查串起来。还是那张orders表需求是查询每个用户最近一笔订单包含商品名、下单时间、金额。最直观的写法是SELECT user_id, product_name, order_time, amount FROM orders GROUP BY user_id;一跑就报 1055报错指向 product_name。这时候按我前面说的三段拆分法来看分组键只有 user_idproduct_name、order_time、amount 都不是分组键也没聚合三者全都不合规。但需求又需要这些字段怎么办如果只要最近一笔的完整信息正确做法是先找出每个用户最近的下单时间再回原表取整行SELECT o.user_id, o.product_name, o.order_time, o.amount FROM orders o JOIN ( SELECT user_id, MAX(order_time) AS max_time FROM orders GROUP BY user_id ) t ON o.user_id t.user_id AND o.order_time t.max_time;如果这个表允许同一个用户在同一秒下两单用 order_time 作为关联条件还可能出现重复行。更稳妥的做法是关联到具体的主键 id把子查询改成查出每个用户最新订单的主键 id再 JOIN 回去。这就是合规写法比GROUP BY 一把梭麻烦的地方但结果完全可控不会出现任何随机值。3. 解决方案全解析从改 SQL 到改配置3.1 首选方案把 SQL 改写标准遇到 1055我的第一反应永远是改 SQL而不是改配置。反向思考一下既然分组后普通字段必须有确定含义那你的需求本质上一定是下面三种之一——要么这个字段该进 GROUP BY要么它该被聚合要么你和数据库都明确取哪个都无所谓。把需求想清楚SQL 自然就写对了。该进 GROUP BY 的场景比如统计用户维度的每日订单SELECT user_id, DATE(order_time) AS order_day, COUNT(*) AS cnt FROM orders GROUP BY user_id, DATE(order_time);这里order_day和user_id就是分组键符合语法要求语义也清楚。该聚合的场景比如想看每个部门的薪资结构SELECT department, MAX(salary) AS max_salary, MIN(salary) AS min_salary, AVG(salary) AS avg_salary, COUNT(*) AS emp_cnt FROM employees GROUP BY department;用聚合函数的好处是语义无可争议执行路径也明确。GROUP_CONCAT 也很好用统计每个部门都有谁时一句GROUP_CONCAT(name)就能拼出名单比在应用层循环拼接省心得多。3.2 一个高频需求的通用解法分组后取某字段极值对应的整行每组取最新一条和每组取金额最大一条是报表里出现频率极高的需求也是 1055 报错的重灾区。这类需求不能简单地用 ANY_VALUE 解决因为你需要的是和极值同属一行的其他字段。我总结了两套通用写法。第一套是子查询关联兼容 5.7 和 8.0。核心思想是先把分组和极值算出来再回表取整行SELECT o.* FROM orders o JOIN ( SELECT user_id, MAX(order_time) AS max_time FROM orders GROUP BY user_id ) t ON o.user_id t.user_id AND o.order_time t.max_time;第二套是窗口函数只在 8.0 可用但写法更简洁性能也通常更好SELECT user_id, product_name, order_time, amount FROM ( SELECT user_id, product_name, order_time, amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn FROM orders ) ranked WHERE rn 1;ROW_NUMBER()给每个用户的订单按时间倒序编号外一层取 rn 1 就是最新订单。这套方案能精确消除同一用户多行并列的歧义如果你希望并列的都返回把ROW_NUMBER()换成RANK()就行。8.0 的项目我建议优先掌握窗口函数它治好了很多以前要写半天的先分组再回表问题。3.3 ANY_VALUE()明确告诉 MySQL 取哪个都行有些场景你确实不关心组内该字段的具体值。比如按用户分组统计订单数想把用户性别带上——正常来说一个人的性别在同一组内是一致的取哪条都一样。这时候可以用ANY_VALUE()包裹SELECT user_id, ANY_VALUE(gender), COUNT(*) AS cnt FROM orders GROUP BY user_id;ANY_VALUE的语义是从组内随便挑一个值返回等于你显式向优化器和后来的维护者声明这个字段不参与逻辑判断取谁都行。这个函数只在你确定组内该字段值都相同或者取哪个都无所谓时才可以用。我见过有人拿ANY_VALUE(amount)配MAX(order_time)用结果报表里出现时间是最新订单的、金额却是另一条订单的这种跨行错配数据非常难排查基本是定时炸弹。3.4 修改 sql_mode会话级、全局级、配置文件有些情况下你没法改应用里的 SQL比如老系统维护阶段SQL 全写死在代码里。这时候才轮到改 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;这里强烈不建议手抄整串模式值。更安全的做法是用 REPLACE 函数只摘掉ONLY_FULL_GROUP_BY保留其他所有模式SET SESSION sql_mode REPLACE(sql_mode, ONLY_FULL_GROUP_BY, ); SET GLOBAL sql_mode REPLACE(sql_mode, ONLY_FULL_GROUP_BY, );配置文件层面编辑my.cnfLinux或my.iniWindows在[mysqld]段下加一行[mysqld] sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION保存后重启 MySQL 生效。但要注意如果你用的是云数据库比如云厂商的 RDS很多有自己的参数组模板你在服务器上的 my.cnf 改了也会被平台侧覆盖。这种要去云控制台找参数组或参数模板找到 sql_mode 改掉再应用到实例。3.5 方案对比到底该选哪个做个表直观对比一下方案适用场景风险推荐度改写 SQL 符合标准所有开发环境、新功能开发逻辑复杂时改造成本高最高窗口函数取组内极值行8.0 环境、每组最新/最大需求需要确认版本和语法高ANY_VALUE()组内字段值相同或取值无所谓的列误用会产生跨行错配数据高但必须慎用聚合函数需求就是求最大、最小、平均、计数语义需事先想清楚高会话级改 sql_mode临时验证、当前连接排查只影响当前连接中全局级改 sql_mode存量系统短期过渡新老行为不一致治标不治本低配置文件改 sql_mode数据库层兜底影响全库隐患大最低优先级记住一句话先改 SQL再想 ANY_VALUE实在不行才动 sql_mode。配置能不动就不动。4. 实战中的高频问题与排查技巧4.1 改了配置为什么还是不生效群里问得最多的就是这个问题。改完 sql_mode 还报 1055基本逃不出三个原因。第一改的是 GLOBAL 级但应用用的是旧连接。前面说了GLOBAL 只对新建连接生效连接池里的老连接要等超时回收或者应用重启才会重新建立。所以改完全局配置应用侧不一定立刻好。第二配置文件被覆盖。云数据库、容器化部署的平台侧通常有参数模板你在容器里的配置改了平台一滚动发布又拉回默认值。这种要去平台控制台改不能只改容器文件。第三改错了层级。你在客户端执行的是SET SESSION只对当前这个连接有效换个工具连进来又恢复原样。我的排查顺序是先执行SELECT SESSION.sql_mode;确认当前连接有没有生效再看SELECT GLOBAL.sql_mode;是否同步最后查配置文件。如果配置文件和 GLOBAL 不一致重启后会被配置文件覆盖这也是常见的改完重启又回去了的原因。4.2 生产环境到底要不要关掉这个模式我的态度很明确不要关除非万不得已。ONLY_FULL_GROUP_BY是帮你提前暴露逻辑问题的不是制造问题的。很多在 5.6 里能跑的 SQL本身就是写法不规范返回的值带有随机性只是当时没人察觉。关了模式等于把炸弹的引信接回去短期看起来安静了长期随时可能炸出数据对不上的大问题。如果存量系统非过渡不可我的做法是用全局级修改做短期兜底同时立刻把有问题的 SQL 清单捞出来通过慢查询日志或者performance_schema.events_statements_summary_by_digest定位 1055 报错的来源 SQL然后逐条改写最终目标还是把默认模式恢复回去。这个过渡期最好不要超过一个迭代周期拖得越久遗留的非标 SQL就越多后面越难还债。4.3 容易被混淆的同类报错1055 之外还有几个错误容易搞混。一个是Column xxx in field list is ambiguous错误码 1052这是字段歧义比如多表 JOIN 时没加表别名和 ONLY_FULL_GROUP_BY 无关。另一个是语法错误 1064一般会有明确的语法提示。这三个错误码放一起别人问你报什么错最好把完整错误信息带出来别只说我报错了好像是 mysql 的。实际项目里我还遇到过一种情况同一个应用部分接口报 1055部分不报。这通常不是配置问题而是不同接口走了不同的 SQL 模板或者 ORM 动态拼 SQL 时某个查询模板刚好踩雷。先抓 SQL 再分析不要一上来就动全局配置。抓 SQL 用SHOW PROFILES;或者直接看应用日志里的异常堆栈比瞎猜高效得多。4.4 MySQL 5.7 与 8.0 的差异5.7.5 引入ONLY_FULL_GROUP_BY默认开启8.0 延续了这个默认所以这两个版本的用户都会遇到 1055。但 8.0 有几个新的注意点。一是窗口函数和 CTE 的出现让很多写法跟 5.7 完全不同。比如取每组最新一条5.7 只能靠子查询关联8.0 直接用ROW_NUMBER()。二是 8.0 移除了NO_AUTO_CREATE_USER这个模式名从老文章抄 sql_mode 字符串时容易踩坑。三是 8.0 对功能依赖的检测在某些场景更严格比如用 CTE 或派生表时MySQL 对列来源的追踪逻辑略有差异偶尔会出现5.7 不报错8.0 报错的情况。遇到版本差异问题最快的办法是直接在目标版本的官方文档搜functional dependency比看各种转载博客靠谱。5. 最后分享几条实操经验写这篇的时候我特意回想了一下这几年处理过的 1055 报错最典型的几个场景都来自报表系统和数仓取数。这类 SQL 往往长到几百行GROUP BY 后面跟着十几个字段SELECT 里还混着各种子查询别名排查起来相当痛苦。我的习惯是三步走第一步把 SELECT 中除了分组字段和聚合函数之外的所有列先注释掉跑通缩小范围第二步依次把注释的列加回来看是哪一列触发的报错第三步判断该列到底是该聚合、还是该进 GROUP BY、还是该用 ANY_VALUE。三步下来大多数问题十几分钟内就能定位。还有几个容易忽略的细节。ONLY_FULL_GROUP_BY在 sql_mode 里是大写加下划线和函数名大小写无关别因为大小写折腾半天。连接池环境下测试修改是否生效一定要在应用实际使用的连接上验证直接连 MySQL 客户端验证通过不代表应用侧就通了。最后给新项目一个建议建表规范里直接写明所有 GROUP BY SQL 必须满足 ONLY_FULL_GROUP_BY从源头减少这类问题比事后救火省事得多。
返回列表