最近在排查一个批量同步数据的问题时发现一个很容易被忽略的配置点useGeneratedKeys。这个配置平时看起来只是“是否回填自增主键”但在 PostgreSQL pgJDBC 批量insert into ... select ...场景下如果全局或局部误开启可能会造成大量插入结果被返回到 Java 侧进而带来内存压力、耗时增加甚至出现 OOM 风险。一、问题背景业务里有一段同步账期中间表的 SQL大致逻辑是!-- 计资名单账期中间表 - 同步在职数据 -- insert idsyncOnJobs parameterTypejava.util.Map useGeneratedKeysfalse insert into xxx.salary_list_middle_monthly ( period, include refidsalaryMonthlyOnJobSyncColumns/ ) select #{period} as periodStr, include refidsalaryMonthlyOnJobSyncColumns/ from xxx.salary_list_middle where p_task_unique_id #{globalTaskUniqueId} and join_date ![CDATA[ ]] #{lastDayOfMonth} and ( enable 1 or (enable 2 and depart_date ![CDATA[ ]] #{lastDayOfMonth}) ) /insert这类SQL的特点是insert into target_table (...) select ... from source_table where ...它不是单条新增而是批量插入。 正常情况下我们只关心插入成功了吗 影响了多少行 不关心每一行插入后的主键ID。 所以这个SQL里显式写了useGeneratedKeysfalse一开始看这个配置好像没什么必要因为 MyBatis 默认一般也是false。但后来发现如果项目中有全局配置或者某个 insert 误开启了useGeneratedKeystrue在 PostgreSQL 场景下可能带来额外风险。二、useGeneratedKeys 到底是干什么的useGeneratedKeys是 MyBatis 的 insert 配置项。 它的作用是执行 insert 后是否通过 JDBC 获取数据库自动生成的主键并回填到 Java 对象中。常见用法如下insert idinsertUser useGeneratedKeystrue keyPropertyid keyColumnid insert into user(name, age) values (#{name}, #{age}) /insertJava 代码User user new User(); user.setName(张三); user.setAge(20); userMapper.insertUser(user); // 插入后数据库生成的 id 会回填到 user.id System.out.println(user.getId());这个场景下useGeneratedKeystrue是合理的。 因为这是单条插入而且业务确实需要拿到数据库生成的主键。三、真正的问题批量 insert-select 不适合开启 generated keys对于下面这种 SQLinsert into salary_list_middle_monthly (...) select ... from salary_list_middle where ...如果一次插入几万行、几十万行正常情况下数据库只需要返回一个影响行数。 例如INSERT 0 50000Java 侧不需要拿到这 50000 行数据。但如果开启了useGeneratedKeystrue MyBatis 底层会走 JDBC 的 generated keys 机制类似于在 PostgreSQL JDBC 驱动中为了实现这个机制可能会通过RETURNING的方式让数据库返回插入结果。如果没有明确指定只返回某个主键列就有可能出现类似效果insert into salary_list_middle_monthly (...) select ... from salary_list_middle where ... returning *这就不一样了。原来只是返回影响行数现在可能变成返回本次插入的所有行数据也就是说如果插入 10 万行表里有几十个字段那么这些数据可能都会作为 ResultSet 返回到 Java 侧。四、为什么这会导致内存问题普通查询的 ResultSet在没有特殊配置的情况下JDBC 驱动可能会把结果集完整拿到客户端。对于正常 select这个问题大家比较容易理解select * from big_table;如果一次查出几十万行Java 内存肯定有压力。但这次比较隐蔽的是我们明明写的是 insert。表面看是insert into ... select ...这时候 Java 侧要处理的就不只是“影响行数”而是一个很大的 ResultSet。所以问题链路可以总结为批量 insert-select ↓ useGeneratedKeystrue ↓ JDBC 触发 RETURN_GENERATED_KEYS ↓ pgJDBC 可能使用 RETURNING 返回插入结果 ↓ 大量行数据被返回到 Java 侧 ↓ ResultSet 占用内存 ↓ 耗时增加 / GC 压力 / OOM 风险五、为什么这个坑容易被忽略因为useGeneratedKeys看起来只是一个“主键回填配置”。很多人会认为我没写 keyProperty应该没事。或者我这个 SQL 不关心主键应该不会有影响。但实际上只要 MyBatis/JDBC 层面启用了 generated keys驱动就可能按“需要返回生成值”的方式去执行 SQL。尤其是 PostgreSQL 这种支持RETURNING的数据库驱动实现上可能会让 insert 返回结果集。所以这个配置不只是“Java 对象是否回填 id”的问题它还可能改变 JDBC 执行 insert 的行为。六、正确做法对于批量同步类 SQL尤其是这种insert into target_table (...) select ... from source_table where ...如果业务不需要拿新增主键建议显式关闭insert idsyncOnJobs parameterTypejava.util.Map useGeneratedKeysfalse即使 MyBatis 默认一般是false也建议在这种大批量 insert 上明确写出来。原因是可以避免被全局配置影响。例如项目里如果配置过setting nameuseGeneratedKeys valuetrue/或者框架封装层默认给 insert 开启了主键回填那么局部 SQL 显式写useGeneratedKeysfalse就能起到兜底保护作用。七、什么时候可以用 useGeneratedKeystrue适合场景单条 insert 主键由数据库生成 业务需要拿到插入后的 id 有明确的 keyProperty 和 keyColumn例如insert idinsertUser useGeneratedKeystrue keyPropertyid keyColumnid insert into user(name, age) values (#{name}, #{age}) /insert不适合场景批量 insert-select 批量 insert 大数据量 参数是 Map 没有实体对象接收主键 业务不关心新增 ID 只是做数据同步尤其是下面这种insert idsyncData parameterTypejava.util.Map insert into target_table (...) select ... from source_table /insert应该明确关闭useGeneratedKeysfalse八、这次问题的核心结论这次踩坑点可以总结成一句话在 PostgreSQL MyBatis 场景下批量insert into ... select ...不要开启useGeneratedKeys。如果开启pgJDBC 可能为了实现 generated keys 机制让 insert 返回大量结果集导致数据堆积在 Java 内存中。更具体一点普通 insert-select 数据库只返回影响行数。 开启 useGeneratedKeys 后 JDBC 会要求返回 generated keys。 PostgreSQL 驱动可能通过 RETURNING 实现。 如果返回列没有明确限制可能返回插入后的整行数据。 大批量插入时Java 侧就会收到一个很大的 ResultSet。所以对于批量同步 SQL推荐写法是insert idsyncOnJobs parameterTypejava.util.Map useGeneratedKeysfalse九、排查建议如果怀疑自己也遇到了类似问题可以从这几个方向排查1. 检查 MyBatis 全局配置重点看有没有setting nameuseGeneratedKeys valuetrue/或者 Spring Boot 配置里有没有类似mybatis: configuration: use-generated-keys: true2. 检查大批量 insert SQL重点关注insert into ... select ...以及批量插入insert into ... values (...), (...), (...)如果这些 SQL 不需要回填主键建议显式写useGeneratedKeysfalse3. 检查是否返回了大量 ResultSet可以通过以下方式辅助判断JVM 内存是否异常升高 Full GC 是否明显增多 接口耗时是否卡在 insert 后 数据库执行时间不长但 Java 方法耗时很长 批量插入数据量越大Java 侧内存压力越明显4. 不要只看 SQL 执行计划EXPLAIN只能分析 SQL 本身的执行路径。但这个问题不一定是 SQL 执行慢而可能是SQL 执行完后JDBC 还在处理返回结果集所以只看数据库执行计划可能看不出来。十、最终建议我的建议是1. 单条 insert 需要主键回填时才使用 useGeneratedKeystrue。 2. 批量 insert-select 默认不要开启 useGeneratedKeys。 3. 对大批量同步 SQL建议显式配置 useGeneratedKeysfalse。 4. 如果项目开启了全局 useGeneratedKeystrue要重点检查所有批量 insert。 5. PostgreSQL 场景下尤其要注意 RETURNING 带来的结果集问题。这次问题本质不是 SQL 写错了而是 MyBatis/JDBC 层面的一个隐性行为。大数据量场景下很多平时看起来无关紧要的配置都会被放大。useGeneratedKeys就是一个典型例子。