全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

大数据量查询优化

传统查询会将全部结果加载到内存,百万级数据导出时导致 OOM。MyBatis 提供流式查询和游标机制,实现逐行处理,内存占用恒定。

传统查询的内存瓶颈

Java
// 危险:100 万条数据全部加载到内存
List<Order> orders = orderMapper.selectAll();
for (Order order : orders) {
  process(order);
}
数据量内存占用表现
1,000 条~1 MB无影响
100,000 条~100 MB可接受
1,000,000 条~1 GBGC 频繁,响应慢
10,000,000 条~10 GBOOM 崩溃

传统 List<T> 查询的内存占用与数据量成正比,数据量超过百万时极易触发 OOM。

流式查询 (ResultHandler)

基本用法

Java
public interface OrderMapper {
  // 使用 ResultHandler 流式处理结果
  void selectAllWithHandler(ResultHandler<Order> handler);
}
XML
<select id="selectAllWithHandler" resultType="Order" 
        resultSetType="FORWARD_ONLY" fetchSize="1000">
  SELECT * FROM order
</select>
Java
public void exportOrders() throws IOException {
  mapper.selectAllWithHandler(context -> {
    Order order = (Order) context.getResultObject();
    writeToFile(order);
  });
}
参数说明
resultSetType="FORWARD_ONLY"只进游标,不支持回滚
fetchSize="1000"每次从数据库获取 1000 条,控制网络往返次数
ResultHandler逐行回调处理函数,不缓存结果到内存

流式导出 CSV

