子查询展开引发嵌套循环导致扫描行数爆炸,相关子查询每次重执行、not in性能极差、多层嵌套阻碍谓词下推、字段表达式破坏索引利用——根本解法是避免相关子查询、用left join替代not in、扁平化视图、优化基表索引与统计信息。

子查询展开后触发嵌套循环,扫描行数爆炸
视图里的子查询不会被缓存或预计算,每次调用都会原样内联展开。如果子查询是相关子查询(比如引用了外层表字段),优化器大概率生成嵌套循环执行计划:外层每返回一行,子查询就重跑一遍。10 万行外层 × 平均 500 行子查询扫描 = 5000 万行总扫描量——这比直接 JOIN 高出一两个数量级。
常见错误现象:EXPLAIN 显示 type = ALL 或 rows 列数值远超基表总行数;单独执行子查询很快,但嵌在视图里就卡住。
- MySQL 和 SQL Server 默认不为相关子查询做哈希连接或物化,除非你显式加提示(如
/*+ HASH_AJ */) - PostgreSQL 9.6+ 对简单 EXISTS 有自动转 JOIN 优化,但遇到聚合、
ORDER BY或多层嵌套时仍会退化 - 子查询里用了
SYSDATE、RAND()或未分析的表,优化器会静默放弃改写,哪怕加了UNNEST提示
NOT IN 子查询强制走临时表,索引完全失效
NOT IN (SELECT ...) 是性能杀手,尤其当子查询结果含 NULL 时,整个条件恒为 UNKNOWN,结果为空——但数据库仍得先建临时表存下全部结果,再逐行比对。这个过程既无法利用 user_logins(user_id, login_time) 上的复合索引,又引发磁盘 I/O。
实操建议:
- 一律用
LEFT JOIN ... ON ... AND condition WHERE ... IS NULL替代NOT IN,AND必须写在ON里,不能挪到WHERE - 确保关联字段上有索引,例如
user_logins(user_id, login_time),否则 JOIN 仍全表扫 - MySQL 5.7 前无视图合并,即使你只查
SELECT user_id FROM v_inactive_users WHERE user_id = 123,也会先物化整个视图再过滤
多层嵌套导致谓词下推失败,基表索引形同虚设
视图 A → B → C 这种链式嵌套超过三层,优化器对中间结果集大小估算偏差极大。一旦某层含 LIMIT、GROUP BY 或窗口函数,外层 WHERE user_id = 123 就再也下推不到最底层基表扫描环节——执行计划里必然出现 Materialize(PG)、Table Spool(SQL Server)或 Using temporary(MySQL)。
验证方法很简单:
- 用
pg_get_viewdef('v_c')或sp_helptext 'v_c'逐层展开,手动拼出最终 SQL - 对拼好的语句跑一次
EXPLAIN:如果这时能命中索引、rows显著下降,问题就锁定在嵌套层级 - MySQL 5.7 及以前必须避免嵌套视图,改用 CTE 或提前聚合的中间表
字段表达式和 SELECT * 破坏列可过滤性,中间结果集膨胀
视图定义里写 UPPER(name) 或 SELECT *,会让优化器无法确认列来源和等值可推导性。比如 WHERE UPPER(name) = 'ABC' 无法命中 name 索引(除非建函数索引),而 SELECT * 会把所有字段拖进中间结果,增大内存占用、网络传输和排序开销。
哪怕基表有 (tenant_id, status) 联合索引,视图里加了 ORDER BY UPPER(name),也可能触发 Using filesort —— 因为排序字段不是基表原始列,优化器不敢下推。
真正起作用的永远是基表索引,不是视图字段名。这点最容易被忽略:你盯着视图定义改来改去,其实该动的是底层表的索引策略和统计信息更新频率。










