最简方案是用 row_number() 配合 where 筛选:在子查询或 cte 中以 partition by 分组、order by 排序编号,再取 rn = 1 的行;必须指定 order by,否则结果不确定,且不可用 rank() 或 dense_rank() 替代。

用 ROW_NUMBER() 配合 WHERE 筛选最简方案
直接在子查询或 CTE 中用 ROW_NUMBER() 按分组排序编号,再取序号为 1 的行——这是最通用、语义最清晰的做法。
注意:必须明确指定 ORDER BY,否则排序不确定,ROW_NUMBER() 结果不可靠;如果只关心“任意一条”,可按主键或时间字段排序,但不能省略 ORDER BY。
-
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY id)是标准写法,id替换为你想优先保留的字段(如created_at DESC取最新) - 别用
RANK()或DENSE_RANK()——它们对相同值会并列编号,无法保证“第一条”是唯一一行 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持,旧版不支持窗口函数
PostgreSQL 专用:DISTINCT ON 更快更简洁
如果只在 PostgreSQL 里用,DISTINCT ON 是比窗口函数更轻量的选择,语法短、执行计划通常更优。
它本质是先按 DISTINCT ON 列分组,再按后续 ORDER BY 取每组第一行,不需要嵌套子查询。
SELECT DISTINCT ON (group_col) * FROM tbl ORDER BY group_col, created_at DESC- 必须把
DISTINCT ON的字段放在ORDER BY开头,否则报错:SELECT DISTINCT ON expressions must match initial ORDER BY expressions - 不能跨数据库移植,其他系统不识别该语法
避免用 FIRST_VALUE() 直接取值的常见误解
FIRST_VALUE() 返回的是窗口内首行的某个字段值,不是整行记录——它常被误当作“取第一条记录”的替代,但实际用途完全不同。
- 例如:
FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC)只返回每个部门最高薪员工的name,不带id、salary等其他字段 - 若强行用多个
FIRST_VALUE()拼凑一行,字段可能来自不同物理行(因各字段的ORDER BY不一致),结果错乱 - 真要整行数据,还是得靠
ROW_NUMBER()+ 过滤,或者DISTINCT ON
性能与 NULL 处理的隐性坑
当分组字段或排序字段含大量 NULL,不同数据库行为不一致,可能让“第一条”意外跳过某些组。
- PostgreSQL 默认把
NULL排在最前(ORDER BY col ASC时),而 MySQL 8.0 默认排最后;显式写ORDER BY col ASC NULLS FIRST可控,但仅 PostgreSQL 和 Oracle 支持该语法 - 大表慎用未索引的
PARTITION BY+ORDER BY组合,ROW_NUMBER()会强制全局排序,可能触发磁盘临时表 - 如果只是去重取样,且不要求“严格第一条”,考虑先用
GROUP BY+ 聚合函数(如MIN(id))拿到 ID 再关联,有时比窗口函数更快
真正难的不是写法,而是想清楚“第一条”到底依赖什么逻辑——时间?ID?业务权重?这个判断一旦模糊,后面所有优化都容易白忙。










