前因:项目一直使用的是PageHelper实现分页功能,项目前期数据量较少一直没有什么问题。随着业务扩增,数据库扩增PageHelper出现了明显的性能问题。几十万甚至上百万的单表数据查询性能缓慢,需要几秒乃至十几秒的查询时间。故此特地研究了一下PageHelper源码,查找PageHelper分页的实现方式。

一段较为简单的查询,跟随debug开始源码探寻之旅。


  1. public ResultContent select(Integer id) {
  2. Page<Test> blogPage = PageHelper.startPage(1,3).doSelectPage( () -> testDao.select(id));
  3. List<Test> test = (List<Test>)blogPage.getResult();
  4. return new ResultContent(0, "success", test);
  5. }

主要保存由前端传入的pageNum(页数)、pageSize(每页显示数量)和count(是否进行count(0)查询)信息。

这里是简单的创建page并保存当前线程的变量副本心里,不做深究。


  1. public static <E> Page<E> startPage(int pageNum, int pageSize) {
  2. return startPage(pageNum, pageSize, DEFAULT_COUNT);
  3. }
  4. public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
  5. return startPage(pageNum, pageSize, count, (Boolean)null, (Boolean)null);
  6. }
  7. public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {
  8. Page<E> page = startPage(pageNum, pageSize);
  9. page.setOrderBy(orderBy);
  10. return page;
  11. }
  12. public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  13. Page<E> page = new Page(pageNum, pageSize, count);
  14. page.setReasonable(reasonable);
  15. page.setPageSizeZero(pageSizeZero);
  16. Page<E> oldPage = getLocalPage();
  17. if(oldPage != null && oldPage.isOrderByOnly()) {
  18. page.setOrderBy(oldPage.getOrderBy());
  19. }
  20. setLocalPage(page);
  21. return page;
  22. }

开始执行真正的select语句


  1. public <E> Page<E> doSelectPage(ISelect select) {
  2. select.doSelect();
  3. return this;
  4. }

进入MapperProxy类执行invoke方法获取到方法名称及参数值


  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. if (Object.class.equals(method.getDeclaringClass())) {
  3. try {
  4. return method.invoke(this, args);
  5. } catch (Throwable t) {
  6. throw ExceptionUtil.unwrapThrowable(t);
  7. }
  8. }
  9. final MapperMethod mapperMethod = cachedMapperMethod(method);
  10. return mapperMethod.execute(sqlSession, args);
  11. }

接着是MapperMethod方法执行execute语句,判断是增、删、改、查。判断返回值是多个,进入executeForMany方法


  1. public Object execute(SqlSession sqlSession, Object[] args) {
  2. Object result;
  3. if (SqlCommandType.INSERT == command.getType()) {
  4. Object param = method.convertArgsToSqlCommandParam(args);
  5. result = rowCountResult(sqlSession.insert(command.getName(), param));
  6. } else if (SqlCommandType.UPDATE == command.getType()) {
  7. Object param = method.convertArgsToSqlCommandParam(args);
  8. result = rowCountResult(sqlSession.update(command.getName(), param));
  9. } else if (SqlCommandType.DELETE == command.getType()) {
  10. Object param = method.convertArgsToSqlCommandParam(args);
  11. result = rowCountResult(sqlSession.delete(command.getName(), param));
  12. } else if (SqlCommandType.SELECT == command.getType()) {
  13. if (method.returnsVoid() && method.hasResultHandler()) {
  14. executeWithResultHandler(sqlSession, args);
  15. result = null;
  16. } else if (method.returnsMany()) {
  17. result = executeForMany(sqlSession, args);
  18. } else if (method.returnsMap()) {
  19. result = executeForMap(sqlSession, args);
  20. } else {
  21. Object param = method.convertArgsToSqlCommandParam(args);
  22. result = sqlSession.selectOne(command.getName(), param);
  23. }
  24. } else if (SqlCommandType.FLUSH == command.getType()) {
  25. result = sqlSession.flushStatements();
  26. } else {
  27. throw new BindingException("Unknown execution method for: " + command.getName());
  28. }
  29. if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
  30. throw new BindingException("Mapper method '" + command.getName()
  31. + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  32. }
  33. return result;
  34. }

这个方法开始调用SqlSessionTemplate、DefaultSqlSession等类获取到Mapper.xml文件的SQL语句


  1. private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  2. List<E> result;
  3. Object param = method.convertArgsToSqlCommandParam(args);
  4. if (method.hasRowBounds()) {
  5. RowBounds rowBounds = method.extractRowBounds(args);
  6. result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
  7. } else {
  8. result = sqlSession.<E>selectList(command.getName(), param);
  9. }
  10. // issue #510 Collections & arrays support
  11. if (!method.getReturnType().isAssignableFrom(result.getClass())) {
  12. if (method.getReturnType().isArray()) {
  13. return convertToArray(result);
  14. } else {
  15. return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
  16. }
  17. }
  18. return result;
  19. }

