本文目录导读:

减少数据库查询是Java性能优化的核心之一,每次数据库查询都涉及网络开销、连接池获取、SQL解析、磁盘I/O等,减少查询次数能显著提升响应速度。
以下是Java开发中最实用、最常用的6大策略,并附有代码案例和适用场景。
批量操作(最直接的减少次数)
将N次单条查询/插入合并为1次批量操作,是最有效的手段。
反例(N次查询):
// 循环内逐条查询,产生N+1次查询
List<User> users = new ArrayList<>();
for (Long id : idList) {
User user = userMapper.selectById(id); // 每次查询都请求一次DB
users.add(user);
}
正例(1次批量查询):
// 使用 IN 查询或 MyBatis 的 foreach List<User> users = userMapper.selectBatchByIds(idList); // 只查1次
MyBatis Mapper 定义:
<select id="selectBatchByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
效果: N次查询变为1次,尤其适用于 idList 较大(如几十上百)的场景。
使用缓存(避免重复查询)
对于读多写少、数据变化不频繁的数据,缓存是第一选择。
本地缓存(Caffeine / Guava Cache)示例:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 使用 Caffeine 构建本地缓存
private final Cache<Long, User> userCache = Caffeine.newBuilder()
.maximumSize(10_000) // 最多缓存1万个
.expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期
.build();
public User getUserById(Long id) {
// 1. 先从缓存取
User cachedUser = userCache.getIfPresent(id);
if (cachedUser != null) {
return cachedUser;
}
// 2. 缓存没有,查数据库
User user = userMapper.selectById(id);
if (user != null) {
userCache.put(id, user); // 放入缓存
}
return user;
}
}
分布式缓存(Redis)示例:
// 使用 Spring Cache 注解,一行代码搞定
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
return userMapper.selectById(id); // 只有第一次或缓存失效时才会执行
}
效果: 高频访问的数据不再请求数据库,命中率90%以上时可减少90%查询。
使用关联查询(避免N+1问题)
ORM框架(如MyBatis、JPA)中,循环中触发懒加载会产生N+1次查询。
反例(N+1问题):
// 获取所有订单,再循环获取每个订单的用户
List<Order> orders = orderMapper.selectAll();
for (Order order : orders) {
// 这里触发懒加载,每次循环都查询一次 user 表
User user = order.getUser();
}
正例(使用JOIN一次查完):
// 使用关联查询,一次性查出订单和用户信息 List<OrderVO> orders = orderMapper.selectOrdersWithUser();
MyBatis 配置(XML):
<resultMap id="OrderWithUser" type="Order">
<id column="id" property="id"/>
<result column="order_name" property="orderName"/>
<!-- 关联用户,一次JOIN查询 -->
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="userName"/>
</association>
</resultMap>
<select id="selectOrdersWithUser" resultMap="OrderWithUser">
SELECT o.*, u.user_name
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
</select>
效果: N+1次查询变为1次JOIN查询。
数据汇总与预计算
对于统计类查询,避免每次都全表扫描。
案例:实时统计 vs 预计算
- 实时查询(每次请求都聚合):
SELECT COUNT(*) FROM order WHERE status = 'PAID' AND create_time > ... - 预计算方案:
- Redis计数器: 下单时
redisTemplate.opsForValue().increment("paid_count"),查询时直接取redis的值。 - 汇总表: 每小时/每天跑定时任务,将统计结果写入
order_summary表,查询时直接查汇总表(如SELECT total_amount FROM order_summary WHERE date = today)。
- Redis计数器: 下单时
延迟加载与分页
不是所有数据都需要一次性查完。
案例:列表页
- 反例:
SELECT * FROM article查出100万条记录,再在前端分页。 - 正例: 使用
LIMIT offset, size或 游标分页,每次只查询当前页数据。
-- 只查询当前页,而不是全表 SELECT id, title, summary FROM article WHERE status = 1 ORDER BY id DESC LIMIT 20 OFFSET 0; -- 只查20条
只查需要的字段
反例: SELECT * 会查询所有列,浪费I/O和带宽。
正例: 只查需要的字段。
// 反例
@Select("SELECT * FROM user")
List<User> getAllUsers();
// 正例:只查ID和名称
@Select("SELECT id, user_name FROM user")
List<UserDTO> getAllUserNames();
// 使用 DTO 接收,减少传输数据量
总结与选择建议
| 场景 | 推荐策略 |
|---|---|
| 循环内查单条数据 | 批量查询(IN / foreach) |
| 热点数据(如用户信息、配置) | 缓存(本地Caffeine / 分布式Redis) |
| 关联查询(如订单+用户) | JOIN(解决N+1问题) |
| 高并发统计(如订单数量) | 预计算(Redis计数器或汇总表) |
| 列表页/大数据量 | 分页(Limit + 排序) |
| 查询全表字段 | 只查需要的字段 |
最佳实践: 这六种策略通常组合使用。
- 先只查需要的字段(减少传输)。
- 对结果集使用分页(减少数据量)。
- 热点数据使用缓存(避免重复查库)。
- 关联数据使用JOIN(避免循环查询)。
- 需要批量处理时使用批量查询(减少次数)。
通过以上方法,大部分系统可以将数据库查询量降低50%-90%,显著提升性能。