题目描述

✅ 183. 从不订购的客户

image-20260928233513388

image-20260928233513389

题意分析

Customers(id, name) 保存客户,Orders(id, customerId) 保存订单。要找的是没有任何订单的客户,并将名称列输出为 Customers。判断依据是客户编号,不能按姓名判断,因为不同客户可能同名。

对每个客户,只要能找到一条所属订单,他就不符合要求;只有一条也找不到时才保留。这可以直接用 NOT EXISTS 表达,也可以先左连接订单,再筛出未匹配行。

结果仍按客户记录输出,不必计数、排序或去重。订单表为空时应保留全部客户;每个客户都有订单时应返回空结果。

解法一:NOT EXISTS 相关子查询

核心思路

[!blue]

外层查询取出当前客户 c,子查询用 o.customerId = c.id 查找他的订单。因为条件引用了外层的 c.id,它称为相关子查询:不同客户对应不同的匹配条件。

EXISTS 只判断子查询有没有返回行,与行中选出的具体数值无关;写 SELECT 1 是为了表达“只检查存在”。前面加 NOT 后,有订单的客户被排除,没有订单的客户被保留。

同一客户有一张还是多张订单,都不会改变存在性判断,也不会把外层客户复制成多行。订单的 customerId 若为空,则不能与客户主键相等,只是不产生匹配,不会影响其他客户的判断。

解题步骤

  1. 扫描 Customers AS c,以客户作为候选结果。
  2. 在 Orders AS o 中查找 o.customerId = c.id 的行。
  3. 用 NOT EXISTS 保留找不到任何匹配的客户。
  4. 输出 c.name AS Customers,不额外按名称去重。

样例中 Joe、Sam 的编号都能匹配到订单,因此被排除;Henry、Max 找不到匹配行,因此返回他们的名称。

代码实现

SELECT c.name AS Customers
FROM Customers AS c
WHERE NOT EXISTS (
    SELECT 1
    FROM Orders AS o
    WHERE o.customerId = c.id
);

复杂度分析

设客户数为 $C$、订单数为 $M$。相关子查询描述逻辑关系,并不意味着数据库一定为每个客户完整扫描一次订单表。

  • 时间复杂度:朴素执行最坏为 $O(CM)$;优化为哈希反连接时通常为 $O(C+M)$;有 Orders(customerId) 索引时也可逐客户查找是否存在匹配。
  • 空间复杂度:若构建订单客户编号的哈希集合,额外空间上界为 $O(M)$;具体临时空间取决于执行计划。

关键点总结

[!green]

  • 相关条件必须写成订单所属客户编号等于当前客户编号。
  • 只需判断有没有订单,不需要求出全部订单数。
  • SELECT 1 中的常量不参与客户筛选,起作用的是子查询是否产生行。
  • 不要直接把它替换为未处理空值的 NOT IN:若子查询列表含 NULL,不匹配的客户也可能因结果为未知而被过滤。

解法二:LEFT JOIN 构造反连接

核心思路

[!blue]

以客户表为左表连接订单表。已有订单的客户会匹配到真实订单行;没有订单的客户也会被保留,但对应的右表列全部由外连接补为 NULL。

Orders.id 是主键,真实订单中的这个字段不为空,所以 WHERE o.id IS NULL 能准确选中补出的未匹配行。匹配到多张订单的客户虽然可能展开为多行,但每行 o.id 都不为空,会全部被过滤。

必须先连接再筛选。若把 o.id IS NULL 塞到 ON 中,由于真实订单主键都非空,反而会让每个客户都无法匹配,最终错误保留全部客户。

解题步骤

  1. 从 Customers AS c 左连接 Orders AS o,匹配条件为 o.customerId = c.id。
  2. 在连接完成后,通过 WHERE o.id IS NULL 只保留没有匹配订单的行。
  3. 输出客户名称,并使用要求的列别名。

代码实现

SELECT c.name AS Customers
FROM Customers AS c
LEFT JOIN Orders AS o
    ON o.customerId = c.id
WHERE o.id IS NULL;

复杂度分析

  • 时间复杂度:若优化器识别为哈希反连接,通常为 $O(C+M)$;索引连接或朴素嵌套循环的开销与前一种写法一样取决于索引和执行计划,朴素最坏为 $O(CM)$。
  • 空间复杂度:采用哈希反连接时上界为 $O(M)$,不计输出;其他执行方式可能使用不同的临时空间。

两种 SQL 写法都能表达同一个反连接,不能仅从文本断定哪一种一定更快。

关键点总结

[!green]

  • 左连接负责保留没有订单的客户,WHERE 再识别这些未匹配行。
  • 用右表主键检查空值,利用的是“真实记录该列非空,补出的记录该列为空”的区别。
  • 两种解法最终都保留订单匹配集合为空的客户,按习惯选择即可。

易错点总结

[!yellow]

  • 相关子查询漏掉客户条件:会变成“整个订单表是否为空”,只要任意客户有订单,所有客户都被排除。
  • 连接订单主键 o.id 与客户主键:两个编号描述不同对象,应连接 o.customerId 与 c.id。
  • 把 IS NULL 写成 = NULL:SQL 的普通等号不能判断空值,必须使用 IS NULL。
  • 使用 INNER JOIN:没有订单的客户先被删除,之后无法再筛出来。
  • 左连接后使用 COUNT(*) = 0:未匹配客户也有一行补空值的记录,COUNT(*) 并不是 0。
  • 按客户名去重:同名的不同客户可能都没有订单,应保留各自的结果行。

相似题目

题目 难度 关联与区别
175. 组合两个表 简单 同样使用外连接保留主表对象,原题展示匹配信息,本题只保留没有关联记录的客户。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/48094053
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!