
1. 批量查询数据库为什么会把内存打爆做数据同步、报表导出、离线清洗这类任务时最容易踩的坑不是 SQL 写错而是「一次查太多」。我见过一个导出任务SQL 本身没问题但一张两千万行的表直接SELECT *JVM 堆内存瞬间被 ResultSet 撑满最后 OOM 挂掉。原因在于MySQL 和 PostgreSQL 的 JDBC 驱动默认会把查询结果全量拉回客户端内存而不是像很多人以为的那样「边查边取」。这就引出了 JDBC 里两个关键参数setFetchSize和addBatch。前者控制查询时每次从数据库拉多少行后者控制写入时攒多少条一起提交。两者方向相反但目标一致——把内存占用和网络往返压到可控范围。这篇就围绕「批量查询」这个场景把setFetchSize流式读取和addBatch批处理两条路径的配置骨架讲清楚同时说明怎么用 TaoToken 统一 Key/API 通道让 AI 工具帮你生成和校验这些配置。适合谁看正在写 JDBC 批量任务、被大数据量查询搞到内存告警、或者想让 AI 辅助生成连接参数骨架的后端同学。下面所有代码都可以直接复制改连接串跑起来。2. TaoToken 前置统一 Key 与 API 通道在动手写 JDBC 之前先说清楚 TaoToken 在这里扮演什么角色。它不是数据库驱动也不碰你的 JDBC 连接而是一个统一的模型 API 通道你只需要在 TaoToken 申请一个 Key就能通过同一套接口调用不同厂商的模型用来辅助生成 JDBC 配置骨架、审查setFetchSize参数是否合理、或者解释某段批处理代码的批次行为。官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址不带 UTMhttps://taotoken.net/api具体到操作层面你需要先拿到 Key再去控制台确认通道可用。常用入口如下模型对话验证模型是否通https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Plan长期编码/Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaudeCodeAnthropic 接入https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite注意TaoToken 只负责模型调用通道数据库连接、驱动版本、事务控制仍然由你自己的 JDBC 代码负责。不要把两者混为一谈。拿到 Key 后你可以让模型帮你做一件很具体的事把下面这份 JDBC 配置骨架里的占位符替换成你的实际参数并检查fetchSize和autoCommit的组合是否合理。这比你自己翻文档快得多。3. 可复制配置setFetchSize 流式读取骨架先看查询侧。核心思路是在executeQuery()之前设置fetchSize并且关闭autoCommit让驱动按批拉取而不是一次性全量返回。3.1 连接参数与 PreparedStatement 配置import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class StreamQueryDemo { public static void main(String[] args) throws SQLException { String url jdbc:mysql://127.0.0.1:3306/demo_db ?useCursorFetchtrue useServerPrepStmtstrue rewriteBatchedStatementstrue; String user your_user; String password your_password; try (Connection conn DriverManager.getConnection(url, user, password)) { // 关键关闭自动提交配合 fetchSize 做流式读取 conn.setAutoCommit(false); String sql SELECT id, name, created_at FROM big_table WHERE status ?; try (PreparedStatement pst conn.prepareStatement( sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { // 关键在 executeQuery 之前设置 fetchSize pst.setFetchSize(500); pst.setString(1, ACTIVE); try (ResultSet rs pst.executeQuery()) { int count 0; while (rs.next()) { // 逐行处理内存中最多保留 fetchSize 行 long id rs.getLong(id); String name rs.getString(name); count; if (count % 10000 0) { System.out.println(已处理 count 行); } } System.out.println(总计处理 count 行); } } conn.commit(); } } }几个参数的作用对照参数作用建议值useCursorFetchtrueMySQL 驱动启用游标分批拉取必须开启useServerPrepStmtstrue服务端预处理配合游标建议开启setFetchSize(500)每次从服务端拉 500 行200–1000setAutoCommit(false)避免驱动缓存整个结果集必须关闭注意setFetchSize必须写在executeQuery()之前。如果你先执行查询再对 ResultSet 设置数据可能已经全量加载设置就失去意义了。这一点在 PostgreSQL 上尤其明显PG 要求autoCommitfalse且fetchSize0才会真正走游标。3.2 PostgreSQL 的差异PostgreSQL 的写法略有不同驱动要求autoCommitfalse且fetchSize大于 0String url jdbc:postgresql://127.0.0.1:5432/demo_db; Connection conn DriverManager.getConnection(url, user, password); conn.setAutoCommit(false); PreparedStatement pst conn.prepareStatement( SELECT id, name FROM big_table WHERE status ?, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); pst.setFetchSize(500); pst.setString(1, ACTIVE); ResultSet rs pst.executeQuery();如果你用的是分布式数据库默认可能就返回部分数据比如 100 条一批这时候setFetchSize的作用是调整每批大小而不是决定是否分批。4. 可复制配置addBatch 批处理写入骨架查询讲完再看写入侧。addBatch解决的是「频繁单条插入导致网络往返过多」的问题。核心是关闭autoCommit攒够一批再executeBatch()。4.1 批处理代码骨架import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; public class BatchInsertDemo { private static final int BATCH_SIZE 1000; public static void main(String[] args) throws SQLException { String url jdbc:mysql://127.0.0.1:3306/demo_db ?rewriteBatchedStatementstrue; String user your_user; String password your_password; try (Connection conn DriverManager.getConnection(url, user, password)) { conn.setAutoCommit(false); String sql INSERT INTO target_table (id, name, created_at) VALUES (?, ?, ?); try (PreparedStatement pst conn.prepareStatement(sql)) { int pending 0; for (int i 0; i 100000; i) { pst.setLong(1, i); pst.setString(2, name_ i); pst.setTimestamp(3, new java.sql.Timestamp(System.currentTimeMillis())); pst.addBatch(); pending; if (pending BATCH_SIZE) { int[] result pst.executeBatch(); System.out.println(提交批次影响行数: result.length); pending 0; } } if (pending 0) { pst.executeBatch(); System.out.println(提交最后一批剩余: pending); } } conn.commit(); } } }rewriteBatchedStatementstrue是 MySQL 驱动的一个优化开关它会把多条 INSERT 重写成一条多值 INSERT显著减少网络往返。PostgreSQL 对应的是reWriteBatchedInsertstrue。4.2 批次大小怎么选批次不是越大越好。批次太大单次提交的数据包过大反而可能触发数据库的max_allowed_packet限制批次太小网络往返次数降不下来。实测下来1000 到 5000 是比较稳的区间。你可以先用 1000 跑一轮看日志里每批的耗时再决定要不要调大。提示executeBatch()返回的是int[]每个元素对应一条语句的影响行数。如果某条失败驱动可能抛BatchUpdateException你可以从异常里拿到部分结果定位是哪一条出的问题。5. 验证请求与成功结果配置写完怎么确认批次真的生效了光看代码不够要看日志和实际行为。5.1 用日志确认 fetchSize 生效在查询循环里加计数打印观察内存曲线。如果fetchSize生效内存占用应该保持平稳而不是随处理行数线性增长。你可以用jconsole或jstat观察堆内存jstat -gc pid 1000如果老年代没有持续膨胀说明流式读取在起作用。5.2 用日志确认 addBatch 生效批处理侧观察executeBatch()的调用频率。如果每 1000 条打印一次「提交批次」说明批次生效。你还可以对比开启和关闭rewriteBatchedStatements的耗时差异long start System.currentTimeMillis(); // ... 批处理逻辑 ... long cost System.currentTimeMillis() - start; System.out.println(总耗时: cost ms);5.3 用 TaoToken 辅助校验配置如果你不确定自己的参数组合是否合理可以把连接串和关键代码片段贴给模型让它帮你检查。通过 TaoToken 的模型对话入口用统一的 Key 调用curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer YOUR_TAOTOKEN_KEY \ -H Content-Type: application/json \ -d { model: your-model, messages: [ {role: user, content: 帮我检查这段 JDBC 配置useCursorFetchtrue, setFetchSize(500), autoCommitfalse是否适合 MySQL 流式读取} ] }返回结果会告诉你参数是否匹配、有没有遗漏。这比自己翻驱动源码快很多。6. 本篇常见错排查6.1 setFetchSize 设置了但没生效最常见的原因是autoCommit没关。MySQL 驱动在autoCommittrue时即使设了fetchSize也可能全量拉取。另一个原因是useCursorFetch没开。检查连接串里这两个参数。6.2 addBatch 后内存反而涨了如果你在循环里不断addBatch却从不executeBatch批次会一直攒在内存里最后 OOM。确保每攒够BATCH_SIZE就提交一次循环结束后再提交剩余部分。6.3 executeBatch 报 BatchUpdateException通常是某条数据违反了约束比如主键冲突、字段超长。从异常里拿getUpdateCounts()找到失败位置单独处理那条数据。不要直接忽略异常继续提交否则后续批次可能全部失败。6.4 分布式数据库的 fetchSize 行为不同有些分布式数据库默认就分批返回setFetchSize只是调整每批大小。如果你发现设置前后行为差异不大先确认数据库本身的默认行为再决定要不要调。6.5 连接串参数写错导致驱动不识别MySQL 和 PostgreSQL 的参数名不一样。MySQL 用useCursorFetchPostgreSQL 不需要这个参数但要求autoCommitfalse。写错参数名驱动会静默忽略不会报错所以一定要对照官方文档确认。7. 用 TaoToken 统一通道继续深入批量查询和批处理的配置骨架到这里就完整了。你可以直接复制上面的代码把连接串换成自己的库跑一轮看日志。如果遇到参数组合不确定、或者想让 AI 帮你生成针对特定数据库的变体可以用 TaoToken 的统一 Key 通道排障/接入问题走 API Keys 管理 接入文档先把 Key 和通道确认好验证模型是否可用走模型对话入口发一条测试请求长期编码/Agent 场景走 Coding Plan把配置生成和校验固化到工作流里API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite模型对话https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite最后留一个实用技巧把fetchSize和BATCH_SIZE做成配置项不要硬编码。不同库、不同表、不同网络环境下的最优值不一样跑一轮压测再定。我一般会先用 500 和 1000 各跑一次看内存和耗时曲线再决定最终值。