ORM中的N+1问题
在orm中有一个经典的问题,那就是N+1问题,比如hibernate就有这个问题,这一般都是不可避免的。
【N+1问题是怎么出现的】
N+1一般出现在一对多查询中,下面以Group和User为例,Group和User是一对多的关系。
在sql中如果我们要查询user表中的字段,并需要让每个user都有group表中的信息,也就是多对一查询,
我们可以用如下sql:
select u.*,g.* from user as u left join group as g on u.group_id = g.id;
这样查询出来的user表是附带了group信息的,也就是比如我要查询一个用户,包括他的所属组的信息都可以一条sql查询出来。
而如果反过来呢?我要用查询group,还需要保证group中有user的信息该怎么办呢?
在sql中我们确实可以用right join来解决:
select g.*,u.* from group as g right join user as u on g.id=u.group_id;
这样查询出来的结果表是group的字段在表中是重复的,只有后面拼接的user信息是不一样的。在sql中这样做确实能满足需求,但是在orm中却不能做到。
在orm中,需要对象和数据库对应起来,所以上面的关系在pojo类中大概是这个样子的:
01 |
public class Group{ |
02 |
private List<User> users; |
03 |
04 |
//set |
05 |
//get |
06 |
} |
07 |
08 |
public class User{ |
09 |
private Group group; |
10 |
|
11 |
//set |
12 |
//get |
13 |
} |
如果是开头说的多对一,则问题好解决,咱们orm生成查询语句的时候
select t0.id,t0.name,t0.age,t1.id,t1.group_name from user as t0 left join group as t1......
类似这样,orm会为每个字段和表名生成别名,这样在进行结果包装的时候只需要t0的字段set到user中,将t1的字段set到group中,最后将group对象set到user中,此时就完成包装了,这样查询出的user对象就可以用user.getGroup().getGroupName()来取值了。
如果是一对多,如果使用right join则group信息是重复的,没办法组装一个Group对象以及一个List<User>对象
从而也没办法把List<User> set到Group对象中,也就没法儿组装结果集了。
所以orm一对多查询只能是先将主表Group查询出来,然后将每个group对应的user对象查询出来,伪代码如下:
1、select * from group;--->组装成List<Group>结合
2、for(Group group:groups){
select * from user where group_id = ?(group.getId()) --->组装成List<User> users;
group.setUsers(users)
}
这样就完成一对多的结果组装了。
可以发现如果我们查询出的group有100行数据,那么我们执行的sql语句是1+100条,100条就是循环中执行的。
这样就出现了N+1问题,严重影响了性能,要知道不断得去数据库提交sql请求是很耗性能的,N+1问题并不是只有hibernate有,而是所有orm都会遇到这个问题,只是各自有各自的解决办法提高性能,然后要从根本上解决这个问题是不可能的。
所以使用了orm的情况下要尽量少使用一对多,如果使用的多对一查询,则需要使用左外连接查询,比如hibernate中有fetch="join"可以设置,默认是fetch="select",为什么不是默认前者呢?这是因为hibernate还有懒加载机制,如果fetch="join"的话就不是懒加载了,不管怎样都会即时加载。
hibernate没怎么用过,但原理是这样的。
【怎么解决N+1问题】
上面说了,N+1问题是orm无法避免的问题,所以是无法根治的,只能优化,提高性能。
拿hibernate来说,我们可以关闭一对多的级联抓取,也就是每次都只把Group查询出来,然后循环List<Group>
在使用hibernate的懒加载去查询每个Group对象对应的List<User>属性,这样当没有用到某个group对象的getUsers()方法时是不会去执行查询的。
再者就是使用二级缓存,虽然第一次查询还是N+1,但是以后查询就会变得很快了,因为结果集是直接从缓存中去取的。
【我们的解决办法】
公司正在做自己的orm,我们也有一种解决方案。
上面不是说了吗,查询出group对象的时候需要遍历group,也就一对多在查询出一的一边的时候需要遍历一的结果集去查询多的一遍,
在查询多的一边的时候生成的sql语句是:
1 |
for ( int i= 0 ;i<groups.size();i++){ |
2 |
select * from user where group_id = groups.get(i).getId(); //伪代码 |
3 |
} |
如果需要循环一百次,我们是不是可以想办法让它只需要循环十次呢?也就是只提交11条sql查询
当然。
1 |
select * from user where group_id = 1 or group_id = 2 or group_id= 3 .....or group_id = 10 ; //伪代码 |
这样只需要对groups进行10次循环就可以查询出所有结果,而且在数据库中or是可以使用索引的,所以性能肯定会高,只要不or多了就行。
一直没有完全理解N+1,今天有机会学习了下,欢迎前辈继续深入赐教。