Redis N+1问题

懂 MyBatis 的插件机制(Interceptor),可以自定义分页插件、数据脱敏插件,而不是用第三方插件踩坑;
懂 ResultMap 的映射原理,能避免 “N+1 查询”(比如合理用association/collection的懒加载、嵌套查询),而不是让查询性能指数级下降。

 

上面“N+1 查询”是 ORM 框架(如 MyBatis、Hibernate)中常见的性能问题,尤其是在处理一对多或多对多关联查询时容易发生。

一、什么是“N+1 查询”?

简单定义: 先执行 1 次查询获取主对象列表(N 条记录),然后对每条记录再执行一次查询来获取关联的子对象。总共执行 1 + N​ 次数据库查询。

二、例子说明(以 MyBatis 为例)

假设有两个表:

  • user用户表(id, name)
  • order订单表(id, user_id, amount)

场景:查询所有用户及其所有订单。 错误写法(N+1 问题)

 
<!-- 1. 先查所有用户(1次查询,返回N条用户记录) -->
<select id="selectUsers" resultMap="userMap">
  SELECT id, name FROM user
</select>

<resultMap id="userMap" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <!-- 2. 对每个用户,执行下面的查询获取订单(N次查询!) -->
  <collection property="orders" column="id" 
              select="selectOrdersByUserId"/>
</resultMap>

<select id="selectOrdersByUserId" resultType="Order">
  SELECT id, amount FROM order WHERE user_id = #{userId}
</select>

 

执行过程

  1. 执行 SELECT id, name FROM user→ 返回 10 个用户
  2. 对第一个用户:SELECT * FROM order WHERE user_id = 1
  3. 对第二个用户:SELECT * FROM order WHERE user_id = 2
  4. ...
  5. 对第十个用户:SELECT * FROM order WHERE user_id = 10

总计:1(主查询) + 10(关联查询)= 11 次数据库查询

三、如何避免 N+1 问题?

方案1:使用 JOIN 查询(推荐)

 
<select id="selectUsersWithOrders" resultMap="userMap">
  SELECT 
    u.id, u.name,
    o.id as order_id, o.amount
  FROM user u
  LEFT JOIN order o ON u.id = o.user_id
</select>

<resultMap id="userMap" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <collection property="orders" ofType="Order">
    <id property="id" column="order_id"/>
    <result property="amount" column="amount"/>
  </collection>
</resultMap>

 

只需 1 次查询,通过 JOIN 一次性获取所有数据。

方案2:使用 MyBatis 的 @FetchType.LAZY+ 批量加载

 
<collection property="orders" column="id" 
            select="selectOrdersByUserId"
            fetchType="lazy"/> <!-- 延迟加载 -->

 

配合批量加载配置(如 MyBatis 的 lazyLoadTriggerMethods或 Hibernate 的 @BatchSize),可以在访问关联对象时批量查询而不是逐条查询。

四、性能对比

假设有 1000 个用户:

  • N+1 查询:1 + 1000 = 1001 次数据库往返
  • JOIN 查询:1 次数据库往返

网络延迟放大效应:如果每次查询耗时 10ms,N+1 需要 10 秒,JOIN 只需 10ms(假设 JOIN 本身不慢)。

五、要点

当问“如何避免 N+1 查询”时,可以这样回答:

  1. 识别场景:一对多/多对多关联查询时容易发生
  2. 解决方案
    • 使用 JOIN 查询替代多次单表查询
    • 合理使用 懒加载​ + 批量加载fetch="lazy"+ @BatchSize
    • 在 MyBatis 中,用 <collection>resultMap属性而不是 select属性
  3. 权衡考虑
    • JOIN 可能导致结果集冗余(用户信息重复),需在应用层去重
    • 数据量极大时,JOIN 可能慢,可考虑分两次查询但用 IN语句批量查
  4. 监控方法:通过 MyBatis 日志查看实际执行的 SQL 条数

 

posted @ 2025-12-17 14:25  野鹤闲人  阅读(3)  评论(0)    收藏  举报