如何优雅的解决n+1查询!!!

我们在写代码的时候非常忌讳出现n+1次查询,这就意味的你的循环有多少次,就会查询多少次数据库,这是很恐怖的场景。

image

因为每次服务调用mysql查询的时候,都是一件很耗费性能的操作,下面我们举个例子,来说说n+1的触发场景及解决方案。

业务需求

需要查询指定用户的订单详细信息,详细信息不仅仅包含订单本身的信息,还包含其它信息。这个时候童鞋们往往会采用,如下所示的方式进行数据获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 /**
* 订单mapper
*/
private OrderMapper orderMapper;

/**
* 订单商品mapper
*/
private OrderFeeMapper orderFeeMapper;

/**
* 查询用户id指定的所有订单列表信息
* @param userId
* @return
*/
public List<OrderDetail> getOrderDetailList(int userId) {
// 查询订单列表数据
List<Order> orderList = orderMapper.getOrders(userId);
List<OrderDetail> orderDetailList = new ArrayList<>();
for (Order order : orderList) {
OrderDetail orderDetail =new OrderDetail();
// 查询订单对应费用信息
OrderFee orderFee = orderFeeMapper.getOrderFeeDetail(order.getOrderId());
orderDetail.setOrderFee(orderFee);
// 添加到集合中
orderDetailList.add(orderDetail);
}
return orderDetailList;
}

如果这个用户订单量少还好,一旦这个用户订单量超级大,这个操作的响应时间将会非常长,长到你无法忍受的地步,那我们要怎么进行优化呢?

n+1改为1+1模式

我们可以将n次查询的条件添加到一个集合中,然后通过in语句一次性查询出我们需要的数据,这样就可以避免n+1次查询的出现,可以大大提高我们的执行效率,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 订单mapper
*/
private OrderMapper orderMapper;

/**
* 订单商品mapper
*/
private OrderFeeMapper orderFeeMapper;

/**
* 查询用户id指定的所有订单列表信息
* @param userId
* @return
*/
public List<OrderDetail> getOrderDetailList(int userId) {
// 查询订单列表数据
List<Order> orderList = orderMapper.getOrders(userId);
List<OrderDetail> orderDetailList = new ArrayList<>();
List<String> orderIdList = new ArrayList<>();
for (Order order : orderList) {
OrderDetail orderDetail =new OrderDetail();
// 添加订单到集合中
orderIdList.add(order.getOrderId());
// 添加到集合中
orderDetailList.add(orderDetail);
}
List<OrderFee> orderFeeList = orderFeeMapper.getOrderFeeList(orderIdList);
// 递归将orderFeeList中费用信息设置到对应订单的orderDetail对象中(具体代码省略)
setOrderDetail(orderFeeList,orderDetailList);
return orderDetailList;
}

连接查询失效场景

童鞋们可能会问为啥不采用mysql连接查询,一下子将相关表数据一起查询出来。这边主要出于如下考虑:

笛卡儿积

连接查询其实就是笛卡尔积的应用,一张表的查询操作可能会很快,但是多张表联查就会非常慢,因为他们的数据量是n*m,所以有时候采用连接查询,还不如分成多次查询来的快。

分库分表

如果系统的数据库采用的是分库分表,这个时候有些表是不能够进行连接查询,我们只能分多次查询,然后组装到一起。

数据来源不一致

如果订单的数据是从第三方接口获取的,那我们自然没办法进行连表查询。

总结

我们写代码的时候一定要特别注意n+1查询出现,循环体内要多检查几遍,是否有子查询的出现。

后记

童鞋们要记住,每一种模式都存在一定的缺陷,数据量不一样,模式的执行效率天差地别。童鞋们有空的话可以思考如下问题:

  1. n+1模式修改为1+1模式需要注意哪些问题?
  2. mysql中in语句长度是否有限制(或者说sql长度是否有限制,如果有那是多少)?
  3. n+1中如果n的数值非常大,要如何优化(因为直接查询组装成in,查询效率也会很差)?
林老师带你学编程 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!