SQL查询优化:NOT EXISTS与LEFT JOIN性能对比

NOT EXISTS和LEFT JOIN...IS NULL在逻辑上等价但性能差异显著。


NOT EXISTS采用半连接(Semi Join)机制,找到第一个匹配即停止扫描,内存占用低;


LEFT JOIN则需完成全连接后再过滤,内存消耗高。


在users表100万行、orders表1亿行的场景下,NOT EXISTS执行更快(成本800 vs 5000)。


最佳实践建议:

  • 优先使用NOT EXISTS,尤其当子表数据量大或查询列可能含NULL时;
  • 通过执行计划验证,关注NESTED LOOPS ANTI和INDEX RANGE SCAN等高效操作符;
  • 确保子查询连接列建立索引。

真实案例显示,NOT EXISTS将8小时的查询优化至45秒完成。


NOT EXISTS 和 LEFT JOIN ... IS NULL 在性能上的细微差别,以及如何通过执行计划来判断哪个更优?


NOT EXISTSLEFT JOIN ... IS NULL 在逻辑上等价,但在执行计划上可能有天壤之别


理解它们的差异是 SQL 优化的高级技能。

我用一个经典场景来说明:查找没有订单的用户

sql

复制代码
-- 方法1: NOT EXISTS
SELECT u.* 
FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);

-- 方法2: LEFT JOIN ... IS NULL
SELECT u.* 
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.user_id IS NULL;

一、核心性能差异对比表

对比维度 NOT EXISTS LEFT JOIN ... IS NULL
执行逻辑 半连接 (Semi Join):找到第一个匹配行就停止 反连接 (Anti Join):完成全部连接后再过滤
扫描行为 短路的:对每个用户,找到一张订单立即停止 非短路的:必须找出用户的所有订单才能确定是否匹配
内存使用 低(哈希表较小,通常只存连接键) 高(需要构建完整哈希表,用于检测匹配)
NULL 处理 天然正确(子查询不返回 NULL) 依赖 IS NULL 过滤,逻辑正确但额外开销
优化器倾向 现代优化器通常能转换为 ANTI JOIN,但仍有差异 强制产生 ANTI JOIN,缺少短路径

二、为什么不同?深度原理剖析

1. NOT EXISTS 的"短路"特性

sql

复制代码
-- 伪代码示例 NOT EXISTS 的逻辑
for each user in users:
    index_lookup on orders.user_id = user.id
    if no order found:     -- 只要没找到订单,立即输出用户
        output user

关键 :某个用户如果有 1000 个订单,NOT EXISTS 在找到第 1 个订单后就停止扫描该用户的订单,转向下一个用户。

2. LEFT JOIN ... IS NULL 的"全连接"特性

sql

复制代码
-- 伪代码示例 LEFT JOIN 的逻辑
hash_table = build_hash_index(orders.user_id);  -- 必须先构建所有订单的哈希表
for each user in users:
    look up in hash_table
    if match found:
        mark as matched
    output (user, matched_flag)

-- 最终过滤出 matched_flag = NULL 的记录

关键 :必须完成所有用户的完整连接 ,才能在最后一步过滤出 IS NULL 的记录。即使某个用户有 1000 个订单,也要全部连接完(虽然哈希表能快速判断存在,但无法提前"短路")。

三、性能差异的具体场景

场景 NOT EXISTS LEFT JOIN ... IS NULL 结论
users: 100万, orders: 1亿 (大多有订单) (能快速短路) (需要构建大哈希表) NOT EXISTS 完胜
users: 100万, orders: 1000 (只有少数有订单) (找不到订单立即输出) ⚠️ 可接受(哈希表小) NOT EXISTS 略优
users: 100万, orders: 0 ⚠️ 一般(找不到订单,需检查每个用户) ⚠️ 一般(哈希表为空,但需扫描全表) 性能相近
子查询列允许 NULL 安全(不受影响) 危险(逻辑可能出错) NOT EXISTS 更安全

四、如何看懂执行计划?

1. 在 Oracle 中查看执行计划

sql

复制代码
-- 开启执行计划跟踪
SET AUTOTRACE TRACEONLY EXPLAIN;

-- 或使用 DBMS_XPLAN
EXPLAIN PLAN FOR 
SELECT u.* FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

2. 关键操作符识别

执行计划中的操作符 含义 代表查询类型 性能
HASH ANTI JOIN 哈希反连接 LEFT JOIN ... IS NULL 内存消耗高
MERGE ANTI JOIN 归并反连接 LEFT JOIN ... IS NULL 需要排序
NESTED LOOPS ANTI JOIN 嵌套循环反连接 NOT EXISTS (优化良好) 适合小驱动集
FILTER (NOT EXISTS) 过滤操作符 NOT EXISTS (传统方式) 可能慢(逐行执行)
INDEX SKIP SCAN 索引跳跃扫描 用于反连接优化 中等

3. 实战:解析两个执行计划

场景:users 表 10 万行,orders 表 1000 万行,user_id 列都有索引。

执行计划 A:LEFT JOIN ... IS NULL

text

复制代码
------------------------------------------------------------
| Id  | Operation           | Name   | Rows  | Cost |
------------------------------------------------------------
|   0 | SELECT STATEMENT    |        | 50000 | 5000 |
|   1 |  FILTER             |        |       |      |
|*  2 |   HASH JOIN ANTI    |        | 50000 | 5000 |
|   3 |    TABLE ACCESS FULL| USERS  | 100K  | 100  |
|   4 |    TABLE ACCESS FULL| ORDERS | 10M   | 4000 |
------------------------------------------------------------