Java
public void exportToCsv(String outputPath) throws IOException {
  try (BufferedWriter writer = new BufferedWriter(
          new OutputStreamWriter(new FileOutputStream(outputPath), "UTF-8"))) {
    // 写入表头
    writer.write("id,order_no,amount,create_time\n");
    
    mapper.selectAllWithHandler(context -> {
      Order order = (Order) context.getResultObject();
      try {
        writer.write(String.format("%d,%s,%.2f,%s\n",
            order.getId(), order.getOrderNo(), order.getAmount(), order.getCreateTime()));
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    });
    
    writer.flush();
  }
}

流式查询内存占用恒定(仅保存 fetchSize 指定的批次数据),适合导出、统计等全表扫描场景。

Cursor 游标查询

基本用法

Java
public interface OrderMapper {
  // 返回 Cursor,支持 try-with-resources
  Cursor<Order> selectAllAsCursor();
}
XML
<select id="selectAllAsCursor" resultType="Order" 
        resultSetType="FORWARD_ONLY" fetchSize="1000">
  SELECT * FROM order
</select>
Java
public void processOrders() {
  try (Cursor<Order> cursor = mapper.selectAllAsCursor()) {
    for (Order order : cursor) {
      process(order);
    }
  } catch (IOException e) {
    log.error("Cursor processing error", e);
  }
}

Cursor vs ResultHandler

维度CursorResultHandler
返回值Cursor<T> 迭代器void,回调处理
遍历方式for-each / iterator回调函数
异常处理try-with-resources回调内部处理
流控制外部控制,可 break/continue内部驱动,全部处理
适用场景需要中途退出或跳过全量处理,不可中断
事务管理需保持在同一事务自动在查询事务内

分页查询优化

传统 offset 分页的问题

XML
<!-- 深度分页:LIMIT 1000000, 20 需要扫描前 100 万行 -->
<select id="selectByPage" resultType="Order">
  SELECT * FROM order LIMIT #{offset}, #{limit}
</select>

offset 越大,数据库需要扫描的行数越多,查询时间线性增长。

游标分页(基于索引)

XML
<!-- 基于自增 ID 的游标分页 -->
<select id="selectByCursor" resultType="Order">
  SELECT * FROM order
  WHERE id > #{lastId}
  ORDER BY id ASC
  LIMIT #{limit}
</select>
Java
public void processAllOrders() {
  Long lastId = 0L;
  int limit = 5000;
  List<Order> batch;
  
  do {
    batch = mapper.selectByCursor(lastId, limit);
    for (Order order : batch) {
      process(order);
      lastId = order.getId();
    }
  } while (batch.size() == limit);
}
分页方式第 1 页耗时第 10000 页耗时扫描行数
offset 分页10 ms5000 ms100 万 +
游标分页10 ms10 ms5000

游标分页基于索引查找,无论数据深度多少页,扫描行数恒等于 LIMIT 值。

批量处理策略

ResultHandler + 分批提交

Java
public void batchProcessOrders() {
  final int batchSize = 1000;
  final List<Order> buffer = new ArrayList<>(batchSize);
  
  mapper.selectAllWithHandler(context -> {
    buffer.add((Order) context.getResultObject());
    
    if (buffer.size() >= batchSize) {
      processBatch(buffer);
      buffer.clear();
    }
  });
  
  // 处理剩余数据
  if (!buffer.isEmpty()) {
    processBatch(buffer);
  }
}

Cursor + 并行处理

Java
public void parallelProcessOrders() throws Exception {
  ForkJoinPool pool = new ForkJoinPool(4);
  
  try (Cursor<Order> cursor = mapper.selectAllAsCursor()) {
    Iterator<Order> iterator = cursor.iterator();
    
    // 分批读取 + 并行处理
    List<Order> batch = new ArrayList<>(1000);
    while (iterator.hasNext()) {
      batch.add(iterator.next());
      
      if (batch.size() >= 1000) {
        pool.submit(() -> processBatch(new ArrayList<>(batch)));
        batch.clear();
      }
    }
    
    if (!batch.isEmpty()) {
      pool.submit(() -> processBatch(batch));
    }
  }
  
  pool.shutdown();
  pool.awaitTermination(1, TimeUnit.HOURS);
}

大数据量查询配置对比

配置传统查询流式查询游标查询
返回值List<T>void + 回调Cursor<T>
内存占用O(N)O(fetchSize)O(fetchSize)
事务范围整个查询整个查询整个查询
流控制无(全部回调)可 break/continue
适用场景小数据量全量导出/统计逐行处理
百万数据内存~1 GB~1 MB~1 MB

关键配置说明

fetchSize 调优

fetchSize网络往返次数 (100 万条)内存占用建议场景
默认 (0)1 次全部加载小数据量
10010,000 次~1 MB低内存环境
10001,000 次~10 MB通用场景
5000200 次~50 MB高性能要求

fetchSize 不是越大越好,过大导致单次网络传输延迟增加;过小导致网络往返频繁。

数据库连接超时处理

Java
public void longRunningQuery() {
  SqlSession session = sqlSessionFactory.openSession();
  try {
    // 设置查询超时时间
    session.getConfiguration()
        .getDefaultStatementTimeout(); // 可在 settings 中配置
    
    mapper.selectAllWithHandler(context -> {
      // 长时间处理逻辑
      process((Order) context.getResultObject());
    });
    
    session.commit();
  } finally {
    session.close();
  }
}

要点总结

  • 传统 List<T> 查询全量加载到内存,百万级数据导致 OOM。
  • ResultHandler 流式查询逐行回调处理,内存占用恒定为 O(fetchSize)。
  • Cursor 游标返回迭代器,支持 try-with-resources 和流控制(break/continue)。
  • 深度分页问题使用游标分页(WHERE id > #{lastId})替代 offset 分页。
  • fetchSize 控制每次从数据库拉取的行数,1000 是通用场景的平衡值。
  • 批量处理场景结合 ResultHandler + 缓冲队列实现分批提交。
  • 大数据量导出必须使用流式查询,避免一次性加载全部结果到内存。

📝 发现内容有误?点击此处直接编辑

← 上一篇 复杂继承映射
下一篇 → 嵌套结果集映射优化
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库