探究mysql預編譯
本文來源於http://cs-css.iteye.com/blog/1847772, 網上的絕大部分關於預編譯的文章都是簡單介紹預編譯有什麼優缺點、怎麼用,而本文作者通過實踐深入探究了預編譯的開啟條件以及對效能的影響。
一.背景:
用Mybatis+mysql的架構做開發,大家都知道,Mybatis內建引數,形如#{xxx}的,均採用了sql預編譯的形式,舉例如下:
Xml程式碼- <select id=”aaa” parameterType=”int” returnType=”Blog”>
- select * from blog where id = #{id}
- </select>
檢視日誌後,會發現這個sql執行時被記錄如下:
Sql程式碼- select * from blog where id =?
之前上網查過一些資料,大致知道mybatis底層使用PreparedStatement,過程是先將帶有佔位符(即”?”)的sql模板傳送至mysql伺服器,由伺服器對此無引數的sql進行編譯後,將編譯結果快取,然後直接執行帶有真實引數的sql。網上關於這個問題的資料較少,但基本結論是,使用預編譯,可以提高sql的執行效率,並且有效地防止了sql注入。我一直對這個結論深信不疑,直到看了一篇名叫“Java中連結MySQL啟用預編譯的先決條件是useServerPstmts=true.
二.問題:
我的疑問有兩點:1.mysql是否預設開啟了預編譯功能?若沒有,將如何開啟? 2.預編譯是否能有效地節省sql執行的成本?
三.探究一:mysql是否預設開啟預編譯?
首先針對第一個問題。懶得開linux虛擬機器了,我電腦上已經安裝了mysql,版本是5.0.18,開啟配置檔案my.ini,在“port=3306“這一行下面加了配置:log=d:/logs/mysql50_log.txt,這樣就開啟了mysql日誌功能,該日誌主要記錄mysql執行sql的過程。重啟mysql,並建立一個庫prepare_stmt_test,在該庫下建一個叫users的表,有id(主鍵)和name兩個欄位。
接著,我建立了一個簡單的java工程,引入jdbc驅動包mysql-connector-java-5.0.3-bin.jar。然後寫了如下的程式碼:
Java程式碼- public static void main(String []a) throws Exception{
- String sql = "select * from users where name = ?";
- Class.forName("com.mysql.jdbc.Driver");
- Connection conn = null;
- try{
- conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root");
- PreparedStatement stmt = conn.prepareStatement(sql);
- stmt.setString(1, "aaa");
- ResultSet rs = stmt.executeQuery();
- rs.close();
- stmt.close();
- }catch(Exception e){
- e.printStackTrace();
- }finally{
- conn.close();
- }
- }
執行這些程式碼後,開啟剛才配置的mysql日誌檔案mysql50_log.txt,日誌記錄如下:
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SHOW VARIABLES
1 Query SHOW COLLATION
1 Query SET autocommit=1
1 Prepare [1]
1 Execute [1] select * from users where name = 'aaa'
1 Quit
日誌格式有點奇怪,明明打出了prepare關鍵字,但沒有我設定的預編譯的語句“select * from users where name = ?”,更令人疑惑的是,剛才說的那篇名叫“Java中連結MySQL啟用預編譯的先決條件是useServerPstmts=true.”的文章裡提到的,若jdbc連線沒有加useServerPrepStmts =true,mysql日誌裡連prepare關鍵字都不會記錄。而我的測試結果是,不加useServerPrepStmts =true,prepare關鍵字是有的,但沒有預編譯的sql模板“select * from users where name = ?”。
可能是我的mysql版本比較老吧,於是我停掉mysql5.0服務,安裝了mysql5.5,依照剛才那樣建庫建表,並啟用了一個新的mysql日誌檔案mysql55_log.txt。一切OK後,我又一次執行了剛才的測試程式,然後開啟日誌檔案mysql55_log.txt,發現了這樣的記錄:
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SHOW VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET autocommit=1
1 Prepare select * from users where name = ?
1 Execute select * from users where name = 'aaa'
1 Close stmt
1 Quit
終於看到sql模板“select * from users where name = ?”了,但仍然有很多疑惑,首先,剛才的mysql5.0到底開啟預編譯了嗎?其次,我並沒有加useServerPrepStmts =true配置,但mysql5.5的確是做了預編譯的操作的,這與“Java中連結MySQL啟用預編譯的先決條件是useServerPrepStmts =true.<!--[if !supportNestedAnchors]--><!--[endif]-->”這篇文章的測試結果大相徑庭。
帶著這些問題,又仔細閱讀了一下CSDN上這篇文章,作者的結論是:jdbc連線mysql時配置useServerPrepStmts引數為true後才能開啟mysql預編譯功能。看來這個useServerPrepStmts引數是很重要的,於是我將剛才程式碼裡的jdbc連線修改如下:
Java程式碼- DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts =false")
執行程式碼後,再次檢視mysql日誌:
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SHOW VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET autocommit=1
1 Query select * from users where name = 'aaa'
130410 15:06:48 1 Quit
果然,日誌了沒有了prepare這一行,說明mysql沒有進行預編譯。這意味著useServerPrepStmts這個引數是起效的,且預設值為true。那麼,為什麼在剛才那篇文章裡,作者得出的結論是useServerPrepStmts預設為false呢?
繼續思考了一陣,我突然意識到,useServerPrepStmts這個引數是jdbc的連線引數,這說明此問題與jdbc驅動程式可能有關係。開啟mysql官網,發現線上的官方文件很強大,支援全文檢索,於是我將“useServerPrepStmts”做為關鍵字,搜尋出了一些資訊,原文如下:
Important change: Due to a number of issues with the use of server-side prepared statements, Connector/J 5.0.5 has disabled their use by default. The disabling of server-side prepared statements does not affect the operation of the connector in any way.
To enable server-side prepared statements, add the following configuration property to your connector string:
useServerPrepStmts=true
The default value of this property is false (that is, Connector/J does not use server-side prepared statements)
這段文字說,Connector/J在5.0.5以後的版本,預設useServerPrepStmts引數為false,Connector/J就是我們熟知的jdbc驅動程式。看來,如果我們的驅動程式為5.0.5或之後的版本,想啟用mysql預編譯,就必須設定useServerPrepStmts=true。我的jdbc驅動用的是5.0.3,這個版本的useServerPrepStmts引數預設值是true。於是我將java工程中的jdbc驅動程式替換為5.0.8的版本,去掉程式碼裡jdbc連線中的useServerPrepStmts引數,再執行,發現mysql5.5的日誌列印如下:
2 Query SHOW SESSION VARIABLES
2 Query SHOW WARNINGS
2 Query SHOW COLLATION
2 Query SET NAMES utf8
2 Query SET character_set_results = NULL
2 Query SET autocommit=1
2 Query select * from users where name = 'aaa'
2 Quit
那麼,mysql5.0呢?我停掉mysql5.5服務,開啟mysql5.0,再執行java程式碼,檢視mysql5.0的日誌:
1 Query SHOW SESSION VARIABLES
1 Query SHOW COLLATION
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SET autocommit=1
1 Query select * from users where name = 'aaa'
1 Quit
果然,在mysql5.0日誌裡,prepare關鍵字沒有了。mysql5.0的日誌格式和mysql5.5的不太一樣,5.0日誌只打印一個“prepare”關鍵字,而不列印預編譯sql模板。
第一個問題解決了,結論就是:mysql是否預設開啟預編譯,與MySQL server的版本無關,而與 MySQL Connector/J(驅動程式)的版本有關,Connector/J 5.0.5及以後的版本預設不支援預編譯,Connector/J 5.0.5之前的版本預設支援預編譯。
四.探究二:預編譯是否能有效地節省sql執行的成本?
首先,我們要明白mysql執行一個sql語句的過程。查了一些資料後,我得知,mysql執行指令碼的大致過程如下:prepare(準備)-> optimize(優化)-> exec(物理執行),其中,prepare也就是我們所說的編譯。開篇時已經說過,對於同一個sql模板,如果能將prepare的結果快取,以後如果再執行相同模板而引數不同的sql,就可以節省掉prepare(準備)的環節,從而節省sql執行的成本。明白這一點後,我寫了如下測試程式:
Java程式碼- public static void main(String []a) throws Exception{
- String sql = "select * from users where name = ?";
- Class.forName("com.mysql.jdbc.Driver");
- Connection conn = null;
- try{
- conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true");
- PreparedStatement stmt = conn.prepareStatement(sql);
- stmt.setString(1, "aaa");
- ResultSet rs1 = stmt.executeQuery();//第一次執行
- s1.close();
- stmt.setString(1, "ddd");
- ResultSet rs2 = stmt.executeQuery();//第二次執行
- rs2.close();
- stmt.close();
- }catch(Exception e){
- e.printStackTrace();
- }finally{
- conn.close();
- }
- }
執行該程式後,檢視mysql日誌:
1 Query SHOW SESSION VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SET autocommit=1
1 Prepare select * from users where name = ?
1 Execute select * from users where name = 'aaa'
1 Execute select * from users where name = 'ddd'
1 Close stmt
1 Quit
按照日誌看來,PreparedStatement重新設定sql引數後,並沒有重新prepare,看來預編譯起到了效果。但剛才我使用的是同一個stmt,如果將stmt關閉呢?
Java程式碼- public static void main(String []a) throws Exception{
- String sql = "select * from users where name = ?";
- Class.forName("com.mysql.jdbc.Driver");
- Connection conn = null;
- try{
- conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true");
- PreparedStatement stmt = conn.prepareStatement(sql);
- stmt.setString(1, "aaa");
- ResultSet rs1 = stmt.executeQuery();//第一次執行
- rs1.close();
- stmt.close();
- stmt = conn.prepareStatement(sql);//重新獲取一個statement
- stmt.setString(1, "ddd");
- ResultSet rs2 = stmt.executeQuery();//第二次執行
- rs2.close();
- stmt.close();
- }catch(Exception e){
- e.printStackTrace();
- }finally{
- conn.close();
- }
- }
mysql日誌列印如下:
1 Query SHOW SESSION VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SET autocommit=1
1 Prepare select * from users where name = ?
1 Execute select * from users where name = 'aaa'
1 Close stmt
1 Prepare select * from users where name = ?
1 Execute select * from users where name = 'ddd'
1 Close stmt
1 Quit
很明顯,關閉stmt後再執行第二個sql,mysql就重新進行了一次預編譯,這樣是無法提高sql執行效率的。而在實際的應用場景中,我們不可能保持同一個statement。那麼,mysql如何快取預編譯結果呢?
搜尋一些資料後得知,jdbc連線引數中有另外一個重要的引數:cachePrepStmts,設定為true後可以快取預編譯結果。於是我將測試程式碼中jdbc連線串改為了這樣:
Java程式碼- conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true&cachePrepStmts=true");
再執行程式碼後,發現mysql日誌記錄又變成了這樣:
1 Prepare select * from users where name = ?
1 Execute select * from users where name = 'aaa'
1 Execute select * from users where name = 'ddd'
OK,現在我們開啟了預編譯,並開啟了快取預編譯的功能,那麼開始效能測試。我向剛才的單表中插入了10000條資料,並做10000次同樣sql模板,不同引數的select。記錄結果如下:
當不開啟預編譯功能時,做5次測試,10000個select總時間為(單位毫秒):
49172,49172,49000,49047,48922
開啟預編譯,但不開啟預編譯快取時,測試數值如下:
50797,50860,50672,50750,50703
開啟預編譯,開啟預編譯快取,測試數值如下:
49547,49250,49593,49093,49078.
從測試結果看來,若開啟預編譯,但不開啟預編譯快取,查詢效率會有明顯下降;但開啟預編譯且開啟預編譯快取時,查詢效率比不開啟快取有提高,但和不開啟預編譯基本持平。
結論出來了:對於Connector/J5.0.5以上的版本,若使用useServerPrepStmts=true開啟預編譯,則一定要同時使用cachePrepStmts=true 開啟預編譯快取,否則效能會下降,若二者都開啟,效能並沒有顯著的提高,這個可能是我測試程式的原因,有待進一步研究。
五.總結:
經過這一系列的探究,能夠得出一些結論:
首先批一下《Java中連結MySQL啟用預編譯的先決條件是useServerPstmts=true.》這篇文章吧,文章寫得很不科學,作者並沒有關注mysql和Connector/J的版本之間的差異,對於mysql,他說mysql一定支援預編譯,事實上,經過我查詢官方文件後,得知MySQL Server 4.1之前的版本是不支援預編譯的;對於Connector/J,他也沒有關注5.0.5這個版本節點。所以,雖然被瀏覽轉載了很多次,但這篇文章的結論仍然是錯誤的,應該也誤導了不少人;對於開啟預編譯和預編譯快取後對效能的影響,這篇文章也沒有涉及,事實上經過我測試,若jdbc驅動是5.0.5及之後的版本,同時開啟預編譯和預編譯快取,sql的執行效能並沒有顯著提高,若jdbc驅動是5.0.5之前的版本,預設開啟了預編譯,則一定要加cachePrepStmts=true,否則mysql的執行效率會比較低。總之,預編譯和預編譯快取一定要同時開啟或同時關閉,不同Connector/J的版本,useServerPrepStmts的預設值會有所不同。
再談談SQL預編譯這個東西,其實“預編譯”這個叫法不是很準確,官方文件裡把它叫做“預準備”。經過我測試,對於mysql,開啟了預編譯快取後,不同connection之間,預編譯的結果是獨立的,是無法共享的,一個connection無法得到另外一個connection的預編譯快取結果,對於這一點,我想mysql的開發人員應該會在以後逐步改進吧。再一點,關於預編譯快取的內容,我查了相關的資料後得知,mysql執行一個預編譯操作後,會將sql模板(即帶佔位符“?”的sql),以及引數列表(模板中用各個佔位符表示)快取,下一次有同樣的sql模板發來的時候,直接將引數傳給這個模板,拼好後execute。雖然mysql的預編譯功能對效能影響不大,但在jdbc中使用PreparedStatement是必要的,可以有效地防止sql注入,這一點大家都明白。