复杂关联查询慢的根本原因是执行计划未适配真实数据分布和参数值,因存储过程固化首次编译计划,而运行时参数差异大、统计过期或类型不匹配导致计划失效;explain plan仅为静态预估,须用display_cursor查真实执行路径并核对starts与a-rows偏差。

复杂关联查询慢,不是因为写在存储过程里,而是执行计划没对上真实数据分布和参数值。 存储过程会缓存首次编译的执行计划,但后续调用时若参数值差异大(比如查“最近1天” vs “最近1年”)、统计信息过期、或字段类型不匹配,优化器仍会硬套旧计划,导致全表扫描、临时表、嵌套循环爆炸等现象。
为什么 EXPLAIN PLAN 看着没问题,实际跑起来却卡住?
EXPLAIN PLAN 是静态预估,不绑定变量、不走真实游标缓存、不触发参数敏感优化。它看不到运行时的真实 sql_id 和 child_number,更不会反映 DBMS_STATS 是否锁住、是否启用 BIND_AWARE。
- 查真实执行路径,必须用
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST')),重点关注STARTS和A-Rows是否与E-Rows偏差超5倍 - 如果发现某张大表的
STARTS是 100 万次,但驱动表只有 200 行,基本可断定优化器把大小表角色弄反了——立刻检查ON条件两侧字段类型是否一致(比如BIGINT参数传给INT字段) - Oracle 中视图的统计信息独立于基表;哪怕你刚对
orders表执行了DBMS_STATS.GATHER_TABLE_STATS,视图v_user_orders的执行计划仍可能沿用过期估算
WHERE 条件放 JOIN 里还是外面?
放外面是常见误区。存储过程里写成 JOIN orders o ON u.id = o.user_id WHERE o.status = 1 AND o.create_time >= @start_date,会让 orders 全量参与连接,哪怕 @start_date 只覆盖最近3天数据。
- 正确做法是显式裁剪:用派生表或 CTE 把过滤提前,例如
JOIN (SELECT user_id, amount FROM orders WHERE status = 1 AND create_time >= @start_date) o ON u.id = o.user_id - MySQL 8.0+ 对 CTE 有物化优化,老版本建议坚持用派生表;SQL Server 和 Oracle 则对 CTE 支持更稳
- 避免在
WHERE或ON中对字段用函数,DATE(o.create_time) = CURDATE()会失效索引;改用o.create_time >= CURDATE() AND o.create_time
LEFT JOIN + WHERE 判断存在性,为什么比 EXISTS 慢?
因为它是先全量连接再过滤,中间结果集可能膨胀几十倍。尤其当 order_items 有百万行、而每个订单平均含 5 条明细时,LEFT JOIN ... WHERE i.order_id IS NOT NULL 实际生成了全部明细行,再丢弃 NULL 行。
- 只判断“是否存在”,无须取从表字段,就用
EXISTS:SELECT o.* FROM orders o WHERE EXISTS (SELECT 1 FROM order_items i WHERE i.order_id = o.order_id) - 如果还要取
i.product_name或i.qty,就必须回到 JOIN,但此时必须确保order_items(order_id)或(order_id, item_id)有有效索引 - 注意
NULL干扰:若ON a.id = b.a_id中b.a_id含大量 NULL,INNER JOIN会静默丢行;LEFT JOIN后加WHERE b.status IS NOT NULL又可能让优化器无法跳过冗余扫描
最容易被忽略的是参数类型与字段类型的隐式转换——它不会报错,但会让索引彻底静默失效;还有视图统计信息滞后、CTE 在低版本 MySQL 中不物化、以及 EXISTS 无法带出从表字段这些边界条件,都得在写存储过程时当场验证,不能凭经验跳过。











