记一次JPA分页查询的SQL优化过程

2,478 阅读2分钟

涉及:

  • Spring Data JPA

  • MySQL


前言

由于公司的基础框架封装使用的是JPA,一开始的分页查询使用了JPA封装的findAll()方法实现,在单表查询以及数据量不大的情况下,这种方式的查询非常容易写,也利于后面的维护

以学生实体StudentEntity为例,写一个简单的分页查询,我只需要下面几个步骤:

//创建一个查询用的实体类,需要查询的实际值 set 进该实体类里
StudentEntity queryEntity = new Studentity();
queryEntity.setName("王");
queryEntity.setAge(10);
//创建一个 ExampleMatcher 对象
ExampleMatcher exampleMatcher = ExampleMatcher.matching()
                .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.startsWith())
                .withMatcher("age", ExampleMatcher.GenericPropertyMatchers.exact());
//查询分页结果
StudentDao.findAll(Example.of(queryEntity, exampleMatcher),PageRequest.of(0, 10, Sort.of("modiFyTime")));

还原生成的SQL语句

通过还原语句,发现JPA生成的语句是下面这样的

select name, age, ... from student where name like '王%', age = 10 order by modifyTime desc limit 10

当 Limit 特别大,到十万甚至百万数据时,会导致查询速度呈几何倍下降,将真实的数据导入后,第一页的查询耗时竟然超过了一秒,到万页时耗时竟然超过了40S,这是不能接受的,优化它成为了必然事件


JPA查询优化

由于 JPA 生成的语句不符合要求,于是我选择了原生 SQL 的方式,自定义原生语句进行查询

优化思路是这样的:

  • 语句是一定要优化的,生成的语句根本无法满足这么大数据量的分页查询需求
  • 要查看不同条件下是否都走了索引,联合索引的建立是否合理

第一点,我们强制让语句走索引,并且只查它的主键来提高查询速度

select id from student order by modifyTime desc limit 0, 10

此时语句的执行速度是非常快的,会从秒级提升到毫秒级,在实际业务中 pageSize 也不会特别大,接口也是有做限制的,在下一步,把这个语句的查询结果当做子查询,把语句变成下面这样

select * from student a, (select id from student order by modifyTime desc limit 0, 10) b where a.id = b.id

配合动态查询条件的联合索引,哪怕是百万级,千万级数据,其查询速度也能维持在毫秒级


优化后的 JPA DAO方法:

@Query(value = "select * from student a, (select id from student where name like contact(:name, '%') and age = :age order by modifyTime desc limit :pageNo, :pageSize) b where a.id = b.id", nativeQuery = true)
List<Student> findAll(String name, int age, int pageNo, int pageSize);

📖 使用JPA面临的另一个问题:

既然是分页,还得解决一个问题是如何把总数一起查出来,比如查询18岁的老王,第一页,查十条,需要返回给前端的还有一共有多少个老王

目前解决方案是把上面语句的查询字段从*改为count(id),进行两次查询,两次毫秒级的查询组合在一起,似乎也不是不能接受