1. 为什么create_time降序分页会成为性能瓶颈在大多数业务系统中我们习惯性地使用create_time字段进行降序分页查询这看似合理的方案背后隐藏着严重的性能隐患。当数据量达到百万级时这种查询方式会导致明显的性能劣化我曾在电商订单系统中亲历过分页查询从200ms骤增到8s的惨痛教训。1.1 传统分页的实现原理典型的create_time分页SQL如下SELECT * FROM orders ORDER BY create_time DESC LIMIT 10 OFFSET 100000;数据库执行这类查询时需要完成三个关键步骤对所有符合条件的数据按照create_time降序排序临时存储排序后的完整结果集从指定偏移量开始返回限定数量的记录当offset值较大时如翻到第1000页数据库实际上需要先读取并排序100010条记录然后丢弃前100000条最终只返回10条。这个过程中产生了大量无效的I/O和CPU计算。1.2 真实场景下的性能测试数据我在测试环境用1000万条订单数据做了对比实验分页方式第10页耗时第100页耗时第1000页耗时LIMIT-OFFSET12ms45ms380ms游标分页8ms9ms11ms当翻页深度达到1万页时传统方式需要4.2秒而优化方案仅需15ms。这种差距在用户高频访问时会直接导致数据库连接池耗尽。2. 高性能分页的替代方案实践2.1 游标分页Cursor Pagination游标分页的核心思想是记住最后一条记录的位置而不是计算绝对偏移量。以订单查询为例// 使用MyBatis-Plus实现 public PageOrder listOrders(Long lastId, int size) { return new LambdaQueryChainWrapper(orderMapper) .gt(Order::getId, lastId) .orderByAsc(Order::getId) .page(new Page(1, size)); }关键改进点使用自增主键替代create_time通过gt(lastId)条件实现定位查询保持排序条件与查询条件一致注意游标分页要求排序字段必须具有唯一性否则会出现记录遗漏或重复。对于非唯一字段如create_time需要组合主键作为二级排序条件。2.2 延迟关联优化对于复杂查询场景可以先将分页操作放在索引上完成再关联获取详细数据SELECT t.* FROM orders t INNER JOIN ( SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 10 ) tmp ON t.id tmp.id;这个方案通过两步查询子查询只操作索引字段通常是主键外层查询通过主键快速定位完整记录实测在千万级数据下查询耗时从原来的2.1s降至230ms。3. 特殊场景下的分页优化技巧3.1 模糊查询分页的解决方案当需要结合LIKE条件进行分页时传统方案性能极差。可以采用以下优化手段// JPA模糊查询分页示例 Query(SELECT o FROM Order o WHERE o.orderNo LIKE %:keyword% AND o.id :lastId ORDER BY o.id ASC) PageOrder searchAfter( Param(keyword) String keyword, Param(lastId) Long lastId, Pageable pageable);配合前端改造首次查询传lastId0后续请求携带最后一条记录的ID页面保留当前批次的所有ID用于异常处理3.2 分布式ID环境的分页处理在使用雪花ID等分布式ID方案时可以利用ID的时间有序性SELECT * FROM orders WHERE id :lastId ORDER BY id DESC LIMIT 10;由于雪花ID的高位是时间戳这种方案既保持了时间排序又避免了create_time索引的竞争。4. 生产环境踩坑实录4.1 索引失效的典型场景在一次大促前的压测中发现分页查询突然变慢。根本原因是-- 错误示例排序字段与条件字段使用不同索引 SELECT * FROM orders WHERE user_id 123 ORDER BY create_time DESC LIMIT 10;优化方案是创建联合索引ALTER TABLE orders ADD INDEX idx_user_create(user_id, create_time);4.2 深度分页的容灾方案对于必须支持跳页的业务场景如后台管理系统可以采用以下折中方案限制最大翻页深度如最多100页结合缓存预生成热门页数据使用Elasticsearch等专门优化搜索的引擎// 基于Spring Cache的分页缓存示例 Cacheable(value orderPages, key #pageNum) public PageOrder getOrderPage(int pageNum) { // 查询逻辑 }5. 框架层面的最佳实践5.1 MyBatis-Plus分页插件优化在配置MyBatis-Plus分页插件时建议Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 使用优化后的分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL) { Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 自动检测order by语句 optimizeOrderBy(boundSql); } }); return interceptor; }5.2 JPA分页的陷阱规避Spring Data JPA的分页查询默认会执行count查询可以通过QueryHint优化QueryHints(value { QueryHint(name org.hibernate.cacheable, value true), QueryHint(name org.hibernate.fetchSize, value 50) }) Query(SELECT o FROM Order o WHERE o.status :status) PageOrder findByStatus(Param(status) int status, Pageable pageable);实际项目中我们通过AOP实现了智能count查询开关当页码较小时自动执行count深度分页时跳过count直接查询。分页优化是一个需要持续调优的过程我在金融级系统中最终采用的混合方案是前100页使用传统分页缓存深度分页改用游标模式异步加载。这种方案在保证用户体验的同时将数据库负载降低了70%。