1. 程式人生 > >MYSQL-JDBC批量新增-更新-刪除

MYSQL-JDBC批量新增-更新-刪除

目錄

  • 1 概述
  • 2 開啟MYSQL服務端日誌
  • 3 深入MYSQL/JDBC批量插入
    • 3.1 從一個例子出發
    • 3.2 JDBC的批量插入操作
    • 3.3 兩個常被忽略的問題
    • 3.5 誤區
  • 4 MYSQL/JDBC批量更新
    • 4.1 MYSQL不支援批量更新
    • 4.2 JDBC的批量更新
    • 4.3 注意一個小問題
  • 5 MYSQL/JDBC批量刪除
  • 6 總結

1 概述

最近在極客時間買了幾個專欄,MYSQL實戰45講,SQL必知必會,如果你想深入MYSQL的話,推薦你看MYSQL實戰45講,非常不錯,並且一定要看留言區,留言區的質量非常高,丁奇老師太太太負責任了,我在極客時間買了不少課程,丁奇老師對大部分評論都進行了回答,這是在其他專欄中很少見的,文章的內容+留言區的問題+丁奇老師的解答都非常不錯。這是目前為止我在極客時間買到的最好的課程。

當然如果你想入門SQL,你可以看下SQL必知必會,該專欄比較簡單,屬於SQL入門課程。

隨著時代的變遷,我越發覺得資料變得越來越重要,無論是大資料、還是人工智慧、物聯網,本質上都是資料在起作用。大資料是涉及從大量資料中收集,儲存,分析和獲取的通用平臺。人工智慧和機器學習是兩種更智慧更有效的方式篩選資料和資訊的技術。移動和物聯網裝置用於從客戶,使用者和受眾收集資料。

因此最近我一直在研究MYSQL和JDBC的高階用法,形成本篇部落格,與大家一起分享。關於MYSQL和JDBC的簡單用法和概述,大家可以參考其他部落格,我就不重複造輪子了。


2 開啟MYSQL服務端日誌

找到 my.ini 檔案,我的電腦上是在 C:\ProgramData\MySQL\MySQL Server 5.7目錄下

然後重啟MYSQL伺服器,在my.ini同級的Data目錄下,你就可以看到 該日誌檔案。


3 深入MYSQL/JDBC批量插入

在url後面一定寫rewriteBatchedStatements=true,開啟批處理。

類似於:jdbc:mysql://127.0.0.1:3306/aaa?characterEncoding=UTF8&useUnicode=true&rewriteBatchedStatements=true



3.1 從一個例子出發

MYSQL 插入 就兩種形式

  • insert into student('name') values('adai')
  • insert into student('name') values('adai'),('hello'),('sky')

對比一下插入效率,3000條資料,資料都是一樣的。

第一種:首先插入3000條資料,3000個insert,在navicat執行,耗時3.360s

然後在服務端看日誌,會發現mysql,是一條一條逐步insert的,總共服務端執行3000次insert

第二種:插入3000條資料,一個insert,在navicat執行,耗時0.241秒

然後看服務端日誌,會發現mysql,是批量插入的,只有一個insert,多個values

這應該非常容易理解,按照計算機理論知識,批量插入效率鐵定比單條插入效率高。

注意,如果批量插入中間出現錯誤,那麼整個insert會失敗,不會插入任何資料,及該條insert批量插入是一個事務操作,要麼全部插入成功,要麼全部都插入失敗


3.2 JDBC的批量插入操作

由於SQL注入等問題,Statement已經用的很少了,JDBC我們主要講preparedStatement的批量插入,核心程式碼如下所示:

       private static final String[] names = {"劉德華", "周杰倫", "張三丰", "諸葛亮", "司馬懿", "呆頭", "張學友", "愛德華", "火星", "太陽"};
    private static final Integer[] ages = {21, 31, 41, 51, 61, 71, 81, 91, 100, 101};
    private static final Integer[] heights = {170, 171, 181, 182, 190, 168, 173, 175, 199, 220};
    private static final Byte[] sexs = {0, 1};
    private static final String[] address = {"中國上海大連西路550號",
            "國北京市朝陽區大山子A東里小區23棟3單元7樓",
            "第五宇宙", "第七宇宙恆星所在處", "浪跡天涯", "太陽背面", "大海最低處", "四姑涼山", "秦嶺", "長城"};

    private static final Integer NUMBER = 100000;

