先看场景:
有一张商品销售表
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()
会硬生生砍掉多余的商品,导致数据丢失。
二、正确的排名函数选择
ROW_NUMBER() | ||
RANK() | ||
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端预聚合”就算加分项。
五、终极答案:不仅要写对,还要说清楚
一个满分回答的层次:
基础解法: ROW_NUMBER()
(指出其局限性)正确解法: RANK()
/DENSE_RANK()
(解释业务语义)性能优化: LATERAL
+LIMIT
或 Map端Top N工程考量:数据倾斜、NULL值处理、去重策略
最后一句可以补上:
“如果面试官允许使用非SQL方式,我会选择在数仓分层中提前用聚合表固化每个品类的Top 3,避免查询时计算。”
总结
面试官想看的,从来不是你能不能写出ROW_NUMBER()
,而是:
是否理解排名函数的差异 是否考虑过数据正确性(并列场景) 是否具备大规模数据下的性能意识
下一次被问到“每个品类Top3,请不要只交出那90%的答案。
文章转载自陈乔数据观止,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




