典型的N+1查询问题:当需要查询主实体及其关联子实体时,传统做法是先查询主实体,再循环查询每个主实体对应的子实体,导致数据库查询次数为 1(主查询) + N(子查询),性能较差。
优化方案示例
以下是一个完整示例,包括表结构、实体类、Mapper接口和优化后的查询代码:
1. MySQL表结构
-- 主表:订单
CREATE TABLE `orders` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(50) NOT NULL COMMENT '订单号',
`total_amount` decimal(10,2) DEFAULT NULL COMMENT '总金额',
PRIMARY KEY (`id`)
);
-- 子表:订单详情
CREATE TABLE `order_items` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_id` bigint NOT NULL COMMENT '订单ID',
`product_name` varchar(100) DEFAULT NULL COMMENT '商品名称',
`quantity` int DEFAULT NULL COMMENT '数量',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`)
);
2. 实体类
// 订单实体
public class Order {
private Long id;
private String orderNo;
private BigDecimal totalAmount;
private List<OrderItem> items; // 订单详情列表
// getters/setters
}
// 订单详情实体
public class OrderItem {
private Long id;
private Long orderId;
private String productName;
private Integer quantity;
// getters/setters
}
3. MyBatis Mapper接口
public interface OrderMapper {
// 查询单个订单
Order selectOrderById(Long id);
// 批量查询订单详情
List<OrderItem> selectItemsByOrderIds(@Param("orderIds") List<Long> orderIds);
}
4. MyBatis XML映射文件
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<select id="selectOrderById" resultType="com.example.entity.Order">
SELECT * FROM orders WHERE id = #{id}
</select>
<select id="selectItemsByOrderIds" resultType="com.example.entity.OrderItem">
SELECT * FROM order_items
WHERE order_id IN
<foreach collection="orderIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper>
5. 优化后的Service代码
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public Order getOrderWithItems(Long orderId) {
// 1. 查询主订单
Order order = orderMapper.selectOrderById(orderId);
if (order == null) {
return null;
}
// 2. 批量查询订单详情(关键优化点)
List<OrderItem> items = orderMapper.selectItemsByOrderIds(Collections.singletonList(orderId));
// 3. 关联数据
order.setItems(items);
return order;
}
}
优化前后对比
优化前(N+1查询):
// 传统N+1查询方式(不推荐)
public Order getOrderWithItemsBad(Long orderId) {
Order order = orderMapper.selectOrderById(orderId);
if (order != null) {
// 循环查询每个订单的详情(导致N次查询)
List<OrderItem> items = new ArrayList<>();
for (Long itemId : order.getItemIds()) {
OrderItem item = itemMapper.selectById(itemId);
items.add(item);
}
order.setItems(items);
}
return order;
}
优化后(批量查询):
// 优化后的批量查询方式
public Order getOrderWithItems(Long orderId) {
Order order = orderMapper.selectOrderById(orderId);
if (order != null) {
// 批量查询所有详情(只需1次查询)
List<OrderItem> items = itemMapper.selectByOrderIds(
Collections.singletonList(orderId));
order.setItems(items);
}
return order;
}
核心优化思路
-
减少数据库交互次数:从多次查询变为两次查询(一次主查询 + 一次批量子查询)
-
利用集合操作替代循环查询:
- 先查询主实体
- 提取所有关联ID
- 使用
IN
语句一次性查询所有关联子实体 - 通过Map分组快速关联数据
-
MyBatis关键配置:
- 使用
<foreach>
标签处理批量查询 - 通过
@Param
注解传递参数列表 - 确保子表有合适的索引(如示例中的
idx_order_id
)
- 使用
这种优化方案在数据量越大时效果越明显,尤其适合一对多关联查询场景。