涉及:
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),进行两次查询,两次毫秒级的查询组合在一起,似乎也不是不能接受