求求你们了,MyBatis 批量插入别再乱用 foreach 了,5000 条数据花了 14 分钟。。...

2023-11-01

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。

mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)

<insert id="batchInsert" parameterType="java.util.List">
    insert into USER (id, name) values
    <foreach collection="list" item="model" index="index" separator=","> 
        (#{model.id}, #{model.name})
    </foreach>
</insert>

这个方法提升批量插入速度的原理是,将传统的:

INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");

转化为:

INSERT INTO `table1` (`field1`, `field2`) 
VALUES ("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2");

在MySql Docs中也提到过这个trick,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中。

理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。

乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。

在资料中也提到了一句话:

Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it into smaller sizes.

它强调,当插入数量很多时,不能一次性全放在一条语句里。可是为什么不能放在同一条语句里呢?这条语句为什么会耗时这么久呢?

我查阅了资料发现:

Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:

  • some database such as Oracle here does not support.

  • in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.

Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.

SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);
for (Model model : list) {
    session.insert("insertStatement", model);
}
session.flushStatements();

Unlike default ExecutorType.SIMPLE, the statement will be prepared once and executed for each record to insert.

从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。

在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有<foreach>的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。

Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.

MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it contains <foreach /> element and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement.

And these steps are relatively costly process when the statement string is big and contains many placeholders.

[1] simply put, it is a mapping between placeholders and the parameters.

从上述资料可知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。

38dc59572bcf2db4470328403bdd6102.png

所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。

