Java案例怎么减少数据库查询?

wen java案例 56

本文目录导读:

Java案例怎么减少数据库查询?

  1. 策略一:批量操作(最直接的减少次数)
  2. 策略二:使用缓存(避免重复查询)
  3. 策略三:使用关联查询(避免N+1问题)
  4. 策略四:数据汇总与预计算
  5. 策略五:延迟加载与分页
  6. 策略六:只查需要的字段
  7. 总结与选择建议

减少数据库查询是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 > ...
  • 预计算方案:
    1. Redis计数器: 下单时 redisTemplate.opsForValue().increment("paid_count"),查询时直接取 redis 的值。
    2. 汇总表: 每小时/每天跑定时任务,将统计结果写入 order_summary 表,查询时直接查汇总表(如 SELECT total_amount FROM order_summary WHERE date = today)。

延迟加载与分页

不是所有数据都需要一次性查完。

案例:列表页

  • 反例: 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 + 排序)
查询全表字段 只查需要的字段

最佳实践: 这六种策略通常组合使用。

  1. 只查需要的字段(减少传输)。
  2. 对结果集使用分页(减少数据量)。
  3. 热点数据使用缓存(避免重复查库)。
  4. 关联数据使用JOIN(避免循环查询)。
  5. 需要批量处理时使用批量查询(减少次数)。

通过以上方法,大部分系统可以将数据库查询量降低50%-90%,显著提升性能。

抱歉,评论功能暂时关闭!