not exists最可靠,因其只判断子查询是否存在匹配行,不依赖值比较,天然规避null陷阱、语义清晰、性能优且跨库一致;典型写法:select c.customer_id, c.name from customers c where not exists (select 1 from orders o where o.customer_id = c.customer_id)。

用 NOT EXISTS 判断客户无购买记录最可靠
直接查 customers 表里那些在 orders 表中找不到对应订单的客户,NOT EXISTS 是语义最清晰、执行效率也通常最好的方式。它避免了 LEFT JOIN 后对 NULL 的误判,也不像 NOT IN 那样被子查询返回 NULL 值直接导致整个结果为空。
典型写法:
SELECT c.customer_id, c.name FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id );
-
SELECT 1是惯用写法,不关心子查询具体返回什么,只判断是否存在 - 子查询中的
WHERE o.customer_id = c.customer_id必须关联外层客户,否则变成全表扫描或恒假 - 如果
orders.customer_id没有索引,这个查询会很慢——务必确认该字段已建索引
LEFT JOIN ... IS NULL 写法容易漏掉空值陷阱
这种写法直观,但实际使用时容易忽略连接键本身为 NULL 的情况。比如客户表里 customer_id 允许为空,或者订单表里 customer_id 存在脏数据,就可能导致结果不准确。
安全写法必须加双重判断:
SELECT c.customer_id, c.name FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id WHERE o.customer_id IS NULL AND c.customer_id IS NOT NULL;
- 必须显式排除
c.customer_id IS NULL,否则可能把客户主键为空的“幽灵客户”也算进去 -
o.customer_id IS NULL只表示没匹配到订单,不代表外键一定有效 - 比
NOT EXISTS多一次哈希连接开销,在大数据量下性能略差
千万别用 NOT IN 查“没有订单”的客户
一旦 orders.customer_id 列里存在任意一个 NULL,整条 NOT IN 查询就会返回空结果集——这是 SQL 三值逻辑的硬伤,不是 bug,但极易踩坑。
反例(危险):
SELECT customer_id, name FROM customers WHERE customer_id NOT IN ( SELECT customer_id FROM orders );
- 只要子查询里有一个
NULL,外部WHERE条件对所有行都判定为UNKNOWN,结果集为空 - 即使你确认当前没
NULL,业务增长后某天插入一条异常订单,查询就突然失效 - 想强行用
NOT IN?得加WHERE customer_id IS NOT NULL过滤子查询,但已失去简洁性优势
区分“从未下单”和“最近30天无下单”要改条件位置
业务常混淆这两个概念。如果目标是“从未下单”,所有逻辑都作用于 orders 表是否存在记录;如果是“最近无下单”,过滤必须放在子查询内部,而不是外层 WHERE。
正确写法(最近30天无下单):
SELECT c.customer_id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_date >= CURRENT_DATE - INTERVAL '30 days'
);
- 时间条件
o.order_date >= ...必须写在子查询里,否则会先筛出所有历史订单,再对外层客户做排除——逻辑完全错误 - 注意
CURRENT_DATE - INTERVAL语法因数据库而异(PostgreSQL/MySQL 有差异,SQL Server 用DATEADD) - 这种写法仍依赖
orders(customer_id, order_date)的联合索引才能高效
外层客户数远大于订单数时,NOT EXISTS 的执行计划通常能走半连接(semi-join)优化;而子查询里漏写关联条件、或忘记给外键建索引,是线上慢查询最常见的两个根因。










