用row_number()可获取每组最小值所在完整行,但会强制去重;需保留所有并列最小值时应改用rank(),其对相同值赋予相同排名并跳过后续序号。

用窗口函数 ROW_NUMBER() 获取每组最小值对应完整行
直接 GROUP BY 只能拿到分组后的最小值(比如 MIN(price)),但拿不到该最小值所在的那条完整记录(比如对应的商品名、ID、时间)。这时候必须用窗口函数。
核心思路:按分组排序,把最小值所在行标为第 1 名,再过滤出所有第 1 行。
示例(查每个部门薪资最低的员工完整信息):
SELECT dept, name, salary
FROM (
SELECT dept, name, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary ASC) AS rn
FROM employees
) t
WHERE rn = 1;
注意:ROW_NUMBER() 会强制去重排序——即使两个员工薪资相同且都是部门最低,也只返回其中一行(按不确定顺序选一个)。如果要保留所有并列最小值,得换用 RANK() 或 DENSE_RANK()。
用 RANK() 保留所有并列最小值记录
当组内多个记录的排序字段值相同时(比如两个员工 salary 都是 5000,且是该部门最低),ROW_NUMBER() 会给它们分配不同序号(如 1 和 2),而 RANK() 会都标为 1,并跳过后续序号(下一个是 3)。
适用场景:你明确需要「所有最小值对应行」,不接受随机丢弃。
修改上例只需替换窗口函数:
SELECT dept, name, salary
FROM (
SELECT dept, name, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary ASC) AS rnk
FROM employees
) t
WHERE rnk = 1;
常见错误:写成 WHERE rnk ——没意义,<code>RANK() 最小就是 1;或者漏掉 ORDER BY 子句,导致排序无定义,结果不可靠。
避免用子查询关联导致性能崩盘
有人会写这种结构:
SELECT e1.* FROM employees e1 WHERE e1.salary = ( SELECT MIN(e2.salary) FROM employees e2 WHERE e2.dept = e1.dept );
看起来直观,但实际执行时,对 e1 每一行都要跑一次子查询,数据量稍大(比如万级)就明显变慢。而窗口函数是一次扫描、一次排序,效率高得多。
另外,这种写法在 salary 为 NULL 时行为不稳定(= NULL 永远不成立),而窗口函数天然跳过 NULL(除非显式用 ORDER BY ... NULLS FIRST)。
实操建议:
- 优先用
ROW_NUMBER()或RANK()+ 子查询包装 - 确保
PARTITION BY和ORDER BY字段有索引(如(dept, salary)复合索引) - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 支持,旧版不支持窗口函数
GROUP BY + 自连接不是可靠解法
还有人尝试先聚合出最小值,再自连接回原表:
SELECT e.* FROM employees e INNER JOIN ( SELECT dept, MIN(salary) AS min_sal FROM employees GROUP BY dept ) m ON e.dept = m.dept AND e.salary = m.min_sal;
这看似能拿到所有并列最小值,但存在两个硬伤:
第一,如果 salary 字段允许 NULL,MIN(salary) 返回 NULL,而 e.salary = NULL 判断恒为 false,整组记录消失;
第二,若业务上要求「取最早入职的那个最低薪员工」,这个写法完全无法控制——它只认数值相等,不区分其他维度。
所以这不是通用解法,仅适合数值唯一、且无额外排序要求的简单场景。
真正复杂的需求(比如“每组最小值中取 created_at 最早的一条”)只能靠嵌套窗口函数,例如:ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary ASC, created_at ASC)。










