MySQL 8.4 升级:两张表 join 的条件列都有索引,执行计划却没用上
一个跨两张表的 join,两边的关联列都有索引,升级到 MySQL 8.4 之后查询突然变慢。抓执行计划发现走了全表扫,两边索引都没用上。
简化后的形态:
SELECT ... FROM legacy_order o
JOIN new_payment p ON o.order_no = p.order_no;
原因是隐式字符集转换:legacy_order 是 2018 年之前建的老表,列上还是 utf8mb3(当年的 utf8),new_payment 是后来建的新表,用的是 utf8mb4。两个列 join 时发生隐式字符集转换,转换函数被套在列上,列上的索引自然就废了。
反直觉的地方:8.0 能走索引不代表没问题
在 8.0 上抓同一个 SQL 的执行计划,o 是能走索引的——这也是为什么这个问题一直没被发现。
8.4 里优化器的代价估算做了调整,对这个 join 选了另一个 join 顺序,隐式转换就落到了带索引的那一边,问题才暴露出来。
严格说这不是 8.4 的 bug。隐式转换一直存在,8.0 时代优化器恰好选中了对自己有利的那个 join 顺序,把问题盖住了。老版本上的「正常」,有时候只是运气好,升级把这份运气抽走了。
处理
短期先加 hint 固定 join 顺序,把执行计划拉回能走索引的那条路。
根治是清存量:把所有 utf8mb3 的表转成 utf8mb4。先查出来有哪些:
SELECT table_schema, table_name, table_collation
FROM information_schema.tables
WHERE table_collation LIKE 'utf8%'
AND table_collation NOT LIKE 'utf8mb4%';
转换:
ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4;
大表不能直接 ALTER,走 gh-ost 做在线变更。
顺带发现的 schema 漂移
测试环境这几张表不知道哪一年被统一改成了 utf8mb4,跟线上 schema 不一致——所以这个问题在测试环境永远复现不出来。schema 漂移检查应该进升级 checklist,不然测试环境给出的「验证通过」是没有意义的。
另一个坑:mysql_native_password 被禁用
一个三年没人动过的内部报表服务,升级后起不来,报:
Authentication plugin 'mysql_native_password' cannot be loaded
临时解法是在配置里打开:
[mysqld]
mysql_native_password = ON
但这个开关在 9.x 里彻底没有了,只能算续命。升级前先把账号扫一遍:
SELECT user, host, plugin FROM mysql.user
WHERE plugin = 'mysql_native_password';
复制与其它默认值变化
replica_parallel_workers 默认值从 0 变成 4;GTID 相关的一批默认值也变了。innodb_buffer_pool_in_core_file 的语义做了微调,自动巡检脚本得整体过一遍,不能沿用老假设。
expire_logs_days 在 8.4 里是真删了。my.cnf 里如果还留着这个参数,实例直接起不来。升级前先校验配置:
mysqld --validate-config --defaults-file=/etc/my.cnf
经验
升级项目的本质是一次被迫的技术债清算。工作量不要按「执行一次滚动升级」来估——滚动升级本身花不了多少时间,剩下的都在处理没人认领的存量服务、上古字符集、漂移的 schema 和过时的配置。