什么是N+1查询?

 

在Session的缓存中存放的是相互关联的对象图。默认情况下,当Hibernate从数据库中加载Customer对象时,会同时加载所有关联的Order对象。以Customer和Order类为例,假定ORDERS表的CUSTOMER_ID外键允许为null,图1列出了CUSTOMERS表和ORDERS表中的记录。

 

n+1查询

以下Session的find()方法用于到数据库中检索所有的Customer对象:

List customerLists=session.find("from Customer as c");

运行以上find()方法时,Hibernate将先查询CUSTOMERS表中所有的记录,然后根据每条记录的ID,到ORDERS表中查询有参照关系的记录,Hibernate将依次执行以下select语句:

select * from CUSTOMERS;
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;

通过以上5条select语句,Hibernate最后加载了4个Customer对象和5个Order对象,在内存中形成了一幅关联的对象图,参见图2。

n+1查询

Hibernate在检索与Customer关联的Order对象时,使用了默认的立即检索策略。这种检索策略存在两大不足:

(1) select语句的数目太多,需要频繁的访问数据库,会影响检索性能。如果需要查询n个Customer对象,那么必须执行n+1次select查询语句。这就是经典的n+1次select查询问题。 这种检索策略没有利用SQL的连接查询功能,例如以上5条select语句完全可以通过以下1条select语句来完成:

select * from CUSTOMERS left outer join ORDERS
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID

以上select语句使用了SQL的左外连接查询功能,能够在一条select语句中查询出CUSTOMERS表的所有记录,以及匹配的ORDERS表的记录。

(2)在应用逻辑只需要访问Customer对象,而不需要访问Order对象的场合,加载Order对象完全是多余的操作,这些多余的Order对象白白浪费了许多内存空间。
为了解决以上问题,Hibernate提供了其他两种检索策略:延迟检索策略和迫切左外连接检索策略。延迟检索策略能避免多余加载应用程序不需要访问的关联对象,迫切左外连接检索策略则充分利用了SQL的外连接查询功能,能够减少select语句的数目。

 

 

附一篇 hibernate的解决n+1的方法

 

    Hibernate的检索策略包括类级别检索策略和关联级别检索策略。

    类级别检索策略有立即检索和延迟检索,默认的检索策略是立即检索。在Hibernate映射文件中,通过在<class>上配置 lazy属性来确定检索策略。对于Session的检索方式,类级别检索策略仅适用于load方法;也就说,对于get、qurey检索,持久化对象都会被立即加载而不管lazy是false还是true.一般来说,我们检索对象就是要访问它,因此立即检索是通常的选择。由于load方法在检索不到对象时会抛出异常(立即检索的情况下),因此我个人并不建议使用load检索;而由于<class>中的lazy属性还影响到多对一及一对一的检索策略,因此使用load方法就更没必要了。

   关联级别检索策略有立即检索、延迟检索和迫切左外连接检索。对于关联级别检索,又可分为一对多和多对多、多对一和一对一两种情况讨论。

   1)立即检索:这是一对多默认的检索策略,此时lazy=false,outer-join=false.尽管这是默认的检索策略,但如果关联的集合是无用的,那么就不要使用这种检索方式。

  2)延迟检索:此时lazy=true,outer-join=false(outer-join=true是无意义的),这是优先考虑的检索方式。

  3)迫切左外连接检索:此时 lazy=false,outer-join=true,这种检索策略只适用于依靠id检索方式(load、get),而不适用于query的集合检索(它会采用立即检索策略)。相比于立即检索,这种检索策略减少了一条sql语句,但在Hibernate中,只能有一个<set>配置成 outer-join=true.

 

多对一和一对一检索策略一般使用<many-to-one>、<one-to-one>配置。<many- to-one>中需要配置的属性是 outer-join,同时还需要配置one端关联的<class>的lazy属性(配置的可不是<many-to-one>中的lazy哦),它们的组合后的检索策略如下:

 

1) outer-join=auto:这是默认值,如果lazy=true为延迟检索,如果lazy=false为迫切左外连接检索

 

2) outer-join=true,无关于lazy,都为迫切左外连接检索。

 

