MySQL批量插入技巧

news2024/11/16 5:51:32

关于MySQL批量插入的一些问题

MySQL一直是我们互联网行业比较常用的数据,当我们使用半ORM框架进行MySQL大批量插入操作时,你是否考虑过这些问题:

  1. 进行大数据量插入时,是否需要进行分批次插入,一次插入多少合适?有什么判断依据?
  2. 使用foreach进行大数据量的插入存在什么问题?
  3. 如果插入批量插入过程中,因为服务器宕机等原因导致插入失败要怎么办?

基于此类问题,笔者以自己日常的开发手段作为依据演示一下MySQL批量插入的技巧。

常见的3种插入方式演示

实验样本数据

为了演示,这里给出一张示例表,除了id以外,有10个varchar字段,也就是说全字段写满的话一条数据差不多1k左右:

CREATE TABLE `batch_insert_test` (
  `id` int NOT NULL AUTO_INCREMENT,
  `fileid_1` varchar(100) DEFAULT NULL,
  `fileid_2` varchar(100) DEFAULT NULL,
  `fileid_3` varchar(100) DEFAULT NULL,
  `fileid_4` varchar(100) DEFAULT NULL,
  `fileid_5` varchar(100) DEFAULT NULL,
  `fileid_6` varchar(100) DEFAULT NULL,
  `fileid_7` varchar(100) DEFAULT NULL,
  `fileid_8` varchar(100) DEFAULT NULL,
  `fileid_9` varchar(100) DEFAULT NULL,
  `fileid_10` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COMMENT='测试批量插入,一行数据1k左右';
使用逐行插入

我们首先采用逐行插入方式分别插入300010w条的数据,这里为了保证实验的准确性,提前进行代码预热,先插入5条数据,然后在进行大批量的插入:

/**
     * 逐行插入
     */
    @Test
    void rowByRowInsert() {
        //预热先插入5条数据
        performCodeWarmUp(5);

        //生成10w条数据
        List<BatchInsertTest> testList = generateBatchInsertTestData();


        long start = System.currentTimeMillis();

        for (BatchInsertTest test : testList) {
            batchInsertTestMapper.insert(test);
        }

        long end = System.currentTimeMillis();
        log.info("逐行插入{}条数据耗时:{}", BATCH_INSERT_SIZE, end - start);

    }

输出结果如下,可以看到当进行3000条数据的逐条插入时耗时在3s左右:

逐行插入3000条数据耗时:3492

而逐行插入10w条的耗时将其2min,插入表现可以说是非常差劲:

05.988 INFO  c.s.w.WebTemplateApplicationTests:55   main                    逐行插入100000条数据耗时:119678
使用foreach语法实现批量插入

Mybatis为我们提供了foreach语法实现数据批量插入,从语法上不难看出,它会遍历我们传入的集合,生成一条批量插入语句,其语法格式大抵如下所示:

 insert into batch_insert_test (id, fileid_1, fileid_2, fileid_3, fileid_4, fileid_5, fileid_6, fileid_7, fileid_8, fileid_9, fileid_10) 
 values (1, '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'),
 (2, '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'),
 (3, '1', '2', '3', '4', '5', '6', '7', '8', '9', '10');

批量插入代码如下所示:

 /**
     * foreach插入
     */
    @Test
    void forEachInsert() {
        /**
         * 代码预热
         */
        performCodeWarmUp(5);


        List<BatchInsertTest> testList = generateBatchInsertTestData();

        long start = System.currentTimeMillis();
        batchInsertTestMapper.batchInsertTest(testList);
        long end = System.currentTimeMillis();

        log.info("foreach{}条数据耗时:{}", BATCH_INSERT_SIZE, end - start);

    }

对应xml配置如下:

<!-- 插入数据 -->
  <insert id="batchInsertTest" parameterType="java.util.List">
    INSERT INTO batch_insert_test (fileid_1, fileid_2, fileid_3, fileid_4, fileid_5, fileid_6, fileid_7, fileid_8, fileid_9, fileid_10)
    VALUES
    <foreach collection="list" item="item" separator=",">
      (#{item.fileid1}, #{item.fileid2}, #{item.fileid3}, #{item.fileid4}, #{item.fileid5},
      #{item.fileid6}, #{item.fileid7}, #{item.fileid8}, #{item.fileid9}, #{item.fileid10})
    </foreach>
  </insert>

实验结果如下,使用foreach进行插入3000条的数据耗时不到1s:

10.496 INFO  c.s.w.WebTemplateApplicationTests:79   main                    foreach3000条数据耗时:403

当我们进行10w条的数据插入时,受限于max_allowed_packet配置的大小,max_allowed_packet定义了服务器和客户端之间传输的最大数据包大小。该参数用于限制单个查询或语句可以传输的最大数据量,默认情况下为4M左右,所以这也最终导致了这10w条数据的插入直接失败了。

Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (106,100,142 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.
使用批处理完成插入

再来看看笔者最推荐的一种插入方式——批处理插入,在正式介绍这种插入方式前,读者先确认自己的链接配置是否添加了这条配置语句,只有在MySQL连接参数后面增加这一项配置才会使得MySQL5.1.13以上版本的驱动批量提交你的插入语句。

rewriteBatchedStatements=true

完成连接配置后,我们还需要对于批量插入的编码进行一定调整,Mybatis默认情况下执行器为Simple,这种执行器每次执行创建的都是一个全新的语句,也就是创建一个全新的PreparedStatement对象,这也就意味着每次提交的SQL语句的插入请求都无法缓存,每次调用时都需要重新解析SQL语句。
而我们的批处理则是将ExecutorType改为BATCH,执行时Mybatis会先将插入语句进行一次预编译生成PreparedStatement对象,发送一个网络请求进行数据解析和优化,因为ExecutorType改为BATCH,所以这次预编译之后,后续的插入的SQLDBMS时,就无需在进行预编译,可直接一次网络IO将批量插入的语句提交到MySQL上执行。


@Autowired
    private SqlSessionFactory sqlSessionFactory;

/**
     * session插入
     */
    @Test
    void batchInsert() {
        /**
         * 代码预热
         */
        performCodeWarmUp(5);


        List<BatchInsertTest> testList = generateBatchInsertTestData();

        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        BatchInsertTestMapper sqlSessionMapper = sqlSession.getMapper(BatchInsertTestMapper.class);

        long start = System.currentTimeMillis();

        for (BatchInsertTest batchInsertTest : testList) {
            sqlSessionMapper.insert(batchInsertTest);
        }
        sqlSession.commit();
        long end = System.currentTimeMillis();
        log.info("批处理插入{}条数据耗时:{}", BATCH_INSERT_SIZE, end - start);

    }

可以看到进行3000条数据插入时,耗时也只需只需2ms左右:

05.226 INFO  c.s.w.WebTemplateApplicationTests:108  main                    批处理插入3000条数据耗时:179

而进行10w条数据批处理插入的时机只需4s左右,效率非常可观。

04.771 INFO  c.s.w.WebTemplateApplicationTests:108  main                    批处理插入100000条数据耗时:4635
原因分析

针对上述三种方式,笔者来解释一下为什么在能够确保不出错的情况下,批处理插入的效率最高,我们都知道MySQL进行插入操作时整体的耗时比例如下:

链接耗时 (30%)
发送query到服务器 (20%)
解析query (20%)
插入操作 (10% * 词条数目)
插入index (10% * Index的数目)
关闭链接 (10%)

由此可知,进行SQL插入操作时,最耗时的操作是链接,这也就是为什么在进行3000条数据插入时,foreach批处理插入的性能的性能表现最出色。因为逐行插入提交时,每一条插入操作都会进行至少两次的网络返回(如果生成的是stament对象则是两次,PreparedStatement则还要加上预编译的网络往返),在大量的插入情况下,所有的语句都需要经历一次最耗时的链接操作,性能自然是下降了不少。

我们再来说说为什么批处理比foreach高效的原因,明明同样是3000条语句的插入,foreach传输的数据包大小也小于批处理,为什么批处理的性能却要好于foreach插入操作呢?

我们在上文讲批处理的时候提到,Mybatis默认情况下,执行器是为SIMPLE,这就意味每次提交的插入操作的SQL语句都是相当于全新的PreparedStatement,都是需要进行预编译的,所以一条插入的SQL则是需要经历预编译和执行两次的网络往返,对应的代码也相当于下面这段JDBC代码:

		  // 创建Statement对象
            PreparedStatement statement = connection.createStatement();

            // 批量插入的数据
            String[] names = {"John Doe", "Jane Smith", "Mike Johnson"};
            int[] ages = {30, 25, 35};
            String[] cities = {"New York", "London", "Paris"};

            // 构建批量插入的SQL语句
            StringBuilder insertQuery = new StringBuilder("INSERT INTO mytable (name, age, city) VALUES ");
            for (int i = 0; i < names.length; i++) {
                insertQuery.append("('").append(names[i]).append("', ").append(ages[i]).append(", '").append(cities[i]).append("')");
                if (i < names.length - 1) {
                    insertQuery.append(", ");
                }
            }

            // 执行批量插入操作
            statement.executeUpdate(insertQuery.toString());

            // 关闭连接和Statement
            statement.close();
            connection.close();

可以看到在每一次使用foreach进行插入操作时,都需要重新创建一个PreparedStatement构建出一个SQL语句,每次提交时MySQL都需要进行一次预编译,这意味着用户每次使用foreach插入时,都需要进行一次预编译的网络IO,也正是这个原因使得其性能相较于批处理会逊色一些。

而批处理则不同,在我们的代码中,我们手动将ExecutorType改为BATCH,这样一来,每次进行批量插入时,Mybatis会先拿着我们的SQL语句创建成一个PreparedStatement提交到MySQL上进行预编译,这样一来本次会话所有相同的SQL语句直接提交时,就无需经过编译检查的操作,后续批量插入效率显著提升。

更高效的插入方式

因为Mybatis对于原生批处理操作做了很多的封装,其中涉及很多校验检查和解析等繁琐的流程,所以通过使用原生JDBC Batch来避免这些繁琐的解析、动态拦截等操作,对于MySQL批量插入也会有显著的提升。

一次插入多少数据量合适

明确要使用批处理进行批量插入之后,我们再来了解下一个问题,一次性批量插入多少条SQL语句比较合适?

对此我们基于100w的数据,分别按照每次1050010002000080000条压测,最终实验结果如下

80000的数据,每次插入10条,耗时:14555
80000的数据,每次插入500条,耗时:5001
80000的数据,每次插入1000条,耗时:3960
80000的数据,每次插入2000条,耗时:3788
80000的数据,每次插入3000条,耗时:3993
80000的数据,每次插入4000条,耗时:3847

在经过笔者的压测实验时发现,在2000条差不多2M大小的情况下插入时的性能最出色。这一点笔者也在网上看到一篇文章提到MySQL的全局变量max_allowed_packet,它限制了每条SQL语句的大小,默认情况下为4M,而这位作者的实验则是插入数据的大小在max_allowed_packet的一半情况下性能最佳。

show variables like 'max_allowed_packet%';  

当然并不一定只有上述条件影响批量插入的性能,影响批量插入的性能原因还有:

1.插入缓存:对于innodb存储引擎来说,插入是需要耗费缓冲池内存的,如果在写密集的情况下,插入缓存会占用过多的缓冲池内存,若插入操作占用大小超过缓冲池的一半,则会影响操其他的操作。

关于缓冲池的大小,可以通过下面这条SQL查看,默认情况下为134M:

show variables like 'innodb_buffer_pool_size';

2.索引的维护:这点相信读者比较熟悉,如果每次插入涉及大量无序且多个索引的维护,导致B+tree进行节点分裂合并等处理,则会消耗大量的计算资源,从而间接影响插入效率。

小结

针对上述三种方式,批处理插入的效率最高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1368838.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

npm run dev,vite 配置 ip 访问

启动项目通过本地 ip 的方式访问 方式一.通过修改 package.json "scripts": {"dev": "vite --host 0.0.0.0",}, 方式二.通过修改 vite.config.ts export default defineConfig({plugins: [vue(), vueJsx()],server: { // 配置 host 与 port 方…

使用串口 DMA 模式接收不定长数据

一、简介 曾经遇到客户有一个需求&#xff0c;需要用串口 DMA 的方式接收不定长度的数据&#xff0c;DMA 有个缺点就是在每次传输前需要设定好传输的字节长度&#xff0c;这种方式显然对于接收不定长度的数据来说没有那么灵活。但 DMA 也有着显著的优点&#xff0c;如可直接访…

AI模型部署基础知识(一):模型权重与参数精度

一般情况来说&#xff0c;我们通过收集数据&#xff0c;训练深度学习模型&#xff0c;通过反向传播求导更新模型的参数&#xff0c;得到一个契合数据和任务的模型。这一阶段&#xff0c;通常使用python&pytorch进行模型的训练得到pth等类型文件。AI模型部署就是将在python环…

华为mux vlan+DHCP+单臂路由用法配置案例

最终效果&#xff1a; vlan 2模拟局域网服务器&#xff0c;手动配置地址&#xff0c;也能上公网 vlan 3、4用dhcp分配地址 vlan 4的用户之间不能互通&#xff0c;但可以和其它vlan通&#xff0c;也能上公网 vlan 3的用户不受任何限制可以和任何vlan通&#xff0c;也能上公网 交…

C# Onnx yolov8 竹签计数、一次性筷子计数

目录 效果 模型信息 项目 代码 数据集 下载 C# Onnx yolov8 竹签计数、一次性筷子计数 效果 模型信息 Model Properties ------------------------- date&#xff1a;2024-01-03T08:55:22.768617 author&#xff1a;Ultralytics task&#xff1a;detect license&#x…

信息论与编码期末复习——计算题+基础汇总(二)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;V…

卷积神经网络|猫狗分类系列--导入kaggle猫狗数据集

解决任何真实问题的重要一步是获取数据&#xff0c;Kaggle提供了大量不同数据科学问题的竞赛。 我们将从 https://www.kaggle.com/competitions/dogs-vs-cats/data 下载猫狗数据集&#xff0c;并对其进行一定的操作&#xff0c;以正确的导入到我们的计算机&#xff0c;为接下…

基于SpringBoot的员工健康管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

python实现目录和文件管理

目录 一&#xff1a;模块介绍&#xff1a; 二&#xff1a;目录创建 三&#xff1a;目录删除 四&#xff1a;目录复制 五&#xff1a;目录移动 六&#xff1a;文件创建 七&#xff1a;文件删除 八&#xff1a;文件读取 一&#xff1a;模块介绍&#xff1a; Python的os和…

Java零基础教学文档第一篇:JavaSE(3)

接上期后续 本期继续分享尚未结束的JavaSE章节 JavaSE属于初入门阶段&#xff0c;内容很多~ 但很基础&#xff0c; 大家需保持耐心&#xff0c;慢慢的学~ 争取你们学习的速度&#xff01; 跟上我更新的速度哦~ 今日新篇章 流程控制语句 【主要内容】 if单选结构 if双选结构…

Java多线程编程中的异常处理策略

第1章&#xff1a;引言 大家好&#xff0c;我是小黑&#xff0c;咱们今天聊聊异常处理。想必大家在写代码的时候都遇到过各种各样的异常吧&#xff1f;有时候&#xff0c;一个小小的异常如果处理不当&#xff0c;就可能导致整个程序崩溃。特别是在多线程环境下&#xff0c;异常…

H2S硫化氢荧光探针之星戈瑞实验室单品

H2S硫化氢荧光探针&#xff08;近红外二区&#xff09;优势和应用 λe x 1064 nm &#xff0c;λem 1100 nm 近红外二区硫化氢荧光探针具有许多优势&#xff0c;使其在生物医学领域具有诸多应用。以下是其主要优势和应用方面&#xff1a; **优势&#xff1a; 1.深度穿透性&…

Git分支学习

Commit 每次 Commit &#xff0c;都会多一个节点&#xff0c;C1是C2的父节点&#xff0c;在C1的基础上产生。 使用 git commit 提交代码分支。 Branch 根据逻辑分解工作到不同的分支&#xff0c;在将分支和提交记录结合起来后&#xff0c;我们会看到两者如何协作。 在 mai…

subversion httpd

通过http访问模式部署SVN的操作步骤如下&#xff1a; 步骤一&#xff1a;安装SVN 步骤二&#xff1a;安装Apache 步骤三&#xff1a;安装mod_dav_svn 步骤四&#xff1a;配置SVN 步骤五&#xff1a;配置Apache 步骤六&#xff1a;浏览器测试访问 步骤一&#xff1a;安装SVN 1.…

2024第15届电子教育、电子商务、电子管理和电子学习国际会议

第十五届电子教育、电子商务、电子管理和电子学习国际会议&#xff08;IC4E 2024&#xff09;将于2024年3月18日-21日在日本福冈举办。本次会议以电子技术为核心&#xff0c;围绕电子教育、电子商务、电子管理以及电子学习等各个方面展开研讨&#xff0c;为相关领域的专家学者们…

yolov8实战第五天——yolov8+ffmpeg实时视频流检测并进行实时推流——(推流,保姆教学)

yolov8实战第一天——yolov8部署并训练自己的数据集&#xff08;保姆式教程&#xff09;_yolov8训练自己的数据集-CSDN博客 yolov8实战第三天——yolov8TensorRT部署&#xff08;python推理&#xff09;&#xff08;保姆教学&#xff09;-CSDN博客 今天&#xff0c;我们继续y…

‘pip‘ 不是内部或外部命令、ImportError: cannot import name ‘SCHEME_KEYS‘

错误一&#xff1a;启动程序中出现致命错误:无法使用“f:\pythonv\scripts\python.exe” G:\pythonv\scripts\ pip.exe” 错误二&#xff1a;‘pip‘ 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 错误三&#xff1a;ImportError: cannot import name SCH…

九州金榜|孩子厌学,不是不想学,而是学不会

不是我不想学&#xff0c;而是我不会学&#xff0c;很多孩子这学习过程中是不是有这种感想&#xff0c;家长也是看孩子非常努力&#xff0c;但是效果却不尽如人意&#xff0c;时间长了&#xff0c;得不到有效的结果&#xff0c;孩子就此产生厌学情绪&#xff0c;这一类孩子原因…

非常好用的三款图片模糊变清晰的软件

在数字时代&#xff0c;照片的清晰度对于呈现高质量的视觉效果至关重要。然而&#xff0c;由于各种原因&#xff0c;我们有时会遇到模糊的照片。这时候&#xff0c;使用适当的软件来提高照片的清晰度就显得尤为重要。本文将介绍一些可以使模糊照片变清晰的软件&#xff0c;帮助…

Windows内存管理(二):内存架构 浅谈一二

《Windows内存管理&#xff08;一&#xff09;&#xff1a;Windows性能监视器(PerfMon)》 Windows内存管理是一个复杂的主题&#xff0c;涉及多个层次和组件。以下是一个分层的概述。 1、虚拟内存管理 Windows使用虚拟内存来给每个进程提供一个看似连续的内存空间&#xff0c…