SQL 统计首单用户,为什么先筛商品再排序会算错?
面试怎么回答
我先明确首单是全站首单还是某品类首单,再按用户在完整有效订单范围内排序。得到首单后才判断商品和统计日期,商品明细用存在性判断,避免一单多商品放大。若先筛目标商品或本月分区,算出的只是局部首单。还要说明有效状态、历史覆盖、时间并列规则和迟到订单对首单结果的修正。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
“这个月有多少用户,人生第一笔有效订单就买了A商品?”需求看起来只有一句话。把商品条件写进最里面的WHERE,SQL可以顺利跑完,却已经换了一道题。
先筛选再排名,得到的是目标范围内首单;先在完整有效订单中排名再筛选,才是在判断全局首单是否满足条件。
“第一次”的含义,取决于比较范围
全站第一次、某店铺第一次、某品类第一次,都可以叫首单,但它们的比较总体不同。用户先买B再买A,买A那一笔是A商品首购,却不是全站首单。
还要区分下单、支付成功和最终有效订单。取消单是否进入比较,退款后是否改写历史首单,都属于业务约定,而不是窗口函数可以替你决定的事情。
建议先把一句自然语言改写成完整定义:以用户为单位,在全部历史有效支付订单中,按支付次序取第一笔,再判断它是否包含A商品,并且支付日期落在目标月份。

先在订单粒度取首单,再判断商品
下面假设valid_orders已经是一笔有效订单一行,order_items是商品明细;同一时间暂用唯一order_id打破并列。示例采用 PostgreSQL 17 语法。
WITH ranked AS (
SELECT user_id, order_id, paid_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY paid_at, order_id
) AS rn
FROM valid_orders
)
SELECT COUNT(*) AS user_count
FROM ranked r
WHERE rn = 1
AND paid_at >= TIMESTAMP '2026-09-01 00:00:00'
AND paid_at < TIMESTAMP '2026-10-01 00:00:00'
AND EXISTS (
SELECT 1 FROM order_items i
WHERE i.order_id = r.order_id
AND i.product_id = 'A'
);
EXISTS只判断是否包含商品,不会因为一笔订单有多条A商品明细而增加用户数。若订单号只在租户内唯一,关联键和用户分组都要补齐租户维度,不能照抄这个简化示例。
时间并列时使用订单号,仅代表一个确定性的约定。若业务要求按真实支付先后排序,应争取上游序列等证据,不能把字典序解释成真实发生顺序。
月份条件也不能随意推到最里面
为了少扫数据,把九月条件放进ranked内部,会把八月已经下过单的老用户重新标成九月首单用户。日期过滤改变了参与排名的历史范围。
大表未必要每天重扫全部历史。可以维护用户首次有效订单表,只对新数据涉及的用户更新,但必须允许迟到的更早订单纠正原值,并明确退款等业务变更是否触发重算。
这个优化的前提是历史首次表可信,不是用性能需求替换业务定义。若系统只保存最近一年,应把指标称为“观察期内首次”,或补齐能够证明更早历史的来源。
一个典型场景:拉新活动突然显得特别有效
假设活动商品的首单购买人数大幅增长,运营认为活动拉来了大量新客。排查先怀疑用户标识变更,随后抽样发现不少人此前已购买其他商品。
根因是先过滤活动商品,再按用户取最早一笔。旧校验只验证了结果中的订单都包含活动商品,没有检查这些用户在其他商品上是否有更早订单。
修正后同时输出“活动商品首购用户”和“全站首单包含活动商品的用户”,保留两种合理需求,但不再混用名称。抽样验收刻意加入先买其他商品、首单多商品、历史迟到等反例。
最后检查未定义的输入
支付时间为空的记录不能自然排出可靠首单,应先进入质量处理或明确的例外规则。用户账号合并、跨端身份变化,也可能改变历史比较对象。
首单模型还应保存判定版本和计算范围。否则一张报表今天显示新客,明天因历史补录变成老客,却没有解释路径,业务就会把口径修正误解为随机波动。