3) outer-join=false,如果lazy=true为延迟检索,否则为立即检索。

 

   可以看到,在默认的情况下(outer-join=auto,lazy=false),对关联的one端对象Hibernate采用的迫切左外连接检索。依我看,很多情况下,我们并不需要加载one端关联的对象(很可能我们需要的仅仅是关联对象的id);另外,如果关联对象也采用了迫切左外连接检索,就会出现select语句中有多个外连接表,如果个数多的话会影响检索性能,这也是为什么Hibernate通过 hibernate.max_fetch_depth属性来控制外连接的深度。对于迫切左外连接检索,query的集合检索并不适用,它会采用立即检索策略。

 

对于检索策略,需要根据实际情况进行选择。对于立即检索和延迟检索,它们的优点在于select语句简单(每张表一条语句)、查询速度快,缺点在于关联表时需要多条select语句,增加了访问数据库的频率。因此在选择即检索和延迟检索时,可以考虑使用批量检索策略来减少select语句的数量(配置batch-size属性)。对于切左外连接检索,优点在于select较少,但缺点是select语句的复杂度提高,多表之间的关联会是很耗时的操作。另外,配置文件是死的,但程序是活的,可以根据需要在程序里显示的指定检索策略(可能经常需要在程序中显示指定迫切左外连接检索)。为了清楚检索策略的配置效果如何,可以配置show_sql属性查看程序运行时Hibernate执行的sql语句。

 

 

