如何優化數據導入?

前言

有些時候我們可能會遇到批量數據導入的場景,所以有些時候是很耗時的,這篇文章就介紹一些加快導入的方法

一次插入多行的值

插入行所需的時間由以下因素決定(參考MySQL 5.7參考手冊:8.2.4.1優化INSERT語句)

連接:30%
向服務器發送查詢:20%
解析查詢:20%
插入行:10% * 行的大小
插入索引:10% * 索引數
結束:10%

可發現大部分時間耗費在客戶端與服務端通信的時間,因此可以使用 insert 包含多個值來減少客戶端和服務器之間的通信。我們通過實驗來驗證下一次插入多行與一次插入一行的效率對比。

準備測試表及數據
由於本次實驗操作包含 drop 等危險操作,因此建議創建普通用戶進行實驗,並權限最小化,以防誤操作。

grant select,insert,create,drop,index,alter on muke.* to 'test_user3'@'127.0.0.1' identified by 'userBcdQ19Ic';

這裏對上面命令解釋一下:
grant 這裏包含了創建用戶及賦權操作
select,insert,create,drop 賦予查詢、寫入、建表及刪表的權限
on muke.* 只對muke這個database有這些權限
to ‘test_user3’@‘127.0.0.1’ 用戶名爲test_user3,host爲127.0.0.1,即test_user3只能在本機使用,如果MySQL服務端跟客戶端不在同一臺機器上,則127.0.0.1替換成客戶端ip地址
identified by ‘userBcdQ19Ic’ 用戶密碼爲userBcdQ19Ic

創建測試表及寫入數據,語句如下

use muke;                 /* 使用muke這個database */

drop table if exists t1;  /* 如果表t1存在則刪除表t1 */

