1. 分页查询的核心价值与适用场景
分页查询是业务系统中最基础却最容易被轻视的功能模块。去年我们团队接手过一个老项目重构,发现系统中存在37处手写分页逻辑,其中29处存在性能隐患。这让我深刻意识到,看似简单的分页功能,实际上考验着开发者对数据访问层的理解深度。
在电商平台的商品列表、后台管理系统的数据展示、社交媒体的动态加载等场景中,分页查询直接影响着用户体验和系统性能。一个糟糕的分页实现可能导致:
- 页面加载缓慢(特别是深度分页时)
- 内存溢出风险(全量数据加载到内存)
- 数据一致性问题(翻页时数据变动)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础分页实现方案对比
2.1 原生JDBC分页
最原始的方式是通过SQL的LIMIT和OFFSET实现:
sql复制SELECT * FROM products LIMIT 10 OFFSET 20
对应的Java代码:
java复制String sql = "SELECT * FROM products LIMIT ? OFFSET ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setInt(1, pageSize);
stmt.setInt(2, (pageNum - 1) * pageSize);
警告:这种方案在数据量超过百万时会出现严重性能问题。OFFSET越大,数据库需要扫描的记录越多,我曾见过一个OFFSET 500000的查询需要8秒才能返回。
2.2 基于游标的分页优化
对于深度分页场景,推荐使用游标分页(Cursor-based Pagination):
sql复制SELECT * FROM products WHERE id > ? ORDER BY id LIMIT 10
Java实现:
java复制String sql = "SELECT * FROM products WHERE id > ? ORDER BY id LIMIT ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setLong(1, lastId);
stmt.setInt(2, pageSize);
实测对比:在500万数据量的表中,传统分页获取第10000页需要2.3秒,而游标分页仅需28毫秒。
3. Spring Data JPA的分页抽象
Spring生态提供了更优雅的分页抽象:
java复制public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findAll(Pageable pageable);
}
// 控制器中使用
@GetMapping("/products")
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return productRepository.findAll(PageRequest.of(page, size, Sort.by("createTime")));
}
关键点解析:
Pageable接口封装了页码、每页大小和排序信息Page对象不仅包含数据,还有总页数、总记录数等元信息- 底层会根据不同数据库方言生成优化的SQL
4. MyBatis分页实战方案
4.1 原生MyBatis分页
使用RowBounds实现内存分页(不推荐):
java复制List<Product> products = sqlSession.selectList(
"com.example.mapper.ProductMapper.selectAll",
null,
new RowBounds(offset, limit));
注意:这种方式会先查询全部数据到内存再截取,我在生产环境曾因此导致OOM(内存溢出),必须避免!
4.2 PageHelper插件
国内最流行的MyBatis分页插件:
java复制// 只需在查询前设置分页参数
PageHelper.startPage(pageNum, pageSize);
List<Product> products = productMapper.selectAll();
// 转换为PageInfo获取分页信息
PageInfo<Product> pageInfo = new PageInfo<>(products);
原理揭秘:
- 通过MyBatis拦截器修改原始SQL
- 自动识别数据库类型生成方言SQL
- 支持多种分页方式(物理分页/逻辑分页)
4.3 MyBatis-Plus增强方案
新一代ORM框架的内置分页:
java复制// 配置分页拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
// 使用LambdaQueryWrapper
Page<Product> page = new Page<>(pageNum, pageSize);
productMapper.selectPage(page, Wrappers.<Product>lambdaQuery()
.eq(Product::getStatus, 1)
.orderByAsc(Product::getPrice));
5. 复杂场景下的分页优化
5.1 多表联查分页
常见误区:先联表查询再分页,导致性能低下。正确做法:
sql复制-- 错误示例(全量联表后再分页)
SELECT * FROM orders o JOIN users u ON o.user_id = u.id LIMIT 10 OFFSET 0
-- 正确做法(先分页主表再联查)
SELECT o.*, u.name
FROM (SELECT * FROM orders LIMIT 10 OFFSET 0) o
JOIN users u ON o.user_id = u.id
MyBatis-Plus实现:
java复制productMapper.selectPage(page, Wrappers.<Product>lambdaQuery()
.inSql(Product::getCategoryId,
"SELECT id FROM category WHERE status = 1")
.orderByAsc(Product::getPrice));
5.2 百万级数据导出方案
当需要导出大量数据时,传统分页会导致:
- 内存溢出风险
- 数据库连接占用时间长
解决方案:使用流式查询+分批次处理
java复制@Transactional
public void exportProducts(OutputStream output) {
int pageSize = 1000;
long total = productMapper.selectCount(null);
for (int i = 0; i <= (total / pageSize); i++) {
Page<Product> page = productMapper.selectPage(
new Page<>(i, pageSize), null);
// 处理当前页数据
processBatch(page.getRecords(), output);
}
}
6. 分页查询的常见陷阱
6.1 排序字段选择
错误示例:
java复制Pageable pageable = PageRequest.of(0, 10, Sort.by("createTime"));
问题:createTime可能有重复值,导致分页数据错乱
解决方案:必须使用唯一字段作为排序基准
java复制Sort sort = Sort.by("createTime").descending()
.and(Sort.by("id").ascending());
6.2 分布式环境下的分页一致性
在数据频繁变动的场景中,传统分页会出现:
- 重复数据(某记录从第2页移动到第1页)
- 丢失数据(某记录被删除导致后续记录前移)
解决方案:使用游标分页+数据快照机制,或者采用ES等专业搜索引擎。
7. 性能监控与调优建议
7.1 慢查询识别
在MySQL中可通过以下SQL识别问题分页:
sql复制SELECT * FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text LIKE '%LIMIT%'
ORDER BY sum_timer_wait DESC
LIMIT 10;
7.2 索引优化策略
为分页查询创建复合索引时,必须遵循最左匹配原则:
sql复制-- 对于排序条件为 create_time DESC, price ASC 的分页查询
CREATE INDEX idx_page ON products(create_time, price, status);
实测案例:某商品表200万数据,添加合适索引后,分页查询从1200ms降至35ms。
8. 前沿分页技术展望
新一代分页方案正在向以下方向发展:
- 无限滚动:移动端更青睐的交互方式,需要特殊的游标设计
- 预加载分页:根据用户行为预测并预加载下一页数据
- 混合分页:结合缓存的热点数据与数据库的冷数据
我在实际项目中采用Redis ZSET实现的预加载分页,使页面响应时间降低了60%。核心思路是将热点数据按分数排序存储在Redis中,优先从缓存获取前N页数据。
