SQL 中如何查询“满足所有条件”的数据?什么是关系除法?
简化版
“满足所有条件”的查询常被称为关系除法问题。典型题是查询选修了所有必修课的学生、购买了所有指定商品的用户。常见写法有 GROUP BY + HAVING COUNT(DISTINCT ...),或者双重 NOT EXISTS。如果条件集合固定且不大,聚合写法更直观;如果要表达严格的“不能缺任何一个”,双重 NOT EXISTS 更贴近数学语义。
详细版
这类问题不是简单的 IN。IN 表示满足任意一个条件,而关系除法要求覆盖条件集合中的全部元素。
例如“买过 A、B、C 三个商品的用户”,不能写 product_id IN ('A','B','C') 后直接取用户,因为那只说明买过其中至少一个。正确思路是按用户分组,统计命中的不同商品数是否等于条件集合大小。
面试中要特别说明去重,否则同一个商品买多次会把计数冲高。
完整版教学
一、问题模型
有用户购买记录表。
有目标商品集合。
要找出购买记录覆盖目标集合的用户。
这就是“用户集合除以商品集合”的关系除法。
业务上常见于权限、课程、标签、任务完成度。
二、GROUP BY + HAVING 写法
SELECT user_id
FROM purchases
WHERE product_id IN ('A', 'B', 'C')
GROUP BY user_id
HAVING COUNT(DISTINCT product_id) = 3;
WHERE 先限定目标集合。
COUNT(DISTINCT product_id) 统计每个用户覆盖了几个目标商品。
等于 3 表示 A、B、C 都覆盖。
三、条件集合来自表时
SELECT p.user_id
FROM purchases p
JOIN required_products r ON r.product_id = p.product_id
GROUP BY p.user_id
HAVING COUNT(DISTINCT p.product_id) = (
SELECT COUNT(DISTINCT product_id) FROM required_products
);
条件集合来自表时,不要把数量写死。
也要保证目标集合本身去重。
四、双重 NOT EXISTS 写法
SELECT u.id
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM required_products r
WHERE NOT EXISTS (
SELECT 1
FROM purchases p
WHERE p.user_id = u.id
AND p.product_id = r.product_id
)
);
它的语义是:不存在一个必需商品,是该用户没有购买过的。
这句话和“用户购买了所有必需商品”完全等价。
五、两种写法对比
| 写法 | 优点 | 注意点 |
|---|---|---|
GROUP BY + HAVING | 简洁,报表友好 | 要去重,条件集合数量要准确 |
双重 NOT EXISTS | 语义严谨 | SQL 较长,阅读门槛高 |
多个 EXISTS | 条件固定时直观 | 条件多时难维护 |
| 多次 JOIN | 可行 | 容易写成复杂笛卡尔结构 |
六、边界问题
用户重复购买同一商品不能重复计数。
目标集合为空时,数学上所有用户都满足,但业务上通常要单独定义。
如果还要求“只买了这些商品,没有买其他商品”,那是另一个问题,要加排除条件。
“满足所有条件”和“只满足这些条件”不是一回事,面试里要先澄清。
七、误区和追问
- 误区:IN 可以表示满足全部条件。
IN表示命中集合中的任意值,不表示全覆盖。 - 误区:COUNT(*) 等于条件数就可以。 重复记录会干扰,通常要
COUNT(DISTINCT ...)。 - 误区:满足所有条件等于只满足这些条件。 前者允许额外条件,后者还要排除额外记录。
- 追问:目标条件来自表怎么办? JOIN 条件表后按目标集合数量比较,或用双重
NOT EXISTS。 - 追问:如何查询一个用户缺哪些必需项? 用条件表反连接用户已完成项。
- 追问:性能怎么优化? 给关系表建立组合索引,如
(user_id, product_id)和条件表主键。
八、面试表达方式
先把问题翻译成“覆盖目标集合”。
然后给聚合写法和双重 NOT EXISTS 写法。
再提醒去重、空集合和“只满足”语义差异。
九、常见误区与追问
- 误区:只记住 SQL 中如何查询“满足所有条件”的数据?什么是关系除法? 的结论就够了。 数据库题通常还要解释索引、事务、锁、执行计划或一致性边界,否则很容易被追问打穿。
- 误区:能查出结果就说明 SQL 或设计没问题。 还要看数据量扩大到 100 万行后是否仍能走合适索引、是否产生临时表或锁等待。
- 误区:所有场景都追求强一致。 读写分离、缓存、异步任务都可能牺牲一部分实时性,关键是说明业务是否允许。
- 追问:线上变慢时你先看什么? 先看慢 SQL、执行计划、扫描行数、锁等待和连接池,再判断是 SQL 写法、索引还是并发问题。
- 追问:如何证明这个方案可落地? 给出一个小数据例子,再补充约束、失败场景和回滚方案,避免只停留在概念层。
十、加强记忆
记 SQL 中如何查询“满足所有条件”的数据?什么是关系除法? 时,把它压成“语义正确、执行高效、并发安全”三件事。先说清这个知识点解决什么数据库问题,再用 1 个带数字的小例子说明数据量一大为什么会出差异,最后补上索引、锁、事务或执行计划里的易错点。这样面试官无论追问 SQL 写法、线上慢查,还是并发一致性,你都能沿着同一条线继续展开。