<think>我们正在讨论N+1查询问题。根据用户提供的引用内容,特别是引用[1]和引用[3],我们可以了解到N+1问题在ORM(对象关系映射)框架中是一个常见问题。下面我将详细解释N+1问题及其解决方案。 ### 什么是N+1查询问题? N+1查询问题是指在使用ORM框架进行数据库查询时,由于对象关联关系导致额外执行大量SQL查询的性能问题。具体表现为: 1. **1次初始查询**:首先执行一次查询获取N个主对象(例如:查询10个客户)。 2. **N次额外查询**:然后对于每个主对象,再执行一次查询获取关联对象(例如:为每个客户查询其订单)。 因此总查询次数为:1(初始查询+ N(关联查询)= N+1查询。 #### 示例场景(引用[1]): 假设有两个表:`customers`(客户表)和`orders`(订单表),一个客户对应多个订单。 - 初始查询:`SELECT * FROM customers LIMIT 10`(获取10个客户) - 循环中查询:对每个客户执行`SELECT * FROM orders WHERE customer_id = ?`(共10次) 总查询次数:1 + 10 = 11次。 ### 为什么N+1问题有害? 1. **性能瓶颈**:当N较大时(如N=1000),数据库需执行1001查询,导致高延迟。 2. **数据库压力**:频繁的短查询消耗数据库连接资源,可能引发连接池耗尽。 --- ### 如何解决N+1查询问题? #### 方法1:**预加载(Eager Loading)**(核心方案) 一次性加载主对象及其关联对象,将N+1查询合并为1查询(或少量查询)。 - **JOIN查询**:通过SQL的`JOIN`语句一次性获取所有数据。 ```sql SELECT * FROM customers LEFT JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (1,2,...,10) ``` - **ORM实现**(以Hibernate为例,引用[3]): 在关联属性上配置`fetch="join"`: ```xml <set name="orders" fetch="join"> <!-- 一次加载所有关联订单 --> <key column="customer_id"/> <one-to-many class="Order"/> </set> ``` #### 方法2:**批量加载(Batch Loading)** 将N次关联查询合并为少量批量查询(如1查询获取所有关联数据)。 - **原理**:先执行初始查询获取主对象ID列表,再通过`IN`语句批量查询关联对象。 ```sql SELECT * FROM orders WHERE customer_id IN (1,2,...,10) -- 1查询代替10次 ``` - **ORM支持**: Hibernate中配置`batch-size`: ```xml <set name="orders" batch-size="20"> <!-- 每批加载20个客户的订单 --> <key column="customer_id"/> <one-to-many class="Order"/> </set> ``` #### 方法3:**延迟加载+手动触发(Lazy Loading with Hint)** 在需要时主动触发批量加载关联对象(避免在循环中逐条加载)。 - **代码示例**(伪代码): ```java List<Customer> customers = session.query("FROM Customer").list(); // 初始查询1次) List<Long> customerIds = customers.stream().map(Customer::getId).collect(Collectors.toList()); // 手动批量加载所有订单(1查询) List<Order> allOrders = session.query("FROM Order WHERE customer_id IN :ids") .setParameter("ids", customerIds) .list(); // 将订单关联到客户 customers.forEach(c -> c.setOrders( allOrders.stream().filter(o -> o.getCustomerId().equals(c.getId())).collect(Collectors.toList()) )); ``` --- ### 解决方案对比 | 方法 | 查询次数 | 适用场景 | |--------------------|----------|------------------------------| | 预加载(JOIN) | 1~2次 | 关联数据量较小 | | 批量加载(IN) | 2次 | 关联数据量大,避免JOIN膨胀 | | 延迟加载+手动触发 | 2次 | 需要精细控制加载时机 | > **注意**:JOIN方式可能导致结果集膨胀(一个客户有M个订单,则返回10*M行),需在ORM中处理重复数据。 --- ### 总结 - **N+1问题本质**:ORM框架的关联加载策略缺陷导致的性能问题。 - **核心解决思路**:将多次单条查询合并为批量查询。 - **最佳实践**: 1. 在开发阶段开启ORM的SQL日志,监控N+1问题。 2. 根据关联数据规模选择预加载或批量加载策略[^1][^3]。 §§相关问题§§ 1. 如何检测应用程序中的N+1查询问题? 2. 在MyBatis中如何避免N+1问题? 3. 使用JOIN预加载时如何解决结果集重复问题?</think>### 什么是N+1查询问题? N+1查询问题是ORM(对象关系映射)框架中常见的性能陷阱,指在获取主对象及其关联对象时,框架执行了**1次主查询 + N次关联查询**(N为主查询结果数量)。这种模式会导致大量低效的数据库查询,严重影响系统性能[^1]。 #### 典型场景示例(引用[1][^1]): ```python # 获取10个用户(1查询) users = User.objects.all()[:10] for user in users: # 循环中为每个用户查询订单(10次查询) orders = user.orders.all() ``` 总查询次数 = **1(主查询+ 10(关联查询) = 11次** --- ### 为什么会产生N+1问题? | 原因 | 说明 | |------|------| | **延迟加载(Lazy Loading)** | ORM默认按需加载关联数据,首次访问关联属性时触发查询 | | **缺乏批量加载机制** | 未在单次查询中预取所有关联数据 | | **ORM配置不当** | 未正确配置关联加载策略(如Hibernate的fetch设置) | --- ### 解决方案(附代码示例) #### 方案1:预加载(Eager Loading) - 最有效 **一次性加载所有关联数据**,将N+1查询合并为1-2次查询[^3]。 ```python # Django ORM(使用select_related/prefetch_related) users = User.objects.prefetch_related('orders')[:10] # 仅执行2次查询: # 1. SELECT * FROM users LIMIT 10 # 2. SELECT * FROM orders WHERE user_id IN (1,2,...,10) # Hibernate配置(引用[3][^3]) <set name="orders" fetch="join"> <!-- 强制JOIN加载 --> <key column="user_id"/> <one-to-many class="Order"/> </set> ``` #### 方案2:批量加载(Batch Loading) **分组执行IN查询**,将N次查询压缩为M次(M = N/批大小) ```java // Hibernate配置批量加载 @BatchSize(size=20) // 每批加载20个用户的订单 private Set<Order> orders; ``` 执行过程: ```sql SELECT * FROM users LIMIT 10; -- 初始查询 SELECT * FROM orders WHERE user_id IN (1,2,3...20); -- 仅1次批量查询 ``` #### 方案3:手动JOIN优化 **直接使用SQL JOIN**避免多次查询: ```sql SELECT users.*, orders.* FROM users LEFT JOIN orders ON users.id = orders.user_id WHERE users.id IN (1,2,...,10) ``` --- ### 各方案性能对比 | 方案 | 查询次数 | 适用场景 | 缺点 | |---------------|----------|---------------------------|--------------------| | 预加载 | 1-2 | 关联数据量适中 | 可能加载冗余数据 | | 批量加载 | 1+(N/M) | 大数据量关联 | 需配置批处理大小 | | 手动JOIN | 1 | 复杂查询/极致性能要求 | 失去ORM抽象优势 | | 原生N+1 | N+1 | **永远不要使用** | 性能灾难 | --- ### 最佳实践 1. **始终监控ORM日志**:检查实际生成的SQL语句 2. **关键路径强制预加载**:对性能敏感接口配置`fetch="join"` 3. **分页场景特别注意**:预加载时避免`SELECT *`(可能导致结果集膨胀) 4. **使用性能分析工具**: - Django Debug Toolbar - Hibernate Statistics > **黄金法则**:当循环内出现数据库查询时,立即考虑N+1风险[^1][^3]。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值