大数据环境下,SQL子查询全表扫描如何引发CPU激增,执行过程有何特点?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1139个文字,预计阅读时间需要5分钟。
子查询查询本身不会导致CPU飙升,但当它被优化器判定为无法下推或无法重写时,MySQL会退化为嵌套循环:
这种扫描不是“扫一次”,而是逐行触发、逐行解码、逐行条件比对,CPU 花在字符串比较、类型转换、内存拷贝上的时间远超 IO 等待。尤其当 rows 达到几十万级,EXPLAIN 的 filtered 又低于 10%,基本可断定是 CPU 瓶颈源头。
子查询未被优化器转成 JOIN,且无合适索引支撑
MySQL 5.7+ 对某些子查询(如非相关子查询、含 GROUP BY 或 DISTINCT 的)会尝试自动重写为 JOIN,但前提是子查询结果集小、字段有索引、且没有 ORDER BY / LIMIT 干扰。一旦失败,就会保留子查询结构,而优化器又选了错误的驱动表——比如拿大表做驱动,小表做子查询,导致小表被反复扫描。
- 检查
EXPLAIN输出中子查询是否标记为DEPENDENT SUBQUERY或UNCACHEABLE SUBQUERY:前者表示每次依赖外层值,后者表示无法缓存结果,二者都意味着重复计算 -
key列为NULL且Extra出现Using where; Using index condition以外的冗余描述(如Full scan on NULL key),说明索引完全没参与 - 把子查询单独拎出来执行一遍,看是否也慢;如果单独快、嵌套后变慢,大概率是统计信息不准或参数设置导致计划失真
隐式转换 + 子查询组合,让 CPU 在每行上做两次 cast
这是最隐蔽的 CPU 杀手。例如 users.id 是 BIGINT,但子查询里写成 WHERE id = '123',MySQL 会对每一行的 id 做 CAST(id AS CHAR) 再比对;而外层又用这个字符串结果去匹配 orders.user_id(也是 BIGINT),再做一次反向转换。等于单行数据要经历两次类型强转 + 两次字符串比较。
这种开销在 EXPLAIN 里不会直接显示,但你会看到 Rows_examined 是 Rows_sent 的数百倍,SHOW PROFILE FOR QUERY N 中 convert 和 compare 占比极高。此时改写为显式类型一致的写法(如 WHERE id = 123)+ 补联合索引,往往立竿见影。
子查询返回大量中间结果,触发临时表和 filesort
当子查询包含 GROUP BY、ORDER BY 或聚合函数,又没走索引排序时,MySQL 必须在内存或磁盘建临时表,并对中间结果做二次排序。这类操作在 EXPLAIN 中表现为 Extra 字段出现 Using temporary; Using filesort,而 rows 值往往接近子查询表的总行数。
CPU 飙升来自两块:一是构建哈希表或排序缓冲区的内存管理开销,二是排序算法(如 quicksort)在大量数据下的比较次数爆炸式增长。即使最终只取 LIMIT 10,前面的排序仍得做完。
- 避免在子查询里写
ORDER BY ... LIMIT后再被外层引用——优化器通常无法下推该LIMIT - 确认子查询的
ORDER BY字段是否已建索引;若只是为取最新几条,优先用MAX()或覆盖索引 +WHERE time > ?替代 - 用
CREATE TEMPORARY TABLE显式物化子查询结果,并在临时表上建索引,有时比让优化器硬扛更稳
实际排查时,最容易被忽略的是:子查询是否真的必要?很多业务逻辑里,IN 可以换成 EXISTS,NOT IN 必须换成 NOT EXISTS(否则遇到 NULL 直接逻辑错乱),而这两者在有索引时几乎不触发全表扫描。别急着加资源,先看 EXPLAIN 里那行 type=ALL 背后,是不是有人把子查询当便利贴用了。
本文共计1139个文字,预计阅读时间需要5分钟。
子查询查询本身不会导致CPU飙升,但当它被优化器判定为无法下推或无法重写时,MySQL会退化为嵌套循环:
这种扫描不是“扫一次”,而是逐行触发、逐行解码、逐行条件比对,CPU 花在字符串比较、类型转换、内存拷贝上的时间远超 IO 等待。尤其当 rows 达到几十万级,EXPLAIN 的 filtered 又低于 10%,基本可断定是 CPU 瓶颈源头。
子查询未被优化器转成 JOIN,且无合适索引支撑
MySQL 5.7+ 对某些子查询(如非相关子查询、含 GROUP BY 或 DISTINCT 的)会尝试自动重写为 JOIN,但前提是子查询结果集小、字段有索引、且没有 ORDER BY / LIMIT 干扰。一旦失败,就会保留子查询结构,而优化器又选了错误的驱动表——比如拿大表做驱动,小表做子查询,导致小表被反复扫描。
- 检查
EXPLAIN输出中子查询是否标记为DEPENDENT SUBQUERY或UNCACHEABLE SUBQUERY:前者表示每次依赖外层值,后者表示无法缓存结果,二者都意味着重复计算 -
key列为NULL且Extra出现Using where; Using index condition以外的冗余描述(如Full scan on NULL key),说明索引完全没参与 - 把子查询单独拎出来执行一遍,看是否也慢;如果单独快、嵌套后变慢,大概率是统计信息不准或参数设置导致计划失真
隐式转换 + 子查询组合,让 CPU 在每行上做两次 cast
这是最隐蔽的 CPU 杀手。例如 users.id 是 BIGINT,但子查询里写成 WHERE id = '123',MySQL 会对每一行的 id 做 CAST(id AS CHAR) 再比对;而外层又用这个字符串结果去匹配 orders.user_id(也是 BIGINT),再做一次反向转换。等于单行数据要经历两次类型强转 + 两次字符串比较。
这种开销在 EXPLAIN 里不会直接显示,但你会看到 Rows_examined 是 Rows_sent 的数百倍,SHOW PROFILE FOR QUERY N 中 convert 和 compare 占比极高。此时改写为显式类型一致的写法(如 WHERE id = 123)+ 补联合索引,往往立竿见影。
子查询返回大量中间结果,触发临时表和 filesort
当子查询包含 GROUP BY、ORDER BY 或聚合函数,又没走索引排序时,MySQL 必须在内存或磁盘建临时表,并对中间结果做二次排序。这类操作在 EXPLAIN 中表现为 Extra 字段出现 Using temporary; Using filesort,而 rows 值往往接近子查询表的总行数。
CPU 飙升来自两块:一是构建哈希表或排序缓冲区的内存管理开销,二是排序算法(如 quicksort)在大量数据下的比较次数爆炸式增长。即使最终只取 LIMIT 10,前面的排序仍得做完。
- 避免在子查询里写
ORDER BY ... LIMIT后再被外层引用——优化器通常无法下推该LIMIT - 确认子查询的
ORDER BY字段是否已建索引;若只是为取最新几条,优先用MAX()或覆盖索引 +WHERE time > ?替代 - 用
CREATE TEMPORARY TABLE显式物化子查询结果,并在临时表上建索引,有时比让优化器硬扛更稳
实际排查时,最容易被忽略的是:子查询是否真的必要?很多业务逻辑里,IN 可以换成 EXISTS,NOT IN 必须换成 NOT EXISTS(否则遇到 NULL 直接逻辑错乱),而这两者在有索引时几乎不触发全表扫描。别急着加资源,先看 EXPLAIN 里那行 type=ALL 背后,是不是有人把子查询当便利贴用了。

