GROUP BY后直接用MIN(id)无法获取完整首行记录,因非聚合字段name语义不明确且不保证与MIN(id)同行;正确解法是用ROW_NUMBER()窗口函数按分组和排序编号后筛选rn=1,或用子查询关联取整行。

GROUP BY 后直接用 MIN(id) 不能拿到“第一条记录”的完整行数据
很多人以为 SELECT group_col, MIN(id), name FROM tbl GROUP BY group_col 就能拿到每组 id 最小的那条完整记录,其实不行——name 值是随机的(MySQL 5.7+ 默认报错,老版本可能返回非对应行的数据)。SQL 标准里,GROUP BY 后的非聚合列如果没有函数包裹,语义不明确,数据库无法保证它和 MIN(id) 来自同一行。
正确做法:用窗口函数 ROW_NUMBER() 筛选每组首行
这是最直观、可读性高且跨数据库兼容性较好的方案(PostgreSQL / SQL Server / Oracle / MySQL 8.0+ 都支持):
SELECT id, group_col, name, value
FROM (
SELECT id, group_col, name, value,
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY id) AS rn
FROM tbl
) t
WHERE rn = 1;
关键点:
-
PARTITION BY group_col定义分组维度,等价于GROUP BY的分组逻辑 -
ORDER BY id决定“第一条”按什么排序,换成created_at就是最早创建的记录 - 如果存在多条
id相同的记录,ROW_NUMBER()仍会强制编号(可用RANK()或DENSE_RANK()替代,但要注意并列时行为不同)
兼容旧版 MySQL(5.7 及以下)的替代方案:关联子查询
在不支持窗口函数的环境中,常用写法是先查出每组最小 id,再关联原表取整行:
SELECT t1.* FROM tbl t1 INNER JOIN ( SELECT group_col, MIN(id) AS min_id FROM tbl GROUP BY group_col ) t2 ON t1.group_col = t2.group_col AND t1.id = t2.min_id;
注意陷阱:
- 若
group_col + id不构成唯一键,可能返回多行(比如两个相同id和相同group_col的记录) - 性能上,子查询结果若很大,
JOIN可能比窗口函数慢;建议给(group_col, id)加联合索引 - 不能直接用
WHERE id IN (SELECT MIN(id) ...),因为没关联分组字段,会跨组误匹配
为什么不用 LIMIT 1 配合 GROUP BY?
GROUP BY ... ORDER BY id LIMIT 1 这类写法在绝大多数数据库中语法错误或逻辑错误。MySQL 曾允许但行为不可靠:LIMIT 作用于整个结果集,不是每个分组;PostgreSQL 和 SQL Server 直接报错。别依赖它。
真正要拿“每组第一条”,核心就两点:明确分组依据(PARTITION BY 或子查询 GROUP BY),以及明确定义“第一”的排序规则(ORDER BY 字段)。漏掉任意一个,结果就不可控。