private static void prepareStatementBatch(Connection connection) throws SQLException {
        String sql = "insert into  student(`username`,`age`,`height`,`sex`,`address`,`create_time`,`update_time`) values(?,?,?,?,?,now(),null)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        long begin = System.currentTimeMillis();
        for (int i = 0; i < NUMBER; i++) {
                String name = names[j] + i;
                int age = ages[j] + i;
                int height = heights[j] + i;
                Byte sex = sexs[0];
                String addre = address[j] + i;
                preparedStatement.setString(1, name);
                preparedStatement.setInt(2, age);
                preparedStatement.setInt(3, height);
                preparedStatement.setByte(4, sex);
                preparedStatement.setString(5, addre);
                preparedStatement.addBatch();
                if ((i + 1) % 500 == 0) {
                    preparedStatement.executeBatch();
                    preparedStatement.clearBatch();
                }
        }
        // 執行剩下的
        preparedStatement.executeBatch();
        long end = System.currentTimeMillis();
        System.out.println("prepareStatementBatch 消耗時間:" + (end - begin));
    }

開啟mysql伺服器日誌:我們可以看到就兩條 insert語句,後面跟了很多values...,從第一個例子可以看出,這樣的執行效率非常高。


3.3 兩個常被忽略的問題

上面的程式碼中出現了

if ((i + 1) % 500 == 0) {
      preparedStatement.executeBatch();
      preparedStatement.clearBatch();
}

我個人覺得有兩個原因:

1. 防止記憶體溢位。
2. MYSQL有一個max_packet_allowed引數,會限制Server接受的資料包大小。有時候大的插入和更新會受 max_allowed_packet 引數限制,導致大資料寫入或者更新失敗。

但是經過我的程式碼實驗,只會出現第一種記憶體溢位的情況,而不會出現第二種max_packet_allowed超出的情況。

因為MYSQL驅動在底層已經對max_packet_allowed進行了處理。 debug 原始碼 進行跟蹤

並且在呼叫preparedStatment.executeBatch()方法後,不需要手動呼叫preparedStatement.clearBatch(),因為MYSQL驅動自己會在呼叫executeBatch()方法後,執行clearBatch()

protected long[] executeBatchInternal() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            ... ... ...                 

            } finally {
                this.statementExecuting.set(false);

                clearBatch(); // clearBatch()在 finally 語句塊中
            }
        }

總結一下,這裡有兩個常被忽略的問題:

  • MYSQL的prepareStatement.executeBatch()方法底層會自動判斷max_allowed_packet大小,然後對

    batch裡面的集合資料分批傳給MYSQL服務端,因此肯定不會報

    com.mysql.jdbc.PacketTooBigException: Packet for query is too large (5372027 > 4194304)

    Mybatis的批量插入不會對max_allowed_packet進行判斷,因此當資料量大的時候,會報這個錯誤

  • preparedStatment.executeBatch()完後,會自動呼叫preparedStatement.clearBatch()方法,無需我們手動再進行呼叫。

    3.4 Mybatis批量插入操作

兩種方式,陣列、List。效果都是一樣的,這裡我用List進行演示,主要用到Mybatis中的<foreach>

標籤

