PrepareStatement的功與過
背景
最近同事說遇到一個問題:"使用JDBC驅動進行PrepareStatemet查詢時,同一個SQL,當傳遞某些參數時執行特別慢(用psql命令行單獨執行又特別快)。初步分析:JDBC里的執行計划選擇不正確,當表的數據分布不均勻,且SQL傳遞參數不一樣時,PrepareStatement仍然用的原來的執行計划”。最后的解決方案和結論是:
- 方案是通過在jdbcURL上設置參數"prepareThreshold=0"解決(項目使用的PostgreSQL數據庫)
- 結論是prepareThreshold參數默認是5,客戶端會編譯並緩存preparestatement的執行計划,設置為0就能禁用客戶端緩存
疑問
- 首先,對解決方案本身沒有疑問,因為設置這個參數后,日志里打印出這個SQL確實快了,問題確實解決了。
- 主要的疑問產生在“客戶端會編譯並緩存preparestatement的執行計划,設置為0就能禁用客戶端緩存”在這個結論上。感覺這個結論是錯誤的,主要因為生成執行計划需要很多數據庫的元信息和統計值等,jdbc驅動很難自己生成執行計划。
PrepareStatement的功與過
PrepareStatement的主要作用是什么呢?有一些經驗的Java程序員都知道:
- 通過參數化可以防止SQL注入
- 可以提高性能,數據庫服務不用每次都硬解析SQL生成執行計划
PrepareStatement會引入哪些問題呢?
- 遇到最多的是因為SQL只編譯解析一次,執行計划的重用導致會忽略實際傳入的參數對執行計划的影響。
prepareThreshold 參數的含義
那么JDBC 驅動到底能不能編譯SQL並生成執行計划么?prepareThreshold=0的含義是什么呢?
參閱了幾遍官方文檔和一篇博客及查看了PostgreSQL JDBC驅動源碼后總算明白了,起碼算是邏輯自洽了。
-
首先,prepareThreshold 是一個開關,開啟server prepare的開關,即開啟重用SQL的開關。
public static void main(String args[]) throws Exception { Class.forName("org.postgresql.Driver"); String url = "jdbc:postgresql://xx.xx.14.173:6362/abase"; Connection conn = DriverManager.getConnection(url,"sa","123456"); PreparedStatement pstmt = conn.prepareStatement("SELECT ?"); // cast to the pg extension interface org.postgresql.PGStatement pgstmt = pstmt.unwrap(org.postgresql.PGStatement.class); // on the third execution start using server side statements pgstmt.setPrepareThreshold(0); for (int i=1; i<=5; i++) { pstmt.setInt(1,i); boolean usingServerPrepare = pgstmt.isUseServerPrepare(); ResultSet rs = pstmt.executeQuery(); rs.next(); System.out.println("Execution: "+i+", Used server side: " + usingServerPrepare + ", Result: "+rs.getInt(1)); rs.close(); } pstmt.close(); conn.close(); }
-
當
pgstmt.setPrepareThreshold(0);
時,結果為:Execution: 1, Used server side: false, Result: 1 Execution: 2, Used server side: false, Result: 2 Execution: 3, Used server side: false, Result: 3 Execution: 4, Used server side: false, Result: 4 Execution: 5, Used server side: false, Result: 5
-
當
pgstmt.setPrepareThreshold(1);
時,結果為:Execution: 1, Used server side: true, Result: 1 Execution: 2, Used server side: true, Result: 2 Execution: 3, Used server side: true, Result: 3 Execution: 4, Used server side: true, Result: 4 Execution: 5, Used server side: true, Result: 5
-
當
pgstmt.setPrepareThreshold(2);
時,結果為:Execution: 1, Used server side: false, Result: 1 Execution: 2, Used server side: true, Result: 2 Execution: 3, Used server side: true, Result: 3 Execution: 4, Used server side: true, Result: 4 Execution: 5, Used server side: true, Result: 5
-
-
SQL的解析過程
postgres=# create table test as select 111 a; /*測試表*/ postgres=> show log_parser_stats ; /*log_parser_stats參數為開啟狀態*/ log_parser_stats ------------------ on postgres=# show log_planner_stats ; /*log_planner_stats參數為開啟狀態*/ log_planner_stats ------------------- on
java 測試程序
public static void main(String args[]) { try { Class.forName("org.postgresql.Driver").newInstance(); String url = "jdbc:postgresql://xx.xx.14.173:6362/abase?prepareThreshold=3"; Connection conn = DriverManager.getConnection(url, "sa", "123456"); int foovalue = 111; PreparedStatement st = conn.prepareStatement("SELECT * FROM test WHERE a = ?"); st.setInt(1, foovalue); ResultSet rs = st.executeQuery(); ResultSet rs1 = st.executeQuery(); ResultSet rs2 = st.executeQuery(); ResultSet rs3 = st.executeQuery(); while (rs3.next()) { System.out.println(rs3.getString(1)); } rs.close(); rs1.close(); rs2.close(); rs3.close(); st.close(); } catch (Exception ee) { System.out.print(ee.getMessage()); } }
通過表查看數據庫csv日志:
postgres=# select command_tag,message from postgres_log ; command_tag | message PARSE | PARSER STATISTICS PARSE | PARSE ANALYSIS STATISTICS PARSE | REWRITER STATISTICS BIND | PLANNER STATISTICS SELECT | execute : SELECT * FROM test WHERE a = $1 PARSE | PARSER STATISTICS PARSE | PARSE ANALYSIS STATISTICS PARSE | REWRITER STATISTICS BIND | PLANNER STATISTICS SELECT | execute : SELECT * FROM test WHERE a = $1 PARSE | PARSER STATISTICS PARSE | PARSE ANALYSIS STATISTICS PARSE | REWRITER STATISTICS BIND | PLANNER STATISTICS SELECT | execute S_1: SELECT * FROM test WHERE a = $1 BIND | PLANNER STATISTICS SELECT | execute S_1: SELECT * FROM test WHERE a = $1
結果:POSTGRESQL解析日志中記錄了三步PARSER STATISTICS->PARSE ANALYSIS STATISTICS ->REWRITER STATISTICS -> BIND PLANNER STATISTICS,當執行第四遍的時候已經忽略了解析的過程,會直接綁定執行計划
結論
- prepareThreshold 是一個是否開啟server prepare開關
- 當為0時,禁用server prepare。相當於每次SQL都是硬解析。
- 當為1時,開啟server prepare。相當於每次都走服務端的緩存。復用已有的SQL解析結果和執行計划,不進行SQL解析。
- 當n大於1時,執行第n次時開啟server prepare。相當於從第N次開始復用SQL解析結果和執行計划,不進行SQL解析。
- 要進行preparestatement時,需要客戶端(jdbc驅動)和服務端(db server)相互配合,都需要編譯和進行緩存,都需要占用內存空間。
- jdbc驅動主要將SQL解析成native SQL format,不是向后台傳送的原生的full sql text
- jdbc驅動要緩存一些結果的類型和結果的元數據信息但不緩存執行計划
- 同時也有一些其他的功能和例外情況需要注意,具體參見官方文檔,比如
- select * 后,做ddl 會對驅動已經緩存的元數據信息有影響
- set search_path、每次參數傳遞時類型不一致等也會對服務器緩存的執行計划有影響,導致選擇錯誤
- 將 preferQueryMode設置為extendedCacheEverything時,可以將常規的statement 也設置成類似preparestatemt的行為,避免過渡的SQL解析