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 写法。
再提醒去重、空集合和“只满足”语义差异。