MIN()返回的是最小标量值而非整行,混用非聚合字段会因粒度不一致导致报错或结果不可靠;须用关联子查询、窗口函数或相关子查询获取完整记录。

为什么直接用 GROUP BY + MIN(id) 拿不到“最小值对应的原始 ID”
很多人以为 MIN(id) 就是那个“属于最小值所在行的 ID”,但其实不是——它只是数值上最小的 ID 值,而当你同时 SELECT 其他非聚合字段(比如 name、score)时,数据库根本不知道该取哪一行的数据。MySQL 5.7+ 默认开启 sql_mode=only_full_group_by,这种写法会直接报错:Expression #2 of SELECT list is not in GROUP BY clause。即使关掉这个模式,结果也是不可靠的:你看到的 name 可能来自任意一条同组记录。
用子查询关联获取完整行:最通用的解法
核心思路是先在子查询里算出每组的最小值(比如 MIN(score)),再用主查询把原表和这个结果 JOIN 起来,匹配 group_col 和 score 两个条件。这样拿到的就是真实存在、且满足“该组中 score 最小”的那条完整记录。
示例(找每个部门 salary 最低的员工完整信息):
SELECT t1.* FROM employees t1 INNER JOIN ( SELECT dept, MIN(salary) AS min_salary FROM employees GROUP BY dept ) t2 ON t1.dept = t2.dept AND t1.salary = t2.min_salary;
注意点:
- 如果一个部门有多个员工 salary 同为最低,这条语句会返回全部——这是正确行为,不是 bug
- 必须确保
salary字段没有 NULL,否则MIN()会忽略它们,JOIN 时可能漏掉数据;需要加WHERE salary IS NOT NULL显式过滤 - 联合索引
(dept, salary)能显著提升子查询和 JOIN 效率
用窗口函数 ROW_NUMBER() 更精准控制“只取一条”
当明确只要每组一条记录(比如有并列时只随机取一个),ROW_NUMBER() 比子查询更直接,也避免了多值冲突问题。
示例(每个部门只取 salary 最低的任意一名员工):
SELECT id, name, dept, salary
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary, id) AS rn
FROM employees
WHERE salary IS NOT NULL
) t
WHERE t.rn = 1;关键细节:
-
ORDER BY salary, id确保排序稳定(相同 salary 时按id排,避免每次执行结果不同) - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持,但旧版本不适用
- 不能用
RANK()或DENSE_RANK()替代,它们对并列值分配相同序号,会导致rn = 1匹配多行
用相关子查询也能做,但性能通常较差
适用于简单场景或临时脚本,逻辑直观但难以优化:
SELECT * FROM employees e1 WHERE salary = ( SELECT MIN(e2.salary) FROM employees e2 WHERE e2.dept = e1.dept );
问题明显:
- 对
e1的每一行都要执行一次子查询,数据量大时响应明显变慢 - 如果
salary为 NULL,外部=判断永远不成立,该行被跳过——不如显式加WHERE salary IS NOT NULL清晰 - 没有索引时,几乎无法避免全表扫描
真正要注意的,不是哪种写法“看起来最短”,而是你是否理解 MIN() 返回的是值、不是行,以及 NULL 值和重复最小值这两点,会在所有方案里反复造成偏差。

















