1. 程式人生 > >MySQL批量插入的分析以及注意事項

MySQL批量插入的分析以及注意事項

目錄

  1、背景

  2、兩種方式對比

    2.1、一次插入一條資料

    2.2、一次插入多條資料

  3、拓展一下

  4、Other

 

1、背景

  我們在工作中基本都會碰到批量插入資料到DB的情況,這個時候我們就需要根據不同的情況選擇不同的策略。

  只要瞭解sql,就應該知道,向table中插入資料的命令,至少有insert和replace這兩種,使用哪一種命令,和自己的業務有關;

  本文就針對insert進行批量插入進行闡述,然後根據自身經歷分享幾個注意事項。

 

2、兩種方式的對比

  即使是insert命令,他也是有多種插入資料的方式的。但這裡就不深入瞭解底層insert是怎麼做的了,那個已經超出本人的知識範疇,哈哈。

  但是我們可以大致瞭解MySQL的執行命令時的初略步驟:

  1、首先建立連線(Socke連線);

  2、Client將要執行的sql命令通過TCP連線,發給Server;

  Client,可以理解為我們用各種語言寫的專案程式(客戶端);

  Server,就是資料庫Server,負責執行。

3、資料庫Server收到資料(sql)後,會解析sql,然後進行處理;

4、將處理結果返回客戶端。

有了上面的流程,我們就開始說insert的兩種插入方式區別,下面是測試使用的表:

CREATE TABLE `user` (
  `id` 		int(11) 	NOT NULL AUTO_INCREMENT COMMENT '編號',
  `name` 	varchar(40) NOT NULL COMMENT '姓名',
  `gender` 	tinyint(1) 	DEFAULT '0' COMMENT '性別:1-男;2-女',
  `addr` 	varchar(40) NOT NULL COMMENT '住址',
  `status` 	tinyint(1) 	DEFAULT '1' COMMENT '是否有效:1-有效;2-無效',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

 

2.1、一次插入一條資料

  最初學習資料庫,都知道使用insert可以實現資料插入,比如向user表中插入一條資料:

mysql> insert into user (id, name, gender, addr, status) values (1, 'aaa', 1, 'beijing', 1);
Query OK, 1 row affected (0.00 sec)

mysql> select * from user;
+----+------+--------+---------+--------+
| id | name | gender | addr    | status |
+----+------+--------+---------+--------+
|  1 | aaa  |      1 | beijing |      1 |
+----+------+--------+---------+--------+
1 row in set (0.00 sec)

  這是最簡單的方式了,當然這是在命令列裡面,當然命令列也是一種客戶端;

  如果是客戶端我們程式碼的程式,比如Java利用jdbc來執行sql,也是傳給MySQL Server上面執行的insert命令;

  上面的insert命令的確是能插入資料資料的,也就是每執行一條insert命令,就需要通過網路將命令傳送給MySQL Server解析執行,如果有上千萬行資料需要插入,那麼是不是需要進行上千萬次連線傳輸呢?雖然現在可以使用連線池,但是傳輸的次數是是躲不掉的。

  使用insert一次插入一條資料的這種方式,絕大多數都是使用這種方式,來進行少量的資料插入!!!

  如果用這種方式進行大量資料的入庫,哈哈,花的時間可以喝好多杯咖啡了。

 

2.2、一次插入多條資料

  上面已經說到了,一次插入一條資料的主要缺陷是:需要建立N次連線,然後傳輸N連線,因為連線池的存在,可以忽略連線耗時,但是傳輸N次的耗時,不可小覷,所以我們可以從這方面進行考慮優化。

  比如,一個工人負責將100塊磚從A點搬到B點,每次搬1塊磚,花費1個單位時間,那麼搬完100塊磚,需要100單位時間(不考慮來回);

  如果一次搬5塊磚,那麼只需要20單位時間,是不是快了很多呢?

  同理,我們使用insert也可以進行批量插入資料:

insert into user 
	(id, name, gender, addr, status) 
values 
	(2, 'bbb', 0, 'shanghai', 1),
	(3, 'ccc', 1, 'hangzhou', 0),
	(4, 'ddd', 0, 'chongqing', 0);

  這樣就可以一次性插入3條資料了。

  對於客戶端來說,只需要進行拼接sql語句即可,然後將拼接後的sql一次性發給MySQL Server就可以了。

  注意,SQL要使用拼接,而不是說預處理!!!

  預處理的作用是避免頻繁編譯sql、sql注入;使用預處理來進行批量插入時,使用迴圈每次設定佔位符值,這個和一次插入一條命令是等價的,如下面的示例,其實執行了3次1條記錄插入:

<?php
    $pdo = new PDO("mysql:host=localhost;dbname=test","root","root");
    $sql = "insert into user (id, name, gender, addr, status) values (?,?,?,?,?)";
    $stmt = $pdo->prepare($sql);

    $stmt->execute(array("5", "eee", "1", "PEK", 1));
    $stmt->execute(array("6", "fff", "0", "SHA", 0));
    $stmt->execute(array("7", "ggg", "1", "LNL", 1));
 ?>

  

  正確的方式:

<?php
    $pdo = new PDO("mysql:host=localhost;dbname=test","root","root");
    $sql = 'insert into user (id, name, gender, addr, status) values ';

    // 可以使用迴圈進行sql拼接
    $sql .= '("5", "eee", "1", "PEK", 1),';
    $sql .= '("6", "fff", "0", "SHA", 0),';
    $sql .= '("7", "ggg", "1", "LNL", 1)'; 

    $pdo->exec($sql);
 ?>

  

  如果是Java可以使用原生JDBC,進行上面一樣拼接,就不寫程式碼了;

  如果Java使用Mybatis的話,可以使用<foreach>標籤,

<insert id="batchInsert" parameterType="list">
    insert ignore into user (id, name, gender, addr, status) values
    <foreach collection="list" item="item" separator=",">
        (
	        #{item.id,jdbcType=INT}, 
	        #{item.name,jdbcType=VARCHAR}, 
	        #{item.gender,jdbcType=BIT},
	        #{item.addr,jdbcType=VARCHAR}, 
	        #{item.status,jdbcType=BIT}
        )
    </foreach>
</insert>

  

3、拓展一下

  批量insert,每次insert的量是多少合適呢?

  以上面工人搬磚的例子,一次搬5塊磚,需要20單位時間,那豈不是1次搬100塊磚,只需要1單位時間了?是這個邏輯,但是這樣是不行的,需要看實際情況!!!

  這個實際情況是什麼呢?不好說,比如一個比較強壯的工人,一次100塊磚,不是難事;如果工人沒那麼強轉,一次100塊磚,可能直接把工人給幹倒了,1塊磚也搬不了,這時可不止100單位時間。

  另外,放磚的B點,是不是能一次接收100塊磚,這也是一個問題。

 

  上面的例子,類比到insert批量插入,就需要注意:

  1、要根據情況設定一次批量插入的資料量,資料量大,在網路中傳輸的事件也越久,出現問題的可能也越大;

  2、除了網路,還要看機器配置,MySQL Server配置差了,sql寫得再好,效率也不會太高;

  3、另外批量這個詞,是指一次插入多條資料,我們除了要注意資料的條數,還要注意一條資料的大小,舉個例子:比如一條記錄的資料量有1M,10條記錄的資料量就10M,這時一次插100條,100M資料,嘿嘿,你試試看!!所以,一次插入多少資料,一定要經過多次測試後再決定,別人1次插100條最優,你可能1次插10條才最優,沒有絕對的最優值(批量插入未必總是比單條插入效率高)。

  4、資料庫有個引數,max_allowed_packet,也就是每一個包(sql)命令大小,預設是1M,那麼sql的長度大於1M就會報錯。你可能會說,咱們把這個引數設成10M,100M不就行了???對呀,沒毛病,但你是DBA嗎?你有許可權嗎?即使調大這一個引數,你要知道影響的可不止你這一張表,而是整個DB Server,那影響的可是很多庫,很多表。

  5、批量插入並不是越快越好,我們可能希望越快越好,這很正常,節省時間嘛。但是我們一定要知道,資料庫分讀寫,有叢集,這就意味著,需要同步!!!如果有分庫分表分割槽的情況,如果短時間內插入的資料量太大,資料庫同步可能就會比較迷了,讀寫資料不一致的情況在所難免了,可能會因為一張表的批量插入,影響整個DB服務組的同步,同時還要考慮併發問題,哈哈哈。 

 

4、Other

  可以注意一下,我在上面寫的insert語句中,基本每一條命令都寫了插入的欄位,如下:

insert into user (id, name, gender, addr, status) values (1, 'aaa', 1, 'beijing', 1);

  

  其實我知道表的各個欄位的排列順序,完全可以省略欄位名,如下:

insert into user values (1, 'aaa', 1, 'beijing', 1);

  

  這兩種方式的效率,這裡就不談了,不過第一種方式,在某些場景有優勢,舉個例子:比如user表中增加create_time、update_time:

CREATE TABLE `user` (
  `id` 		int(11) 	NOT NULL AUTO_INCREMENT COMMENT '編號',
  `name` 	varchar(40) NOT NULL COMMENT '姓名',
  `gender` 	tinyint(1) 	DEFAULT '0' COMMENT '性別:1-男;2-女',
  `addr` 	varchar(40) NOT NULL COMMENT '住址',
  `status` 	tinyint(1) 	DEFAULT '1' COMMENT '是否有效',
  `create_time` timestamp  DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp  DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  

  如果沒有強制要求create_time和update_time必須從客戶端接收,那麼完全可以用預設值,insert的時候不用下面的語句:

--- 強制create_time和update_time使用client傳遞值
insert into user 
	(id, name, gender, addr, status, create_time, update_time) 
values 
	(2, 'bbb', 0, 'shanghai', 1, '2019-11-09 18:00:00', '2019-11-09 18:00:00'),
	(3, 'ccc', 1, 'hangzhou', 0, '2019-11-09 18:00:00', '2019-11-09 18:00:00'),
	(4, 'ddd', 0, 'chongqing', 0, '2019-11-09 18:00:00', '2019-11-09 18:00:00');

--- create_time和update_time不需要強制使用client傳遞值,可以使用預設值
insert into user 
	(id, name, gender, addr, status) 
values 
	(2, 'bbb', 0, 'shanghai', 1),
	(3, 'ccc', 1, 'hangzhou', 0),
	(4, 'ddd', 0, 'chongqing', 0);

  

  類似的,對於有些欄位有預設值,並且批量插入的時候,都使用預設值時,可以省略該欄位,因為拼接sql的時候可以少拼接一點,網路傳輸的資料就少一點,能提升一點是一點吧,這個還得看實際情況。

&n