<insert id="insert">
    insert into student
    (`username`,`age`,`height`,`sex`,`address`,`create_time`,`update_time`) values
    <foreach collection="studentList" item="student" index="index" separator=",">
       (#{student.username},#{student.age},#{student.height},#{student.sex},#{student.address},now(),null)   
     </foreach>

檢視後臺MYSQL伺服器日誌:

2019-10-31T14:35:50.817807Z   141 Query insert into student(`username`,`age`,`height`,`sex`,`address`,`create_time`,`update_time`) values
            ('劉德華0',21,170,0,'中國上海大連西路550號0',now(),null)
         , 
            ('周杰倫0',31,171,0,'國北京市朝陽區大山子A東里小區23棟3單元7樓0',now(),null)
         , 
            ('張三丰0',41,181,0,'第五宇宙0',now(),null)
         , 
            ('諸葛亮0',51,182,0,'第七宇宙恆星所在處0',now(),null)
         , 
            ('司馬懿0',61,190,0,'浪跡天涯0',now(),null)
         , 
            ('呆頭0',71,168,0,'太陽背面0',now(),null)
         , 
            ('張學友0',81,173,0,'大海最低處0',now(),null)
         , 
            ('愛德華0',91,175,0,'四姑涼山0',now(),null)
         , 
            ('火星0',100,199,0,'秦嶺0',now(),null)
         , 
            ('太陽0',101,220,0,'長城0',now(),null)
            ... ... ... ... ... ...
            ... ... ... ... ... ...
            ... ... ... ... ... ...

可以看到,Mybatis底層就是使用一個insert,多個value的插入操作。

不過要特意留意,Mybatis的批量插入操作,不會像JDBC的preparedStatement.execute()一樣,會自動判斷MYSQL伺服器的 max_allow_packet大小,然後進行分批傳輸。Mybatis會將所有的value拼接在一起,然後將這整個insert語句傳給MYSQL伺服器去執行。如果這整個sql語句超出了 max_allow_packet,那麼錯誤將會產生。

總結:

不管是MYSQL、JDBC、Mybatis批量插入,底層都是一個 insert、多個values組合。

3.5 誤區

很多人將批量插入效率很高的原因,歸結於客戶端跟服務端互動變少了,因為客戶端一次會“攢”很多value,然後再發給服務端,這是不準確的,批量插入效率很高的原因,主要是因為 insert ... value() ...value() ...value()這個SQL特性,這個sql特性省下的時間遠遠超過 客戶端和MYSQL服務端的互動所省下的時間。

4 MYSQL/JDBC批量更新


4.1 MYSQL不支援批量更新

MYSQL是不支援批量更新的

注意:這裡的批量更新指的是

update student set username = "adai" where id = 1;
update student set age = 22 where id = 3;
update student set address= '上海' where id = 6;
update student set username = ‘daitou’ and address= '上海' where id = 11;
... ... ... ... ... ...
... ... ... ... ... ...

類似於上面完全不同的update語句,MYSQL服務端只用執行一次,就能全部更新。

但是我們可以利用一些sql技巧,來完成批量更新。但是也有很大的侷限性,例如要寫很多 CASE ... WHEN。

UPDATE table SET title = (CASE
WHEN id = 1 THEN ‘Great Expectations’
WHEN id = 2 THEN ‘War and Peace’
...
END)
WHERE id IN (1,2,...)

在實際開發中如果遇到大批量更新,一般做法是 事務+單條更新

START TRANSACTION;
UPDATE ...;
UPDATE ...;
UPDATE ...;
UPDATE ...;
COMMIT;


4.2 JDBC的批量更新

既然在MYSQL中是不支援批量更新的,那麼JDBC的 preparedStatement.addBatch()preparedStatement.executeBatch() 又是如何執行的呢?

經過程式碼實驗:

當sql是update的時候

for (...){
    ...
    ...
    preparedStatement.addBatch()    
}
preparedStatement.executeBatch()
for(...){
    ...
    ...
    preparedStatement.executeUpdate() 
}

更新15000行資料:

沒有使用批量更新 12372

使用批量更新 12227

更新50000行資料:

沒有使用批量更新 41295

使用批量更新 39754

更新100000行資料:

沒有使用批量更新 80820

使用批量更新 78839

更新300000行資料:

沒有使用批量更新 241400

使用批量更新 230104


更新500000行資料:

沒有使用批量更新 410912

使用批量更新 398941

檢視MYSQL伺服器日誌:發現批量更新和單獨更新的日誌都是一樣的

update student set ..... where id = ..;
update student set ..... where id = ..;
update student set ..... where id = ..;
update student set ..... where id = ..;

可以看出,使用批量更新,會比單獨更新快一些,這主要是因為客戶端和服務端互動次數變少,所省下的時間開銷。這也進一步證實了在 批量插入insert的時候,主要是insert ... value() ...value() ...value()這個sql特性大大的減少了時間花費。而不是像很多其他部落格說的是因為客戶端和服務端的互動次數減少。


4.3 注意一個小問題

在使用批量插入/更新的時候,如果已經將批量的sql傳給了MYSQL伺服器,那麼即使停止了客戶端程式,這些sql也會被執行。


5 MYSQL/JDBC批量刪除

無論是MYSQL還是JDBC,批量刪除和批量更新一樣。


6 總結

可以看出 JDBC的 preparedStatment.addBatch()preparedStatment.executeBatch()用在批量增加insert時,能夠極高的提高效率,但是用在 update 和 delete時,能夠提升部分效率。但是遠遠沒有批量插入提升的多。

沒有特殊情況限制,我們在insert、update、delete的時候,建議開啟事務,然後執行完畢,手動commit。

SQL執行最快的方式如下:

connection.setAutoCommit(false);
for(int i = 0; i < NUMBER; i++){
    ...
    ...
    preparedStatement.addBatch(); //NUMBER值不能太大,否則會記憶體溢位。
}
preparedStatement.executeBatch();
connection.commit();
作者:一杯熱咖啡AAA
出處:https://www.cnblogs.com/AdaiCoffee/
本文以學習、研究和分享為主,歡迎轉載。如果文中有不妥或者錯誤的地方還望指出,以免誤人子弟。如果你有更好的想法和意見,可以留言討論,謝謝