MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异

做完 MySQL 8.0 迁移,业务压测通过、切流上线,第二天早高峰就接到告警:一批原本几十毫秒的 SQL 突然跑到几秒,CPU 与磁盘 IO 同步飙高。更让人困惑的是,DBA 逐条比对新旧环境的执行计划,发现索引还是那几个索引,表结构也一行没改。

大多数人第一反应是加索引、改写 SQL、扩 buffer pool,甚至怀疑 8.0 有 bug。问题是:8.0 的基于成本的优化器(CBO)换了成本模型、换了统计信息口径、还换了连接算子,同一份物理数据在两套成本下会走出完全不同的路径,你调的那些参数根本不在病灶上。

本文分享一套迁移后的计划劣化排查与治理方法:成本常量对齐 + 统计信息重建 + 计划 diff 工具链,全部可直接落地。

一、诊断起点:别急着加索引,先比三类成本

先给结论:8.0 的计划变慢,八成出在下面这张表里被忽略的三处差异上。

对比项 5.7 的口径 8.0 的口径
页读取成本 内存页与磁盘页都记 1.0 磁盘页 1.0,内存页降到 0.2
统计信息 高频值 + 高低值选择性 纳入 histogram_json 直方图参与基数估计
连接算子 嵌套循环 + Block Nested Loop 无索引可用时优先 Hash Join
默认排序规则 utf8mb4_general_ci 居多 utf8mb4_0900_ai_ci,逐位比较、pad_space 语义变化

排查顺序建议固定为:先看成本模型导致的选路差异,再看统计信息导致的行数估算偏差,最后看新算子带来的成本转移。三类问题的修复手段完全不同,混在一起查只会越查越乱。

**核心结论:**计划变了不等于索引坏了,先确认优化器的成本账本换了版本。

二、成本常量:内存页变便宜,选路悄悄偏移

内存页成本从 1.0 降到 0.2,是 8.0 最常被忽略的一处改动,它会双向扭曲选路。

  • 随机读被低估 :优化器认为在 buffer pool 里捞一页只要 0.2 个成本单位,于是大范围区间扫描 + 逐行回表的路径被整体抬高,原本 5.7 会掐断的宽范围查询在 8.0 被放行。
  • 全表扫相对更划算 :页几乎都命中内存时,一次顺序扫的成本被稀释,某些点查场景反而选了全表扫,表现为数据越热、计划越离谱。

在新旧两套环境分别执行下面这段,对比成本估算与真实读取量:

sql 复制代码
-- 5.7 与 8.0 各跑一次,对比估算行数、真实扫描行数
EXPLAIN FORMAT=TREE
SELECT o.id, o.amount, c.name
FROM orders o JOIN customers c ON c.id = o.cust_id
WHERE o.created_at >= '2024-01-01';

SHOW SESSION STATUS LIKE 'Handler_read%';

估算行数与 Handler_read_next 量级差 10 倍以上,基本可判定是成本模型选路问题。

修复优先级:把热查询做成覆盖索引 (消除回表,成本差异自然消失),必要时用 FORCE INDEX 临时锁死计划,同时把扫描范围收敛到能走 ref 的粒度。

三、统计信息:直方图与采样让估算整体偏移

第二类差异藏在统计信息里,8.0 把直方图纳入基数估计后,同样的数据会算出不同的 rows。

  • 直方图默认缺席 :新建表不会自动生成直方图,但优化器的估算逻辑已经按有直方图的口径实现,选择性估计落到默认档位,等值条件的估算行数可能虚高或虚低一个数量级。
  • 采样页数放大偏差 :innodb_stats_persistent_sample_pages 调小能提速统计收集,代价是分布信息失真,长尾条件的执行计划会抖动。
  • 迁移期间数据形态变了:批量导入后统计信息仍是导入前的状态,估算口径与真实数据严重脱节。

迁移完成后立即重建统计与直方图,这是成本最低的一步:

sql 复制代码
-- 切流后立即执行,按实际过滤条件补直方图
ANALYZE TABLE orders UPDATE HISTOGRAM ON cust_id, created_at
WITH 64 BUCKETS;

SELECT table_name, column_name, JSON_EXTRACT(histogram, '$.num_buckets')
FROM information_schema.column_statistics;

先让估算回到真实分布,再判断计划是否真的错了。

四、执行算子:新算子把成本挪到了别处

第三类差异是算子层的:8.0 引入的新算子会改变同一条 SQL 的成本结构。

  • Hash Join 抢走连接路径 :8.0.18 起,无索引可用时优化器会选 Hash Join 替代 Block Nested Loop,内存占用与临时结构开销变大,大结果集连接的耗时从 IO 型变成内存型。
  • 排序规则变更导致索引失效 :库默认改成 utf8mb4_0900_ai_ci 后,字符串比较是逐位的且 pad_space 语义不同,旧表列的隐式比较可能无法命中索引,计划从 ref 退化为全表扫。
  • Descending Index 与 Skip Scan 改变可用路径:新增路径不一定更快,但会挤掉原本稳定的索引顺序扫描。
