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的時候可以少拼接一點,網絡傳輸的數據就少一點,能提升一點是一點吧,這個還得看實際情況。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM