PrepareStatement的功與過


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解析


免責聲明!

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



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