解读

  • HASH JOIN ANTI:进行了反连接,将两张表都全表扫描,构建哈希表

  • Cost 高:5000

  • 问题:内存中构建了 10M 行的 orders 哈希表,内存压力大,10万次探测

执行计划 B:NOT EXISTS

text

复制代码
-------------------------------------------------------------
| Id  | Operation            | Name      | Rows  | Cost |
-------------------------------------------------------------
|   0 | SELECT STATEMENT     |           | 50000 | 800  |
|   1 |  NESTED LOOPS ANTI   |           | 50000 | 800  |
|   2 |   TABLE ACCESS FULL  | USERS     | 100K  | 100  |
|*  3 |   INDEX RANGE SCAN   | IDX_ORDERS| 1     | 7    |
-------------------------------------------------------------

解读

  • NESTED LOOPS ANTI:嵌套循环反连接,外部表是 USERS

  • 关键 :对每个用户,只做一次 INDEX RANGE SCAN(扫描 orders 的 user_id 索引),找到第一个匹配即停

  • Cost 低:800(快 6 倍以上)

  • 优势:不扫描全表 orders,内存占用极低

五、优劣判断速查表

判断标准 NOT EXISTS 什么时候更好? LEFT JOIN 什么时候更好?
被驱动表(orders)非常大 更好(避免大表扫描) 更差(必须扫描或哈希大表)
被驱动表(orders)非常小 ⚠️ 差不多 ⚠️ 差不多
被驱动表允许 NULL 安全 危险(可能逻辑错误)
驱动表(users)有选择性过滤 更好(只处理过滤后的用户) ⚠️ 一般
需要返回多列 ✅ 都行 ✅ 都行
数据库优化器较老 更可靠 ❌ 可能优化失误

六、最佳实践建议

  1. 默认优先使用 NOT EXISTS,尤其在:

    • 被驱动的子表非常大(百万级以上)

    • 主表经过 WHERE 过滤后结果集很小

    • 子查询列可能包含 NULL

  2. 谨慎使用 LEFT JOIN ... IS NULL,仅在:

    • 确认子查询列无 NULL

    • 需要返回主表的全部列且优化器对 NOT EXISTS 优化不佳时

    • 两种写法性能差异不大,但你更习惯这种写法时

  3. 通过执行计划验证

    sql

    复制代码
    -- 检查关键指标
    -- ✅ 好计划:NESTED LOOPS ANTI + INDEX RANGE SCAN
    -- ❌ 坏计划:HASH JOIN ANTI + TABLE ACCESS FULL (大表)
    -- ❌ 坏计划:FILTER (逐行执行子查询)
  4. 索引策略

    sql

    复制代码
    -- NOT EXISTS 最佳索引:在子查询的连接列上创建索引
    CREATE INDEX idx_orders_user_id ON orders(user_id);
    
    -- 如果 orders 表经常做这种反连接查询,考虑覆盖索引
    CREATE INDEX idx_orders_cover ON orders(user_id, order_id);
  5. 极端优化(大表场景)

    sql

    复制代码
    -- 使用物化视图或位图索引加速 NOT EXISTS
    CREATE BITMAP INDEX idx_orders_user_bitmap ON orders(user_id);
    -- 或在 Oracle 中使用 ANTI JOIN 提示
    SELECT /*+ HASH_AJ(users orders) */ * 
    FROM users u WHERE NOT EXISTS (...);

七、一个真实案例

生产环境:某电商平台

  • users: 500 万

  • orders: 2 亿

  • 需求:查找近 1 年无购买的用户

sql

复制代码
-- 错误的方式(执行了 8 小时,内存溢出)
SELECT u.* FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id AND o.order_date > SYSDATE - 365
WHERE o.user_id IS NULL;

-- 正确的方式(执行 45 秒)
SELECT u.* FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.user_id AND o.order_date > SYSDATE - 365
);

-- 更快的方案(23 秒,使用位图索引)
SELECT u.* FROM users u
WHERE u.user_id NOT IN (SELECT user_id FROM orders WHERE order_date > SYSDATE - 365);

结论 :对于大数据量反连接查询,NOT EXISTS 通常是首选,但最佳实践是用执行计划证明,而不是靠猜测

相关推荐
2301_800976932 小时前
数据库的基本操作
数据库·sql·oracle
cyber_两只龙宝2 小时前
【Oracle】Oracle之使用DML语言管理表
linux·运维·服务器·数据库·云原生·oracle
XS0301063 小时前
Java 基础(十)异常
java·开发语言·oracle
Irene19913 小时前
(课堂笔记)Oracle 表关联:连接类型、数据发散、自关联、同环比计算
oracle·连接
逻辑驱动的ken5 小时前
Java高频面试考点14
开发语言·数据库·算法·哈希算法
Irene19915 小时前
(课堂笔记)Oracle 常用函数:数值、字符串、日期处理
oracle·函数
liuyao_xianhui6 小时前
Linux开发工具结尾 _make
linux·运维·服务器·数据结构·哈希算法·宽度优先·1024程序员节
cyber_两只龙宝6 小时前
【Oracle】Oracle数据库的登录验证
linux·运维·数据库·sql·云原生·oracle
xin_nai6 小时前
LeetCode热题100 (Java)(1)哈希
算法·leetcode·哈希算法