SQL 怎么找出买齐所有指定商品的用户?
本文目录
面试怎么回答
这是集合包含,也叫关系除法。先明确目标商品集合和有效购买口径,再只统计命中的不同目标商品,数量等于目标集合大小才算买齐。买了额外商品不影响包含关系,重复购买不能增加种类数。另一种写法是双重 NOT EXISTS,表示不存在用户没买到的目标商品。目标为空时,要从约定的用户全集和业务规则出发处理。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
运营要找一批用户:指定的 A、B、C 三种商品,他们必须全部买过,买过其他商品也没关系。这道题很像 GROUP BY 加 COUNT,但“买了三次”和“买齐三种”之间,差着一整个集合判断。
买齐全部是集合包含问题:统计命中的不同目标商品,或证明不存在尚未买到的目标。
先把“买过”定义清楚
是历史上支付成功过,还是活动期内购买?发生退款后还算不算?同一商品买多个规格,按 SPU、SKU 还是活动商品编码判断?这些不是写完 SQL 后补充的小细节,而是输入集合的定义。
下面约定 target_products 是活动指定商品,valid_purchases 是按统一时间、支付和退款规则清洗后的有效购买记录,商品标识均非空。题目要求“包含全部目标”,不要求用户只买这些商品。
写法一:命中种类数等于目标种类数
先把目标集合去重,再限定购买记录只能统计目标商品。下面使用通用 SQL 写法,适用于 PostgreSQL 17:
WITH target AS (
SELECT DISTINCT product_id
FROM target_products
WHERE product_id IS NOT NULL
), matched AS (
SELECT p.user_id, p.product_id
FROM valid_purchases p
JOIN target t ON p.product_id = t.product_id
)
SELECT user_id
FROM matched
GROUP BY user_id
HAVING COUNT(DISTINCT product_id) = (
SELECT COUNT(*) FROM target
);
这里的 DISTINCT 不是随手加的保险,而是把购买明细转换为用户已经覆盖的商品种类。买十次 A 仍然只覆盖 A,买了 D 也不应该增加对 A、B、C 的覆盖数量。
目标集合也要去重,否则活动配置重复一条 B,目标数变成四,而用户最多只能命中三种。生产里非空商品标识应作为质量约束,不能静默过滤了配置错误还宣称活动规则正确。
用四个用户检查,就能看出区别
| 用户 | 有效购买记录 | 是否买齐 A、B、C |
|---|---|---|
| U1 | A、A、A | 否,缺 B、C |
| U2 | A、B、C | 是 |
| U3 | A、B、C、D | 是,额外购买不影响 |
| U4 | A、B | 否,缺 C |
如果用 COUNT(*) = 3,U1 会被误选;如果统计用户买过的所有不同商品,再要求等于三,U3 又会被错删。一个是把次数当种类,一个是忘了先限制目标范围。
写法二:不存在“他没买到的目标”
另一种表达是双重 NOT EXISTS。candidate_users 为约定的候选用户全集,且一人一行;target 与上一段一样,是去重后的非空目标集合:
WITH target AS (
SELECT DISTINCT product_id
FROM target_products
WHERE product_id IS NOT NULL
)
SELECT u.user_id
FROM candidate_users u
WHERE NOT EXISTS (
SELECT 1
FROM target t
WHERE NOT EXISTS (
SELECT 1
FROM valid_purchases p
WHERE p.user_id = u.user_id
AND p.product_id = t.product_id
)
);
从内往外读:找不到购买记录的目标商品,就是用户缺少的商品;如果连这样的目标都不存在,才算买齐。这通常称为关系除法问题,不是要求写一个名叫 DIVIDE 的函数。
两种表达谁更快不能凭语法判断,要看优化器改写、目标集合大小、数据分布和执行计划。目标很小时,先筛购买范围再聚合往往容易控制数据量,但仍需实际验证。
一个典型场景:重复购买拿到了资格
比如活动资格任务只用购买行数等于目标数。一位用户连续购买三次 A,也被发进“集齐三件”名单。最初可能怀疑活动配置串了,检查配置却发现 A、B、C 都没错。
问题出在统计单位:SQL 数的是购买记录,不是命中的商品种类。此前测试每个用户每种商品只有一行,恰好掩盖错误。修改时应把重复购买、额外商品、目标重复和退款后失效纳入测试,并保留缺失商品名单,便于解释资格为什么不成立。
目标为空,是最容易漏掉的边界
如果没有任何目标,双重 NOT EXISTS 会返回候选用户全集,因为不存在未满足的目标;第一种聚合写法则因没有 matched 行而不返回用户。两者在这个边界上不等价。
数学上的空集包含关系,不应替活动系统做业务决策。可以约定空目标视为配置错误并阻断,也可以明确所有候选人满足;但不能默默采用某一种 SQL 的偶然输出。