MyBatis批量insert-select踩坑:useGeneratedKeys=true 可能让PostgreSQL返回大量插入结果

最近在排查一个批量同步数据的问题时,发现一个很容易被忽略的配置点: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 就是一个典型例子。

相关推荐
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码代码逐行目检
前端·后端·go
贰先生1 小时前
Xiuno BBS X版 用户封禁系统
后端
karry_k1 小时前
PostgreSQL 在 MyBatis 中执行正常 SQL 失效:一次 DELETE USING 踩坑记录
java·后端
ServBay1 小时前
不会写代码也能建站?AI 时代,非技术创始人如何从零搭建自己的 Web 项目
后端·mcp
Moladev2 小时前
如何在 Electron 中接入 OpenAI 兼容的大模型 API:Snaptium 的主进程代理实践
后端
Oneslide2 小时前
根分区爆满却找不到大文件?深度解析 Linux df 与 du 不一致的经典故障
后端
魏祖潇2 小时前
framework 整合实战——DDD/TDD/SDD 三件套在 framework 仓的真实落地
人工智能·后端
神奇小汤圆2 小时前
责任链模式 + 策略模式:优雅处理多级请求的方式
后端
神奇小汤圆2 小时前
没啃透无锁队列,高并发底层你只懂了皮毛!
后端