信号 8.0 现象 处置
计划里出现 Hash Join 内存峰值抬高 为连接键补索引,强制回到 nested loop
字符串条件走全表扫 排序规则不匹配 统一列与常量的 collation,或改写为显式 CAST
出现 Skip Scan 索引前缀基数过高 调整索引列顺序或加组合索引

**核心结论:**算子换了,成本就换了账本,别用旧版本的经验读新计划。

五、复现与对比:把两套环境的计划 diff 出来

靠肉眼比对几十条 SQL 的计划不现实,用脚本把两套环境的执行计划拉成结构化 JSON 后做 diff,定位效率高一个量级。

下面这段 Python 脚本用 PyMySQL 连接两套环境,逐条执行 EXPLAIN FORMAT=JSON 并输出差异:

python 复制代码
import json, pymysql

SQLS = ["SELECT * FROM orders WHERE cust_id = 12345",
        "SELECT * FROM orders WHERE created_at >= '2024-01-01'"]

def plans(host):
    conn = pymysql.connect(host=host, user='dba',
                           password='***', database='shop')
    with conn.cursor() as cur:
        out = []
        for s in SQLS:
            cur.execute("EXPLAIN FORMAT=JSON " + s)
            out.append(json.loads(cur.fetchone()[0]))
    return out

old, new = plans('mysql57.example.com'), plans('mysql80.example.com')
for sql, a, b in zip(SQLS, old, new):
    print('=' * 60, sql)
    print('5.7 :', a['query_block'].get('cost_info', a['query_block']))
    print('8.0 :', b['query_block'].get('cost_info', b['query_block']))

先 diff 估算行数与访问类型,再 diff 成本数字,三步内就能锁定是哪一类差异。

诊断链路固定为:EXPLAIN FORMAT=JSON 采集 → cost_info / rows 差异定位 → 回查统计信息与 collation。

六、治理清单:按三类差异逐项收口

切流前把这张清单过一遍,能挡掉大部分计划劣化:

差异类型 排查动作 治理手段
成本模型 比对 cost_info 与 Handler_read_next 补覆盖索引、收敛扫描范围、临时 FORCE INDEX
统计信息 查 column_statistics 是否为空 ANALYZE TABLE + UPDATE HISTOGRAM、调高采样页
执行算子 看计划是否出现 Hash Join / 全表扫 补连接键索引、统一 collation、复核索引列序

落地时注意两点:迁移窗口内先跑 ANALSZIZE 再切流 ,避免旧统计带病上线;监控计划漂移 ,对 EXPLAIN FORMAT=JSON 做定时采集,一旦 cost_info 变化就报警。

**核心结论:**把成本模型、统计信息、执行算子三类差异写进迁移 checklist,8.0 升级才真正可控。

结语

8.0 迁移后的 SQL 变慢,很少是引擎退步,更多是成本账本换版后没人对账。内存页成本降到 0.2、直方图进入基数估计、Hash Join 取代 BNL,这三处改动各自都会让计划悄悄偏移,叠加起来就是早高峰的那条红线。

把本文的诊断链路固化成迁移流水线的一环:切流前 diff 两套环境的计划、重建统计与直方图,切流后持续采集 cost_info 做漂移告警,同类问题就不会再来第二次。

计划劣化的根因不在索引,而在优化器算账的口径变了------对上账本,SQL 自然归位。

相关推荐
我叫张土豆1 小时前
Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密
android
ZENERGY-众壹2 小时前
告警风暴怎么压?100MW 电站监控平台告警引擎架构复盘
数据库·架构·iot·电站监控
zhengfei6112 小时前
Go-fastdfs /reload 未授权访问: 有效PoC
数据库·sql
DevOpenClub3 小时前
高考教育信息问答 API:会话参数、资料引用与流式响应
java·数据库·高考
yuanyuan3343 小时前
2026枣庄带检测报告伸缩门品牌测评|本地服务
大数据·数据库·人工智能·伸缩门
wdyan2973 小时前
带着小白学习-微服务-5 实战:配置中心-数据库连接
数据库·学习·微服务
大维4 小时前
DBX 实测:从 SQLite 查询到 MiniMax-M3 自然语言生成 SQL
数据库·sql·sqlite
booboosui4 小时前
Android 悬浮窗藏不住自己:一个标志位与 140 毫秒
android
传奇开心果4 小时前
【Flutter踩坑排雷】第5课:扩展方法冲突坑|同名扩展引发编译歧义,故障规避手段
android·flutter