news 2026/3/31 22:49:10

MyBatis查询巨慢,排查发现是N+1问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis查询巨慢,排查发现是N+1问题

一个列表查询接口,20条数据要3秒。

查了半天,发现是MyBatis的N+1问题。

改了一行配置,从3秒优化到50毫秒。


问题现象

接口:查询订单列表,每个订单要显示用户名

实体类

@Data public class Order { private Long id; private Long userId; private String orderNo; private BigDecimal amount; private User user; // 关联用户 }

Mapper

<resultMap id="orderResultMap" type="Order"> <id column="id" property="id"/> <result column="user_id" property="userId"/> <result column="order_no" property="orderNo"/> <result column="amount" property="amount"/> <association property="user" column="user_id" select="selectUserById"/> </resultMap> <select id="selectOrderList" resultMap="orderResultMap"> SELECT * FROM orders WHERE status = 1 LIMIT 20 </select> <select id="selectUserById" resultType="User"> SELECT * FROM users WHERE id = #{userId} </select>

问题:查询20条订单,居然执行了21条SQL!


什么是N+1问题

第1条SQL:查询订单列表(返回20条) 第2条SQL:查询第1个订单的用户 第3条SQL:查询第2个订单的用户 ... 第21条SQL:查询第20个订单的用户

1次查询订单 + N次查询用户 = N+1次查询

每条SQL都有网络开销,20条订单就要21次数据库交互,当然慢。


如何发现N+1问题

方法一:开启SQL日志

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

看到一堆重复的SELECT * FROM users WHERE id = ?就是了。

方法二:用druid监控

spring: datasource: druid: stat-view-servlet: enabled: true

访问/druid看SQL执行次数。

方法三:Arthas监控

# 监控SQL执行 watch com.mysql.cj.jdbc.StatementImpl execute "{params,returnObj}" -x 2

解决方案

方案一:改用JOIN查询(推荐)

一条SQL搞定:

<resultMap id="orderResultMap" type="Order"> <id column="id" property="id"/> <result column="user_id" property="userId"/> <result column="order_no" property="orderNo"/> <result column="amount" property="amount"/> <association property="user" javaType="User"> <id column="user_id" property="id"/> <result column="user_name" property="name"/> </association> </resultMap> <select id="selectOrderList" resultMap="orderResultMap"> SELECT o.id, o.user_id, o.order_no, o.amount, u.name as user_name FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.status = 1 LIMIT 20 </select>

效果:1条SQL,50ms搞定。

方案二:开启懒加载 + 批量查询

如果不想改SQL,可以开启懒加载和批量查询:

mybatis: configuration: lazy-loading-enabled: true aggressive-lazy-loading: false default-executor-type: batch

但这个方案不如JOIN彻底。

方案三:手动批量查询

public List<Order> getOrderList() { // 1. 查询订单 List<Order> orders = orderMapper.selectOrderList(); // 2. 收集userId Set<Long> userIds = orders.stream() .map(Order::getUserId) .collect(Collectors.toSet()); // 3. 批量查询用户 List<User> users = userMapper.selectByIds(userIds); Map<Long, User> userMap = users.stream() .collect(Collectors.toMap(User::getId, u -> u)); // 4. 组装数据 orders.forEach(order -> { order.setUser(userMap.get(order.getUserId())); }); return orders; }

SQL

<select id="selectByIds" resultType="User"> SELECT * FROM users WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

效果:2条SQL,比N+1好很多。


不同方案对比

方案SQL数量复杂度适用场景
JOIN查询1简单关联
批量查询2复杂关联
懒加载N+1很少访问关联数据

collection也会有N+1

<!-- 查询用户及其订单列表 --> <resultMap id="userResultMap" type="User"> <id column="id" property="id"/> <collection property="orders" column="id" select="selectOrdersByUserId"/> </resultMap>

同样的问题:查10个用户,会执行11条SQL。

解决

<resultMap id="userResultMap" type="User"> <id column="id" property="id"/> <result column="name" property="name"/> <collection property="orders" ofType="Order"> <id column="order_id" property="id"/> <result column="order_no" property="orderNo"/> </collection> </resultMap> <select id="selectUserWithOrders" resultMap="userResultMap"> SELECT u.id, u.name, o.id as order_id, o.order_no FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.status = 1 </select>

MyBatis-Plus方案

如果用MyBatis-Plus,可以用@TableField注解:

@Data @TableName("orders") public class Order { private Long id; private Long userId; @TableField(exist = false) // 非数据库字段 private User user; }

然后在Service层手动组装:

public List<Order> getOrderList() { List<Order> orders = orderMapper.selectList(wrapper); // 批量查询用户 Set<Long> userIds = orders.stream() .map(Order::getUserId) .collect(Collectors.toSet()); Map<Long, User> userMap = userService.listByIds(userIds) .stream() .collect(Collectors.toMap(User::getId, u -> u)); orders.forEach(o -> o.setUser(userMap.get(o.getUserId()))); return orders; }

性能对比

测试数据:100条订单

方案SQL数量耗时
N+1(原始)1013200ms
JOIN查询145ms
批量查询260ms

提升:70倍!


远程排查经验

有次生产环境接口响应变慢,我在外面用星空组网连到公司内网,打开druid监控一看,一个接口执行了500多条SQL。

典型的N+1问题,改成JOIN查询立马解决。

远程能直接看监控、看日志,排查效率高很多。


总结

场景推荐方案
简单一对一关联JOIN查询
复杂多表关联批量查询
一对多关联JOIN或批量
很少用关联数据懒加载

避免N+1的原则

  1. 不要在resultMap里用select属性
  2. 关联查询优先用JOIN
  3. 必须分开查就用批量查询
  4. 开启SQL日志及时发现问题

一句话:看到association/collection里有select属性,基本就是N+1。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/19 7:59:27

实体行为分析5大模型横评:云端2小时完成,成本不到10块

实体行为分析5大模型横评&#xff1a;云端2小时完成&#xff0c;成本不到10块 引言&#xff1a;为什么企业需要标准化行为分析测试&#xff1f; 作为企业架构师&#xff0c;你是否遇到过这样的困扰&#xff1a;想对比不同AI安全模型的效果&#xff0c;却发现本地测试环境差异…

作者头像 李华
网站建设 2026/3/27 18:27:45

AI侦测服务体验报告:5家主流云GPU性价比大比拼

AI侦测服务体验报告&#xff1a;5家主流云GPU性价比大比拼 1. 为什么中小企业CTO需要关注AI安全能力 作为中小企业技术负责人&#xff0c;你可能已经意识到传统安全防护手段在应对新型网络威胁时的局限性。AI驱动的安全检测技术正在改变游戏规则&#xff0c;它能够通过持续学…

作者头像 李华
网站建设 2026/3/19 20:23:17

StructBERT模型优化:提升情感分析准确率方法

StructBERT模型优化&#xff1a;提升情感分析准确率方法 1. 中文情感分析的技术挑战与需求背景 在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;情感分析是理解用户情绪、挖掘舆情价值的核心任务之一。尤其在中文语境下&#xff0c;由于语言结构复杂、表达含蓄、…

作者头像 李华
网站建设 2026/3/22 22:58:23

AI学习路径优化:先云端体验,再决定是否买显卡

AI学习路径优化&#xff1a;先云端体验&#xff0c;再决定是否买显卡 1. 为什么建议先云端体验AI&#xff1f; 对于刚接触AI领域的小白用户来说&#xff0c;最大的困惑往往是&#xff1a;我是否需要投入上万元购买高端显卡&#xff1f;其实完全不必着急做决定。就像买车前会先…

作者头像 李华
网站建设 2026/3/24 3:53:39

1小时1块玩AI:2024最新云端GPU服务横评

1小时1块玩AI&#xff1a;2024最新云端GPU服务横评 引言&#xff1a;为什么你需要云端GPU服务&#xff1f; 作为一名技术博主&#xff0c;我深知测评各类AI工具和平台的痛点。自购测试设备不仅成本高昂&#xff08;一块RTX 4090显卡就要上万元&#xff09;&#xff0c;而且随…

作者头像 李华
网站建设 2026/3/27 6:15:27

中文情感分析API开发:StructBERT指南

中文情感分析API开发&#xff1a;StructBERT指南 1. 背景与需求&#xff1a;为什么需要中文情感分析&#xff1f; 在当今数字化时代&#xff0c;用户生成内容&#xff08;UGC&#xff09;如评论、弹幕、社交媒体发言等呈爆炸式增长。对于企业而言&#xff0c;理解这些文本背后…

作者头像 李华