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();
出處: https://www.cnblogs.com/AdaiCoffee/
本文以學習、研究和分享為主,歡迎轉載。如果文中有不妥或者錯誤的地方還望指出,以免誤人子弟。如果你有更好的想法和意見,可以留言討論,謝謝!