MySQL 深分页如何进行性能优化?

MySQL 深分页如何进行性能优化?

作者:一名八年经验的 Java 开发工程师

引言:深分页,真的只是"翻页慢"那么简单吗?

在面试中,你是否遇到过这样的问题?

"你了解 MySQL 深分页的性能问题吗?如何优化?"

又或者在真实项目中,当你翻到第 1000 页的数据时,接口突然变得异常缓慢,甚至超时崩溃。你打开慢 SQL 日志,看到那条熟悉的 LIMIT 100000, 20,心里默默叹了口气:"啊,又是深分页惹的祸。"

作为一名有 8 年开发经验的 Java 工程师,我在多个后台系统和数据中心项目中都遇到过深分页带来的性能瓶颈。起初我们只是加索引、调 SQL,但最终发现:分页的本质,其实是数据访问策略的设计问题

这篇文章将带你深入理解:

  • 为什么深分页会拖垮数据库?
  • 如何结合实际业务场景进行优化?
  • 有哪些可落地的代码实践?
  • 如何用一个高性能的游标分页方案替代传统分页?

一、背景介绍

在日常业务开发中,分页查询是非常常见的需求。例如在管理后台系统中,展示订单列表、用户列表、日志记录等都需要分页加载。

通常我们会使用类似下面的 SQL:

sql 复制代码
SELECT * FROM orders ORDER BY create_time DESC LIMIT 100000, 20;

上述查询语句的含义是:跳过 100000 条记录,取第 100001 到 100020 条数据。这种分页方式我们称为"深分页"

二、深分页的性能问题

MySQL 在执行 LIMIT offset, size 的时候,会先扫描 offset + size 条记录,然后抛弃前 offset 条,只返回 size 条。

也就是说,LIMIT 100000, 20 实际上扫描了 100020 行,仅返回 20 行。若表数据量和 offset 很大,性能将急剧下降,甚至拖垮数据库。

三、业务场景分析

假设你在做一个订单系统,运营人员需要查看历史订单数据,而这些订单数据量非常庞大(千万级),他们经常会翻到第 1000 页查看订单(每页 20 条)。

你发现系统在翻页到后面时接口响应非常慢,排查后定位到是 MySQL 查询耗时严重。

这时候我们就需要 优化深分页的查询方式


四、优化思路

✅ 方法一:使用覆盖索引

vbnet 复制代码
SELECT id FROM orders ORDER BY id LIMIT 100000, 20;
SELECT * FROM orders WHERE id IN (...);

通过先查主键,再回表查详细数据,减少回表成本。

✅ 方法二:记录上一次的游标(推荐)

使用**"基于游标的分页"**,也叫作"Keyset Pagination"或"Seek 方法"。

思路是:不要使用 LIMIT offset,而是通过上一次查询的最后一条数据的主键或排序字段作为游标,下次查询直接从该游标之后开始。

sql 复制代码
SELECT * FROM orders WHERE id > ? ORDER BY id ASC LIMIT 20;

优势:

  • 避免跳过大量数据,性能优越
  • 更适合实时数据流或按时间排序的场景

五、实战代码:Java 工具类实现游标分页

我们封装一个通用的分页工具类,支持游标分页,适用于 Spring + MyBatis/MyBatis-Plus 项目。

📦 1. 分页请求参数类

arduino 复制代码
public class CursorPageRequest {
    /**
     * 游标字段,如 id、时间戳等
     */
    private Long cursor;

    /**
     * 每页数量
     */
    private int pageSize = 20;

    public CursorPageRequest() {}

    public CursorPageRequest(Long cursor, int pageSize) {
        this.cursor = cursor;
        this.pageSize = pageSize;
    }

    public Long getCursor() {
        return cursor;
    }

    public void setCursor(Long cursor) {
        this.cursor = cursor;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
}

📦 2. 通用分页返回类

kotlin 复制代码
import java.util.List;

public class CursorPageResponse<T> {
    private List<T> data;
    private Long nextCursor;
    private boolean hasMore;

    public CursorPageResponse(List<T> data, Long nextCursor, boolean hasMore) {
        this.data = data;
        this.nextCursor = nextCursor;
        this.hasMore = hasMore;
    }

    public List<T> getData() {
        return data;
    }

    public Long getNextCursor() {
        return nextCursor;
    }

    public boolean isHasMore() {
        return hasMore;
    }
}

📦 3. MyBatis 示例 Mapper 接口(以订单为例)

java

复制

less 复制代码
@Mapper
public interface OrderMapper {

    /**
     * 查询游标分页数据
     * @param cursor 上次最后一条记录的 ID
     * @param pageSize 每页数量
     */
    @Select("SELECT * FROM orders " +
            "WHERE (:cursor IS NULL OR id > #{cursor}) " +
            "ORDER BY id ASC LIMIT #{pageSize}")
    List<OrderDO> listByCursor(@Param("cursor") Long cursor, @Param("pageSize") Integer pageSize);
}

📦 4. Service 层封装分页逻辑

ini 复制代码
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public CursorPageResponse<OrderDO> listOrdersByCursor(CursorPageRequest request) {
        List<OrderDO> list = orderMapper.listByCursor(request.getCursor(), request.getPageSize());

        Long nextCursor = null;
        boolean hasMore = false;

        if (!list.isEmpty()) {
            // 获取最后一条记录的 ID 作为下次游标
            nextCursor = list.get(list.size() - 1).getId();
            hasMore = list.size() == request.getPageSize();
        }

        return new CursorPageResponse<>(list, nextCursor, hasMore);
    }
}

六、接口调用示例

前端每次请求:

ini 复制代码
GET /orders?cursor=10021&pageSize=20

返回:

json 复制代码
{
  "data": [...],
  "nextCursor": 10041,
  "hasMore": true
}

前端将 nextCursor 作为下次请求的 cursor 参数实现"加载更多"效果。


七、总结

分页方式 优点 缺点
LIMIT offset, size 简单通用 深分页性能差
游标分页(Keyset) 性能优秀,避免跳过大量数据 不支持跳页,只支持顺序加载

建议 :在数据量大、分页页码深的业务场景中,优先使用 游标分页,尤其是管理后台、数据分析系统、接口服务等。


八、建议与扩展

  • 游标字段最好是唯一递增的,比如主键 ID、时间戳等
  • 多字段组合游标可以支持更复杂的排序场景
  • 可以将分页封装为 AOP 或 BaseService 通用组件

如果你有兴趣,我也可以进一步封装这个分页工具为一个独立的 Spring Boot Starter,欢迎点赞收藏交流 👏

相关推荐
Sylvia-girl1 小时前
Java——抽象类
java·开发语言
阿芯爱编程4 小时前
2025前端面试题
前端·面试
Touper.4 小时前
Redis 基础详细介绍(Redis简单介绍,命令行客户端,Redis 命令,Java客户端)
java·数据库·redis
m0_535064604 小时前
C++模版编程:类模版与继承
java·jvm·c++
FreeBuf_4 小时前
黄金旋律IAB组织利用暴露的ASP.NET机器密钥实施未授权访问
网络·后端·asp.net
虾条_花吹雪5 小时前
Using Spring for Apache Pulsar:Message Production
java·ai·中间件
tomorrow.hello5 小时前
Java并发测试工具
java·开发语言·测试工具
Moso_Rx5 小时前
javaEE——synchronized关键字
java·java-ee
张小洛5 小时前
Spring AOP 是如何生效的(入口源码级解析)?
java·后端·spring
倔强青铜三6 小时前
苦练Python第18天:Python异常处理锦囊
人工智能·python·面试