开始进入PageHelper的真正实现,Plugin通过实现InvocationHandler进行动态代理获取到相关信息


  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. try {
  3. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
  4. if (methods != null && methods.contains(method)) {
  5. return interceptor.intercept(new Invocation(target, method, args));
  6. }
  7. return method.invoke(target, args);
  8. } catch (Exception e) {
  9. throw ExceptionUtil.unwrapThrowable(e);
  10. }
  11. }

PageInterceptor 实现Mybatis的Interceptor 接口,进行拦截


  1. public Object intercept(Invocation invocation) throws Throwable {
  2. try {
  3. Object[] args = invocation.getArgs();
  4. MappedStatement ms = (MappedStatement)args[0];
  5. Object parameter = args[1];
  6. RowBounds rowBounds = (RowBounds)args[2];
  7. ResultHandler resultHandler = (ResultHandler)args[3];
  8. Executor executor = (Executor)invocation.getTarget();
  9. CacheKey cacheKey;
  10. BoundSql boundSql;
  11. if(args.length == 4) {
  12. boundSql = ms.getBoundSql(parameter);
  13. cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
  14. } else {
  15. cacheKey = (CacheKey)args[4];
  16. boundSql = (BoundSql)args[5];
  17. }
  18. this.checkDialectExists();
  19. List resultList;
  20. if(!this.dialect.skip(ms, parameter, rowBounds)) {
  21. if(this.dialect.beforeCount(ms, parameter, rowBounds)) {
  22. Long count = this.count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  23. if(!this.dialect.afterCount(count.longValue(), parameter, rowBounds)) {
  24. Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
  25. return var12;
  26. }
  27. }
  28. resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
  29. } else {
  30. resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
  31. }
  32. Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
  33. return var16;
  34. } finally {
  35. this.dialect.afterAll();
  36. }
  37. }

转到ExecutorUtil抽象类的pageQuery方法


  1. public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException {
  2. if(!dialect.beforePage(ms, parameter, rowBounds)) {
  3. return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
  4. } else {
  5. parameter = dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
  6. String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
  7. BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
  8. Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
  9. Iterator var12 = additionalParameters.keySet().iterator();
  10. while(var12.hasNext()) {
  11. String key = (String)var12.next();
  12. pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
  13. }
  14. return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
  15. }
  16. }

在抽象类AbstractHelperDialect的getPageSql获取到对应的Page对象


  1. public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
  2. String sql = boundSql.getSql();
  3. Page page = this.getLocalPage();
  4. String orderBy = page.getOrderBy();
  5. if(StringUtil.isNotEmpty(orderBy)) {
  6. pageKey.update(orderBy);
  7. sql = OrderByParser.converToOrderBySql(sql, orderBy);
  8. }
  9. return page.isOrderByOnly()?sql:this.getPageSql(sql, page, pageKey);
  10. }

进入到MySqlDialect类的getPageSql方法进行SQL封装,根据page对象信息增加Limit。分页的信息就是这么拼装起来的


  1. public String getPageSql(String sql, Page page, CacheKey pageKey) {
  2. StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
  3. sqlBuilder.append(sql);
  4. if(page.getStartRow() == 0) {
  5. sqlBuilder.append(" LIMIT ? ");
  6. } else {
  7. sqlBuilder.append(" LIMIT ?, ? ");
  8. }
  9. return sqlBuilder.toString();
  10. }

将最后拼装好的SQL返回给DefaultSqlSession执行查询并返回


  1. public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  2. try {
  3. MappedStatement ms = configuration.getMappedStatement(statement);
  4. return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  5. } catch (Exception e) {
  6. throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
  7. } finally {
  8. ErrorContext.instance().reset();
  9. }