重点来了。上面讲的是,如果非要用<foreach>的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看 http://www.mybatis.org/mybatis-dynamic-sql/docs/insert.html 中 Batch Insert Support 标题里的内容)

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
    SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
    List<SimpleTableRecord> records = getRecordsToInsert(); // not shown
 
    BatchInsert<SimpleTableRecord> batchInsert = insert(records)
            .into(simpleTable)
            .map(id).toProperty("id")
            .map(firstName).toProperty("firstName")
            .map(lastName).toProperty("lastName")
            .map(birthDate).toProperty("birthDate")
            .map(employed).toProperty("employed")
            .map(occupation).toProperty("occupation")
            .build()
            .render(RenderingStrategy.MYBATIS3);
 
    batchInsert.insertStatements().stream().forEach(mapper::insert);
 
    session.commit();
} finally {
    session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
        "insert into tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
    ps.setString(1,name);
    ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。

总结一下,如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用 <foreach>的插入的话,需要将每次插入的记录控制在 20~50 左右。

参考资料

  1. https://dev.mysql.com/doc/refman/5.6/en/insert-optimization.html

  2. https://stackoverflow.com/questions/19682414/how-can-mysql-insert-millions-records-fast

  3. https://stackoverflow.com/questions/32649759/using-foreach-to-do-batch-insert-with-mybatis/40608353

  4. https://blog.csdn.net/wlwlwlwl015/article/details/50246717

  5. http://blog.harawata.net/2016/04/bulk-insert-multi-row-vs-batch-using.html

  6. https://www.red-gate.com/simple-talk/sql/performance/comparing-multiple-rows-insert-vs-single-row-insert-with-three-data-load-methods/

  7. https://stackoverflow.com/questions/7004390/java-batch-insert-into-mysql-very-slow

  8. http://www.mybatis.org/mybatis-dynamic-sql/docs/insert.html

版权声明:本文为CSDN博主「huanghanqian」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/huanghanqian/article/details/83177178

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

求求你们了,MyBatis 批量插入别再乱用 foreach 了,5000 条数据花了 14 分钟。。... 的相关文章

  • SFTP Java - 管道关闭 Jsch 异常

    我正在研究一种 java 方法 将文件从一个位置复制到另一个远程位置 我的代码如下 我尝试使用jsch 0 1 42 0 1 50 0 1 54 public static void processFiles ArrayList
  • 超时时杀死或终止子进程?

    我想尽可能快地重复执行子进程 然而 有时这个过程会花费太长的时间 所以我想杀死它 我使用 signal signal 如下所示 ppid pipeexe pid signal signal signal SIGALRM stop handl
  • SQL 未插入到 Yii 中具有关系的表中

    我正在尝试创建一个用户 但所有值都没有插入到数据库中 Systems user 表与partys 表有关系 因为party id 是sytems user 的主键 没有插入任何内容 甚至没有错误 它只是返回到 创建 页面 这是我的架构 Ta
  • Java 日期和 MySQL 时间戳时区

    我正在编辑一段代码 其基本功能是 timestamp new Date 然后坚持下去timestamp中的变量TIMESTAMPMySQL 表列 然而 通过调试我看到Date显示在正确时区的对象 GMT 1 当持久化在数据库上时 它是GMT
  • mysql 准备好的语句错误:MySQLSyntaxErrorException

    我使用准备好的语句编写了选择语句 每次尝试运行都会出现此错误 我如何克服这个错误 我的jdbc连接器是mysql connector java 5 1 13 bin jar 我的代码 public Main add ad to getAdD
  • jasper 报告文件中出现错误

    首先 我在 iReport 5 1 0 中创建一个 R D1 jrxml 文件 我执行该报告的 Java 代码如下所示 import java sql Connection import java sql DriverManager imp
  • 如何让 Camel FTP 按需只获取一次

    我对骆驼还很陌生 我一直在尝试让 Camel 根据需要仅通过 FTP 获取单个文件一次 我无法让它发挥作用 这是我尝试过的 让我知道什么是最好的方法以及我的代码有什么问题 1 读取文件后发送一条空消息当收到空消息时 停止路由 from di
  • 将 ASCII 字符转换为“”unicode 表示法的脚本

    我正在对 Linux 区域设置文件进行一些更改 usr share i18n locales like pt BR 并且需要格式化字符串 例如 d m Y H M 必须以 Unicode 指定 其中每个 在本例中为 ASCII 字符表示为
  • java.lang.IllegalStateException:FragmentManager 已被销毁

    活动中onResume我称之为 volley request 的方法 它获取项目列表 然后将它们加载到此活动内的 ListFragment 中 当我第一次进入活动时 一切正常 但当我重新进入活动时 ListFragment 为空 并且控制台
  • 如何使用 python-gnupg 加密大型数据集而不占用所有内存?

    我的磁盘上有一个非常大的文本文件 假设它是 1 GB 或更多 还假设该文件中的数据有 n每 120 个字符一个字符 我在用python gnupg https pythonhosted org python gnupg 对此文件进行加密 由
  • 如何查找列表/集合是否包含在另一个列表中

    我有一个产品 ID 列表 我想找出哪些订单包含所有这些产品 订单表的结构如下 order id product id 1 222 1 555 2 333 显然我可以通过 PHP 中的一些循环来做到这一点 但我想知道是否有一种优雅的方法可以纯
  • 优雅地避免 Java 中的 NullPointerException

    考虑这一行 if object getAttribute someAttr equals true 显然这一行是一个潜在的错误 属性可能是null我们会得到一个NullPointerException 因此我们需要将其重构为以下两个选择之一
  • 对 pandas 数据框中的每一列应用函数

    我如何以更多的熊猫方式编写以下函数 def calculate df columns mean self df means for column in df columns columns tolist cleaned data self
  • 如何将字符串解析为map

    有一个像A B C D E F这样的字符串 如何将其解析为map 我会使用拆分 String text A B C D E F Map
  • SWT StyledText 有高度限制吗?

    我正在尝试创建一个应用程序 其中包含在 ScrolledComposite 中显示的 StyledText 框 我在 StyledText 框中显示大量行时遇到困难 超过 2 550 行似乎会导致问题 StyledText 框本身不能有滚动
  • Synapse Notebook 参考 - 使用参数从另一个笔记本调用 Synapse Notebook

    我有一个带有参数的突触笔记本 我试图从另一个笔记本调用该笔记本 我正在使用 run 命令 我应该如何将参数从基本笔记本传递到正在调用的笔记本 另外 对我来说 上述答案不起作用 作为对此问题的单独解决方案 下面是一个答案 打开笔记本并转到最右
  • 升级后 pip 损坏

    我做了 pip install U easyinstall 然后 pip install U pip 来升级我的 pip 但是 当我尝试使用 pip 时 我现在收到此错误 root d8fb98fc3a66 which pip usr lo
  • Elastic Beanstalk 上的 Django + MySQL - 查询 MySQL 时出错

    当我在 Elastic beanstalk 上托管的 Django 应用程序上查询 MySQL 时 出现错误 错误说 admin login 处出现操作错误 1045 用户 adminDB 172 30 23 5 的访问被拒绝 使用密码 Y
  • 如何指定一个变量作为类或类实例的成员变量?

    在最新的 Python 2 7 x 中 给定类定义内的任何成员变量 该成员变量是否始终处于类级别 因为它是由该类的所有实例共享的单个变量 在类的定义中 如何指定 类定义中的哪些成员变量属于该类 因此由该类的所有实例共享 以及 哪些属于该类的
  • Spring - 使用存储过程时使用 simplejdbccall 进行批量更新

    我正在使用 spring jdbc 模板 使用存储过程创建记录 public Long create City obj SimpleJdbcCall jdbcCall new SimpleJdbcCall getJdbcTemplate g

随机推荐

  • c++类模板与继承

    继承中父子类和模板类的结合情况 1 父类是一般类 子类是模板 类 2 父类是模板类 子类是一般类 3 父类和子类都是模板类 1 include
  • 定义简单的切面

    连接点方法 preform public interface Performance void preform 定义实现类 Component public class Concert implements Performance Over
  • 【安装Pytorch】

    1 查看已有的虚拟环境 conda env list 2 创建自己的虚拟环境 conda create n name python 3 8 示例 conda create n pytorch python 3 8 param name 3
  • 2个月月活突破1亿,增速碾压抖音,出道即封神的ChatGPT,现在怎么样了?ChatGPT它会干掉测试?

    从互联网的普及到智能手机 都让广袤的世界触手而及 如今身在浪潮中的我们 已深知其力 前阵子爆火的ChatGPT 不少人保持观望态度 现如今 国内关于ChatGPT的各大社群讨论 似乎沉寂了不少 现在怎么样了 我们先来复盘 ChatGPT是一
  • 极限的运算法则

    极限的运算法则 两个无穷小的和也是无穷小 定理 有限个无穷小的和也是无穷小 无穷多个无穷小的和是1 定理 有界函数与无穷小的乘机也是无穷小 推论 常数与无穷小的乘积也是无穷小 推论 有限个无穷小的乘积也是无穷小 无限多个无穷小的乘积不一定是
  • 解决eclipse突然打不开了的问题

    网上搜了有大概六种 我又get一个解决方案 找到属于自己的方案才是关键 解决方式 7种 1 通过在命令行中输入 where java 找到除jdk目录下的所有java相关程序 直接删掉 一般会在C WINDOWS system32下 2 内
  • SQL刷题:SQL必知必会

    MySQL函数参考教程 1 多字段排序用逗号隔开 对多个字段排序的时候 同样满足默认ASC select cust id order num from Orders order by cust id order date desc sele
  • PicGo使用阿里云OSS配置图床

    PicGo使用阿里云OSS配置图床 前言 安装 PicGo PicGo molunerfinn com 以上是PIcGo的配置阿里云OSS需要的值 1 阿里云创建用户 登录阿里云 鼠标移到头像位置则可看到 点击访问控制 创建新用户 开启op
  • 摩拜单车红包骑行引发的潮汐问题思考

    前言 摩拜刚刚推行红包骑行的时候写的 最近比较忙一直忘记了 今天有时间发出来 言语不当之处 还望见谅 希望各位发表意见 谢谢 PS 摩拜单车相关人员已经看过 表示还可以 谢谢 2017 年 共享单车有很多 最先进入人们眼帘的应该是那一个类似
  • Part 5:Pandas 数据统计函数【汇总类统计、唯一去重和按值计数、相关系数和协方差】

    Pandas 数据统计函数 1 汇总类统计 2 唯一去重和按值计数 3 相关系数和协方差 import pandas as pd 1 预备步骤 对数据进行读取和预处理 将温度都改为Int类型 fpath datas beijing tian
  • jquery正则表达式写法

    const FloatRegex 0 9a zA Z 1 if FloatRegex test value this message error 只能输入数字 字母 下划线 小圆点 return
  • 计算机基础知识总结

    文章目录 1 Linux常用基本命令 文件管理 用户管理 进程管理 软件仓库搭建 远程登陆服务器 文件传输 定时任务 延迟任务 2 C 基础知识 面向对象的四大特征 C 和C语言有什么不同 new delete 和 malloc free
  • 【翻译】API版本管理:它是什么,为什么这么难?

    如果你想在API技术专家之间展开一场辩论 只要让他们分享他们对 API版本 的看法 可以肯定的是 你会在短时间内发现一些强烈的感受 术语 API版本 已经成为 改变API 的同义词 这是理清支持已发布的API持续变化而又不给API消费者带来
  • java中方法名,对象名,变量名,标识符,关键字的规则

    java中方法名 对象名 变量名 标识符 关键字的规则 总绪 方法名 对象名 变量名 标识符 关键字 总绪 所有命名规则必须遵循以下规则 1 名称只能由字母 数字 下划线 符号组成 2 不能以数字开头 3 名称不能使用JAVA中的关键字 4
  • 机试之前最后的知识点总结。

    题目描述 C 头文件大全 解题思路一 include
  • 敏捷Agile: Epic vs UserStory vs Task

    Epic Story Task Epic Epic是User Story逻辑上的集合 一个Epic可以被break down成多个小的User Story 一个Epic可能需要多个Sprint才能完成 User Story vs Task
  • TensorFlow各版本下载地址

    TensorFlow各版本自主下载地址 cpu版本 pip install 链接 https pypi python org pypi tensorflow 2 0 0 下载地址链接 https pypi org project tenso
  • MATLAB实现基本的PSO粒子群算法优化目标函数(求函数最小值的解),写成函数的形式,并举例演示如何使用

    粒子群优化算法 Particle Swarm Optimization 是一种进化计算机技术 evolutionary computation 源于对鸟类捕食行为的研究 是一种基于迭代的优化工具 事实上 像PSO这种同属于元启发式优化算法有
  • java中静态代码块详解

    今天在项目中看到这行代码 静态代码块 很久没用静态代码块了 今天来复习一下 依旧按照为什么 是什么 怎么用 的思想 去学习 为什么 先说说代码中为什么要使用这种写法吧 还记得静态代码块的特点吗 随着类的加载而执行 而且只执行一次 对 没错
  • 求求你们了,MyBatis 批量插入别再乱用 foreach 了,5000 条数据花了 14 分钟。。...

    近日 项目中有一个耗时较长的Job存在CPU占用过高的问题 经排查发现 主要时间消耗在往MyBatis中批量插入数据 mapper configuration是用foreach循环做的 差不多是这样 由于项目保密 以下代码均为自己手写的de