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