GROUP BY + HAVING COUNT(DISTINCT product_id) >= 2 是最直接解法,因“至少购买两种产品”需按用户聚合统计不同产品数,而非订单行数;COUNT(*) >= 2 错误统计重复下单,必须用 DISTINCT;嵌套查询仅在需用户全字段时适用,但性能较差。

为什么 GROUP BY + HAVING COUNT(DISTINCT ...) 是最直接解法
“至少购买过两种产品”本质是按用户聚合后统计其购买的不同产品数量,不是查某次订单含多种商品。嵌套查询容易绕弯,比如先子查询再外层过滤,但实际用单层分组更稳、更易读、性能更好。
常见错误是写成 HAVING COUNT(*) >= 2——这统计的是订单行数(可能同一产品多次下单),不是产品种类数。必须用 COUNT(DISTINCT product_id) 或等价字段。
- 假设订单表叫
orders,字段含user_id、product_id - 若产品用名称标识,确保
product_name无歧义(如不同品牌同名需加product_sku) - MySQL 5.7+、PostgreSQL、SQL Server 都支持
COUNT(DISTINCT ...);SQLite 也支持,但旧版 SQLite 可能报错
SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(DISTINCT product_id) >= 2;
嵌套查询真要用?那就用相关子查询控制粒度
当主表不是订单明细,而是用户表(如 users),且需要返回用户完整信息时,嵌套才合理。此时外层扫 users,内层对每个 user_id 查其不同产品数。
注意:相关子查询性能弱于分组,数据量大时明显变慢;且不能在子查询里用 GROUP BY 直接返回多行,必须用聚合函数包裹。
- 子查询必须返回单值,所以用
(SELECT COUNT(DISTINCT o.product_id) FROM orders o WHERE o.user_id = u.user_id) - 别漏
WHERE中的关联条件,否则变成笛卡尔积,结果全错 - 某些数据库(如早期 MySQL)对相关子查询优化差,建议加
orders(user_id, product_id)复合索引
SELECT * FROM users u WHERE ( SELECT COUNT(DISTINCT o.product_id) FROM orders o WHERE o.user_id = u.user_id ) >= 2;
EXISTS 嵌套写法适合“存在至少两种”的逻辑表达
如果只关心“有没有两种”,不关心具体几种,EXISTS 比 COUNT 更轻量。它只要找到两个不同 product_id 就停,不用全扫完。
但写法稍绕:需自连接或两次 EXISTS 判断,且要确保两个产品不重复。最容易出错的是没加 p1.product_id != p2.product_id 条件,导致同一产品匹配两次。
- 推荐双
EXISTS写法,比自连接更清晰 - 内层
EXISTS必须关联外层user_id,否则查的是全局是否存在两种产品 - PostgreSQL 和 SQL Server 对这种写法优化较好;MySQL 8.0+ 也行,但低于 8.0 可能退化为全表扫描
SELECT DISTINCT u.user_id
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o1 WHERE o1.user_id = u.user_id
) AND EXISTS (
SELECT 1 FROM orders o2 WHERE o2.user_id = u.user_id
AND o2.product_id != (
SELECT MIN(o3.product_id)
FROM orders o3
WHERE o3.user_id = u.user_id
)
);别忽略 NULL 和去重逻辑的坑
product_id 为 NULL 时,COUNT(DISTINCT product_id) 会忽略它——这是标准行为,但如果你业务中 NULL 表示“未指定产品”,那它不该参与“至少两种”的计数,反而是好事。
真正容易踩坑的是:产品表有冗余记录(比如历史数据未清理)、订单表未做清洗(同一订单拆成多行但 product_id 重复),导致 DISTINCT 失效。
- 上线前务必用
SELECT user_id, COUNT(*), COUNT(DISTINCT product_id) FROM orders GROUP BY user_id ORDER BY 3 DESC LIMIT 5抽样验证 - 如果产品用名称而非 ID,注意大小写、空格、前后缀(如 “iPhone 15” vs “iPhone15”),先统一清洗再统计
- Oracle 用户注意:
COUNT(DISTINCT ...)在较老版本中不支持,得用ROW_NUMBER() OVER (PARTITION BY user_id, product_id ORDER BY 1)配合去重临时表
复杂点从来不在语法,而在你手里的数据干不干净、字段语义清不清楚。写完查一遍 user_id 对应的实际订单和产品列表,比背一百条 SQL 规则都管用。

















