本文目录导读:

- 方案一:SQL级联 + 索引优化(最推荐,改SQL即可)
- 方案二:IN子查询 + 分批/缓存(适合多表且JOIN太慢)
- 方案三:缓存层(Redis/本地缓存,高频查询)
- 方案四:宽表 + 数据冗余(终极但需权衡)
- 核心加速对照表
- 给你一个最落地的建议
关于Java关联查询提速,我给你一个从最落地到最复杂的实战案例全攻略,这里假设你用的是MySQL + MyBatis,这是最常见的组合。
核心思路:减少数据库连接数 + 利用索引 + 避免N+1查询。
场景假设
- 表A:订单表 (order) — 100万数据
- 表B:用户表 (user) — 10万数据
- 需求:查询最近1000笔订单,并带上用户名称。
原始慢查询(反面案例)
// UserMapper
User selectById(Long userId);
// OrderService
for (Order order : orderList) {
User user = userMapper.selectById(order.getUserId()); // 循环查数据库!
order.setUserName(user.getName());
}
问题:N+1查询,1000笔订单 -> 1001次数据库查询。
SQL级联 + 索引优化(最推荐,改SQL即可)
原理:让数据库一次JOIN出结果,减少应用层循环。
写JOIN SQL
<select id="selectOrdersWithUser" resultMap="orderUserMap">
SELECT o.*, u.id as u_id, u.name as u_name
FROM `order` o
LEFT JOIN `user` u ON o.user_id = u.id
WHERE o.create_time > #{startTime}
ORDER BY o.create_time DESC
LIMIT 1000
</select>
必加索引(性能关键)
-- 订单表:user_id字段加索引(用于JOIN) ALTER TABLE `order` ADD INDEX idx_user_id (`user_id`); -- 用户表:id字段已是主键索引,不用加 -- 订单表:按时间排序时,给create_time加索引(避免文件排序) ALTER TABLE `order` ADD INDEX idx_create_time (`create_time`);
结果:1次查询搞定,索引命中后性能提升10-100倍。
IN子查询 + 分批/缓存(适合多表且JOIN太慢)
当JOIN太复杂(多对多,或表非常大)时,用先查主表,再批量查关联表。
// 1. 先只查主表(只查询1000条订单,速度快)
List<Order> orders = orderMapper.selectRecentOrders(1000);
// 2. 提取所有userId,去重
Set<Long> userIds = orders.stream()
.map(Order::getUserId)
.collect(Collectors.toSet());
// 3. IN查询一次拿到所有用户(批量查询)
List<User> users = userMapper.selectBatchByIds(new ArrayList<>(userIds));
// 4. 构建Map,内存中关联
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, u -> u));
// 5. 赋值
orders.forEach(o -> o.setUserName(userMap.get(o.getUserId()).getName()));
对比:
- 原始:1001次查询
- 优化后:2次查询(订单查1次 + 用户查1次)
适用场景:表非常大,JOIN导致临时表太大、内存不足。
缓存层(Redis/本地缓存,高频查询)
适合:用户表几乎不常变,但被频繁关联。
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
public User getById(Long id) {
// 1. 先从缓存查
String json = redisTemplate.opsForValue().get("user:" + id);
if (json != null) {
return JSON.parseObject(json, User.class);
}
// 2. 缓存没有,查数据库
User user = userMapper.selectById(id);
// 3. 写入缓存(设置过期时间)
redisTemplate.opsForValue().set("user:" + id, JSON.toJSONString(user), 3600, TimeUnit.SECONDS);
return user;
}
}
效果:第二次查询直接走内存,毫秒级。
宽表 + 数据冗余(终极但需权衡)
原理:把关联字段直接冗余到主表中,避免关联。
-- 订单表增加 user_name 字段 ALTER TABLE `order` ADD COLUMN user_name VARCHAR(100); -- 插入/更新时同步(异步或触发器) UPDATE `order` o JOIN `user` u ON o.user_id = u.id SET o.user_name = u.name;
优点:查询直接 SELECT * FROM order,零关联。
缺点:
- 数据冗余,更新用户名称时需要同步更新所有订单。
- 适合写少读多的场景(如日志、报表)。
核心加速对照表
| 方案 | 机制 | 适用场景 | 性能提升 |
|---|---|---|---|
| SQL JOIN + 索引 | 数据库一次关联 | 两表大小适中,关联字段有索引 | 10-50倍 |
| IN子查询 + 分批 | 减少数据库连接数 | 大表关联或多对多 | 5-10倍 |
| 缓存层 | 减少数据库查询 | 高频查询、低频更新的关联表 | 100-1000倍 |
| 宽表/冗余 | 空间换时间 | 读多写极少、报表统计 | 10-30倍 |
给你一个最落地的建议
第一步:先检查索引(EXPLAIN你的查询,看type是不是ref或index,有无Using filesort)。
第二步:如果业务简单,直接改SQL为JOIN(方案一),一般是性价比最高的。
第三步:如果JOIN后还是慢(超过1秒),换成方案二(IN分批查询),并用MyBatis的@MapKey直接构建Map。
第四步:如果还是高频,加Redis缓存(方案三)。
不要一开始就上宽表或分库分表,容易过度设计。