核心思路是用LEFT JOIN以orders表为主表连接payments表,再通过WHERE p.id IS NULL筛选无匹配付款记录的订单;必须避免误用INNER JOIN或把判空条件写入ON子句。

用 LEFT JOIN 找出有订单但没付款的记录
核心思路是:以 orders 表为主表,左连接 payments 表,再筛选出 payments.id 为 NULL 的行。这表示该订单在付款表里找不到匹配记录。
常见错误是误用 INNER JOIN 或把过滤条件写在 ON 子句里却没意识到它会影响连接逻辑。
- 必须用
LEFT JOIN,不能用INNER JOIN—— 后者会直接排除掉无付款的订单 -
WHERE p.id IS NULL必须写在WHERE子句,而不是ON子句;如果写成ON o.id = p.order_id AND p.id IS NULL,多数数据库(如 MySQL、PostgreSQL)会因语义歧义返回空结果或意外行为 - 注意字段名一致性:
payments表里关联订单的字段通常是order_id,不是id;实际要查的是p.order_id是否匹配,但判空要用p.id(或任意非空约束字段)
SELECT o.id, o.created_at FROM orders o LEFT JOIN payments p ON o.id = p.order_id WHERE p.id IS NULL;
为什么不能用 NOT EXISTS 或 NOT IN?
它们也能实现,但各有隐患,LEFT JOIN + IS NULL 在大多数场景下更可控、可读性更强,尤其当涉及多字段关联或需要返回订单详情时。
-
NOT IN (SELECT order_id FROM payments)遇到payments.order_id中有NULL值时,整条查询会返回空结果 —— 这是 SQL 三值逻辑的经典陷阱 -
NOT EXISTS虽安全,但子查询需手写相关条件(如WHERE p.order_id = o.id),稍不注意就写成无关联子查询,导致性能爆炸 - 如果后续要扩展查“最近7天有订单但未付款”,
LEFT JOIN可直接在WHERE加o.created_at >= '...' AND p.id IS NULL,逻辑清晰;而NOT EXISTS得嵌套一层,可读性下降
索引对性能的影响很实际
没有索引时,10万订单 + 8万付款记录,这个查询可能跑几秒;加对索引后通常压到 20ms 内。
- 必须确保
payments.order_id有索引(单列或作为联合索引的首列) -
orders.id一般已有主键索引,不用额外处理 - 如果常按时间范围查(比如“昨天的未付款订单”),建议在
orders.created_at上建索引,并考虑复合索引(created_at, id)提升覆盖查询效率
MySQL 和 PostgreSQL 的 NULL 处理一致吗?
在这个场景下完全一致 —— LEFT JOIN 后未匹配行的右表所有字段都是 NULL,IS NULL 判定行为标准且可靠。
- 唯一要注意的是:某些 ORM(如 Django ORM 的
exclude(payment__isnull=False))生成的 SQL 可能绕弯,最好用原始 SQL 或确认其生成的确实是IS NULL - SQL Server 也一样,但旧版本(2005 以前)对
TEXT/NTEXT字段的IS NULL有特殊限制,现在基本不用考虑 - 别信“用
COALESCE(p.id, 0) = 0更安全”这种说法 —— 它会阻止索引使用,且逻辑等价性依赖p.id不为 0,纯属画蛇添足
真正容易被忽略的,是业务上“无付款”是否包含“付款失败但已记录”的情况——如果你的 payments 表里有 status 字段,那还得加 AND (p.status IS NULL OR p.status = 'failed'),否则会漏掉那些失败但留了记录的订单。

















