优先选 not exists,因它能正确处理 null 值,而 not in 遇子查询含 null 时返回空集;查“有订单但无付款”需先 inner join orders,再用 not exists 检查 payments.order_id;索引应建在 payments(order_id) 和 orders(user_id) 上。

子查询里用 NOT EXISTS 还是 NOT IN?
优先选 NOT EXISTS。它能正确处理 NULL 值,而 NOT IN 遇到子查询结果含 NULL 时会整个返回空集——这是线上查不到数据最常被忽略的原因。
典型场景:用户表 users 和订单表 orders 关联,但付款信息在另一张 payments 表里,且 payments.order_id 可能为 NULL(比如订单创建后还没生成付款记录)。
-
NOT IN写法:WHERE u.id NOT IN (SELECT user_id FROM payments)→ 一旦payments.user_id有NULL,整条语句不返回任何行 -
NOT EXISTS写法:WHERE NOT EXISTS (SELECT 1 FROM payments p WHERE p.user_id = u.id)→ 安全、语义清晰、支持索引
怎么写“有订单但无付款”的嵌套逻辑?
必须分两层过滤:先确认用户有订单,再排除其中有付款的用户。不能只查 users 表后单靠 NOT EXISTS 到 payments,否则会把“没下过单也没付过款”的用户也捞进来。
正确结构是:外层从 orders 或关联 users 的订单出发,内层用 NOT EXISTS 检查该订单是否无对应付款记录。
SELECT DISTINCT u.id, u.name FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE NOT EXISTS ( SELECT 1 FROM payments p WHERE p.order_id = o.id );
- 用
DISTINCT防止同一用户多个订单重复出现 - 关联
orders是必要前提,否则逻辑不成立 - 子查询里
WHERE p.order_id = o.id必须对齐外层订单,不是user_id
性能差怎么办?加什么索引?
慢通常卡在子查询全表扫描。关键索引不是堆在 payments 主键上,而是要覆盖子查询的查找条件。
- 必须有
payments(order_id)索引(单列即可,order_id是子查询WHERE条件) - 如果还经常按用户查,可建联合索引
payments(user_id, order_id),但本场景优先保order_id -
orders(user_id)也要有,否则INNER JOIN可能走全表
执行前用 EXPLAIN 看子查询是否走了索引——如果显示 type: ALL,说明索引没生效。
为什么 LEFT JOIN + IS NULL 不推荐?
语法看似简单:LEFT JOIN payments p ON o.id = p.order_id WHERE p.order_id IS NULL,但实际容易出错。
- 如果
payments表里一条订单对应多笔付款(比如部分退款),LEFT JOIN会产生笛卡尔积,DISTINCT成本高 - 若
payments为空表,LEFT JOIN结果集大小等于orders行数,而NOT EXISTS可提前终止 - 语义不如
NOT EXISTS直观:“不存在付款记录”比“左连后字段为空”更贴近业务意图
真正要留意的是子查询的关联字段是否允许 NULL,以及外层是否真的只想要“有订单”这个前提——漏掉这层,结果就完全偏了。











