
1. 这个报错的真实含义先搞懂 only_full_group_by 是什么很多人第一次见到this is incompatible with sql_modeonly_full_group_by时第一反应是去百度关键词复制一串SET sql_mode 或SET GLOBAL sql_mode 执行完发现当前会话好了重启 MySQL 又原形毕露。还有一部分人直接把 MySQL 配置文件里的sql_mode删掉结果过段时间别的同事一跑 SQL 又炸了。这个错误之所以难搞是因为它背后涉及 MySQL 的 SQL 模式设计以及 GROUP BY 语义的变化而不是简单的某个参数坏了。先解释only_full_group_by到底是什么。它是 MySQL 5.7 开始默认启用的一个 SQL 模式。在这个模式下SELECT 出来的列必须满足以下条件之一被 GROUP BY 明确包含或者是聚合函数SUM、COUNT、MAX、MIN 等的参数或者是在功能上依赖于 GROUP BY 列。听起来有点绕我用一个生活化的例子说明。假设你有一个订单表想按城市分组统计每个城市的订单总额SELECT city, SUM(amount) FROM orders GROUP BY city;这个没问题city在 GROUP BY 里SUM(amount)是聚合函数。但你如果想顺便查一下这个城市里某个客户的手机号SELECT city, customer_phone, SUM(amount) FROM orders GROUP BY city;这段 SQL 在 MySQL 5.7 之前是可以执行的结果中customer_phone是每组里碰巧取到的某一行的值具体是哪一行完全由 MySQL 内部执行计划决定不确定也不可控。在only_full_group_by模式下MySQL 直接拒绝这种语义不确定的查询告诉你customer_phone不在 GROUP BY 子句中与only_full_group_by不兼容。所以这个报错的本质其实是 MySQL 在强制执行一种更严谨的 SQL 标准防止你拿到一个随机的结果自己还不知道。很多从 MySQL 5.6 或更早版本升级上来的项目旧代码里大量使用不完全 GROUP BY写法一升级就全网报错这就是这个报错在社区里如此高频的根本原因。这里我先把核心观点说清楚这个报错不是 bug是 MySQL 在帮你纠错。但现实情况是很多业务代码的历史包袱很重改 SQL 的成本可能很高或者某些查询范式在业务上确实能接受取其中一行的语义。这时候就需要区分场景来制定解决方案而不是一刀切关掉模式或者一股脑改代码。接下来我按先理解、再选型、后实操的顺序把这几年实际项目中处理这个报错的经验完整写出来。2. 为什么 MySQL 5.7 之后默认开启这个模式SQL 标准的回归要真正理解解决思路你得知道这个模式是怎么来的以及它和 MySQL 的发展历史之间有什么关联。2.1 从 MySQL 5.6 到 5.7 的语义收紧MySQL 5.6 及更早版本中GROUP BY的处理非常宽松。你甚至可以把所有非聚合列都塞进 SELECT 列表MySQL 不会报错只是行为是随机选择该分组中的某一行。对于排序后的分组查询很多人还利用这个特性实现取每组最新一条记录比如SELECT * FROM ( SELECT * FROM logs ORDER BY create_time DESC ) t GROUP BY user_id;这个写法在 5.6 里能碰巧拿到每个用户最新的日志实际上依赖的是临时表的输出顺序非常脆弱。5.7 开启only_full_group_by之后这种写法直接报错。官方给出的理由是不符合 SQL92 / SQL99 标准因为 SQL 标准要求 SELECT 中出现的非聚合列必须出现在 GROUP BY 中。这背后其实是一个大的方向MySQL 从 5.7 开始包括后来的 8.0一直在向标准 SQL 靠拢同时也在收拾历史上积累的太随意的语法行为。only_full_group_by只是其中一项其它还包括默认字符集从 latin1 改为 utf8mb4、密码认证插件的变化等。所以它在升级中成为高频报错不奇怪。2.2 这个报错影响哪些查询不是所有 GROUP BY 查询都会触发。只有 SELECT 列表、HAVING 条件、ORDER BY 列表中出现了既不在 GROUP BY 中、又不是聚合函数的列时才会报错。我整理了一份常见触发场景对比场景示例 SQL是否触发报错聚合统计SELECT city, COUNT(*) FROM t GROUP BY city否多列分组SELECT 与 GROUP BY 完全一致SELECT a, b, SUM(c) FROM t GROUP BY a, b否非聚合列不在 GROUP BY 中SELECT a, b, SUM(c) FROM t GROUP BY a是用了 ANY_VALUE 包装SELECT a, ANY_VALUE(b), SUM(c) FROM t GROUP BY a否子查询先排序再分组取首行SELECT * FROM (SELECT * FROM t ORDER BY id DESC) x GROUP BY uid是注意第二行的场景SELECT a, b, SUM(c) FROM t GROUP BY a, b完全合法因为 b 在 GROUP BY 里。所以很多时候你不需要改逻辑只要把业务上本来就该一起分组的列补充进 GROUP BY 子句即可这是成本最低的修法。2.3 报错信息里隐藏的排查线索MySQL 的报错信息其实把问题列名直接抛出来了。比如ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column mydb.t.customer_phone which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_modeonly_full_group_by注意Expression #2它告诉你 SELECT 列表中的第二个表达式有问题。你可以顺着这个编号去核对 SELECT 子句中第几个列出的字段。后面那句mydb.t.customer_phone直接点名了数据库、表和列名。如果是一个长 SQL这条信息能帮你快速定位而不是肉眼一行行找。另外如果你用的是老版本客户端工具比如 Navicat 的旧版本连 MySQL 5.7执行某个查询时报这个错那么大概率是连接账户的sql_mode里包含only_full_group_by但服务器端没有。同一套代码在 A 机器上正常在 B 机器上报错十有八九是两台服务器的sql_mode配置不一致。这类环境差异导致的问题后面讲排查步骤时我会专门说。3. 四种主流解决思路的对比与适用场景正因为这个报错的根源不同解决方案也分成了几个流派。我在实际项目中每种都试过下面按侵入性从低到高的顺序梳理一遍并给出它们各自的适用场景。3.1 方案一修改 SQL 语句本身根治且最推荐把 SELECT 中非聚合的非分组列要么加入 GROUP BY要么用聚合函数包裹要么用ANY_VALUE()明确表达我就要这组里随便一行。具体来说如果业务上确实需要按 a 分组同时展示 b且 b 在同一个分组内值都相同那把 b 加进 GROUP BY 即可语义完全一致性能几乎没有损失。如果 b 在分组内值不同但你只需要任一值可以用ANY_VALUE(b)。MySQL 5.7.5 起支持8.0 里也保留了这个函数。如果 b 在分组内不同且你需要的是某个特定规则下的值比如每组里 id 最大的行的 b那就要改写为子查询或窗口函数。举一个实际例子。有个订单明细表order_items结构大概是order_id, user_id, product_name, amount, created_at。业务需要按user_id分组展示每个用户最近一次下单的产品名称。旧代码可能写成SELECT user_id, product_name, MAX(created_at) FROM order_items GROUP BY user_id;这个在 5.6 能跑5.7 直接报错因为product_name不是聚合列。正确写法是SELECT i.user_id, i.product_name, i.created_at FROM order_items i JOIN ( SELECT user_id, MAX(created_at) AS max_created_at FROM order_items GROUP BY user_id ) recent ON i.user_id recent.user_id AND i.created_at recent.max_created_at;如果你的 MySQL 版本是 8.0可以更优雅地用窗口函数SELECT user_id, product_name, created_at FROM ( SELECT user_id, product_name, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM order_items ) ranked WHERE rn 1;改 SQL 的缺点是可能要动很多历史查询优点是能真正理清业务语义后续升级到 MySQL 8.0 也不会再有兼容问题。我的建议是如果你的报错 SQL 数量不多比如几十条以内优先改 SQL这是唯一不会留技术债的方案。3.2 方案二修改当前会话的 sql_mode临时应急如果只是临时跑一次报表、执行一次性数据修复脚本不想动任何代码和全局配置可以在查询前执行SET SESSION sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION;原理是SESSION级别的修改只对当前连接有效连接断开设定的值就恢复默认。这个sql_mode字符串是把 MySQL 5.7 默认值里的ONLY_FULL_GROUP_BY去掉后的完整列表。如果你懒得记忆也可以直接SET SESSION sql_mode ;把全部模式清空但这会连带关闭严格模式STRICT_TRANS_TABLES可能导致之后插入超长字符串被截断而不是报错掩盖数据质量问题。所以我不建议直接置空宁可多写几个模式名保留严格模式。会话级修改适合的场景你正在用mysql命令行或者某个 GUI 工具连数据库临时手动执行几条查询不想影响其他同事。这个方案的缺点是只要断开重连就失效而且如果你用的是连接池每个连接都需要设置一遍很麻烦。所以它只适合应急。3.3 方案三修改全局 sql_mode影响所有新连接如果你确认整个项目的 SQL 风格统一采用宽松 GROUP BY写法而且短期內没有精力改代码可以修改全局配置SET GLOBAL sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION;执行后新建立的连接会使用新的全局sql_mode已存在的连接不受影响——这个细节经常被忽略。如果你是用 Navicat 直接执行的那么当前 Navicat 的这个连接改完后并没有生效你需要重新连接或者再执行一次会话级设置。如果你用的是项目代码里的连接池需要重启应用或等连接池回收连接后才能让全部请求走到新模式。全局修改解决的是整个实例的默认行为问题适合你完全掌控的测试环境或者单机部署的小项目。但在生产环境这种修改会影响所有库的所有用户如果别的业务线依赖严格的 GROUP BY 校验来防止错误查询那这就是破坏行为。所以在生产环境改全局之前一定要先检查同一 MySQL 实例上是否还有其他业务的库确认没有影响再动。3.4 方案四修改配置文件并重启持久化最稳妥的生产方案上面的SET GLOBAL只对当前运行中的 MySQL 实例有效重启 MySQL 服务后会回到配置文件里的值。要永久生效需要编辑配置文件。Linux 下通常是/etc/my.cnf或/etc/mysql/my.cnf也可能在/etc/mysql/conf.d/或/etc/mysql/mysql.conf.d/下。Windows 下是my.ini一般在 MySQL 安装目录下。在[mysqld]段下添加或修改一行[mysqld] sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION注意不能写成sql-modeMySQL 参数名用下划线。添加完成后重启 MySQL 服务systemctl restart mysqld或老版本service mysql restart重启后用下面的语句验证SELECT global.sql_mode; SELECT session.sql_mode;两个值应该都包含你设置的完整字符串并且不要再出现ONLY_FULL_GROUP_BY。配置文件方案和全局设置方案的区别在于前者是SET GLOBAL的持久化版本重启后仍然生效后者重启会丢。所以生产环境真正要动默认行为必须改配置文件。这里我想多说一句配置文件方案虽然稳妥但它仍然会屏蔽only_full_group_by的校验是一种妥协方案。如果公司有数据库规范最好评估一下是否允许这么做。很多时候 DBA 会拒绝改全局配置要求开发侧改 SQL。这个博弈是行业常态你需要提前想好说辞。4. 按环境类型分类的操作指南Linux / Windows / Docker / 云数据库同一个解决方案在不同部署方式下的操作细节差异很大。我按实际工作中最常见的四种环境分别列一下操作步骤避免你查了一堆资料还对不上自己的环境。4.1 Linux 下通过配置文件修改以 CentOS / Ubuntu 为例先确认 MySQL 配置文件的加载顺序。mysqld读取配置时后面的配置会覆盖前面的同名项。一般顺序是/etc/my.cnf-/etc/mysql/my.cnf-/etc/mysql/conf.d/*.cnf-/etc/mysql/mysql.conf.d/*.cnf。如果你在/etc/my.cnf里写了又被后面的文件覆盖就会产生我明明改了却没用的假象。建议在写之前先查看当前生效值mysql -uroot -p -e SELECT global.sql_mode;这里能看到当前实际的sql_mode值你复制出来把ONLY_FULL_GROUP_BY删掉剩下的部分作为你要写入配置的字符串。然后编辑配置文件vi /etc/my.cnf在[mysqld]下加入如果没有[mysqld]段就新建一段。保存后重启systemctl restart mysqld重启完成后再执行一次验证命令确认ONLY_FULL_GROUP_BY不在列表内。如果是在 Ubuntu 上MySQL 配置路径通常是/etc/mysql/mysql.conf.d/mysqld.cnf方法同理。4.2 Windows 下修改 my.iniWindows 安装的 MySQL配置文件一般在安装目录下。比如C:\Program Files\MySQL\MySQL Server 8.0\my.ini也可能是C:\ProgramData\MySQL\MySQL Server 8.0\my.ini。快速定位方法在 MySQL 命令行里执行SELECT basedir; SELECT datadir;basedir是安装目录通常my.ini就放在安装目录或其父级。用记事本打开找到[mysqld]段添加或修改sql_mode行。然后以管理员身份打开服务管理器找到 MySQL 服务右键选择重启。Windows 下还有一个注意点如果你的服务名是MySQL80可以用命令行net stop MySQL80 net start MySQL80验证方式与 Linux 一致。4.3 Docker 容器里的 MySQL用 Docker 跑 MySQL 的人越来越多容器内修改配置文件有两种思路。思路一进入容器改配置并重启容器。先找到容器名或 IDdocker ps | grep mysql然后进入容器docker exec -it mysql-container bash容器内配置文件路径通常是/etc/mysql/my.cnf或/etc/mysql/mysql.conf.d/mysqld.cnf。使用容器内的编辑器可能没有 vi需要先apt-get update apt-get install -y vim修改然后退出容器重启容器docker restart mysql-container这种方式的缺点是容器一旦被删除重建配置就丢了所以我推荐另一种思路在 Docker 启动命令里挂载配置。启动时用-v参数把宿主机上的配置文件挂载进容器docker run -d \ --name mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyour_password \ -v /my/custom/my.cnf:/etc/mysql/conf.d/my.cnf \ -v /my/data/mysql:/var/lib/mysql \ mysql:8.0宿主机上的/my/custom/my.cnf里写好[mysqld]段及sql_mode配置。容器启动时conf.d下的文件会被自动读取。这样配置被纳入版本管理容器销毁重建也不怕。如果你用的是docker-compose还可以在 volumes 里加一行volumes: - /my/custom/my.cnf:/etc/mysql/conf.d/my.cnf注意挂载到/etc/mysql/conf.d/下的文件必须属于 root 且权限不能过高否则 MySQL 可能忽略。如果遇到挂载后配置不生效的情况可以先docker exec进容器查看/etc/mysql/conf.d/目录权限。4.4 云数据库RDS / 云 MySQL的处理方式云数据库一般不允许你登录物理机修改配置文件也没有systemctl restart mysql这种操作。各云厂商的控制台通常会提供参数设置或参数组功能在里面搜索sql_mode点击编辑把ONLY_FULL_GROUP_BY从值中移除保存后提交参数组实例通常会自动重启或按维护时间生效。我在腾讯云、阿里云、AWS RDS 上都操作过流程大同小异。关键点有两个云数据库的参数修改一般需要提交或应用才会真正推送只保存不提交等于白改。RDS 实例的参数通常区分静态参数和动态参数sql_mode可能是动态的修改后不需要重启也可能需要重启。提交后观察实例状态如果显示修改生效中等一段时间再重连。另外云数据库可能有多个实例共享同一个参数组改参数组会影响所有关联实例修改前要留意当前参数组绑定了哪些实例别误伤了其他环境。5. 最容易被忽略的坑连接池、只读账户、字符序与隐藏的 GROUP BY陆陆续续处理过十几个项目这个报错之后我发现很多问题不是出在不会改配置而是出在对 MySQL 运行机制的细节把握不到位。下面几个坑每一个我都见过同事踩过。5.1 连接池里老连接带着旧 sql_mode最常见的一种情况你执行了SET GLOBAL sql_mode ...然后程序里依然报错。原因很简单应用连接池里的数据库连接是在修改前建立的连接建立的那一刻sql_mode已经被确定为旧值全局修改不会自动同步到已存在的会话级变量。解决办法有几种重启应用让连接池重新建立连接。如果你的连接池支持配置连接初始化 SQL比如 HikariCP 的connectionInitSqlDruid 的connectionInitSqls可以加入这样一行SET SESSION sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION这样每次连接新建时都会自动设置会话级sql_mode即使服务器全局配置没改应用也能绕开。这种方式的好处是只对当前应用生效不影响数据库全局适合那些不能改数据库配置、只能改应用代码的场景。5.2 只读账户没权限执行 SET GLOBAL如果你的数据库账号不是 root而是一个只读或普通开发账号执行SET GLOBAL sql_mode ...时会收到ERROR 1227 (42000): Access denied。这种时候你需要向 DBA 申请执行权限或者使用上面说的连接池初始化 SQL 方式只改会话级。会话级设置只需要对应的SET SESSION权限普通账号通常拥有自己的会话权限。5.3 修改配置文件后没有重启以及大小写敏感问题配置文件修改后必须重启 MySQL 服务才能生效这点很多人知道。但还有两个细节sql_mode值里的模式名是大小写不敏感的strict_trans_tables和STRICT_TRANS_TABLES都行只是惯例上全大写。配置文件中一个参数名如果出现多次后面的会覆盖前面的。检查整个配置文件看看sql_mode是否在多个位置被设置如果有必须统一。5.4 隐式 GROUP BYDISTINCT 与聚合混用有些报错的 SQL 里根本没有GROUP BY为什么也报这个错因为DISTINCT和聚合函数混用时MySQL 内部会生成一个分组操作。例如SELECT DISTINCT department_id, AVG(salary) FROM employees;这个语句虽然没有 GROUP BY但AVG(salary)是聚合函数department_id是非聚合列。MySQL 会把department_id作为分组依据此时要求所有非聚合列都必须出现在 SELECT 中。如果还有一个非聚合列没被 SELECT比如你只写SELECT DISTINCT department_id, AVG(salary)其实没毛病但如果写成SELECT department_id, employee_name, AVG(salary) FROM employees;没有 GROUP BY却有聚合和非聚合混用这在默认sql_mode下会报错因为它等价于把所有行当成一个分组而department_id、employee_name都不在分组里。这类问题要按聚合查询的规则去理解而不是单纯靠加一个 GROUP BY解决。5.5 ORDER BY 与 GROUP BY 的联动陷阱only_full_group_by不只约束 SELECT 列表还约束 ORDER BY 列表。看这个例子SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id ORDER BY order_date;order_date既不在 GROUP BY 里又不是聚合函数MySQL 会报错。正确的语义应该是按分组后的统计结果排序比如ORDER BY cnt DESC或者把order_date也加入 GROUP BY如果业务上是按user_id, order_date分组的话。这种错误尤其在写排行榜类的 SQL 时容易犯因为人下意识想按最近日期排。5.6 字符序与 collation 对分组结果的影响还有一个冷门相关点MySQL 的 GROUP BY 对字符串列的比较受排序规则collation影响。不同 collation 下相同字符可能被视为同一组或不同组。比如utf8mb4_general_ci下a和A视为同一组utf8mb4_bin下视为不同组。如果你改了sql_mode后发现某些分组数据变多了或变少了先检查列的 collation而不是怀疑配置改错。严格模式下这种情况不会导致报错但会让查询结果和预期不符。所以排查报错问题的同时如果有条件顺便核对一下业务结果集是否仍然符合预期。6. 完整排查链路与验证方法前面讲了不少原理和方案下面按实际排查的顺序给一个可直接照着做的操作流程。这个流程是我平时处理问题时的思路适合从收到报错到确认修复全流程使用。6.1 第一步确认当前的 sql_mode 值先连接数据库执行SELECT global.sql_mode; SELECT session.sql_mode;对比两行输出。如果全局和会话不一致说明你的客户端或连接池在连接初始化时做了特殊设置或者中间有人执行过SET SESSION。如果只有会话级包含ONLY_FULL_GROUP_BY全局没有那问题出在连接端的初始化脚本上修改全局配置不会有任何作用。6.2 第二步定位报错 SQL 的具体语句从应用日志中找出完整 SQL清空参数值里的特殊字符直接在命令行中复现。建议用EXPLAIN查看执行计划前先确认语句本身能跑通。如果 SQL 很长可以用EXPLAIN配合FORMATJSON输出有时候能更清晰地看到分组和排序的层级。复现报错后分析 SELECT 列表中哪些列是非聚合且非分组的把它们找出来。6.3 第三步根据业务语义决定修法这一步是核心决策点。逐一检查每个问题列这个列在同一个分组内值是否永远相同如果是把它加进 GROUP BY。值不同但取哪一个都无所谓用ANY_VALUE()包裹。值不同且必须取特定规则下的值用子查询或窗口函数改写。是否可以用聚合函数表达语义如果它只需要展示每组某个最大值/最小值对应的字段可以改写为 JOIN 子查询。收集完所有问题修改 SQL在命令行重新执行确认不再报错且结果集与业务预期一致。6.4 第四步决定是否修改全局或配置只有当你不打算修改 SQL、且确认全局宽松模式不会影响其它业务时才去改全局或改配置文件。任何改动前先备份现有sql_mode值SELECT global.sql_mode;把输出保存到本地回滚时用。修改后按前面 4.x 的方式验证增量效果并关注是否影响其他业务。6.5 第五步用测试用例做回归验证不要只验证报错语句本身。我建议把项目里的 GROUP BY 类查询做成一个规则文件整理几条典型的 SQL按单个字段分组查多条聚合统计按多个字段分组查明细查询分组内最新一条记录旧写法和新写法DISTINCT 与聚合函数混用。将这些 SQL 在修改前后分别执行对比结果数量是否一致结果内容是否合理。如果之前用的随机行值在新模式下变为了确定的关联查询结果数量总数可能不变但某些字段值不一样这时候需要业务确认。6.6 第六步监控与告警修复之后建议在日志平台加一条关键字告警比如监控incompatible with sql_modeonly_full_group_by在应用错误日志中出现的次数。这个告警能帮助你及早发现是否还有漏网的 SQL。我在公司内部做过类似统计很多项目第一次修完之后过了几天又冒出来几条新的报错都是之前没覆盖到的报表任务或后台管理页面。这些任务平时跑得少某个月度任务触发时才被发现。所以事后监控比事前排查更重要。7. 实际案例复盘一个生产环境的完整处理过程分享一下我最近处理的一个真实案例把所有环节串起来。某公司的订单报表模块在升级 MySQL 5.7 后频繁报错核心报表 SQL 简化为SELECT date(create_time) AS day, order_type, channel, COUNT(*) AS order_cnt, SUM(amount) AS total_amount, pay_time FROM orders WHERE create_time 2024-01-01 GROUP BY date(create_time), order_type;报错列是channel和pay_time。channel在语义上是跟随order_type的独立字段同一种 order_type 下可能有不同 channel所以不能直接加进 GROUP BY否则分组粒度会变细订单数会分裂。而pay_time明显是想取该分组内某个代表性的支付时间但业务方说他们只是想看最近一条支付时间并不需要精确到每个渠道。处理过程是这样先确认order_type和channel的关系。业务上order_type是主分类channel是子维度。报表里只想按order_type分组channel展示的是分组内最大的渠道于是用ANY_VALUE(channel)包裹。pay_time需要的是分组内最新的支付时间用MAX(pay_time)直接表达。改后的 SQLSELECT date(create_time) AS day, order_type, ANY_VALUE(channel) AS channel, COUNT(*) AS order_cnt, SUM(amount) AS total_amount, MAX(pay_time) AS pay_time FROM orders WHERE create_time 2024-01-01 GROUP BY date(create_time), order_type;在测试环境跑一遍结果集行数与旧版一致channel值在大多数分组内与旧版随机值相等极少数不一致业务确认可以接受。把项目中其它类似 SQL 一并修改形成了一版兼容改造清单。同时通知 DBA拒绝修改全局sql_mode因为该公司还有其它金融类业务依赖严格模式。最终通过应用连接池的初始化 SQL单独为报表模块的连接设置了宽松sql_mode形成了一种双轨制常规业务走严格模式报表服务走宽松模式。这种做法在不少公司是可行的数据库层面保持默认严格模式特定应用通过连接池初始化 SQL 改变自己连接的行为。既保证了全局安全又解决了历史负担。不过要注意使用连接池的初始化 SQL 后该应用连接下所有 SQL 都会受到单会话模式影响所以只适合你确信没有风险的应用模块。8. MySQL 8.0 以及更高版本的新变化前面说的基本都是 MySQL 5.7 的处理方式。如果你的项目已经升级到了 MySQL 8.0有几个新情况需要注意。8.1 默认 sql_mode 仍然包含 ONLY_FULL_GROUP_BYMySQL 8.0 的默认sql_mode依然包含ONLY_FULL_GROUP_BY所以 5.7 的报错在 8.0 上会原样出现。处理方法大同小异只是有些版本把NO_AUTO_CREATE_USER这类古老模式移除了默认模式字符串更短了。例如 MySQL 8.0 默认值是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后剩余部分就是我前面推荐你使用的值。8.2 窗口函数让取每组某行的改写更简单MySQL 8.0 开始支持窗口函数很多原本要写 JOIN 子查询的场景可以直接用ROW_NUMBER()、FIRST_VALUE()等函数解决。例如上一节中的每个用户最近一条订单8.0 写法SELECT user_id, product_name, create_time FROM ( SELECT user_id, product_name, create_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM order_items ) t WHERE rn 1;这种写法语义清晰、性能也可控强烈建议新代码一律使用窗口函数替代分组取随机行的旧套路。8.3 新版本下不建议直接清空 sql_mode网上有些文章建议直接执行SET GLOBAL sql_mode ;来解决问题。这个做法在 5.7 时代就很危险在 8.0 里更危险因为 8.0 的严格模式对数据类型检查更严格清空后可能导致很多隐式转换问题被掩盖。我见过一个案例某人把sql_mode清空后日期字段0000-00-00能插入数据但后续查询时 JDBC 驱动因为日期无法转换直接抛异常。这种隐藏问题比报错本身更难排。所以除非你就是想临时绕过某些校验且完全知道后果否则不要清空sql_mode。8.4 云数据库的 MySQL 8.0 参数组注意事项云厂商的 MySQL 8.0 参数组里sql_mode的可选值可能与开源版不同。有些云厂商额外增加了自己的模式项比如PAD_CHAR_TO_FULL_LENGTH或者在控制台默认值里加入了NO_ENGINE_SUBSTITUTION的云版本变体。修改前尽量从控制台当前值复制出来只删除ONLY_FULL_GROUP_BY其余原样保留降低意外风险。9. 一些长期建议如何让这个报错从此不再出现回到开头说的——这个报错是 MySQL 在提示你的 SQL 不符合标准语义。所以长期最优解不是出了一次问题改一次配置而是从团队规范和代码审查层面根治。第一新代码的 SQL 审查加一条规则凡出现 GROUP BY 的语句SELECT 列表中出现的非聚合列必须全部出现在 GROUP BY 中。这条规则写进团队的数据库设计规范文档里做代码评审时重点检查。如果确实需要组内某个字段的特定值明确要求写出子查询或窗口函数禁止依赖 ANY_VALUE 隐藏语义。第二监控优先。所有项目在日志平台配置incompatible with sql_modeonly_full_group_by的关键字告警一旦有报错立即推送。I 见过太多项目是报表跑完才发现数据不对而 SQL 日志里早就躺着被拦截的报错没人看。第三升级评估时把sql_mode变化列为一等检查项。从 5.6 升 5.7、从 5.7 升 8.0都要先跑一遍所有业务 SQL 的静态扫描——现在很多数据库审核工具如 Yearning、Archery 或者开源 SQL 审核平台都支持规则导入你可以把only_full_group_by规则写进审核引擎在开发阶段就拦住问题 SQL。第四理解成本与收益。如果团队里有新人经常写这种 SQL与其反复说教不如建立一个小项目把常见的不规范写法编译为一个正确写法对照表放在团队 Wiki 里。例如错误写法正确写法SELECT a, b, SUM(c) FROM t GROUP BY aSELECT a, ANY_VALUE(b), SUM(c) FROM t GROUP BY a若 b 无特定要求SELECT a, b, SUM(c) FROM t GROUP BY aSELECT a, b, SUM(c) FROM t GROUP BY a, b若 b 在组内确定SELECT * FROM t GROUP BY uid改写为窗口函数 子查询这类对照表比大段文字更能帮助团队达成一致。另外我想强调一下在接手老系统时不要在没有任何评估的情况下直接改sql_mode配置。虽然这个操作能立刻消除报错但它会让隐藏的 SQL 语义问题继续存活在代码里未来某天数据结果错了排查成本会更高。我的习惯是能改 SQL 就改 SQL改不了就谈清楚业务接受度最后才动全局配置。如果项目真的进入了维护模式、人员流失严重、改动成本极高那么用连接池初始化 SQL 的方式做会话级定点宽松是相对可控的折衷方案。最后再分享一个小技巧即使你决定不修改全局配置也可以先在测试环境把sql_mode设置为完全相同的当前生产值然后跑一遍全量回归测试找出所有受影响的 SQL。这个模拟生产的做法能帮助你在开发阶段预判兼容问题。我曾经用它在一个即将上线的项目里提前抓出 23 条有问题的 SQL全部在测试阶段改完上线后零报错。那条规则也一直保留在我们的发布检查清单里每次版本发布前都会重新执行一次。