登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 窗口函数怎么给每组记录编号后分页

来源:17golang原创

时间:2026-09-08 03:44:25 395浏览 收藏

报表里经常要做这样的分页:每个客户只看自己的订单,第 2 页取组内第 11 到 20 条;或者每个部门只保留金额最高的前 3 条。关键不是给整张结果集编号,而是先用 PARTITION BY 划分组,再用窗口函数产生组内编号,最后在外层查询过滤编号。

最稳妥的写法是 ROW_NUMBER() OVER (PARTITION BY 分组列 ORDER BY 业务排序列, 唯一键),把结果放进 CTE 或派生表,再用 WHERE rn BETWEEN 起始行 AND 结束行 分页。排序列相同的记录必须补一个唯一键,否则翻页顺序可能漂移。
要点速览
  • PARTITION BY 决定“每组重新编号”,不会减少结果行。
  • ROW_NUMBER 适合每组严格取固定条数;并列展示要考虑 RANKDENSE_RANK
  • 窗口编号生成后再由外层筛选,分页排序要和编号排序保持一致。

为什么要先编号再分页

GROUP BY 更适合把多行聚合成一行,而这里仍要保留每一条订单,只是想知道它在所属客户中的位置。窗口函数正好是在保留明细行的同时计算相关行。MySQL 手册把 ROW_NUMBER() 定义为当前行在分区中的编号,把 RANK() 定义为允许跳号的排名,把 DENSE_RANK() 定义为不跳号的排名。

假设表为 customer_orders,需要对已支付订单按创建时间倒序排列。只写 ORDER BY created_at DESC 不够:同一时刻可能有多笔订单,数据库可以在这些并列值之间采用不同顺序。把主键 order_id 作为第二排序键,才能让第 11 条和第 12 条有稳定边界。

MySQL 窗口函数中 customer_orders 表、customer_id 分区、created_at 与 order_id 排序键、ROW_NUMBER 编号列和组内分页边界的查询结构图
图1:分区、排序键和组内行号共同构成每组分页的静态查询关系。

ROW_NUMBER、RANK 和 DENSE_RANK 怎么选

三者都可以写在同一个窗口定义里,但“并列”处理方式不同。下面的选择表比死记函数名更实用:

函数相同排序值适合的结果
ROW_NUMBER()仍然逐行编号每组精确取 10 条、组内明细分页
RANK()并列同名次,后面跳号排行榜需要体现名次间隔
DENSE_RANK()并列同名次,后面不跳号取每组前 3 个金额档位

如果业务说“每组前 3 条”,通常选 ROW_NUMBER;如果说“每组前 3 名,最后一名允许并列”,则要确认是允许结果超过 3 行,常见写法是 RANKDENSE_RANK。不能只看函数返回的数字,还要先确定产品对并列记录的定义。

用 CTE 写出可控的组内分页 SQL

窗口函数产生的别名不能在同一层的 WHERE 中直接使用,因此先放进 CTE,再由外层筛选。下面示例取每个客户组内第 11 到 20 条已支付订单:

WITH ranked_orders AS (
  SELECT
    order_id,
    customer_id,
    amount,
    created_at,
    -- 先按客户分组,再用主键稳定打破相同时间
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY created_at DESC, order_id DESC
    ) AS rn
  FROM customer_orders
  WHERE status = 'paid'
)
SELECT order_id, customer_id, amount, created_at, rn
FROM ranked_orders
-- 外层才按组内编号截取第 11 到 20 条
WHERE rn BETWEEN 11 AND 20
ORDER BY customer_id, rn;

这里的 WHERE status = 'paid' 在编号之前执行,所以编号只针对已支付订单。如果把状态条件放到最外层,未支付订单会先占用行号,得到的“已支付第 11 条”就会偏移。最终的 ORDER BY customer_id, rn 也要和阅读顺序一致,避免查询结果看起来跨组跳动。

如果分页参数是第 page 页、每页 page_size 条,可以在应用层计算 start = (page - 1) * page_size + 1end = page * page_size,再绑定为两个参数。不要把用户输入直接拼接进 SQL;边界还应限制为正数,并处理页码超过组内总行数的空结果。

MySQL CTE 组内分页查询中状态过滤、ROW_NUMBER 编号、rn 边界筛选和客户排序输出之间的静态关系图
图2:过滤时机、编号层和外层分页边界的关系,帮助定位页码偏移问题。

常见问题

为什么不能直接写 WHERE rn

rn 是窗口表达式在查询结果阶段产生的别名,同层 WHERE 不能直接引用它。使用 CTE 或派生表包一层,把编号变成外层可过滤的列。

排序字段相同会不会导致分页重复或漏数据?

可能会。给业务排序字段补上唯一键,并在编号层和最终展示层使用同一套排序规则,才能让页边界稳定。

MySQL 5.7 能否直接使用这套写法?

不能把它当成兼容写法。本文依赖 MySQL 8.0 及更高版本提供的窗口函数和 CTE;旧版本需要升级,或改用更难维护的变量方案。

把“分组”“组内顺序”“分页边界”拆成三个明确层次后,这类 SQL 就不再是给结果集临时编号的小技巧,而是一条可解释、可维护的查询结构。生产环境还应结合实际数据量查看执行计划,重点关注分区和排序带来的临时表与排序成本。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>