浅谈PageHelper插件分页实现原理及大数据量下SQL查询效率问题解决
前因:项目一直使用的是PageHelper实现分页功能,项目前期数据量较少一直没有什么问题。随着业务扩增,数据库扩增PageHelper出现了明显的性能问题。几十万甚至上百万的单表数据查询性能缓慢,需要几秒乃至十几秒的查询时间。故此特地研究了一下PageHelper源码,查找PageHelper分页的实现方式。
一段较为简单的查询,跟随debug开始源码探寻之旅。
-
public ResultContent select(Integer id) {
-
Page<Test> blogPage = PageHelper.startPage(1,3).doSelectPage( () -> testDao.select(id));
-
List<Test> test = (List<Test>)blogPage.getResult();
-
return new ResultContent(0, "success", test);
-
}
主要保存由前端传入的pageNum(页数)、pageSize(每页显示数量)和count(是否进行count(0)查询)信息。
这里是简单的创建page并保存当前线程的变量副本心里,不做深究。
-
public static <E> Page<E> startPage(int pageNum, int pageSize) {
-
return startPage(pageNum, pageSize, DEFAULT_COUNT);
-
}
-
-
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
-
return startPage(pageNum, pageSize, count, (Boolean)null, (Boolean)null);
-
}
-
-
public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {
-
Page<E> page = startPage(pageNum, pageSize);
-
page.setOrderBy(orderBy);
-
return page;
-
}
-
-
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
-
Page<E> page = new Page(pageNum, pageSize, count);
-
page.setReasonable(reasonable);
-
page.setPageSizeZero(pageSizeZero);
-
Page<E> oldPage = getLocalPage();
-
if(oldPage != null && oldPage.isOrderByOnly()) {
-
page.setOrderBy(oldPage.getOrderBy());
-
}
-
-
setLocalPage(page);
-
return page;
-
}
开始执行真正的select语句
-
public <E> Page<E> doSelectPage(ISelect select) {
-
select.doSelect();
-
return this;
-
}
进入MapperProxy类执行invoke方法获取到方法名称及参数值
-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-
if (Object.class.equals(method.getDeclaringClass())) {
-
try {
-
return method.invoke(this, args);
-
} catch (Throwable t) {
-
throw ExceptionUtil.unwrapThrowable(t);
-
}
-
}
-
final MapperMethod mapperMethod = cachedMapperMethod(method);
-
return mapperMethod.execute(sqlSession, args);
-
}
接着是MapperMethod方法执行execute语句,判断是增、删、改、查。判断返回值是多个,进入executeForMany方法
-
public Object execute(SqlSession sqlSession, Object[] args) {
-
Object result;
-
if (SqlCommandType.INSERT == command.getType()) {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.insert(command.getName(), param));
-
} else if (SqlCommandType.UPDATE == command.getType()) {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.update(command.getName(), param));
-
} else if (SqlCommandType.DELETE == command.getType()) {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.delete(command.getName(), param));
-
} else if (SqlCommandType.SELECT == command.getType()) {
-
if (method.returnsVoid() && method.hasResultHandler()) {
-
executeWithResultHandler(sqlSession, args);
-
result = null;
-
} else if (method.returnsMany()) {
-
result = executeForMany(sqlSession, args);
-
} else if (method.returnsMap()) {
-
result = executeForMap(sqlSession, args);
-
} else {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = sqlSession.selectOne(command.getName(), param);
-
}
-
} else if (SqlCommandType.FLUSH == command.getType()) {
-
result = sqlSession.flushStatements();
-
} else {
-
throw new BindingException("Unknown execution method for: " + command.getName());
-
}
-
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
-
throw new BindingException("Mapper method '" + command.getName()
-
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
-
}
-
return result;
-
}
这个方法开始调用SqlSessionTemplate、DefaultSqlSession等类获取到Mapper.xml文件的SQL语句
-
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
-
List<E> result;
-
Object param = method.convertArgsToSqlCommandParam(args);
-
if (method.hasRowBounds()) {
-
RowBounds rowBounds = method.extractRowBounds(args);
-
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
-
} else {
-
result = sqlSession.<E>selectList(command.getName(), param);
-
}
-
// issue #510 Collections & arrays support
-
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
-
if (method.getReturnType().isArray()) {
-
return convertToArray(result);
-
} else {
-
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
-
}
-
}
-
return result;
-
}
开始进入PageHelper的真正实现,Plugin通过实现InvocationHandler进行动态代理获取到相关信息
-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-
try {
-
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
-
if (methods != null && methods.contains(method)) {
-
return interceptor.intercept(new Invocation(target, method, args));
-
}
-
return method.invoke(target, args);
-
} catch (Exception e) {
-
throw ExceptionUtil.unwrapThrowable(e);
-
}
-
}
PageInterceptor 实现Mybatis的Interceptor 接口,进行拦截
-
public Object intercept(Invocation invocation) throws Throwable {
-
try {
-
Object[] args = invocation.getArgs();
-
MappedStatement ms = (MappedStatement)args[0];
-
Object parameter = args[1];
-
RowBounds rowBounds = (RowBounds)args[2];
-
ResultHandler resultHandler = (ResultHandler)args[3];
-
Executor executor = (Executor)invocation.getTarget();
-
CacheKey cacheKey;
-
BoundSql boundSql;
-
if(args.length == 4) {
-
boundSql = ms.getBoundSql(parameter);
-
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
-
} else {
-
cacheKey = (CacheKey)args[4];
-
boundSql = (BoundSql)args[5];
-
}
-
-
this.checkDialectExists();
-
List resultList;
-
if(!this.dialect.skip(ms, parameter, rowBounds)) {
-
if(this.dialect.beforeCount(ms, parameter, rowBounds)) {
-
Long count = this.count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
-
if(!this.dialect.afterCount(count.longValue(), parameter, rowBounds)) {
-
Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
-
return var12;
-
}
-
}
-
-
resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
-
} else {
-
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
-
}
-
-
Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
-
return var16;
-
} finally {
-
this.dialect.afterAll();
-
}
-
}
转到ExecutorUtil抽象类的pageQuery方法
-
public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException {
-
if(!dialect.beforePage(ms, parameter, rowBounds)) {
-
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
-
} else {
-
parameter = dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
-
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
-
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
-
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
-
Iterator var12 = additionalParameters.keySet().iterator();
-
-
while(var12.hasNext()) {
-
String key = (String)var12.next();
-
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
-
}
-
-
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
-
}
-
}
在抽象类AbstractHelperDialect的getPageSql获取到对应的Page对象
-
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
-
String sql = boundSql.getSql();
-
Page page = this.getLocalPage();
-
String orderBy = page.getOrderBy();
-
if(StringUtil.isNotEmpty(orderBy)) {
-
pageKey.update(orderBy);
-
sql = OrderByParser.converToOrderBySql(sql, orderBy);
-
}
-
-
return page.isOrderByOnly()?sql:this.getPageSql(sql, page, pageKey);
-
}
进入到MySqlDialect类的getPageSql方法进行SQL封装,根据page对象信息增加Limit。分页的信息就是这么拼装起来的
-
public String getPageSql(String sql, Page page, CacheKey pageKey) {
-
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
-
sqlBuilder.append(sql);
-
if(page.getStartRow() == 0) {
-
sqlBuilder.append(" LIMIT ? ");
-
} else {
-
sqlBuilder.append(" LIMIT ?, ? ");
-
}
-
-
return sqlBuilder.toString();
-
}
将最后拼装好的SQL返回给DefaultSqlSession执行查询并返回
-
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
-
try {
-
MappedStatement ms = configuration.getMappedStatement(statement);
-
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
-
} catch (Exception e) {
-
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
-
} finally {
-
ErrorContext.instance().reset();
-
}
至此整个查询过程完成,原来PageHelper的分页功能是通过Limit拼接SQL实现的。查询效率低的问题也找出来了,那么应该如何解决。
首先分析SQL语句,limit在数据量少或者页数比较靠前的时候查询效率是比较高的。(单表数据量百万进行测试)
select * from user where age = 10 limit 1,10;结果显示0.43s
当where条件后的结果集较大并且页数达到一个量级整个SQL的查询效率就十分低下(哪怕where的条件加上了索引也不行)。
select * from user where age = 10 limit 100000,10;结果显示4.73s
那有什么解决方案呢?mysql就不能单表数据量超百万乃至千万嘛?答案是NO,显然是可以的。
SELECT a.* FROM USER a
INNER JOIN
(SELECT id FROM USER WHERE age = 10 LIMIT 100000,10) b
ON a.id = b.id;
结果0.53s
完美解决了查询效率问题!!!其中需要对where条件增加索引,id因为是主键自带索引。select返回减少回表可以提升查询性能,所以采用查询主键字段后进行关联大幅度提升了查询效率。
PageHelper想要优化需要在拦截器的拼接SQL部分进行重构,由于博主能力有限暂未实现。能力较强的读者可以自己进行重构
附上PageHelper的git地址:https://github.com/pagehelper/Mybatis-PageHelper/
浅谈PageHelper插件分页实现原理及大数据量下SQL查询效率问题解决的更多相关文章
- mysql大数据量下的分页
mysql大数据量使用limit分页,随着页码的增大,查询效率越低下. 测试实验 1. 直接用limit start, count分页语句, 也是我程序中用的方法: select * from p ...
- 大数据量下,分页的解决办法,bubuko.com分享,快乐人生
大数据量,比如10万以上的数据,数据库在5G以上,单表5G以上等.大数据分页时需要考虑的问题更多. 比如信息表,单表数据100W以上. 分页如果在1秒以上,在页面上的体验将是很糟糕的. 优化思路: 1 ...
- Mysql优化-大数据量下的分页策略
一.前言 通常,我们分页时怎么实现呢? 1 SELECT * FROM table ORDER BY id LIMIT 1000, 10; 但是,数据量猛增以后呢? 1 SELECT * FROM t ...
- 任何抛开业务谈大数据量的sql优化都是瞎扯
周三去某在线旅游公司面试.被问到了一个关于数据量大的优化问题.问题是:一个主外键关联表,主表有一百万数据,外键关联表有一千万的数据,要求做一个连接. 本人接触过单表数据量最大的就是将近两亿行历史数据( ...
- [转]Sql server 大数据量分页存储过程效率测试附代码
本文转自:http://www.cnblogs.com/lli0077/archive/2008/09/03/1282862.html 在项目中,我们经常遇到或用到分页,那么在大数据量(百万级以上)下 ...
- 浅谈dedecms模板引擎工作原理及其自定义标签
浅谈dedecms模板引擎工作原理: 理解织梦模板引擎有什么意思? 可以更好地自定义标签.更多在于了解织梦系统,理解模板引擎是理解织梦工作原理的第一步. 理解织梦会使我们写PHP代码是更顺手,同时能学 ...
- 【ASP.NET MVC系列】浅谈ASP.NET MVC 视图与控制器传递数据
ASP.NET MVC系列文章 [01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作 ...
- SQL优化-大数据量分页优化
百万数据量SQL,在进行分页查询时会出现性能问题,例如我们使用PageHelper时,由于分页查询时,PageHelper会拦截查询的语句会进行两个步骤 1.添加 select count(*)fro ...
- MySQL大数据量分页性能优化
mysql大数据量使用limit分页,随着页码的增大,查询效率越低下. 测试实验 1. 直接用limit start, count分页语句, 也是我程序中用的方法: select * from p ...
随机推荐
- 图像分析函数:skimage.measure中的label、regionprops
算法解释详细,有算法执行过程动态GIF图的:https://blog.csdn.net/icvpr/article/details/10259577 算法文字解释的简介易懂的:https://www. ...
- Java调用windows命令
JAVA调用windows的cmd命令 用起来会让程序变得更加简洁明了,非常实用. 核心就是使用 Runtime类. cmd的xcopy就有很强大的文件夹,文件处理功能. 下面就以xcopy来说明,如 ...
- Git与SVN交叉使用
将本地git项目添加到远程svn中 git svn [svnprojpath] svnprojpath为原创svn项目路径 -- 文件夹路径,你要放到哪个文件夹 官方文档中带有-s参数,但我这边加了会 ...
- Python基础——6面向对象编程
类和实例 类是抽象的模版,例如汽车:而实例则是拥有相同方法的类的实现,例如汽车里面有大众.宝马.奔驰等等,这些车都能在地面上跑,但是它们的具体数据可以不一样. calss Student(object ...
- log4j控制指定包下的日志
最近观察日志发现如下两个问题: 1.项目用的是springboot项目,整合了rabbitmq,项目启动后,会自动监控rabbitmq谅解是否正常,导致控制台一直输出监控日志,此时就想阻止该类日志输出 ...
- python基础语法、数据结构、字符编码、文件处理 练习题
考试范围 '''1.python入门:编程语言相关概念2.python基础语法:变量.运算符.流程控制3.数据结构:数字.字符串.列表.元组.字典.集合4.字符编码5.文件处理''' 考试内容 1.简 ...
- JS&Java实现常见算法面试题
Github上的算法repo地址:https://github.com/qcer/Algo-Practice (如果你觉得有帮助,可以给颗星星收藏之~~~) 一.Java实现部分 参见随笔分类的算法部 ...
- myBatista批量查询和插入
<select id="queryCompanyByDistrict" resultType="WyCompany"> SELECT * FROM ...
- [转帖]Sqlcmd使用详解
Sqlcmd使用详解 2018年09月17日 13:36:39 吥輕誩放棄 阅读数:3053 版权声明:版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.n ...
- 简单实现计算机上多个jdk环境切换
实现多个jdk环境切换,大致有两种方式 安装两个jdk,并配置相应的环境变量,在java的控制面板中修改设置 非主要的jdk仅仅是用来测试,并不常用,故只要让ide配置对应的jdk位置就可以了,属于懒 ...