应使用ROW_NUMBER() OVER(PARTITION BY category ORDER BY price DESC, id ASC)配合CTE或子查询取每组价格最高且ID最小的完整记录,避免GROUP BY+MAX()导致字段不匹配及多值不确定性问题。

用 ROW_NUMBER() 或 RANK() 实现组内排序取首行
直接在分组后取“最大值对应的那一行的其他字段”,不能靠 GROUP BY + MAX() 拼凑,因为 MAX(price) 和 name 不一定来自同一记录。正确做法是先按组排序,再筛选序号为 1 的行。
推荐用 ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC):每组内按价格降序编号,相同价格也不会并列,确保只取一条(哪怕有并列最高价,也只随机取其一)。
常见错误是写成 SELECT category, MAX(price), name FROM t GROUP BY category —— 这在多数数据库会报错(如 PostgreSQL、SQL Server),MySQL 5.7+ 严格模式下同样不合法。
- 若需保留所有并列最高价的记录,改用
RANK();若只要一条,用ROW_NUMBER() -
ORDER BY里可加多列防不确定性,比如ORDER BY price DESC, id ASC - 别忘了
PARTITION BY后面跟的是分组字段,不是聚合字段
用窗口函数配合子查询或 CTE 获取完整属性行
窗口函数不能直接出现在 WHERE 子句中,所以得嵌套一层。CTE 是最清晰的写法:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC, id ASC) AS rn FROM products ) SELECT id, category, name, price FROM ranked WHERE rn = 1;
如果数据库不支持 CTE(如旧版 MySQL),就用派生表:
SELECT id, category, name, price FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC, id ASC) AS rn FROM products ) t WHERE rn = 1;
注意:子查询别名 t 在 MySQL 中是强制要求的,漏掉会报 Every derived table must have its own alias 错误。
性能和兼容性差异:MySQL 8.0+、PostgreSQL、SQL Server 都支持,但 SQLite 要 3.25+
ROW_NUMBER() 在主流数据库中已普遍可用,但版本门槛不同:
- MySQL:仅 8.0+ 支持窗口函数;5.7 及之前只能用自连接或相关子查询(性能差、易出错)
- PostgreSQL:9.4+ 完整支持,且
RANK()/DENSE_RANK()行为稳定 - SQL Server:2005+ 就支持,但旧版本不支持
FILTER等增强语法 - SQLite:3.25+(2018 年后)才引入,老项目需确认版本
如果必须兼容 MySQL 5.7,替代方案是关联子查询:WHERE price = (SELECT MAX(price) FROM products p2 WHERE p2.category = products.category),但无法解决同组多条最大值时的“取哪条”问题,也不带排序稳定性。
为什么不用 GROUP BY + JOIN 回原表?
有人尝试先算出每组 MAX(price),再连回原表匹配:SELECT t1.* FROM products t1 JOIN (SELECT category, MAX(price) AS max_p FROM products GROUP BY category) t2 ON t1.category = t2.category AND t1.price = t2.max_p。
这看似合理,但存在两个硬伤:
- 当组内有多个相同最高价时,会返回多行——而你可能只需要一行代表
- 如果
price字段允许NULL,MAX()会忽略它,但连表时t1.price = t2.max_p会让NULL匹配失败,漏掉数据 - 没有指定次级排序依据(如时间、ID),结果不可预测,不同执行计划可能返回不同行
窗口函数天然带排序语义和确定性,比拼接逻辑更可靠,也更容易扩展(比如要取每组第 2 高的记录,只需改 rn = 2)。

