CREATE TABLE `t1` (	      /* 創建表t1 */
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` varchar(20) DEFAULT NULL,
  `b` int(20) DEFAULT NULL,
  `c` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 ;

drop procedure if exists insert_t1; /* 如果存在存儲過程insert_t1,則刪除 */
delimiter ;;
create procedure insert_t1()        /* 創建存儲過程insert_t1 */
begin
  declare i int;                  /* 聲明變量i */
  set i=1;                        /* 設置i的初始值爲1 */
  while(i<=10000)do			      /* 對滿足i<=1000的值進行while循環 */
    insert into t1(a,b) values(i,i); /* 寫入表t1中a、b兩個字段,值都爲i當前的值 */
    set i=i+1;                       /* 將i加1 */
  end while;
end;;
delimiter ;
call insert_t1();               /* 運行存儲過程insert_t1 */

導出一條 SQL 包含多行數據的數據文件
爲了獲取批量導入數據的 SQL,首先對測試表的數據進行備份,備份的 SQL 爲一條 SQL 包含多行數據的形式(執行環境爲 Centos7 命令行)。

[root@mysqltest muke]# mysqldump -utest_user3 -p'userBcdQ19Ic' -h127.0.0.1 --set-gtid-purged=off --single-transaction --skip-add-locks  muke t1 >t1.sql

這裏對上面 mysqldump 所使用到的一些參數做下解釋:

-utest_user3 用戶名,這裏使用的是root用戶
-p’userBcdQ19Ic’ 密碼
-h127.0.0.1 連接的MySQL服務端IP
set-gtid-purged=off 不添加SET @@GLOBAL.GTID_PURGED
–single-transaction 設置事務的隔離級別爲可重複讀,即REPEATABLE READ,這樣能保證在一個事務中所有相同的查詢讀取到同樣的數據
–skip-add-locks 取消每個表導出之前加lock tables操作。
muke 庫名
t1 表名
t1.sql 導出數據到這個文件

查看文件 t1.sql 內容,可看到數據是單條 SQL 有多條數據,如下

......
DROP TABLE IF EXISTS `t1`;		
/* 按照上面的備份語句備份的數據文件包含drop命令時,需要特別小心,在後續使用備份文件做導入操作時,應該確定所有表名,防止drop掉業務正在使用的表 */
......
CREATE TABLE `t1`......
......
INSERT INTO `t1` VALUES (1,'1',1,'2019-05-24 15:44:10'),(2,'2',2,'2019-05-24 15:44:10'),(3,'3',3,'2019-05-24 15:44:10')......
......

導出一條SQL只包含一行數據的數據文件

[root@mysqltest muke]# mysqldump -utest_user3 -p'userBcdQ19Ic' -h127.0.0.1 --set-gtid-purged=off --single-transaction --skip-add-locks --skip-extended-insert muke t1 >t1_row.sql

mysqldump命令參數解釋:
–skip-extended-insert 一條SQL一行數據的形式導出數據
備份文件t1_row.sql內容如下

......
INSERT INTO `t1` VALUES (1,'1',1,'2019-05-24 15:44:10');
INSERT INTO `t1` VALUES (2,'2',2,'2019-05-24 15:44:10');
INSERT INTO `t1` VALUES (3,'3',3,'2019-05-24 15:44:10');
......

導入時間的對比
首先導入一行 SQL 包含多行數據的數據文件:

[root@mysqltest ~]# time mysql -utest_user3 -p'userBcdQ19Ic' -h127.0.0.1 muke <t1.sql

real	0m0.230s
user	0m0.007s
sys		0m0.003s

耗時0.2秒左右。

導入一條SQL只包含一行數據的數據文件:

[root@mysqltest ~]# time mysql -utest_user3 -p'userBcdQ19Ic' -h127.0.0.1 muke <t1_row.sql

real	0m31.138s
user	0m0.088s
sys		0m0.126s

耗時31秒左右。

結論
一次插入多行花費時間0.2秒,一次插入一行花費了31秒,對比效果明顯,因此建議有大批量導入時,推薦一條insert語句插入多行數據。

關閉自動提交

對比開啓和關閉自動提交的效率
Autocommit 開啓時會爲每個插入執行提交。可以在InnoDB導入數據時,關閉自動提交。如下:

SET autocommit=0;
INSERT INTO `t1` VALUES (1,'1',1,'2019-05-24 15:44:10');
INSERT INTO `t1` VALUES (2,'2',2,'2019-05-24 15:44:10');
INSERT INTO `t1` VALUES (3,'3',3,'2019-05-24 15:44:10');
......
COMMIT;

使用生成的t1_row.sql,在insert前增加:

SET autocommit=0;

在insert語句後面增加:

COMMIT;
[root@mysqltest muke]# time mysql -utest_user3 -p'userBcdQ19Ic' -h127.0.0.1 muke <t1_row.sql

real		 0m1.036s
user		0m0.062s
sys		   0m0.108s

開啓自動提交的情況下導入是31秒
關閉自動提交的情況下導入是1秒左右,因此導入多條數據時,關閉自動提交,讓多條 insert 一次提交,可以大大提升導入速度。
原因分析

因爲批量導入大部分時間耗費在客戶端與服務端通信的時間,所以多條 insert
語句合併提交可以減少客戶端與服務端通信的時間,並且合併提交還可以減少數據落盤的次數。

參數調整

影響MySQL寫入速度的主要兩個參數:innodb_flush_log_at_trx_commit、sync_binlog。

參數解釋
innodb_flush_log_at_trx_commit:控制重做日誌刷新到磁盤的策略,有0 、1和2三種值。

0:master線程每秒把redo log buffer寫到操作系統緩存,再刷到磁盤;
1:每次提交事務都將redo log buffer寫到操作系統緩存,再刷到磁盤;
2:每次事務提交都將redo log buffer寫到操作系統緩存,由操作系統來管理刷盤。

sync_binlog:控制binlog的刷盤時機,可配置0、1或者大於1的數字。

0:二進制日誌從不同步到磁盤,依賴OS刷盤機制;
1:二進制日誌每次提交都會刷盤;
n(n>1) : 每n次提交落盤一次。

寫入速度測試
爲了對比以上兩個參數對MySQL寫入速度的影響,我們通過壓力工具sysbench測試寫入速度。
第一步:設置兩個參數的值:

mysql> set global innodb_flush_log_at_trx_commit=1;
Query OK, 0 rows affected (0.01 sec)

mysql> set global sync_binlog=1;
Query OK, 0 rows affected (0.00 sec)

第二步:準備數據:

[root@mysqltest ~]# sysbench --test=/usr/share/sysbench/tests/include/oltp_legacy/insert.lua --mysql-user=test_user3 --mysql-password='userBcdQ19Ic' --mysql-host=127.0.0.1 --mysql-db=muke --oltp-table-size=0 --oltp-tables-count=10  prepare

第三步:進行寫入測試:

[root@mysqltest ~]# sysbench --test=/usr/share/sysbench/tests/include/oltp_legacy/insert.lua --mysql-user=test_user3 --mysql-password='userBcdQ19Ic' --mysql-host=127.0.0.1  --mysql-db=muke --oltp-table-size=100000 --max-time=100 --oltp-tables-count=10  run

正式環境壓測建議壓半小時以上。

第四步:記錄測試結果:
在這裏插入圖片描述

第五步:清除壓測數據:

[root@mysqltest ~]# sysbench --test=/usr/share/sysbench/tests/include/oltp_legacy/insert.lua --mysql-user=test_user3 --mysql-password='userBcdQ19Ic' --mysql-host=127.0.0.1  --mysql-db=muke --oltp-table-size=100000 --oltp-tables-count=10  cleanup
innodb_flush_log_at_trx_commit	sync_binlog					TPS				結論
1														1									316.83			雙一情況寫入速度最慢
1														0									526.97	
0														1									497.42	
0														0									2379.9			都設置爲0的情況下,寫入速度最快
2														1									515.76	
2														0									2169.51	

結論
從實驗結果可以看出,innodb_flush_log_at_trx_commit設置爲0、同時sync_binlog設置爲0時,寫入數據的速度是最快的。如果對數據庫安全性要求不高(比如你的測試環境),可以嘗試都設置爲0後再導入數據,能大大提升導入速度。

總結

一次插入多行的值;
關閉自動提交,多次插入數據的 SQL 一次提交;
調整參數,innodb_flush_log_at_trx_commit 和 sync_binlog 都設置爲0(當然這種情況可能會丟數據)。

參考資料

該文爲本人學習的筆記,方便以後自己複習。參考
唐漢明 等著;深入淺出MySQL(第2版):18.4.2 優化INSERT語句
慕課網專欄:https://www.imooc.com/read/43
取其精華整合而成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章