如何不使用while循环读取查询返回的完整ResultSet数据——针对大体积MDB文件的优化需求
解决大体积MDB文件ResultSet读取性能问题
我之前处理过几GB级别的Access数据库迁移,太懂这种逐行循环慢到崩溃的感觉了!你的核心问题其实不是while循环本身,而是单条数据处理+频繁数据库写入带来的IO开销,再加上JDBC默认的小批量读取放大了这个问题。下面给你几个可行的优化方案,亲测有效:
1. 调大JDBC Fetch Size,减少数据库往返次数
UCanAccess默认的fetch size很小(通常只有几十条),这意味着程序要反复和数据库建立连接取数据,光是网络IO就占了大半时间。你可以在创建Statement后设置一个合适的fetch size,一次拉取更多数据到内存:
Statement orders = conn.createStatement(); // 根据内存情况调整,1GB的MDB建议设为1000-2000 orders.setFetchSize(1000); ResultSet result = orders.executeQuery("SELECT [ORDERID], [ORDERDATE], [QUANTIRY], [PAYMENT] FROM [ORDERS]");
2. 批量插入代替单条save,降低事务开销
你现在每次循环都调用orderRepository.save(),相当于每一条数据都开启一次事务、写入一次,这对数据库来说是极大的性能浪费。换成批量插入,攒够一批数据再一次性写入:
@Autowired private OrderRepository orderRepository; String msAccDB = "jdbc:ucanaccess://" + datafile.mdb; // 用try-with-resources自动关闭资源,避免泄漏 try (Connection conn = DriverManager.getConnection(msAccDB); Statement orders = conn.createStatement()) { orders.setFetchSize(1000); ResultSet result = orders.executeQuery("SELECT [ORDERID], [ORDERDATE], [QUANTIRY], [PAYMENT] FROM [ORDERS]"); List<Order> batchList = new ArrayList<>(1000); int batchCount = 0; while (result.next()) { Order order = new Order(); order.setOrderId(result.getString("ORDERID")); order.setOrderDate(result.getString("ORDERDATE")); order.setQuantity(result.getString("QUANTIRY")); order.setPayment(result.getString("PAYMENT")); batchList.add(order); batchCount++; // 每攒够1000条就批量保存 if (batchCount % 1000 == 0) { orderRepository.saveAll(batchList); batchList.clear(); } } // 处理最后一批不足1000条的数据 if (!batchList.isEmpty()) { orderRepository.saveAll(batchList); } } catch (SQLException e) { e.printStackTrace(); }
3. 用JDBC原生批量操作替代JPA,砍掉ORM开销
如果JPA的saveAll还是不够快,可以直接用JDBC的addBatch()和executeBatch(),跳过ORM的对象转换开销,超大数据量下提升非常明显:
String insertSql = "INSERT INTO orders (order_id, order_date, quantity, payment) VALUES (?, ?, ?, ?)"; try (Connection conn = DriverManager.getConnection(msAccDB); Statement orders = conn.createStatement(); PreparedStatement pstmt = conn.prepareStatement(insertSql)) { orders.setFetchSize(1000); ResultSet result = orders.executeQuery("SELECT [ORDERID], [ORDERDATE], [QUANTIRY], [PAYMENT] FROM [ORDERS]"); int batchCount = 0; while (result.next()) { pstmt.setString(1, result.getString("ORDERID")); pstmt.setString(2, result.getString("ORDERDATE")); pstmt.setString(3, result.getString("QUANTIRY")); pstmt.setString(4, result.getString("PAYMENT")); pstmt.addBatch(); batchCount++; if (batchCount % 1000 == 0) { pstmt.executeBatch(); conn.commit(); batchCount = 0; } } // 处理剩余批次 if (batchCount > 0) { pstmt.executeBatch(); conn.commit(); } } catch (SQLException e) { e.printStackTrace(); }
4. 可选:用Stream处理ResultSet(避免显式while)
如果你确实不想写while循环,可以把ResultSet转换成Stream来处理,本质上还是迭代,但代码更简洁:
// 工具方法:把ResultSet转成Order的Stream private Stream<Order> resultSetToStream(ResultSet result) throws SQLException { return Stream.generate(() -> { try { return result.next() ? mapToOrder(result) : null; } catch (SQLException e) { throw new RuntimeException(e); } }).takeWhile(Objects::nonNull); } // 工具方法:ResultSet映射为Order对象 private Order mapToOrder(ResultSet result) throws SQLException { Order order = new Order(); order.setOrderId(result.getString("ORDERID")); order.setOrderDate(result.getString("ORDERDATE")); order.setQuantity(result.getString("QUANTIRY")); order.setPayment(result.getString("PAYMENT")); return order; } // 使用示例 try (Connection conn = DriverManager.getConnection(msAccDB); Statement orders = conn.createStatement()) { orders.setFetchSize(1000); ResultSet result = orders.executeQuery("SELECT [ORDERID], [ORDERDATE], [QUANTIRY], [PAYMENT] FROM [ORDERS]"); AtomicInteger batchCount = new AtomicInteger(0); resultSetToStream(result) .collect(Collectors.groupingBy(it -> batchCount.incrementAndGet() / 1000)) .values() .forEach(batch -> orderRepository.saveAll(batch)); } catch (SQLException e) { e.printStackTrace(); }
注意事项
- 批量大小别设太大,不然容易内存溢出,1GB的MDB设1000-2000条比较合适;
- 必须用
try-with-resources自动关闭连接、Statement、ResultSet,避免资源泄漏; - Access数据库对并发写入支持有限,不建议用多线程并行处理,容易引发锁冲突。
内容的提问来源于stack exchange,提问作者user11311667
相关产品推荐
相关产品推荐

