GROUP BY后不能直接用LIMIT 1,因LIMIT作用于最终结果集而非每组;应使用ROW_NUMBER()窗口函数按分组排序编号后筛选rn=1,或兼容旧版用相关子查询。

为什么 GROUP BY 后不能直接用 LIMIT 1
因为 LIMIT 是作用于最终结果集的全局限制,不是每个分组内独立生效。你写 SELECT ... GROUP BY x LIMIT 1,只会返回任意一个分组的聚合结果,而不是每个分组各取一条——这和需求完全相反。
用窗口函数 ROW_NUMBER() 最通用可靠
主流数据库(PostgreSQL、SQL Server、Oracle、MySQL 8.0+、SQLite 3.25+)都支持,逻辑清晰且可控。核心思路:先给每组内的记录按某字段排序编号,再过滤出每组 rn = 1 的行。
示例(查每个部门薪资最高的员工):
SELECT dept, name, salary
FROM (
SELECT dept, name, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE rn = 1;
-
PARTITION BY dept对应你要的分组维度,必须和预期GROUP BY字段一致 -
ORDER BY salary DESC决定“TOP 1”依据,可换成created_at DESC、id ASC等 - 如果存在并列(如两人同最高薪),
ROW_NUMBER()只选其一;要保留全部并列项,改用RANK()或DENSE_RANK()
MySQL 5.7 或旧版 SQLite 怎么办?用相关子查询兜底
这些版本不支持窗口函数,但可用子查询 + JOIN 或 WHERE ... IN 模拟。性能较差,数据量大时明显卡顿,仅作兼容方案。
示例(MySQL 5.7):
SELECT e1.dept, e1.name, e1.salary FROM employees e1 WHERE e1.salary = ( SELECT MAX(e2.salary) FROM employees e2 WHERE e2.dept = e1.dept );
- 该写法假设“TOP 1”由最大值定义;若需取最新时间那条,得改成
SELECT ... ORDER BY created_at DESC LIMIT 1子查询(MySQL 5.7 支持) - 注意
NULL值:如果某分组全为NULL薪资,MAX()返回NULL,外层可能漏掉整组——需加IS NOT NULL判断 - 没有索引时,子查询会为每行重新扫描一次表,务必在
(dept, salary)或(dept, created_at)上建联合索引
别忽略排序稳定性与业务语义
即使语法跑通,结果也可能不符合业务预期。比如“每个用户最近一次登录”,不能只靠 MAX(login_time),因为可能有多个同时间记录,而你实际需要的是其中某一条完整信息(如 IP 或设备类型)。
- 用
ROW_NUMBER()时,在ORDER BY中加入次要排序字段避免随机性:ORDER BY login_time DESC, id DESC - 如果业务上允许并列(如“所有最高分学生”),就别硬用
ROW_NUMBER(),改用RANK()并保留rn = 1 - 某些场景其实不需要“取 TOP 1 记录”,而是“取 TOP 1 的某个聚合值”,这时直接
GROUP BY ... MAX(x)更高效,别绕路
窗口函数是当前最稳的选择,但具体用 ROW_NUMBER() 还是 RANK()、是否加二级排序、要不要处理 NULL,全取决于你“TOP 1”背后的真实业务含义——这点比语法本身更重要。










