最可靠方案是用row_number()配合子查询或cte,需写全partition by和order by,外层筛选rn=1;group by无法返回整行,误用min/max易导致字段错配。

用 ROW_NUMBER() 窗口函数取每组第一条最可靠
直接 GROUP BY 无法返回整行数据,必须借助窗口函数。最通用、语义清晰的方式是用 ROW_NUMBER() 配合子查询或 CTE。
常见错误是误用 MIN(id) 或 MAX(created_at) 再 JOIN,结果可能错配字段(比如取到某组最小 id 对应的 name,但该行其他字段不匹配)。
- 写法示例(以用户按部门分组取最早注册者为例):
SELECT user_id, dept, name, created_at FROM ( SELECT user_id, dept, name, created_at, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY created_at ASC) AS rn FROM users ) t WHERE rn = 1; -
PARTITION BY dept定义分组依据,ORDER BY created_at ASC决定“第一条”的逻辑——升序取最早,降序取最新 - 注意:不同数据库对窗口函数支持程度不同;MySQL 8.0+、PostgreSQL、SQL Server、Oracle 均支持;SQLite 3.25+ 支持;旧版 MySQL 必须用变量模拟,易出错
MySQL 5.7 及更早版本怎么绕过窗口函数限制
没有 ROW_NUMBER() 就得靠自连接或变量,但变量行为不稳定(尤其在复杂查询或并行执行下),推荐优先用自连接。
典型陷阱:用 GROUP BY dept + MIN(created_at) 后直接 SELECT 其他列,会导致非聚合列值不可预测(MySQL 的 SQL_MODE 若没开 ONLY_FULL_GROUP_BY,会随机返回某行)。
- 安全写法(自连接):
SELECT u1.user_id, u1.dept, u1.name, u1.created_at FROM users u1 LEFT JOIN users u2 ON u1.dept = u2.dept AND u1.created_at > u2.created_at WHERE u2.user_id IS NULL;
- 原理:找不出“同部门有更早记录”的行,即为该组最早
- 性能隐患:大表上
LEFT JOIN易产生笛卡尔积,务必确保(dept, created_at)有联合索引
PostgreSQL 中用 DISTINCT ON 更简洁
这是 PostgreSQL 特有语法,比窗口函数更短,但仅限该数据库,且排序字段必须包含在 DISTINCT ON 列表中。
不能写成 DISTINCT ON (dept) ORDER BY created_at —— 这会报错,因为 created_at 不在 DISTINCT ON 里。
- 正确写法:
SELECT DISTINCT ON (dept) user_id, dept, name, created_at FROM users ORDER BY dept, created_at ASC;
-
ORDER BY必须以DISTINCT ON字段开头(这里是dept),再跟排序字段(created_at),否则结果不可控 - 如果想按
created_at DESC取最新一条,就写ORDER BY dept, created_at DESC
WHERE 子句里不能直接用窗口函数
很多人试图这样写:
SELECT * FROM users WHERE ROW_NUMBER() OVER (PARTITION BY dept ORDER BY created_at) = 1;——这会报错,因为窗口函数不能出现在
WHERE 中。原因:SQL 执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,而窗口函数在 SELECT 阶段才计算,WHERE 阶段还不存在。
- 必须用子查询或 CTE 提前算出
rn,再在外层过滤:WHERE rn = 1 - CTE 写法更易读(尤其多层逻辑时):
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY created_at) AS rn FROM users ) SELECT user_id, dept, name, created_at FROM ranked WHERE rn = 1;
- 别名不能在同级
SELECT中引用(比如SELECT ..., ROW_NUMBER()... AS rn WHERE rn = 1是非法的)
实际用哪一种,取决于你用的数据库版本和是否接受方言特性。窗口函数是跨平台首选,但得确认环境支持;DISTINCT ON 虽简洁,换库就得重写;老 MySQL 的自连接方案看着啰嗦,反而最稳——关键是别在没索引的字段上跑这类查询。











