取每种物料第一行数据的 ROW_NUMBER 窗口函数最稳

编程狮(w3cschool.cn) 2026-10-27 07:04:17 浏览数 (52)
反馈

做报表时常遇到这种需求:一张出入库表里每种物料有多条记录,只要取“每种物料的第一行数据”——可能是最早一笔,也可能是最新一笔。很多新手会用子查询或变量去硬拼,结果在 MySQL 5.x 上能跑、换库就挂。最稳的通用写法是窗口函数 ROW_NUMBER:按物料分区、按时间排序,再筛编号等于 1 的行。本文讲清这种首行数据的取法,以及不同数据库的等价写法,照着抄就能稳定出数。先给结论:现代库优先 ROW_NUMBER,老版本用“分组极值关联”,并列场景再换 RANK。

取每种物料第一行数据

一、先定“第一行”的含义(前置与边界)

“第一行”必须明确是按哪个字段排序、取最大还是最小。边界有三个:分区键(按物料还是按物料加仓库)、排序字段(时间还是自增 id)、并列时取哪条。把这些写在注释里,查询才不会被业务方反复打回。想系统学查询基础,可参考 SQL 教程。

实际项目里,分区键往往不止物料一个:同一物料在不同仓库的“首行”是独立计算的。所以动手前先和业务确认维度,别想当然按物料一列分区。配置上,窗口函数需要数据库支持(PostgreSQL、MySQL 8、SQL Server 都支持);老版本要换写法。这个前置想清楚,后面 SQL 才不会返工,也方便接手的人一眼看懂意图,减少分组取数类需求的沟通成本。如果只按物料分区,同一个物料在 A 仓和 B 仓的首行会被合并成一条,业务看到的数字就会错位。所以分区键要和业务维度严格对齐,这是取首行数据不出错的前提。

举个例子:stock_log 里有 material_id、warehouse_id、op_time、qty 四列。如果业务说“每种物料在每个仓的最早入库记录”,分区键就必须是 (material_id, warehouse_id),排序用 op_time。把维度定错,哪怕 SQL 能跑,出来的首行数据也是错的。所以前置这一步不是啰嗦,而是把“第一行”的语义锁死,后续写法才有意义。

二、ROW_NUMBER 标准写法(配置与运行)

核心是用 ROW_NUMBER() OVER (PARTITION BY 物料 ORDER BY 时间) 给每行编个号,再在外层取 rn=1。运行前确认数据库版本支持窗口函数。关于 MySQL 具体语法,可看 MySQL 教程。

SELECT material_id, op_time, qty
FROM (
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY material_id ORDER BY op_time) AS rn
  FROM stock_log
) t
WHERE rn = 1;

这段 SQL 的逻辑是:内层按物料分区、按时间升序编号,最早一笔就是 rn=1;外层只留编号 1。如果要看“最新一笔”,把 ORDER BY 改成降序即可。配置好分区和排序,这条首行数据查询就能稳定复用,不用每次重新拼子查询,可读性和可维护性都更好。

注意 ORDER BY 子句可以加第二排序键来稳定并列:例如 ORDER BY op_time, id,当同一时刻有多笔时按 id 兜底。这样即使出现并列,编号也稳定,首行数据不会因为执行计划变化而漂移。窗口函数让“分区 + 排序 + 取首”三步合一,是取首行数据最标准、也最不容易出错的写法。

三、用子查询做低版本兼容(验证与预期)

MySQL 5.x 没有窗口函数,可以用“分组取极值再关联”的写法等价实现。预期结果是一样的首行数据:每个物料只留一条。可参考 Python 教程 理解分组聚合思路。

SELECT s.material_id, s.op_time, s.qty
FROM stock_log s
JOIN (
  SELECT material_id, MIN(op_time) AS first_time
  FROM stock_log
  GROUP BY material_id
) f ON s.material_id = f.material_id AND s.op_time = f.first_time;

