暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

面试官:每个品类Top 3商品怎么取?90%的人只会ROW_NUMBER()

陈乔数据观止 2026-04-03
51

先看场景:

有一张商品销售表 product_sales
,字段:category_id
(品类ID)、product_id
(商品ID)、sales
(销售额)。
请取出每个品类下销售额最高的前3个商品

脑海中的代码是不是这样的:

SELECT *
FROM (
  SELECT *,
         ROW_NUMBER() OVER(PARTITION BY category_id ORDER BY sales DESC) rn
  FROM product_sales
) t
WHERE rn <= 3;

恭喜你,你属于那“90%的人”。
但面试官心里可能已经在摇头——因为这道题的考点,从来不是ROW_NUMBER()
会不会写。


一、ROW_NUMBER()
的问题在哪里?

当销售额相同时,ROW_NUMBER()
会随机给一个“第1名”、一个“第2名”,即使两人销售额一样。

业务诉求通常是

  • 销售额相同 → 并列排名
  • 并列后,Top 3 可能是 4 个商品(例如第3名并列两人)

ROW_NUMBER()
硬生生砍掉多余的商品,导致数据丢失。


二、正确的排名函数选择

函数
行为
是否适合Top 3
ROW_NUMBER()
唯一序号,相同值随机排序
❌ 丢失并列
RANK()
相同值同排名,下一个跳过
✅ 保留并列,可能超过3条
DENSE_RANK()
相同值同排名,下一个连续
✅ 保留并列,且排名紧凑

2.1 使用 RANK()
 或 DENSE_RANK()

WITH ranked AS (
  SELECT *,
         RANK() OVER(PARTITION BY category_id ORDER BY sales DESC) rk
  FROM product_sales
)
SELECT *
FROM ranked
WHERE rk <= 3;

此时如果第3名有多个商品,全部保留


三、更高级的追问:内存与性能陷阱

如果每个品类有100万商品,有1万个品类,你怎么优化?

90%的人会愣住,因为上面的窗口函数会导致全表排序 + 分区排序,产生巨大shuffle(尤其是Hive/Spark)。

3.1 动态过滤 + 横向连接(LATERAL CROSS APPLY)

在支持LATERAL
的数据库(PostgreSQL、Oracle、SQL Server、Flink SQL)中:

SELECT c.category_id, p.*
FROM (
SELECT DISTINCT category_id FROM product_sales
) c
CROSS JOIN LATERAL (
SELECT product_id, sales
FROM product_sales
WHERE category_id = c.category_id
ORDER BY sales DESC
LIMIT 3
) p;

这相当于每个品类独立取Top 3,避免全局排序,支持索引。


四、数据倾斜怎么办?

当某个品类(比如“饮料”)销量极高,而其他品类很小时:

  • 普通窗口函数:会卡在饮料这个分区
  • 解决方案:
    • 加盐打散(随机前缀)
    • 分步聚合 + Top N 归并
    • 使用近似算法(若允许误差)

面试时能说出“盐值打散”或“Map端预聚合”就算加分项。


五、终极答案:不仅要写对,还要说清楚

一个满分回答的层次:

  1. 基础解法ROW_NUMBER()
    (指出其局限性)
  2. 正确解法RANK()
     / DENSE_RANK()
    (解释业务语义)
  3. 性能优化LATERAL
     + LIMIT
     或 Map端Top N
  4. 工程考量:数据倾斜、NULL值处理、去重策略

最后一句可以补上:

“如果面试官允许使用非SQL方式,我会选择在数仓分层中提前用聚合表固化每个品类的Top 3,避免查询时计算。”


总结

面试官想看的,从来不是你能不能写出ROW_NUMBER()
,而是:

  • 是否理解排名函数的差异
  • 是否考虑过数据正确性(并列场景)
  • 是否具备大规模数据下的性能意识

下一次被问到“每个品类Top3,请不要只交出那90%的答案。

文章转载自陈乔数据观止,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论