MySQL 5.7及更早版本对相关子查询默认采用嵌套循环执行模型,外层表每行触发一次子查询执行;IN/NOT IN会物化子查询结果为临时表,超限则转磁盘表引发高I/O;应改用LEFT JOIN或EXISTS,并避免索引失效写法。
子查询触发嵌套循环执行模型
mysql 5.7 及更早版本对相关子查询(correlated subquery)默认采用嵌套循环(nested loop)执行模型:外层表每读取一行,就完整执行一次子查询。比如 where id not in (select account_id from contacts),若主表有 1 万行,子查询最多被执行 1 万次——哪怕子查询本身能走索引,重复解析、打开表、扫描的开销也呈线性放大。
实操建议:
- 用
EXPLAIN查看执行计划,若出现DEPENDENT SUBQUERY或UNCACHEABLE SUBQUERY,基本确认是嵌套循环模式 - 升级到 MySQL 8.0+ 后,优化器可能自动转为半连接(Semi-Join)或物化(Materialization),但不保证所有场景生效
- 避免在
WHERE中写依赖外层字段的子查询,尤其不要用NOT IN配合可能含NULL的列
NOT IN / IN 子查询强制生成临时表
IN (SELECT ...) 和 NOT IN (SELECT ...) 在 MySQL 中会先将子查询结果物化为内部临时表。当结果集超过 tmp_table_size(默认 16MB)时,临时表会退化为磁盘表,引发大量 I/O —— 这正是你看到 “Copying to tmp table” 占用 90% 时间的原因。
常见错误现象:
- 执行时间突然从毫秒级跳到几十秒
-
SHOW PROCESSLIST显示状态长期卡在Copying to tmp table - 子查询返回 10 万+ 行时性能断崖式下降
实操建议:
- 改用
LEFT JOIN ... WHERE right_table.id IS NULL替代NOT IN - 改用
EXISTS替代IN(EXISTS找到第一行即停,不生成临时表) - 确保子查询里加
DISTINCT或提前用派生表过滤,减小临时表体积
子查询无法复用索引的典型写法
即使子查询本身有索引,某些写法也会让优化器放弃使用:
-
WHERE YEAR(created_at) = 2023:函数作用于索引列,导致索引失效 -
WHERE status IN ('active', 'pending') AND name LIKE '%john%':LIKE前缀模糊匹配使后续条件难走联合索引 - 子查询中
ORDER BY + LIMIT未配合WHERE过滤,导致全表扫描后排序
实操建议:
- 把时间范围条件写成
created_at >= '2023-01-01' AND created_at - 对高频查询字段建联合索引,顺序按
WHERE等值条件 → 范围条件 →ORDER BY字段排列 - 子查询里别轻易加
ORDER BY,除非真需要排序结果;LIMIT单独用意义不大,必须配合有效过滤条件
phpMyAdmin 自身加重了子查询的感知延迟
phpMyAdmin 不是性能瓶颈源头,但它会放大子查询慢的问题:
- 默认开启
profiling时,额外收集执行阶段耗时,增加微小开销 - 结果集渲染逻辑会等完整结果返回才开始输出,不像命令行客户端边 fetch 边显示
- Web 层超时(如 Nginx 的
fastcgi_read_timeout)可能比数据库wait_timeout更早中断连接,导致“卡死”假象
容易被忽略的一点:你看到的“几十秒”,很可能前 3 秒是数据库执行,剩下全是 phpMyAdmin 渲染长文本、生成 HTML 表格、浏览器解析 DOM 的时间——尤其是字段含大文本或 JSON 时。$cfg['LimitChars'] 设得太小反而让渲染更快,但掩盖了真实问题。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











