SQL关联查询CPU占用100%,如何排查因缺失索引导致的嵌套循环问题?
- 内容介绍
- 相关推荐
本文共计905个文字,预计阅读时间需要4分钟。
基本原因不是用了JOIN,而是执行计划选择了嵌套循环。外层驱动表返回大量行、内层被重复扫描无索引字段。每次外层返回一行,内层全表扫描一次,1万行×10万行=1亿次逻辑读,全压在CPU上做匹配和比较。
常见触发场景:LEFT JOIN 时左表没加WHERE过滤、ON条件里用了函数(如UPPER(a.name) = UPPER(b.name))、或内表连接字段完全没索引。
- MySQL默认在小结果集驱动大表时倾向
Nested Loop,但一旦驱动表实际返回行数远超预估(比如统计信息过时),就会失控 - SQL Server和Oracle也会在缺失索引+低选择性条件下 fallback 到
Nested Loops,尤其当优化器误判内表能走Index Seek却实际走了Index Scan时 - PostgreSQL在
enable_nestloop=on且无可用Hash Join路径时,同样可能硬上嵌套循环
用EXPLAIN/SHOW PLAN快速识别缺失索引的嵌套循环
别猜,直接看执行计划里有没有“Rows远大于Actual Rows”或“Missing Index提示”,以及内表访问类型是不是ALL(MySQL)、Clustered Index Scan(SQL Server)或Seq Scan(PostgreSQL)。
- MySQL:
EXPLAIN FORMAT=TREE SELECT ...中若看到<not executable>或using join buffer,大概率是内表没索引被迫走Block Nested Loop - SQL Server:SSMS里看图形执行计划,右键内表操作符 → “Missing Index Details”,会直接给出
CREATE INDEX语句模板 - PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS) SELECT ...中重点看Buffers: shared read=XXXXX是否异常高,再结合Seq Scan节点确认是否缺索引
加索引前必须核对的三个条件
不是所有JOIN字段都适合加索引。盲目建索引反而拖慢写入、浪费内存,还可能让优化器选错执行路径。
- 确认该字段在
ON或WHERE中是**等值匹配**(非>、LIKE '%abc'、IS NULL等无法走索引的用法) - 检查该字段的**选择性**:用
SELECT COUNT(DISTINCT col)/COUNT(*) FROM table算出比值,低于0.01(即1%)说明重复值太多,单列索引效果差,应考虑联合索引 - 验证该表是否已存在**覆盖索引**:比如要查
user_id, name, email,而现有索引是(user_id, name),那加(user_id, name, email)就能避免回表,比单独给email加索引更有效
临时缓解:强制改用Hash Join或Merge Join
当无法立刻加索引(比如生产库禁止DDL),可用Hint或配置临时绕过嵌套循环。但这只是止血,不是根治。
- MySQL 8.0+:
SELECT /*+ HASH_JOIN(t2) */ * FROM t1 JOIN t2 ON t1.id = t2.t1_id - SQL Server:
SELECT * FROM t1 INNER LOOP JOIN t2 ON ...→ 改成INNER HASH JOIN或INNER MERGE JOIN - PostgreSQL:设
SET enable_nestloop = off;(当前session生效),但注意这会影响所有查询,需配合SET LOCAL在事务内使用 - 注意:SQL Server中
HASH JOIN会吃大量内存,若max_server_memory不足,可能引发tempdb争用,反而加剧CPU压力
最常被忽略的一点:即使加了索引,如果连接字段类型不一致(比如INT vs VARCHAR),MySQL和SQL Server仍会放弃走索引,安静地退回嵌套循环——务必用SHOW CREATE TABLE核对两边字段类型和字符集。
本文共计905个文字,预计阅读时间需要4分钟。
基本原因不是用了JOIN,而是执行计划选择了嵌套循环。外层驱动表返回大量行、内层被重复扫描无索引字段。每次外层返回一行,内层全表扫描一次,1万行×10万行=1亿次逻辑读,全压在CPU上做匹配和比较。
常见触发场景:LEFT JOIN 时左表没加WHERE过滤、ON条件里用了函数(如UPPER(a.name) = UPPER(b.name))、或内表连接字段完全没索引。
- MySQL默认在小结果集驱动大表时倾向
Nested Loop,但一旦驱动表实际返回行数远超预估(比如统计信息过时),就会失控 - SQL Server和Oracle也会在缺失索引+低选择性条件下 fallback 到
Nested Loops,尤其当优化器误判内表能走Index Seek却实际走了Index Scan时 - PostgreSQL在
enable_nestloop=on且无可用Hash Join路径时,同样可能硬上嵌套循环
用EXPLAIN/SHOW PLAN快速识别缺失索引的嵌套循环
别猜,直接看执行计划里有没有“Rows远大于Actual Rows”或“Missing Index提示”,以及内表访问类型是不是ALL(MySQL)、Clustered Index Scan(SQL Server)或Seq Scan(PostgreSQL)。
- MySQL:
EXPLAIN FORMAT=TREE SELECT ...中若看到<not executable>或using join buffer,大概率是内表没索引被迫走Block Nested Loop - SQL Server:SSMS里看图形执行计划,右键内表操作符 → “Missing Index Details”,会直接给出
CREATE INDEX语句模板 - PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS) SELECT ...中重点看Buffers: shared read=XXXXX是否异常高,再结合Seq Scan节点确认是否缺索引
加索引前必须核对的三个条件
不是所有JOIN字段都适合加索引。盲目建索引反而拖慢写入、浪费内存,还可能让优化器选错执行路径。
- 确认该字段在
ON或WHERE中是**等值匹配**(非>、LIKE '%abc'、IS NULL等无法走索引的用法) - 检查该字段的**选择性**:用
SELECT COUNT(DISTINCT col)/COUNT(*) FROM table算出比值,低于0.01(即1%)说明重复值太多,单列索引效果差,应考虑联合索引 - 验证该表是否已存在**覆盖索引**:比如要查
user_id, name, email,而现有索引是(user_id, name),那加(user_id, name, email)就能避免回表,比单独给email加索引更有效
临时缓解:强制改用Hash Join或Merge Join
当无法立刻加索引(比如生产库禁止DDL),可用Hint或配置临时绕过嵌套循环。但这只是止血,不是根治。
- MySQL 8.0+:
SELECT /*+ HASH_JOIN(t2) */ * FROM t1 JOIN t2 ON t1.id = t2.t1_id - SQL Server:
SELECT * FROM t1 INNER LOOP JOIN t2 ON ...→ 改成INNER HASH JOIN或INNER MERGE JOIN - PostgreSQL:设
SET enable_nestloop = off;(当前session生效),但注意这会影响所有查询,需配合SET LOCAL在事务内使用 - 注意:SQL Server中
HASH JOIN会吃大量内存,若max_server_memory不足,可能引发tempdb争用,反而加剧CPU压力
最常被忽略的一点:即使加了索引,如果连接字段类型不一致(比如INT vs VARCHAR),MySQL和SQL Server仍会放弃走索引,安静地退回嵌套循环——务必用SHOW CREATE TABLE核对两边字段类型和字符集。

