
聊聊数据库里的 fetchsize 参数一次查询到底该往内存里塞多少数据做后端开发的兄弟十有八九都遇到过这么个怪事同一个查询在测试环境跑得飞快切到生产环境就卡成幻灯片或者同一个接口别人调用就像喝水一样顺畅你这边一调数据库连接池先报警了。这时候如果排查下来SQL没毛病、索引也都对那大概率问题就出在一个很少有人认真对待的参数上——fetchsize。我在交付数据库相关项目时几乎每次评审SQL和DAO层代码都要跟开发同事反复确认这个参数。不夸张地说这个参数用不好轻则查询变慢、内存飙升重则直接把应用服务器搞到OOM。但你要是问很多人“fetchsize到底意味着什么”多半得到的回答是“不就是每次从数据库捞多少行嘛设大点不就完了”——真没这么简单。这篇文章我就想把这个参数彻底聊透它底层是怎么工作的不同数据库驱动对它的处理有何不同实际项目里该怎么选值、怎么排查问题。全部都是我在项目里踩过坑之后总结出来的东西保证比你翻官方文档来得实在。1. 先搞清楚 fetchsize 到底在控制什么1.1 一条查询的执行链路想要理解fetchsize不能只盯着JDBC那一层干看得先弄清楚查询结果从数据库服务器到你的Java应用中间到底走了什么路。一条SQL执行完毕后结果集并不是“唰”地一下全部出现在你面前而是有一个拉取过程的。我第一次画出这条链路时才明白很多性能问题的根源其实不在SQL本身。整体流程可以简化成这么几步应用发出查询请求数据库服务器执行SQL生成结果集。但这会儿结果集还留在数据库进程里JDBC驱动要负责把数据通过网络传输到客户端最终装进ResultSet里供应用读取。这里的关键就是驱动是“一口气”把所有数据搬过来还是“分批次”搬控制这个批次的界限就是fetchsize。从链路可以看出fetchsize并不是数据库服务器的参数而是客户端JDBC驱动与数据库服务器交互时的一个取数单位。它默认取值在不同驱动里差别很大有的默认一次拉10行有的默认一次拉全部要是不摸清底细就很容易踩进“隐性坑”里。1.2 一个容易忽略的事实它和网络交互次数强相关既然fetchsize决定每次网络往返拉多少行那么一个很大的结果集如果fetchsize设得很小JDBC驱动就会频繁地和数据库服务器进行多次网络往返直到所有数据都拉到客户端。每多一次往返就多一次网络延迟这在大结果集场景下会显著拉长查询总时间。我做过一个极端测试一张200万行的表做全量导出fetchsize设为10的时候跑了将近40分钟才拉完本以为是SQL太慢后来把fetchsize调到1000总时间直接降到2分钟以内。两者的SQL、索引、数据库负载完全一样唯一的差别就是网络往返次数。由此也可以反向推fetchsize设得非常大比如设为Integer.MAX_VALUE效果就趋向于“一次拉完所有数据”——客户端内存压力会急剧上升。所以这本质上是一个在“网络往返开销”和“客户端内存开销”之间找平衡的参数。提示很多人在排查慢查询时习惯性地只看SQL执行计划和数据库监控往往漏掉了客户端取数机制。如果发现数据库端查询很快但应用拿到全部数据要很久那就必须怀疑fetchsize的配置。2. 不同数据库驱动的默认行为和脾气2.1 Oracle默认一次只取10行藏着个经典大坑Oracle的JDBC驱动ojdbc对fetchsize的处理是我接触过的数据库里最“有个性”的。它默认的fetchsize是10意味着如果你不去显式设置查询返回100万行就要进行10万次网络往返。第一次给一个Oracle项目做数据导出时我就被默认值坑过2.6万行的查询结果愣是跑了十几分钟。这个坑特别隐蔽的原因在于Oracle官方文档说defaultRowPrefetch的值是10但不少框架和中间件会在底层悄悄覆盖它。比如某些老版本的MyBatis或者项目里引入的某些数据访问组件封装之后到底是10还是1000根本看不出来。我见过最有意思的一次排查同一个SQL在PL/SQL Developer里执行只要1秒换到Java程序里执行要30秒真相揭开时才发现就是fetchsize默认值在作怪。另外Oracle驱动还有一个很多人不知道的“隐式取数”行为。当你的SQL带主键等值条件且明确最多只返回一行时驱动会智能地尝试直接取一条绕开fetchsize的限制。但普通查询没这么好的待遇所以该设还得设。2.2 MySQL默认全部捞回流式读取另有一番天地MySQL的Connector/J驱动默认情况下和Oracle完全相反——它默认把查询结果一次性全部拉到客户端内存。对于小结果集这是最省事的因为应用代码读取时就是纯本地操作速度极快。但一旦查询涉及几百万行数据默认行为就会直接击穿JVM堆内存。此外MySQL还支持一种流式读取方式把fetchsize设为Integer.MIN_VALUE或者使用useCursorFetchtrue配合较小的fetchsize驱动不会一次性拉全量数据而是按需从服务器逐批获取。这个在某些场景下是救命稻草比如一张大表要做全表扫描同步标准做法就是用游标配合小fetchsize否则内存必然爆。但也正因为MySQL的默认行为是“全量拉取”很多开发者在迁移到Oracle、达梦、人大金仓这类数据库时会莫名遇到“同样的代码、同样的数据量怎么回传慢这么多”的问题十有八九就是没适配新数据库的fetchsize行为。2.3 PostgreSQL、达梦、人大金仓这些又是什么表现PostgreSQL的JDBC驱动默认是0这个0的含义是“由驱动自行决定”底层往往是把所有结果直接拉到客户端行为上和MySQL的全量拉取差不多。PG的驱动其实也支持以游标方式分批取数但需要设置autocommitfalse且fetchsize大于0否则它会自动用全量拉取的方式处理。至于达梦、人大金仓这类国产数据库兼容性做得好的版本基本都会参考Oracle的行为在JDBC层支持fetchsize的显式设置但默认值未必和Oracle一致。建议在项目启动时打印一下驱动版本和关键参数或直接在DAO层显式固定住fetchsize不要依赖默认值。我还测试过对Hibernate、MyBatis这类框架的适配情况MyBatis对fetchsize没有统一配置入口需要在select标签里配fetchSizeHibernate则可以通过hibernate.jdbc.fetch_size全局指定。框架不背这个锅最终生效的还是底层JDBC驱动但不同框架覆盖默认值的时机和方式不同排查时得沿着调用链一路找下去。3. 哪些场景必须调整 fetchsize哪些场景别乱动3.1 大结果集导出与批处理最值得调整的场景如果你写过数据导出、报表生成、数据迁移这类功能那fetchsize就是你的生死线。这类场景有一个共同特点查询本身没什么可优化的SQL该走的索引都走了但结果集就是很大动辄几十万行以上。我个人的经验值是这样单次导出10万行以内的数据fetchsize设在100到200之间内存和性能比较均衡10万到100万行设在500到1000超过百万行建议配合游标使用fetchsize设在1000到5000之间。很多资深DBA还会在服务端配合设置fetch批次的网络缓冲区大小但这属于更进阶的操作普通应用层开发只要控制好fetchsize就够了。另外做批量导出时不要在一个事务里把所有数据捞完。如果业务允许分段查询配合合理的fetchsize要比一次大查询更稳。我见过有同事写导出功能一条SQL捞500万行fetchsize设成10000结果导出任务跑到一半应用内存直接飙到4个G最后OOM重启。分段加控制fetchsize两全其美。3.2 分页查询默认配置通常就够分页查询比如常见的LIMIT offset, size或ROWNUM分页和fetchsize的关系没那么大。因为最终返回的结果集本身就很小一页就几十行不管fetchsize默认是10还是全量拉取对整体性能影响微乎其微。这里没必要专门花心思调这个参数反而应该把精力放在分页SQL的写法上避免深分页导致的全表扫描。真正的例外是那种“假分页”SQL查出全部数据在应用层内存里做分页切片。这种写法本身就不推荐无论fetchsize怎么调都只是延迟了爆内存的时间而已。如果有系统还在用这种方案建议优先重构而不是纠结fetchsize。3.3 任务调度和定时同步选错值真的会出事故定时同步任务比如凌晨从业务库同步数据到数仓表面上看和导出类似但它更怕的是内存抖动和连接占用。同步任务通常由调度框架统一管理如果一个任务占用了连接池里的一堆连接且长时间不释放其他业务线程就会因为没有连接而排队等待严重的会拖垮整个应用。所以同步场景里fetchsize不能太小太小意味着连接持有时间变长也不能太大太大会出现内存峰值。我一般建议这类任务单独使用一个独立连接池连接池最大连接数控制在2到3个fetchsize设置500左右在内存和速度之间取一个相对稳妥的中间值。如果数据库是Oracle还可以在同步任务执行期间显式设置defaultRowPrefetch来规避驱动默认值带来的慢速拉取问题。4. 参数到底怎么选一个经验向的计算方法4.1 先估算自己的内存预算跟很多开发聊fetchsize时我发现一个共性问题大家不知道自己的JVM到底能承受多大的结果集。实际上这个是可以算出来的。一条数据的字节数大致等于各字段类型字节数的总和加上行头开销一般30到50字节。假如你查询的表有20个字段平均每行数据在800字节左右fetchsize设为1000那么客户端一次拉取占用的内存大概是800KB。看起来不多但如果连接池有20个连接同时执行同样配置的查询内存占用就是16MB。再算上结果集包装对象、JDBC驱动内部的缓冲实际开销通常要再乘以2到3倍。所以比较稳妥的做法是先估算单行大致大小再估算应用JVM堆内存里能分给查询结果集的比例一般不要超过堆的20%倒推出合适的fetchsize。比如JVM堆是4G查询缓冲预算800MB单行800字节fetchsize设为1000时单连接缓冲不到1MB100个并发连接也只占100MB还有很大余量。4.2 结合网络延迟做权衡内网环境下数据库和应用服务器之间延迟一般在0.5毫秒到2毫秒之间fetchsize设大一点降低往返次数收益很直接。如果应用和数据库跨机房网络延迟到10毫秒以上那fetchsize的影响会成倍放大——一次拉一行光等网络就够呛。我做过一个对比在跨机房环境下同样的查询fetchsize10耗时23秒fetchsize100耗时7秒fetchsize1000耗时4.5秒但fetchsize5000的时候耗时反而回升到5秒左右因为驱动内部和GC的负担上来了。这个实验虽不算严格科学但它说明一个道理跨网络场景下把fetchsize设大一些通常是对的但并不是越大越好要找到拐点。4.3 不同驱动和框架的配置示例这里给出几个常见的配置写法可以直接抄作业JDBC原生的写法Statement stmt conn.createStatement(); stmt.setFetchSize(500); ResultSet rs stmt.executeQuery(select * from big_table); // 拉取结果时驱动会按每次500行分批获取PreparedStatement也可以PreparedStatement ps conn.prepareStatement(select * from big_table where status ?); ps.setFetchSize(500); ps.setInt(1, 1); ResultSet rs ps.executeQuery();MyBatis在Mapper XML里设置select idselectBigData resultTypecom.example.BigData fetchSize500 select * from big_table /selectSpring JdbcTemplate的话需要重写部分逻辑JdbcTemplate jdbcTemplate new JdbcTemplate(dataSource); jdbcTemplate.setFetchSize(500); // 或者通过StatementCallback在内部设置Hibernate全局设置在application.properties或hibernate.cfg.xml中配置spring.jpa.properties.hibernate.jdbc.fetch_size500注意MyBatis的fetchSize是逐条SQL配置的如果一个项目里几百条SQL都想统一处理只能写拦截器或者在每个select标签里配这点跟Hibernate的全局配置不太一样。5. 实战踩过的坑与排查指南5.1 从“内存爆了”到“查询变慢”的排查记录有一个项目让我印象很深数据同步服务每天凌晨从Oracle拉取数据到MySQL某天开始频繁报OOM运维直接半夜打电话把我叫起来。排查的第一步我看了堆转储发现大量byte数组占用了超过70%的空间——这是JDBC驱动接收数据的典型特征。第二步查代码发现同步SQL没有显式设置fetchsize走的是Oracle驱动的默认值10按说默认值10不会导致OOM才对但问题是这个代码用了循环加批量插入的方式先查出一批数据遍历插入再查下一批。由于默认fetchsize小遍历还没结束驱动又在后台分批拉取后续数据堆里堆积了大量等待处理的结果集数据。最后把fetchsize调到200并把遍历逻辑改成边拉边处理问题就消失了。这个案例的教训是fetchsize不只是“内存大或小”的问题它还控制着结果数据的“到达节奏”。如果消费速度跟不上拉取速度积压是必然的。要保证整个链路的节奏是一致的。5.2 “我设了fetchsize怎么没效果”的排查思路这个问题几乎每隔一段时间就会有人来问。先别急着怀疑数据库驱动按下面几个方向排查先确认设置方法对不对。Statement.setFetchSize()必须在executeQuery()之前设置而且只对当前Statement生效。如果你用的是连接池连接池返回的是代理Statement某些情况下配置不会完全透传最好的办法是在获取到真实Statement之后再设置。再确认是否被框架覆盖。有些ORM框架有自己的查询配置比如Hibernate的批量抓取batch_fetch_size和JDBC fetchsize是两码事不要把两者混为一谈。MyBatis中如果配置了特定的ResultHandler也可能影响fetchsize的生效方式。还要确认数据库驱动版本。不同版本的驱动对fetchsize的实现会有差别尤其国产数据库某个小版本可能根本不支持这个参数。不放心的话可以写个小程序手动设置fetchsize后打印Statement.getFetchSize()和ResultSet.getFetchSize()看看实际生效的值是多少。5.3 别忽略fetchsize和事务的关联很多人在设置fetchsize时忽略了一个重要前提对于某些数据库fetchsize只在非自动提交模式下才真正生效。典型的就是PostgreSQL和MySQL的游标模式。MySQL的游标读取必须先关闭自动提交。也就是说如果你用默认的autocommittrue即使设置了useCursorFetchtrue和fetchsize驱动也可能不会走流式路径。我这里给一个踩坑代码示例很多人就是这样调了半天没效果Connection conn dataSource.getConnection(); conn.setAutoCommit(false); // 别忘了关闭自动提交 PreparedStatement ps conn.prepareStatement(select * from huge_table); ps.setFetchSize(100); ResultSet rs ps.executeQuery(); // 处理完结果后必须提交或回滚事务来释放游标对MySQL来说还需要在连接URL里加上useCursorFetchtrue参数jdbc:mysql://localhost:3306/test?useCursorFetchtrue否则光在代码里设置fetchsize是不起作用的。而Oracle和达梦对自动提交的依赖没那么强但建议依然保持显式控制事务边界避免因游标未关闭导致的资源泄漏。5.4 常见问题速查表现象可能原因排查方向查询数据库端只要2秒应用端等了20秒fetchsize过小导致网络往返过多将fetchsize调至500~1000再测试设置fetchsize后没有效果连接池代理Statement问题、框架覆盖、未设非自动提交打印getFetchSize()查看实际值应用OOM堆里全是byte数组fetchsize过大或拉取节奏过快调小fetchsize优化消费逻辑设置了游标读取但数据仍全量加载MySQL未开启useCursorFetch或未关autocommit检查连接URL参数与自动提交设置批量导出内存波动剧烈每批拉取数据量与消费速度不匹配拆成分段查询设置合理fetchsize不同数据库之间性能差异大默认fetchsize行为不同统一显式设置fetchsize6. 一个可以落地的调优模板6.1 根据查询特点归类配置在我目前参与的项目里我一般会把查询按特点分成几类然后分别配置fetchsize。这样做的好处是规则统一新同事接手也不会一头雾水。代码中需要有一个统一管理JDBC配置的地方。如果直接用JDBC可以用Java配置类集中定义不同类型的fetchsize常量。如果使用MyBatis则通过一个自定义拦截器按SQL类型自动注入fetchsize避免几百个Mapper逐个去配。简单查询和小结果集查询保持默认或设置100以内。分页查询不特别设置。大结果集导出类查询根据数据量设500到2000之间对流式处理场景可以更大一些。6.2 配合连接池配置更稳连接池设置也和fetchsize有直接关系。如果fetchsize设得较大单个查询占用连接的时间会拉长连接池的最大连接数可以适当缩小反过来fetchsize小但并发高连接数就得加大否则会出现连接等待。我常用的一组配置是Druid或HikariCP中最大连接数根据QPS和单次查询时长动态评估。比如一个查询平均耗时200msQPS峰值为100理论上需要100*0.220个连接再留30%的冗余最大连接数设30左右就够了。这时候fetchsize设置在500左右整体表现非常稳定。对于大批量同步类任务我还会单独开一个数据源不从业务连接池里取连接防止同步任务长时间占用连接拖垮在线业务。6.3 把fetchsize纳入SQL评审清单最后一点建议把fetchsize的检查纳入SQL评审的固定项。凡是包含大表查询、全表扫描、导出类SQL的代码评审必须问一句这个查询的结果集会多大fetchsize设了吗如果是数据库迁移或新增数据源还要检查驱动默认值是否合适。我所在团队把这条写进了代码规范以后因为fetchsize导致的生产问题明显少了很多。这个参数本身不难难的是在脑子里时刻有这个弦每次写数据访问代码时多问自己一句。不要指望靠默认值走天下。7. 从实战角度总结几个记住就不会亏的原则再分享一段个人经验收尾。这些原则都是我在项目里用真金白银踩出来的照着做基本不会出大错。第一永远不要依赖驱动的默认fetchsize除非你的查询结果集很小而且你清楚默认值是多少。默认值在Oracle和MySQL里完全是两个极端生产环境切换数据库时第一件事就是把所有数据访问层的fetchsize统一显式设置一遍。第二fetchsize不是越大越好。它只是一个控制“每次网络拉取行数”的参数真正的目的是平衡网络往返和内存占用。一个常见误区是以为把fetchsize设得巨大就能让SQL变快——在数据量不大时没差别在数据量大时直接OOM。第三排查fetchsize问题时不要只看代码。连接池、ORM框架、JDBC驱动版本、数据库配置每一层都可能对这个参数产生干扰。最快的定位方式是写一个最小化测试程序直连数据库测试同一个SQL分别设置不同的fetchsize比对耗时就能快速锁定问题。第四对于强调稳定性的生产系统宁可fetchsize稍微偏小也不要冒进。比如500行一批耗时多一两秒但内存占用可控系统稳定压倒一切。而同步工具、迁移任务这类跑批场景则可以激进一点。希望这篇文章能帮你在将来遇到fetchsize问题时少走一些弯路。