验证时重点看两件事:每个物料是否真只返回一条,以及并列时是否出现多行。若同一时刻有多笔,MIN 关联会返回多条,需要再加一个决胜字段(如自增 id)去重。这种写法兼容性最好,但性能上不如窗口函数,数据量大时要评估索引。把验证做扎实,首行数据才不会在边界月漏算,报表数字才站得住脚。

补充一点:如果分区键要加仓库,子查询里的 GROUP BY 也要同步加 warehouse_id,ON 条件同样补上,否则会在跨仓时把别的仓记录关联进来。低版本写法没有窗口函数替你分区,所有维度都得手动对齐,这也是它比 ROW_NUMBER 更容易写错的地方,验证时务必按维度逐条核对。

四、常见失败与修复(失败与排错)

最常见的失败是“分区键写错”:本应按物料加仓库分区,却只按物料,导致跨仓数据混在一起。修复办法是补全 PARTITION BY 的列。另一个坑是排序字段选了会重复的列,编号不稳定。关于页面展示基础,可看 HTML 教程。

失败处理上,先用小样本验证:挑两个物料,人工核对外层结果是否等于“它们各自的第一条”。如果结果多行,说明并列没处理,需要加决胜列。性能排错时,给 PARTITION BY 和 ORDER BY 涉及的列建组合索引,窗口函数的扫描会快很多。把失败分支和索引一起考虑,取首行数据的查询上线才稳,也不会因为一次大表扫描把数据库拖慢。

还有一个隐蔽失败:在 ORDER BY 里用了 NOW() 或随机值这类非确定性表达式,每次执行编号都变,首行数据就不可复现。修复是排序字段必须来自数据本身(时间、id),绝不能用函数实时值。上线前用 EXPLAIN 看执行计划,确认走了分区裁剪和索引,而不是全表排序,否则数据一大就超时。

五、并列与性能取舍(适用与对比)

当“第一行”可能并列时,用 RANK 还是 ROW_NUMBER 是常见选择。对比来看:ROW_NUMBER 永远只留一条(并列也随机挑一),RANK 会给并列都标 1,需要应用层再决定。关于用版本控制保存可复现 SQL,可看 Git 版本控制。

适用边界上:报表只要“代表行”就选 ROW_NUMBER;审计要“所有并列首笔”就选 RANK。性能上,窗口函数在大多数现代库里最优,但超大表仍建议先按分区键过滤再编号。把这些对比和边界讲清楚,分组取数类需求就不会每次都从头推导。窗口函数让首行数据的取法既标准又好维护,也方便后续按新维度快速改写。

从性能取舍看,若表很大且只要少数物料的首行,先 WHERE material_id IN (...) 缩小范围再开窗,比全表开窗再过滤快得多。反之若要对全表取首行做快照,窗口函数仍是最优解。把“先过滤后开窗”当成习惯,首行数据查询在数据量增长时也不会突然变慢。

SQL 取每种物料首行写法怎么选

SQL 取首行查询的预期输出

总结

取每种物料的第一行数据,最稳的是 ROW_NUMBER 窗口函数:按物料分区、按时间排序、筛编号 1。低版本库用分组极值关联兼容,并列场景用 RANK 兜底。把分区、排序和并列规则定清楚,首行数据查询就能稳定复用。

常见问题

Q:ROW_NUMBER 和 RANK 取首行有什么差别?

ROW_NUMBER 即使并列也只留一条,RANK 会给所有并列行都标 1;只要“代表行”用前者,要“所有并列首笔”用后者。

Q:MySQL 5.x 没有窗口函数怎么办?

用“按物料分组取极值,再和原表关联”的写法等价实现,注意并列时可能返回多行,要加决胜字段去重。

Q:分区键只写物料会出什么问题?

会忽略仓库等维度,把不同仓的物料记录混在一起编号,导致取到的不是真正意义上该仓的首行数据。

延伸学习

0 人点赞