欢迎光临
我们一直在努力

游标分页和偏移量详解

        这两种都是数据库分页查询的方式,核心区别在于如何定位到要返回的那一页数据。

偏移量分页(Offset Pagination)

最常见的分页方式,用 LIMIT + OFFSET 实现:

sql

— 第 1 页,每页 10 条
SELECT * FROM articles ORDER BY id LIMIT 10 OFFSET 0;

— 第 100 页
SELECT * FROM articles ORDER BY id LIMIT 10 OFFSET 990;

原理:数据库先扫描并丢弃前 OFFSET 条记录,再返回后面的 LIMIT 条。

问题:

  • 深分页性能差:OFFSET 990 意味着数据库要真的数过前 990 行再丢掉,页码越大越慢。

  • 数据漂移:翻页过程中如果有新数据插入或删除,会导致某些记录重复出现或被跳过。比如你正在看第 2 页,此时前面插入了一条新数据,那么原来的第 2 页第一条会被挤到第 3 页,你翻页时就会漏掉它。

  • 优点:实现简单,可以随机跳转到任意页码(比如直接跳到第 50 页)。

    游标分页(Cursor Pagination)

    也叫基于键集的分页(Keyset Pagination),不用偏移量,而是用"上一页最后一条记录的位置"作为起点:

    sql

    — 第 1 页
    SELECT * FROM articles ORDER BY id LIMIT 10;

    — 下一页:传入上一页最后一条的 id(假设是 10)
    SELECT * FROM articles
    WHERE id > 10
    ORDER BY id
    LIMIT 10;

    原理:通过 WHERE id > 上一页最后一条的id 直接定位到起始位置,数据库可以利用索引直接跳到该位置,不需要扫描前面的数据。

    优点:

  • 性能稳定:无论翻到第几页,查询速度都差不多,因为走了索引定位。

  • 无数据漂移:基于具体记录的键值定位,插入/删除新数据不会导致重复或遗漏。

  • 缺点:

  • 不能随机跳页:只能一页页往后翻(或往前翻),无法直接跳到第 50 页。

  • 排序字段有限制:通常需要排序字段唯一且有序(如 id、创建时间+id 组合),否则游标定位会出问题。

  • 实现稍复杂:游标需要编码传递,前端也要配合。

  • 对比总结

    维度偏移量分页游标分页
    实现方式 LIMIT + OFFSET WHERE key > last_value
    深分页性能 差(越翻越慢) 好(恒定)
    数据漂移 有 无
    随机跳页 支持 不支持
    适用场景 后台管理、页数少的列表 信息流、无限滚动、大数据量

    怎么选

    • 后台管理系统、数据量不大、需要跳页 → 偏移量分页

    • 社交信息流(微博、Twitter、朋友圈)、无限下拉加载、数据量大 → 游标分页

    • 很多大厂的信息流(如 Twitter API、GitHub API)都强制使用游标分页,就是为了避免深分页的性能和数据一致性问题。

    一个折中方案是:前几页用偏移量分页(用户体验好、能跳页),超过一定深度后改用游标分页。

    游标分页与偏移量分页:详细解释

    一、先看一个共同问题

    假设有一张巡查记录表 inspection,按 id 排序,每页显示 10 条。用户要一页一页往下看。

    分页要解决的核心问题只有一个:下一页从哪里开始?

    两种分页方式,就是对这个问题的两种回答。


    二、偏移量分页(Offset Pagination)

    2.1 定义

    用 "跳过多少条" 来定位下一页的起点。

    sql

    SELECT * FROM inspection ORDER BY id LIMIT 10 OFFSET 0; — 第1页
    SELECT * FROM inspection ORDER BY id LIMIT 10 OFFSET 10; — 第2页
    SELECT * FROM inspection ORDER BY id LIMIT 10 OFFSET 20; — 第3页

    LIMIT 10 OFFSET 20 的含义:跳过前 20 条,取接下来的 10 条。

    写成 LIMIT 20, 10 是一样的(MySQL 的简写,前者是 offset,后者是 count)。

    2.2 执行过程

    以 LIMIT 1000000, 10 为例:

    text

    第 1 步:从第 1 行开始扫描
    第 2 步:扫描到第 1000010 行(1000000 + 10)
    第 3 步:丢弃前 1000000 行
    第 4 步:返回最后 10 行

    关键点:MySQL 必须先取到第 1000010 行,才知道要丢哪些。它没法"直接跳到第 1000001 行"。

    2.3 为什么叫"偏移量"

    OFFSET 1000000 就是"偏移量"——从起点偏移了多少条。它是一个绝对位置的概念。

    2.4 优点

    • 支持跳页:可以直接跳到第 1 页、第 50 页、最后一页

    • 能算总页数:COUNT(*) 除以每页条数

    • 写法简单:一条 SQL 搞定

    • 前端友好:页码组件天然对应 offset

    2.5 缺点

    • 深分页慢:offset 越大,扫描和丢弃的行越多

    • 数据变动时不稳定:翻页过程中有插入/删除,会导致重复或遗漏

    数据变动的例子:

    text

    第1页:id 100, 99, 98, …, 91
    (此时插入一条新记录 id 101)
    第2页:LIMIT 10, 10 → 实际返回 id 91, 90, …, 82

    id=91 在第 1 页出现过,第 2 页又出现了。因为新插入把整体往后挤了一位。

    2.6 适用场景

    • 后台管理系统的表格(运营要跳页、要看总数)

    • 数据量不大、offset 不深的场景

    • 需要"跳到最后一页"的场景


    三、游标分页(Cursor Pagination)

    3.1 定义

    用 "上一页最后一条记录的排序字段值" 来定位下一页的起点。

    sql

    — 第1页
    SELECT * FROM inspection ORDER BY id LIMIT 10;

    — 假设第1页最后一条的 id = 91
    — 第2页:从 id=91 之后开始
    SELECT * FROM inspection WHERE id < 91 ORDER BY id DESC LIMIT 10;

    — 假设第2页最后一条的 id = 82
    — 第3页
    SELECT * FROM inspection WHERE id < 82 ORDER BY id DESC LIMIT 10;

    那个 id=91、id=82 就是游标(cursor)——它标记了"上一页读到哪里"。

    3.2 执行过程

    以 WHERE id < 91 ORDER BY id DESC LIMIT 10 为例:

    text

    第 1 步:通过 id 索引直接定位到 91 的位置
    第 2 步:从 91 往前(更小的 id)扫描 10 条
    第 3 步:返回这 10 条

    关键点:不需要扫描前 91 条,直接定位到 91,然后取 10 条。扫描量恒定为 10。

    3.3 为什么叫"游标"

    游标(cursor)本意是"光标"——它指向当前读到的位置。下一页从光标处继续,而不是从头数起。

    3.4 优点

    • 性能恒定:不管翻到第几页,扫描量都是每页条数

    • 数据一致性好:翻页过程中插入/删除不影响结果,不会重复或遗漏

    • 适合无限滚动:天然匹配"加载更多"

    3.5 缺点

    • 不能跳页:只能"下一页",不能直接跳到第 50 页

    • 不能算总页数:不知道总共有多少页

    • 排序字段必须唯一且有序:通常用 (时间, ID) 组合,单用时间可能重复

    • 前端要维护游标状态:每次请求要带上上一页的游标

    3.6 适用场景

    • App 信息流(微信朋友圈、微博、抖音)

    • 无限滚动的列表

    • 开放 API(GitHub、Twitter 都用游标)

    • 数据频繁变动的场景


    四、一张图看懂区别

    text

    偏移量分页:从起点数,跳过 N 条

    起点 ──────────────────────────────►
    跳过 1000000 条 取 10 条
    └──── 扫描并丢弃 ────┘

    问题:每次都要从头数

    游标分页:从上一页结束的位置继续

    起点 ──────────────────────────────►
    游标位置
    │
    ▼ 取 10 条

    优势:直接定位,不从头数


    五、对比表

    维度偏移量分页游标分页
    定位方式 跳过 N 条 从上一页最后一条之后
    SQL 写法 LIMIT offset, count WHERE 排序字段 > 游标值
    扫描量 随 offset 增大而增大 恒定(每页条数)
    深分页性能 差 好
    能否跳页 能 不能
    能否算总页数 能 不能
    数据变动时 可能重复/遗漏 稳定
    前端复杂度 低(页码) 中(维护游标)
    典型场景 后台管理表格 App 信息流、API

    六、用生活例子类比

    偏移量分页:像看纸质书,你要看第 100 页,得从第 1 页开始翻,翻过前 99 页才能到第 100 页。

    游标分页:像用书签,你看到第 100 页,夹个书签,下次直接从书签处继续,不用从头翻。

    这就解释了为什么游标分页快:它不需要"翻过前面的页",直接定位到当前位置。


    七、代码示例对比

    偏移量分页

    java

    // 请求:page=1000, size=10
    int offset = (page – 1) * size; // 9990
    String sql = "SELECT * FROM inspection ORDER BY id LIMIT " + offset + ", " + size;
    // 扫描 10000 行,丢弃 9990 行,返回 10 行

    游标分页

    java

    // 请求:cursor=91, size=10(cursor 是上一页最后一条的 id)
    String sql = "SELECT * FROM inspection WHERE id < " + cursor + " ORDER BY id DESC LIMIT " + size;
    // 直接定位到 91,取 10 行
    // 返回结果时,把本页最后一条的 id 作为下一页的 cursor 一起返回


    八、总结

    偏移量分页:用"跳过多少条"定位,简单但深分页慢,适合能跳页的后台管理。

    游标分页:用"上一页读到哪里"定位,性能恒定且数据稳定,适合顺序翻页的信息流和 API。

    本质区别:偏移量是绝对位置(第 N 条),游标是相对位置(从某条之后)。绝对位置要

    数,相对位置不用数。

    在mybatisplus中默认的是偏移量如果想用游标分页,可以绕开page,自己写QueryWrapper

    // 假设上一页最后一条的 ID 是 lastId Long lastId = 10020L; int size = 10;

    // MyBatis-Plus 写法 List<Inspection> list = inspectionMapper.selectList(     new LambdaQueryWrapper<Inspection>()         .gt(Inspection::getId, lastId)  // 游标条件:id > lastId         .orderByAsc(Inspection::getId)         .last("LIMIT " + size)          // 直接手写 LIMIT,不走分页插件 );

    这样生成的 SQL 就是 WHERE id > 10020 ORDER BY id ASC LIMIT 10,完全避开了 OFFSET,性能稳定,可以看看这个高效翻页

    偏移量的代价当要查询的数据跳页过大的时候,会出现严重的性能浪费,一般的大厂的解决方案,是偏移量固定为最大1000,后面的通过游标分页来搜索完成,在实际的场景中要根据自己的业务场景和数据库选择,灵活的配置和兜底

    赞(0)
    未经允许不得转载:171主机测评 » 游标分页和偏移量详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址