关联子查询实现“每个分类前三名”的核心思路是:对主表每行执行子查询,统计同分类中分数更高的记录数,筛选该数≤2的行;需用(category, score)联合索引优化性能,并处理null及并列语义。

用关联子查询实现“每个分类前三名”的核心思路
直接用 ORDER BY + LIMIT 3 行不通,因为那是对整个表排序取前3;必须让数据库对每个分类单独计数、逐个比较。关联子查询的本质是:对主表的每一行,执行一次子查询,统计“同分类中比它分数更高(或相等)的记录有多少条”,再筛选出这个数量 ≤ 2 的行(即排名 ≤ 3)。
MySQL 5.7 / 8.0 兼容写法(无窗口函数)
适用于不支持 ROW_NUMBER() 的旧版本,也便于理解排名逻辑。关键点在于子查询里的 WHERE 条件要同时绑定分类和排序字段:
SELECT t1.category, t1.name, t1.score
FROM products t1
WHERE (
SELECT COUNT(*)
FROM products t2
WHERE t2.category = t1.category
AND t2.score > t1.score
)
- 子查询统计的是“同一分类下 score 更高的记录数”,所以
对应排名 1/2/3(0、1、2 个更高者) - 如果存在并列(如两个 95 分),该写法会同时返回它们——这是“并列不跳过”语义,符合多数业务场景
- 性能敏感时务必给
(category, score)加联合索引,否则子查询会全表扫描
处理并列与严格 Top-N 的区别
上面的写法属于“并列保留”,但有时需要“严格只取三条,即使有并列也截断”。这时得改用变量模拟或升级到 MySQL 8.0+:
- 若用变量(
@rank := @rank + 1),需先按category, score DESC排序,且变量在分组内重置逻辑易出错,不推荐用于生产 - MySQL 8.0+ 直接用
ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC)最稳,但注意它强制去重排名(95,95,90 → 1,2,3),而RANK()才是并列跳过(95,95,90 → 1,1,3) - PostgreSQL / SQL Server 同理,优先选窗口函数而非关联子查询
容易被忽略的 NULL 和边界情况
当 score 字段含 NULL 时,t2.score > t1.score 判断会失效(任何与 NULL 的比较结果都是 UNKNOWN),导致这些记录被漏掉:
- 加
AND t1.score IS NOT NULL到主查询WHERE条件里 - 或统一用
COALESCE(score, -999999)替换排序字段(根据业务设定默认低分) - 另外,如果某分类实际只有 2 条记录,上述写法自然返回全部 2 条——这是正确行为,不必额外补空
关联子查询看着简单,但每多一层嵌套、每多一个 NULL 或并列,就多一个隐性分支;上线前最好用真实数据量测一遍执行计划,尤其确认 EXPLAIN 中子查询是否走了索引。










