ROW_NUMBER()是最可靠方案,需配合PARTITION BY分组、ORDER BY排序,并用子查询或CTE筛选rn=1;MySQL 5.7及更早版本须用相关子查询或JOIN替代。

用 ROW_NUMBER() 配合子查询或 CTE 拿到每组 Top 1 完整行
直接在 GROUP BY 后用 SELECT * 是非法的,SQL 不允许在聚合查询中混选未分组字段。真正可行的是先给每组内行打序号,再筛出序号为 1 的记录。
核心思路:按分组字段(如 category)分区,按排序字段(如 sales DESC)排序,用 ROW_NUMBER() 生成组内序号,最后外层过滤 rn = 1。
-
ROW_NUMBER()保证每组严格从 1 开始编号,即使有并列也不会重复或跳过 - 如果需要处理并列情况(比如多个最高分),改用
RANK()或DENSE_RANK(),但要注意它们可能返回多个rn = 1 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持
- 避免在子查询里漏写
ORDER BY——ROW_NUMBER()的ORDER BY是必须的,否则语法报错
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
FROM products
)
SELECT id, name, category, sales
FROM ranked
WHERE rn = 1;
MySQL 5.7 或更老版本没有窗口函数?用相关子查询硬刚
老 MySQL 不支持 ROW_NUMBER(),也没 CTE,只能靠相关子查询:对每一行判断“是否存在同组中更大值”,不存在即为最大。
这个方法逻辑清晰但性能差,尤其数据量大时会触发全表扫描 × 行数次。
- 子查询里必须包含全部分组条件,比如
WHERE t2.category = t1.category,漏掉就变成全局比较 - 排序字段不能为
NULL,否则> t1.sales判断失效;可加AND t2.sales IS NOT NULL防御 - 若存在多行并列最高,该方法只随机返回其中一行(因无稳定排序),不保证可重现
SELECT t1.id, t1.name, t1.category, t1.sales
FROM products t1
WHERE NOT EXISTS (
SELECT 1 FROM products t2
WHERE t2.category = t1.category
AND t2.sales > t1.sales
);
LEFT JOIN 自关联也能做,但容易漏掉空匹配
把表自连接,找“本组内没有更大值”的行,本质和子查询等价,但写法不同。
关键陷阱在于 JOIN 条件和 WHERE 的配合:必须让“找不到更大值”的行在右表为 NULL,再用 IS NULL 筛出。
- 错误写法:
ON t1.category = t2.category AND t2.sales > t1.sales WHERE t2.id IS NULL✅ 正确 - 错误写法:
ON t1.category = t2.category AND t2.sales > t1.sales WHERE t2.sales IS NULL❌ 若sales允许为 NULL,会误判 - JOIN 后结果集膨胀明显,比子查询更吃内存,小表可用,大表慎用
SELECT t1.id, t1.name, t1.category, t1.sales FROM products t1 LEFT JOIN products t2 ON t1.category = t2.category AND t2.sales > t1.sales WHERE t2.id IS NULL;
ORDER BY 和 LIMIT 1 在 GROUP BY 里为什么不行?
因为标准 SQL 中,GROUP BY 后的 SELECT 列要么是分组键,要么是聚合函数结果,ORDER BY ... LIMIT 1 不是合法的聚合表达式。
某些 MySQL 旧版本(开启 sql_mode=ONLY_FULL_GROUP_BY 关闭时)看似能跑,实际行为不可靠:它会从每组任意挑一行返回,不保证是最大值那行,也不保证跨执行一致。
- 哪怕加了
ORDER BY sales DESC,在GROUP BY查询中仍被忽略 - PostgreSQL、SQL Server 等会直接报错:
column "xxx" must appear in the GROUP BY clause or be used in an aggregate function - 别依赖 MySQL 的“隐式 ANY_VALUE”行为,它掩盖逻辑缺陷
真正要拿完整行,窗口函数是现代 SQL 的标准解法;兼容性要求高时,子查询最稳。但无论哪种,都得想清楚并列怎么处理、NULL 怎么对待、性能能不能扛——这些细节不处理好,线上查出来的“第一”可能根本不是你要的那个。

















