news 2026/4/16 16:10:59

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/4/16 4:11:02

AI+UEBA深度解析:云端实验环境已配好,首小时仅需1元

AIUEBA深度解析&#xff1a;云端实验环境已配好&#xff0c;首小时仅需1元 1. 什么是UEBA&#xff1f;为什么需要它&#xff1f; UEBA&#xff08;用户和实体行为分析&#xff09;就像给企业安全系统装上一个"行为侦探"。它不依赖已知的攻击特征&#xff0c;而是通…

作者头像 李华
网站建设 2026/4/16 13:03:52

AI安全极客套装:渗透测试+AI检测二合一,按天租赁更灵活

AI安全极客套装&#xff1a;渗透测试AI检测二合一&#xff0c;按天租赁更灵活 引言&#xff1a;自由职业者的双重挑战 作为一名同时接安全和AI项目的自由职业者&#xff0c;你是否经常遇到这样的困境&#xff1a;笔记本同时运行Kali Linux和PyTorch时频繁死机&#xff0c;两个…

作者头像 李华
网站建设 2026/4/16 12:41:23

小米风波遭遇反噬:公关止血难掩文化裂痕,粉丝经济的信任拷问

文 | 大力财经当全年奖金成为舆情危机的“买单筹码”&#xff0c;小米的重罚决策在舆论场引发了分裂式讨论。一边是部分网友称赞其 “对用户负责、处罚果断”&#xff0c;另一边则是米粉群体的集体寒心与行业对其企业文化的深层质疑。这场因 “大熊” 事件引发的高管处罚风波&a…

作者头像 李华
网站建设 2026/4/16 10:56:15

StructBERT轻量级情感分析:CPU优化部署指南

StructBERT轻量级情感分析&#xff1a;CPU优化部署指南 1. 中文情感分析的技术挑战与需求 在自然语言处理&#xff08;NLP&#xff09;的实际应用中&#xff0c;中文情感分析是企业客服、舆情监控、用户反馈挖掘等场景的核心技术之一。相比英文&#xff0c;中文缺乏明显的词边…

作者头像 李华
网站建设 2026/4/16 9:19:52

中文情感分析实战:StructBERT模型性能测试

中文情感分析实战&#xff1a;StructBERT模型性能测试 1. 引言&#xff1a;中文情感分析的应用价值与挑战 随着社交媒体、电商平台和用户评论系统的普及&#xff0c;中文情感分析已成为自然语言处理&#xff08;NLP&#xff09;领域的重要应用方向。其核心任务是自动识别文本…

作者头像 李华
网站建设 2026/4/16 10:57:05

【26年1月显示器支架臂推荐清单】教父级机械臂选购指南!用好单/双/三屏支架桌面空间大一倍!

【26年1月显示器支架臂推荐清单】教父级机械臂选购指南&#xff01;涵盖NB/AOC/HKC/北弧/松能/微星/戟创/友狮/京东京造等品牌新手必看显示屏支架臂购买攻略&#xff01;序欢迎来到2026年1月显示器支架臂推荐合集&#xff01;我是「ximagine」很多人问桌面改造的第一步该买什么…

作者头像 李华