用LEFT JOIN + IS NULL或NOT EXISTS替代EXCEPT/MINUS:LEFT JOIN适合右表小且有索引的场景,NOT EXISTS在右表大但可利用相关列索引时更优;务必避免NOT IN(NULL导致结果为空);多列差集需在ON和WHERE中覆盖所有字段;手写方式不自动去重排序,需显式加DISTINCT和ORDER BY。

SQL里没有EXCEPT或MINUS时怎么写差集
标准SQL中EXCEPT(SQL Server、PostgreSQL)和MINUS(Oracle)确实能直接做集合差,但MySQL 8.0之前完全不支持,SQLite默认也不支持,很多旧系统或ORM生成的SQL还卡在兼容模式。这时候硬套EXCEPT会直接报错:ERROR 1064 (42000): You have an error in your SQL syntax。
真正可行的替代方案是用LEFT JOIN + IS NULL,或者NOT EXISTS。两者语义一致,但执行计划可能不同:
-
LEFT JOIN适合两表关联字段有索引、且右表结果集较小的场景; -
NOT EXISTS在右表数据量大、但能利用子查询中相关列索引时往往更快; - 别用
NOT IN——只要右表的列含NULL,整个结果就变空,这是高频翻车点。
用LEFT JOIN实现A减B的完整写法
假设要查“在orders表中出现过,但没在refunded_orders表中出现过的订单ID”:
SELECT o.order_id FROM orders o LEFT JOIN refunded_orders r ON o.order_id = r.order_id WHERE r.order_id IS NULL;
关键点在于:WHERE r.order_id IS NULL必须写在ON条件之后,不能写成WHERE o.order_id NOT IN (SELECT order_id FROM refunded_orders)。后者不仅可能因NULL失效,还会让优化器难以走索引。
如果涉及多列差集(比如要对比(user_id, product_id)组合),ON条件和WHERE判断都得覆盖全部字段:
SELECT a.user_id, a.product_id FROM purchases a LEFT JOIN returns b ON a.user_id = b.user_id AND a.product_id = b.product_id WHERE b.user_id IS NULL;
NOT EXISTS写法及性能注意点
语义更贴近“对A中每一行,检查B中是否存在匹配项”,适合B表带条件过滤的场景:
SELECT o.order_id
FROM orders o
WHERE NOT EXISTS (
SELECT 1
FROM refunded_orders r
WHERE r.order_id = o.order_id
AND r.status = 'completed'
);
这里r.status = 'completed'是额外筛选条件,用LEFT JOIN也能写,但容易漏掉WHERE位置导致逻辑错误。而NOT EXISTS天然把过滤塞进子查询里,更安全。
注意:子查询里SELECT 1只是占位,写SELECT *或SELECT order_id都没区别,但别写SELECT COUNT(*)——那会强制全表扫描。
EXCEPT/MINUS和手写差集的真实差异
EXCEPT和MINUS默认去重且隐式排序,而LEFT JOIN和NOT EXISTS保留原表顺序、不做去重。如果你需要严格等价,得手动加DISTINCT和ORDER BY:
SELECT DISTINCT o.order_id FROM orders o LEFT JOIN refunded_orders r ON o.order_id = r.order_id WHERE r.order_id IS NULL ORDER BY o.order_id;
另外,EXCEPT要求两边列数、类型、顺序完全一致;手写方式则灵活得多——你可以只比ID,也可以比拼接后的字符串,甚至加计算字段。但灵活性也意味着更容易写出语义错误的条件,比如漏掉IS NULL判断,或者误把=写成!=。
实际调试时,先单独跑右表子查询,确认它返回的确实是你要排除的值;再看LEFT JOIN后r.*是否真为NULL——这一步省略,90%的差集问题都查不出原因。

















