优先用 NOT EXISTS,因为 NOT IN 遇子查询含 NULL 时整个条件恒为 UNKNOWN,被 WHERE 过滤导致空结果;NOT EXISTS 只判断行存在性,天然免疫 NULL,语义准确且跨库一致。

子查询用 NOT EXISTS 还是 NOT IN?
直接说结论:优先用 NOT EXISTS。因为 NOT IN 遇到子查询结果含 NULL 时会整个返回空集——不是没数据,而是逻辑失效,连一行都不出来。
常见错误现象:
- 表
orders里product_id允许为NULL,用NOT IN (SELECT product_id FROM orders WHERE product_id = 123)后查不到任何人,哪怕实际有用户真没买过 -
NOT IN要求子查询列完全非空,而业务表里外键字段常带NULL(比如订单未确认、商品下架后置空)
实操建议:
- 写法统一用
NOT EXISTS,语义清晰且对NULL安全 - 子查询中必须关联外层用户表,否则变成“只要有人买过就排除所有人”
SELECT u.user_id, u.name FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.user_id AND o.product_id = 123 );
LEFT JOIN + IS NULL 怎么写才不漏人?
这是 NOT EXISTS 的等价替代方案,但容易在连接条件上出错。
使用场景:
- 需要同时查出用户信息和“是否买过”的标记(比如加一列
has_bought),这时LEFT JOIN更直观 - 某些旧版 MySQL(5.6 以前)对
NOT EXISTS优化不佳,JOIN 反而快
容易踩的坑:
- 把过滤条件
o.product_id = 123放在ON子句里,而不是WHERE—— 放错位置会导致左表被意外过滤 - 忘记加
o.user_id IS NULL判断,只靠o.product_id IS NULL,结果把买过其他商品的用户也混进来了
正确写法:
SELECT u.user_id, u.name FROM users u LEFT JOIN orders o ON u.user_id = o.user_id AND o.product_id = 123 WHERE o.user_id IS NULL;
性能差?检查有没有联合索引 子查询或 JOIN 效率低,90% 是缺索引,不是写法问题。
参数差异:
- 单列索引
product_id对NOT EXISTS帮助有限,因为还要快速定位到某用户的订单 - 理想索引是
(user_id, product_id)或(product_id, user_id),顺序取决于查询主驱动表
性能影响:
- 没索引时,
NOT EXISTS可能触发对每个用户的全表扫描orders -
EXPLAIN看执行计划,重点确认type是ref或eq_ref,不是ALL
实操建议:
- 在
orders(user_id, product_id)上建联合索引(InnoDB 下覆盖常用查询) - 如果
product_id查询频率远高于user_id,可换为(product_id, user_id)
用户量大时,为什么结果偶尔不一致? 本质是读一致性问题,不是语法错误。
使用场景:
- 在高并发下单系统中,一个事务刚插入订单,另一个查询还没提交,此时
NOT EXISTS可能查不到这条记录 - 不是 bug,是数据库隔离级别决定的(默认
REPEATABLE READ下,查询看到的是事务开始时的快照)
容易被忽略的地方:
- 开发时本地单线程测不出,上线后流量一上来就出现“用户明明刚下单,却还在未购买列表里”
- 不要试图用
SELECT ... FOR UPDATE解决——这只会让查询变慢、锁表,且违背“只读筛选”的初衷
稳妥做法:
- 接受短暂不一致,这是 OLTP 系统的常态
- 若业务强要求实时性(如风控拦截),应改用应用层缓存+事件驱动更新状态,而不是依赖即席 SQL 扫描

















