嵌套查询与 N+1 问题
嵌套查询通过 select 属性触发子查询获取关联数据,但不当使用会导致 N+1 问题,严重影响性能。
嵌套查询原理
XML
<resultMap id="blogResultMap" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="author"
column="author_id"
javaType="Author"
select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResultMap">
SELECT * FROM blog WHERE id = #{id}
</select>
<select id="selectAuthor" resultType="Author">
SELECT * FROM author WHERE id = #{id}
</select>
执行流程:
- 执行
selectBlog查询博客信息。 - 读取
author_id列值。 - 将
author_id传入selectAuthor,查询作者信息。
N+1 问题
当查询返回 N 条主记录时,每条记录的关联属性都会触发一次子查询,导致 1 + N 次数据库访问:
Java
// 1 次查询获取 100 篇博客
List<Blog> blogs = mapper.selectBlogList();
// 每篇博客触发 1 次作者查询,共 100 次
for (Blog blog : blogs) {
System.out.println(blog.getAuthor().getName());
}
// 总计:1 + 100 = 101 次查询
N+1 问题在使用子查询方式(select 属性)且访问关联属性时自动触发。
优化方案一:使用 Join
用一条 JOIN 替代 N+1 次查询:
XML
<resultMap id="blogAuthorResult" type="Blog">
<id property="id" column="blog_id"/>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="name" column="author_name"/>
</association>
</resultMap>
<select id="selectBlogsWithAuthors" resultMap="blogAuthorResult">
SELECT b.id blog_id, b.title blog_title,
a.id author_id, a.name author_name
FROM blog b
LEFT JOIN author a ON b.author_id = a.id
</select>
Join 方式将 N+1 次查询降为 1 次,是最直接的优化方案。
优化方案二:延迟加载(懒加载)
不访问关联属性时不触发子查询:
XML
<!-- mybatis-config.xml -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
XML
<resultMap id="blogResultMap" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="author"
column="author_id"
javaType="Author"
select="selectAuthor"
fetchType="lazy"/>
</resultMap>
| fetchType 值 | 行为 |
|---|---|
lazy | 延迟加载,访问属性时才查询 |
eager | 立即加载,查询主数据时同时查询 |
aggressiveLazyLoading=false时仅访问关联属性才加载;若为true,调用任意方法都会触发全部关联加载。
优化方案三:批量查询
MyBatis 3.5+ 支持 multi fetchType,批量收集关联 ID 后一次性 IN 查询:
XML
<association property="author"
column="author_id"
javaType="Author"
select="selectAuthor"
fetchType="lazy"/>
XML
<!-- mybatis-config.xml -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
三种方案对比
| 方案 | SQL 次数 | 适用场景 | 缺点 |
|---|---|---|---|
| Join | 1 次 | 数据量小,关联简单 | 列名冲突需别名 |
| 懒加载 | 按需 | 关联数据使用不确定 | 需合理配置触发策略 |
| 批量查询 | 2 次 | N 较大且 IN 查询高效 | 需 IN 查询支持 |
嵌套查询传递多列
关联查询需要多列时,使用 {} 语法:
XML
<collection property="comments"
column="{postId=id, status=state}"
ofType="Comment"
select="selectComments"/>
<select id="selectComments" resultType="Comment">
SELECT * FROM comments
WHERE post_id = #{postId} AND status = #{status}
</select>
要点总结
- 嵌套查询通过 select 属性触发子查询,存在 N+1 风险。
- N+1 问题:1 次主查询 + N 次关联查询,性能严重下降。
- Join 优化:一次查询完成,适合数据量小的场景。
- 懒加载:按需加载,通过 fetchType="lazy" 配置。
- 批量查询:收集关联 ID 后 IN 查询,减少 SQL 次数。
- 传递多列参数使用
{key1=col1, key2=col2}语法。
📝 发现内容有误?点击此处直接编辑