至此整个查询过程完成,原来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查询效率问题解决的更多相关文章

  1. mysql大数据量下的分页

    mysql大数据量使用limit分页,随着页码的增大,查询效率越低下. 测试实验 1.   直接用limit start, count分页语句, 也是我程序中用的方法: select * from p ...

  2. 大数据量下,分页的解决办法,bubuko.com分享,快乐人生

    大数据量,比如10万以上的数据,数据库在5G以上,单表5G以上等.大数据分页时需要考虑的问题更多. 比如信息表,单表数据100W以上. 分页如果在1秒以上,在页面上的体验将是很糟糕的. 优化思路: 1 ...

  3. Mysql优化-大数据量下的分页策略

    一.前言 通常,我们分页时怎么实现呢? 1 SELECT * FROM table ORDER BY id LIMIT 1000, 10; 但是,数据量猛增以后呢? 1 SELECT * FROM t ...

  4. 任何抛开业务谈大数据量的sql优化都是瞎扯

    周三去某在线旅游公司面试.被问到了一个关于数据量大的优化问题.问题是:一个主外键关联表,主表有一百万数据,外键关联表有一千万的数据,要求做一个连接. 本人接触过单表数据量最大的就是将近两亿行历史数据( ...

  5. [转]Sql server 大数据量分页存储过程效率测试附代码

    本文转自:http://www.cnblogs.com/lli0077/archive/2008/09/03/1282862.html 在项目中,我们经常遇到或用到分页,那么在大数据量(百万级以上)下 ...

  6. 浅谈dedecms模板引擎工作原理及其自定义标签

    浅谈dedecms模板引擎工作原理: 理解织梦模板引擎有什么意思? 可以更好地自定义标签.更多在于了解织梦系统,理解模板引擎是理解织梦工作原理的第一步. 理解织梦会使我们写PHP代码是更顺手,同时能学 ...

  7. 【ASP.NET MVC系列】浅谈ASP.NET MVC 视图与控制器传递数据

    ASP.NET MVC系列文章 [01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作 ...

  8. SQL优化-大数据量分页优化

    百万数据量SQL,在进行分页查询时会出现性能问题,例如我们使用PageHelper时,由于分页查询时,PageHelper会拦截查询的语句会进行两个步骤 1.添加 select count(*)fro ...

  9. MySQL大数据量分页性能优化

    mysql大数据量使用limit分页,随着页码的增大,查询效率越低下. 测试实验 1.   直接用limit start, count分页语句, 也是我程序中用的方法: select * from p ...

随机推荐

  1. poi包冲突问题(excel)

    1. 所需jar包 涉及的poi (1)poi-3.14.jar  (HSSF) 依赖:commons-logging-1.2.jar.log4j-1.2.17.jar.commons-codec.1 ...

  2. SQLServer之创建唯一聚集索引

    创建唯一聚集索引典型实现 唯一索引可通过以下方式实现: PRIMARY KEY 或 UNIQUE 约束 在创建 PRIMARY KEY 约束时,如果不存在该表的聚集索引且未指定唯一非聚集索引,则将自动 ...

  3. 基于SpringMVC拦截器和注解实现controller中访问权限控制

    SpringMVC的拦截器HandlerInterceptorAdapter对应提供了三个preHandle,postHandle,afterCompletion方法. preHandle在业务处理器 ...

  4. Redis常用数据结构

    Redis常用数据结构包括字符串(strings),列表(lists),哈希(hashes),集合(sets),有序集合(sorted sets). redis的key最大不能超过512M,可通过re ...

  5. Nero8刻录引导系统光盘镜像图文教程

    刻录可引导的Windows系统光盘一直是电脑使用者较为需要的,今天,倡萌抽空写了这篇图文教程,希望对于菜鸟级的朋友有所帮助,大虾请飘过.本教程以最为强大的刻录软件Nero 8做为工具(其他版本的Ner ...

  6. kubernetes1.14.0部署

    2019/4/6/使用kubeadm安装部署kubernetes集群: 前提:1.各节点时间同步:2.各节点主机名称解析:dns OR hosts:3.各节点iptables及firewalld服务被 ...

  7. 06-JavaScript的流控制语句

    06-JavaScript的流控制语句 JavaScript的流控制语句主要分为三大类: 顺序控制:因为JS是一门解释性语言,所以从上至下按顺序依次执行 分支控制:主要分为if条件语句和swith开关 ...

  8. OracleSql语句学习(五)

    --数据库对象数据库对象包含:表,视图,索引,序列视图VIEN视图在SQL语句中体现的角色与表一样,但是视图并非真实存在的表,它只是对应一条查询语句的结果集 使用视图通常是为了重用子查询,简化SQL语 ...

  9. lombok的简单使用小结

    1.idea安装lombok插件 关于lombok如何在idea中使用,下面这篇博客写的很到位,并且提供了本地安装对应idea版本的lombok插件的地址.如果无法通过idea直接安装lombok,可 ...

  10. spring boot 表单验证

    1 设置某个字段的取值范围 1.1 取值范围验证:@Min,@Max ① 实例类的属性添加注解@Min ② Controller中传入参数使用@Valid注解 1.2 不能为空验证:@NotNull ...