最稳方案是用 ROW_NUMBER() 窗口函数,需带 PARTITION BY 和含唯一列的 ORDER BY(如 price DESC, id DESC),避免排序不稳定;自连接易因 NULL 或条件疏漏导致漏数据或多结果。

用 ROW_NUMBER() 窗口函数最稳
绝大多数场景下,直接用 ROW_NUMBER() 按分组排序取第一,比自连接简洁、可读性强、执行计划也更可控。它不依赖主键或唯一性,也不怕重复值干扰结果。
常见错误是写成 ORDER BY 里只放一个字段,导致同分组内排序不稳定——比如多个记录的 score 相同,数据库可能每次返回不同行。
- 必须在
ORDER BY中加入足够区分度的列(如id或created_at),例如:ORDER BY score DESC, id DESC - 别漏写
PARTITION BY,否则整个表只算一个组 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也行,但旧版不支持
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY category ORDER BY price DESC, id DESC
) AS rn
FROM products
) t WHERE rn = 1;自连接方案容易掉进空结果坑
用 LEFT JOIN 找“没有更大值”的记录,逻辑上没错,但实际中常因 NULL 处理或 JOIN 条件疏漏导致漏数据,尤其当分组字段含 NULL 值时直接失效。
典型错误现象:某组明明有数据,查询结果却为空;或者同一组返回多条(因为没加严格去重条件)。
-
JOIN条件必须同时匹配分组字段和“更大”条件,例如:t1.category = t2.category AND t1.price - 分组字段为
NULL时,t1.category = t2.category永远不成立,整组被过滤——得提前COALESCE(category, 'N/A')处理 - 如果最大值有重复,会返回多行;想强制一条,还得套一层
MIN(id)或加LIMIT 1(但不推荐,不可控)
SELECT t1.* FROM products t1 LEFT JOIN products t2 ON t1.category = t2.category AND t1.price < t2.price WHERE t2.id IS NULL;
别用 GROUP BY + MAX() 直接查整行
这是新手最常写的错法:SELECT category, MAX(price), name FROM products GROUP BY category。MySQL 5.7 严格模式下直接报错,其他数据库虽能跑,但 name 值是随机的——它根本不是对应最大 price 的那条记录。
原因在于 SQL 标准规定:出现在 SELECT 列表中、又不在 GROUP BY 里的非聚合字段,语义未定义。MySQL 旧版默认“随便挑一个”,但没人能保证挑得对。
- 即使你确认 MySQL 版本允许这种写法,也别信它返回的
name - 想靠
ORDER BY ... LIMIT 1配合GROUP BY?不行,LIMIT在分组之后才生效,无法控制每组内的顺序 - 真要兼容老环境且不用窗口函数,优先考虑
ROW_NUMBER()的 CTE 写法,而不是硬凑GROUP BY
性能差异主要看索引是否覆盖
窗口函数和自连接的实际速度,不取决于写法本身,而取决于能否走索引。两者都要求:分组字段 + 排序字段组合上有有效索引。
比如按 category 分组、按 price 取最大,理想索引是:INDEX(category, price)。缺了这个,全表扫描一上来就拖垮性能。
- 自连接在大数据量时容易触发嵌套循环,若没索引,复杂度接近 O(n²)
-
ROW_NUMBER()通常走一次排序 + 窗口扫描,O(n log n),但若内存不足被迫落盘,也会变慢 - PostgreSQL 中,如果加了
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW这类范围定义,可能影响优化器选择,一般用默认的UNBOUNDED就行
窗口函数不是银弹,但只要你的数据库版本够新,就别回头折腾自连接——那个 PARTITION BY 和 ORDER BY 的顺序、以及排序字段的确定性,才是实际落地时最容易被跳过的细节。

















