分页查询是业务系统中最基础却最容易被忽视的技术点。我在金融、电商等多个千万级用户系统中处理过性能问题,发现80%的慢SQL都与错误的分页实现有关。真正的分页不只是简单的LIMIT语句,而是需要综合考虑数据量、查询效率、用户体验的完整解决方案。
典型场景举例:当用户列表达到百万级时,常见的LIMIT 1000000, 10写法会导致数据库完整扫描前100万条记录。去年我们系统就因为这个问题,在促销期间出现了数据库CPU飙升至90%的险情。正确的分页实现应该像翻书一样——不需要记住前面所有页码的内容,而是能直接定位到目标页的起始位置。
java复制// 典型错误示例(性能陷阱)
String sql = "SELECT * FROM orders LIMIT " + (pageNum-1)*pageSize + "," + pageSize;
这种写法有三大致命缺陷:
关键指标实测:当offset达到10万时,MySQL查询耗时从5ms暴增至1200ms
java复制// 基于最后记录ID的优化方案
String sql = "SELECT * FROM orders WHERE id > ? ORDER BY id LIMIT ?";
preparedStatement.setLong(1, lastId);
preparedStatement.setInt(2, pageSize);
优势对比:
| 方案类型 | 10万数据耗时 | 100万数据耗时 | 是否支持跳页 |
|---|---|---|---|
| 传统LIMIT | 320ms | 2900ms | 是 |
| 游标分页 | 8ms | 12ms | 否 |
xml复制<!-- 拦截器配置示例 -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
<property name="reasonable" value="true"/>
</plugin>
</plugins>
核心参数说明:
reasonable:页码越界时自动修正pageSizeZero:当pageSize=0时返回全部结果supportMethodsArguments:支持接口参数方式java复制public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.dept = ?1")
Page<User> findByDepartment(String dept, Pageable pageable);
}
// 调用示例
Page<User> users = userRepository.findByDepartment(
"dev",
PageRequest.of(1, 20, Sort.by("createTime").descending())
);
分页对象包含的关键信息:
totalElements:总记录数totalPages:总页数content:当前页数据列表sql复制-- 优化前(性能差)
SELECT * FROM products ORDER BY sales DESC LIMIT 100000, 10;
-- 优化后(先定位ID再关联)
SELECT t.* FROM products t
JOIN (SELECT id FROM products ORDER BY sales DESC LIMIT 100000, 10) tmp
ON t.id = tmp.id;
实测性能提升:
在微服务架构中,常规分页会遇到三大难题:
解决方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 内存分页 | 实现简单 | 数据量大时OOM风险 |
| 分片结果归并 | 支持大数据量 | 开发复杂度高 |
| 预计算+缓存 | 性能最优 | 实时性受影响 |
sql复制-- 低效写法(全表扫描)
SELECT COUNT(*) FROM huge_table;
-- 优化方案1:近似计数
EXPLAIN SELECT TABLE_ROWS
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'huge_table';
-- 优化方案2:维护计数表
CREATE TABLE table_counter (
table_name VARCHAR(100) PRIMARY KEY,
count BIGINT NOT NULL
);
分页参数校验规则:
响应数据结构示例:
json复制{
"success": true,
"data": {
"list": [...],
"total": 1024,
"pageNum": 1,
"pageSize": 20,
"hasNext": true
}
}
建议在拦截器中添加如下监控指标:
java复制// 分页查询监控点
Metrics.timer("db.query.pagination")
.tag("table", tableName)
.record(() -> {
// 执行分页查询
});
// 关键阈值告警
if(pageNum > 100 && pageSize > 100) {
log.warn("Deep pagination detected: {} {}", pageNum, pageSize);
}
推荐监控看板包含:
在订单系统中,通过这套监控我们发现:用户历史订单第5页之后的访问量骤降90%,于是将默认分页策略改为"只加载前5页,点击'加载更多'再异步获取",使系统负载降低40%。