用窗口函数替代子查询更可靠,直接套用相关子查询(如(SELECT COUNT(*) FROM articles a2 WHERE a2.category = a1.category AND a2.likes > a1.likes))可提升性能与可读性。

用窗口函数替代子查询更可靠
直接套用相关子查询(比如 (SELECT COUNT(*) FROM articles a2 WHERE a2.category = a1.category AND a2.likes > a1.likes) )看似能跑通,但遇到同点赞数时会漏掉或重复——因为 COUNT 只统计“严格更大”,无法处理并列排名。实际生产中应优先用 <code>ROW_NUMBER()、RANK() 或 DENSE_RANK(),它们对并列的语义明确且可控。
推荐用 DENSE_RANK():相同点赞数得同一排名,后续不跳号(比如 1,1,2,3),最贴近“前3名”的业务理解。
-
DENSE_RANK() OVER (PARTITION BY category ORDER BY likes DESC)是核心表达式,必须写在 SELECT 或 CTE 中 - 不能在 WHERE 中直接调用窗口函数,需先用 CTE 或子查询包裹
- MySQL 8.0+、PostgreSQL、SQL Server、Oracle 均支持;SQLite 3.25+ 也支持,但旧版不支持
MySQL 8.0+ 的标准写法
这是目前最通用、可读性最强的方案,兼容多数现代数据库:
WITH ranked AS (
SELECT
id,
title,
category,
likes,
DENSE_RANK() OVER (PARTITION BY category ORDER BY likes DESC) AS rk
FROM articles
)
SELECT id, title, category, likes
FROM ranked
WHERE rk <= 3;注意:id 和 title 要按需选取,避免 SELECT * 带入冗余字段;ORDER BY likes DESC 决定排名顺序,升序会翻转结果。
- 如果类目下不足3篇文章,结果自然少于3条,无需额外处理
- 若需固定每类返回恰好3条(不足则补 NULL),就得引入 LEFT JOIN 补全逻辑,复杂度陡增,一般不需要
- 索引建议:在
(category, likes)上建联合索引,大幅提升开窗排序效率
老版本 MySQL(5.7 及更早)的兜底方案
没有窗口函数时,只能靠变量模拟排名,但存在执行顺序不确定的风险——MySQL 不保证 ORDER BY 在变量赋值前完成,容易出错。
安全做法是用自连接计数(即相关子查询),但要修正并列问题:
SELECT a1.id, a1.title, a1.category, a1.likes FROM articles a1 WHERE ( SELECT COUNT(DISTINCT a2.likes) FROM articles a2 WHERE a2.category = a1.category AND a2.likes > a1.likes ) < 3;
关键改动是把 COUNT(*) 换成 COUNT(DISTINCT a2.likes),这样相同点赞数只算一次,实现类似 DENSE_RANK() 的效果。
- 性能较差:对每行都执行一次子查询,数据量大时明显变慢
- 仍可能因索引缺失导致全表扫描,务必确保
category和likes有适当索引 - 如果业务允许“取任意3条”而非“严格前三名”,可用
LIMIT 3配合GROUP BY+ORDER BY,但结果不可控
容易被忽略的边界情况
真实数据里,NULL 值和空类目常引发意外截断:
-
category为NULL的文章会被分到同一组,若业务上它代表“未分类”,通常应排除:WHERE category IS NOT NULL -
likes为NULL时,ORDER BY likes DESC会把它排在最后(不同数据库行为略有差异),若不想让它参与排名,加AND likes IS NOT NULL - 某些数据库(如 PostgreSQL)默认把
NULLS FIRST,需显式写NULLS LAST才保持一致 - 时间精度高的场景(如实时榜单),要注意事务隔离级别——未提交的点赞更新可能导致两次查询结果不一致
窗口函数本身不解决数据一致性,它只是按当前快照排序。真正要稳,得配合应用层缓存或物化视图。

















