最近在做SQL索引优化的时候经常需要批量插入一些数据,采用存储过程来进行批量插入是一个很好的选择,但是在插入100w数据时我本地耗时需要24分钟有点顶不住,本文会讲解如何通过存储过程批量插入数据,并且提供两个提升插入速度的方法。
DROP TABLE IF EXISTS `order_info`;
CREATE TABLE `order_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(100) NOT NULL COMMENT '订单编号',
`customer_id` bigint(20) NOT NULL COMMENT '客户编号',
`goods_id` bigint(20) NOT NULL COMMENT '商品ID',
`goods_title` varchar(100) NULL COMMENT '商品标题',
`order_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '订单状态 1:待支付 2:已支付 3:已发货 4、已收货',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='订单信息表';
在测试的时候插入数据量可以调小一点,一次别插入太多,如果存储过程不加事务插入10w条数据我本地耗时143秒,插入100w数据我本地耗时24分钟太慢了,可以先看下面高效插入数据方案。
## 创建一个插入数据的存储过程
DROP PROCEDURE IF EXISTS insert_procedure;
delimiter;;
CREATE PROCEDURE insert_procedure ()
BEGIN
# 定义循环值
DECLARE i INT DEFAULT 1;
# 开启事务
START TRANSACTION;
# 开始循环插入
WHILE ( i <= 1000000 ) DO
INSERT INTO `order_info`(`order_no`,`customer_id`, `goods_id`, `goods_title`, `order_status`, `create_time`) VALUES (CONCAT('ON00000',i), CEIL(RAND() * 100), CEIL(RAND() * 100), CONCAT('笔记本电脑',i), MOD(i, 4)+1, NOW());
SET i = i + 1;
END WHILE;
END;;
delimiter;
# 调用存储过程插入数据
CALL insert_procedure ();
MySQL版本:8.0.18
如果MySQL不做任何配置,我本地固态盘使用MySQL8.0插入10w条数据耗时142s,插入数据量越大可能等比耗时更长,一般表中都会创建一些索引,在插入数据的时候也会变更索引,尤其是唯一索引会增长插入数据的时间,要想加快插入速度有多种方法,硬件上的优化就不说了,这里只说三个方法够我们做SQL索引优化测试即可。
将表中索引全部删除,包括主键索引,尤其是自增主键索引还有唯一索引,自己生成ID保证自增不重复即可,这里以10w条数据做测试对比,插入100w数据耗时太长。
我本地10w条数据插入有自增主键索引插入耗时142s,删除主键索引改用自己生成ID值插入耗时139s,这个数据量还比较小,有兴趣可以加大数据量测试,数据量越大差值越明显。
只需要把把存储过程中的SQL改一下把让 ID 使用 i 的值即可
INSERT INTO `order_info`(id,`order_no`,`customer_id`, `goods_id`, `goods_title`, `order_status`, `create_time`) VALUES (i, CONCAT('ON00000',i), CEIL(RAND() * 100), CEIL(RAND() * 100), CONCAT('笔记本电脑',i), MOD(i, 4)+1, NOW());
在存储过程中添加事务,存储过程中的每次新增语句都会开启一个自己的事务,控制所有新增都在一个事务中,10w条数据插入耗时从142s提升到20s,速度大大提升,但是有个问题这20s其它插入操作需要等待,线上业务需要考量一下,本地SQL索引优化测试倒是一个很不错的选择。
## 创建一个插入数据的存储过程
DROP PROCEDURE IF EXISTS insert_procedure;
delimiter;;
CREATE PROCEDURE insert_procedure ()
BEGIN
# 定义循环值
DECLARE i INT DEFAULT 1;
#定义一个错误的变量,类型是整形,默认是0
DECLARE t_error INTEGER DEFAULT 0;
#捕获到sql的错误,就设置t_error为1
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET t_error=1;
# 开启事务
START TRANSACTION;
# 开始循环插入
WHILE ( i <= 1000000 ) DO
INSERT INTO `order_info`(`order_no`,`customer_id`, `goods_id`, `goods_title`, `order_status`, `create_time`) VALUES (CONCAT('ON00000',i), CEIL(RAND() * 100), CEIL(RAND() * 100), CONCAT('笔记本电脑',i), MOD(i, 4)+1, NOW());
SET i = i + 1;
END WHILE;
#如果捕获到错误
IF t_error=1 THEN
#回滚
ROLLBACK;
ELSE
#提交
COMMIT;
END IF;
END;;
delimiter;
# 调用存储过程插入数据
CALL insert_procedure ();
这种方案是适合存储过程没有使用统一事务插入,每一次插入都需要开启事务然后提交,对存储过程中使用了统一事务插入提升不大。
MySQL有两个配置是控制日志文件写入的,在计算器中最耗时的操作就是IO,MySQL默认是会同步写入redo日志和binlog日志的,我们插入100w数据就需要同步写入100w次redo日志和100w次binlog日志,这是非常耗时的,如果能改成异步批量写入则可以大大加快新增数据的速度,但是可能会导致数据库宕机时数据丢失问题,这里不做详细说明。
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW VARIABLES LIKE 'sync_binlog';
我的MySQL是Linux版的配置文件在/etc/mysql/my.cnf
,window 上的 MySQL 配置文件默认是在 C:\Program Files\MySQL\MySQL Server 8.0\my-default.ini。
# 打开/etc/mysql/my.cnf
vi /etc/mysql/my.cnf
[mysqld]
下添加如下配置## 2表示在每次事务提交的时候会把log buffer刷到文件系统中去,但并不会立即刷写到磁盘。
innodb_flush_log_at_trx_commit = 2
## 0为不进行强行的刷新操作,而是由文件系统控制刷新日志文件
sync_binlog = 0
service mysqld restart
# 或
service mysql restart
修改前
修改后
插入速度还是比使用统一事务插入差很多。
我的需求是为了做SQL索引优化测试需要批量插入一些数据,这里最适合我的是4.2中添加统一事务来插入方案。
要想高效插入数据还有很多种方法,我这里只是为了做SQL索引优化测试使用,这个插入耗时我还可以接受,有其它好的方法可以一起交流。