not exists插入前必须显式关联子查询与外层表字段,否则因不相关导致恒真/恒假;正确写法如where not exists(select 1 from orders t where t.order_id = s.order_id),且关联字段需类型一致、非null并建对应索引。

NOT EXISTS 插入前必须检查子查询的关联条件是否正确
直接写 NOT EXISTS (SELECT 1 FROM target_table) 是错的——这会导致子查询不关联外层,变成恒真或恒假判断。实际插入时要么全跳过、要么全插入,完全失去“去重”意义。
正确做法是让子查询中用 WHERE 显式关联源数据与目标表的关键字段,比如主键或业务唯一键:
INSERT INTO orders (order_id, customer_id, amount) SELECT s.order_id, s.customer_id, s.amount FROM staging_orders s WHERE NOT EXISTS ( SELECT 1 FROM orders t WHERE t.order_id = s.order_id );
- 关联字段(如
order_id)必须在两个表中类型兼容,否则隐式转换可能使索引失效 - 如果用复合唯一约束(如
(customer_id, order_date)),WHERE条件里必须全部列出,缺一不可 - MySQL 8.0+ 和 PostgreSQL 对这种写法优化较好;SQL Server 需确保目标表上有对应索引,否则
NOT EXISTS可能退化为嵌套循环全表扫描
对比 INSERT IGNORE / ON CONFLICT 的适用场景
NOT EXISTS 是标准 SQL,但不是所有数据库都支持等效的“冲突忽略”语法。它适合你明确控制插入逻辑、且不能修改目标表结构(比如没主键或唯一索引)的场景。
而 INSERT IGNORE(MySQL)或 ON CONFLICT DO NOTHING(PostgreSQL)更轻量,但要求目标表已有唯一约束或主键——否则会报错或静默失败。
- 没有唯一索引?只能用
NOT EXISTS或先CREATE UNIQUE INDEX - 需要记录哪些行被跳过?
NOT EXISTS可配合RETURNING(PostgreSQL)或临时表捕获源数据,ON CONFLICT也能用RETURNING,但INSERT IGNORE不行 - Oracle 用户注意:
NOT EXISTS可用,但更推荐MERGE INTO,语义更清晰且执行计划通常更稳定
性能瓶颈常出在目标表缺少对应索引
NOT EXISTS 的子查询每次都要查目标表,如果没有索引,就是对每条源记录做一次全表扫描。10 万条源数据 × 目标表 100 万行 = 千亿级比较,根本跑不完。
- 务必在子查询
WHERE中用到的字段上建索引,例如上面例子中的orders(order_id) - 复合条件就建复合索引,顺序按子查询
WHERE中的字段出现顺序来,比如WHERE t.customer_id = s.customer_id AND t.order_date = s.order_date→ 索引为(customer_id, order_date) - PostgreSQL 中可加
EXISTS子句的EXPLAIN ANALYZE看是否走了 Index Only Scan;SQL Server 看执行计划里是否有 “Index Seek” 节点
NULL 值会让 NOT EXISTS 判断意外失效
如果关联字段可能为 NULL(比如 order_id 允许为空),t.order_id = s.order_id 在任一端为 NULL 时结果恒为 UNKNOWN,导致整个 NOT EXISTS 返回 TRUE,重复插入发生。
- 最稳妥的是在
WHERE条件中排除NULL:WHERE t.order_id = s.order_id AND t.order_id IS NOT NULL - 或者改用
IS NOT DISTINCT FROM(PostgreSQL / SQL Standard),它把NULL = NULL视为TRUE,但 MySQL 和 SQL Server 不支持该语法 - 更彻底的解法:在 ETL 源头清洗掉空值,或目标表字段设为
NOT NULL并加默认值
NULL 却没意识到比较逻辑已失效。











