Java Jdbc減小交互提高批量處理性能,到底該如何優化纔好?

不拾掇Java有好幾年了(N>3吧),之因此寫這篇文章實際上是純粹是爲了給開發人員一些好的使用jdbc真正去減小交互和提高批量處理batch update性能的例子;  若是你是DBA,那麼工做之餘你能夠把這篇文章推薦給開發看一下, 也許這些例子他已經知道了, 假若他不知道,那麼也算一種福利了。 java

能考慮到在應用程序client和 數據庫服務器DB server間減小交互時間,批量更新處理的絕對是有助於重構和優化代碼的好同志;  但這種優化必定要注意方法,若是是自行去從新發明一種輪子的話, 效果每每是不如人意的。 sql

例如Tom Kytes曾在他的著做裏提到這樣2個例子,他去協助開發的2家企業的在研發應用的過程當中,分別經過應用程序本身去在Oracle中實現了user profile和advanced queue的功能, 有必定經驗的朋友確定會知道這2樣功能其實Oracle Enterprise Edition企業版軟件都是原生態支持的,而本身在DB中去實現它們,最終結果天然是項目的失敗。 數據庫

相似的有朋友在開發過程當中,爲了優化Oracle JDBC中的批量更新update操做,想到了這樣的方式,例如要插入INSERT 15000行數據,則在JAVA層面 將15000條INSERT語句拼接在一個PL/SQL block裏,這15000條SQL涉及到的變量仍使用PreparedStatement.setXXX方法帶入,其在JAVA層面的SQL STRING,如: c#

begin 
  --我是一個拼接起來的SQL匿名塊 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
 insert into insertit values(?,?,?,?); 
.....................
  commit ; end;

如上15000個INSERT拼接成一個PL/SQL block,一次性PreparedStatement.execute()提交給DB,經過這樣來減小Jdbc Thin Client與DB Server之間的交互。先不說別的,光在JAVA裏循環控制拼接SQL的寫法多少是要花點時間的。 緩存

這種寫法和 JDBC裏PreparedStatement.setExecuteBatch、或者PreparedStatement+addBatch()+executeBatch()的執行效率究竟如何呢? 服務器

咱們在一個簡單的JAVA程序裏測試這三者寫法的實際性能,並窺探其在DB中的表現,如下爲JAVA代碼(多年不寫,就勿糾結代碼風格): oracle


/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package apptest;

import oracle.jdbc.*;
import java.sql.*;

/**
 *
 * @author xiangbli
 */
public class Apptest {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws SQLException {
        // TODO code application logic here

        try {
            Class.forName("oracle.jdbc.driver.OracleDriver");
        }catch(Exception e){}

        Connection cnn1=DriverManager.getConnection("jdbc:oracle:thin:@192.168.56.101:1521:cdb1", "c##maclean", "oracle");
        Statement stat1=cnn1.createStatement();
        cnn1.setAutoCommit(false);

   ResultSet rst1=stat1.executeQuery("select * from v$version");
   while(rst1.next())
   {
       System.out.println(rst1.getString(1));

   }
        long startTime = System.currentTimeMillis();
             long stopTime = System.currentTimeMillis();

   String str="begin \n  --我是一個拼接起來的SQL匿名塊 \n";
   int i;
     for(i=0;i<=15000; i++)

     {

     str= str.concat(" insert into insertit values(?,?,?,?); \n");

     }  
     str=str.concat("  commit ; end; ");
    System.out.print(str);

    cnn1.createStatement().execute("alter system flush shared_pool");

    System.out.print("\n alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第一次測試 \n");

     PreparedStatement  pstmt = cnn1.prepareStatement(str);
     int j;
     for (j=0;j<=15000;j++)
     {

         pstmt.setInt(1+j*4, 1);
         pstmt.setInt(2+j*4, 1);
         pstmt.setInt(3+j*4, 1);
         pstmt.setInt(4+j*4, 1);

     }

  //  System.out.println (" Statement Execute Batch Value " +((OraclePreparedStatement)pstmt).getExecuteBatch());

startTime = System.currentTimeMillis();
     pstmt.execute();
 stopTime = System.currentTimeMillis();
     System.out.println("拼接15000條INSERT SQL 第一次運行的耗時 Elapsed time was " + (stopTime - startTime) + " miliseconds.");

     startTime = System.currentTimeMillis();
     pstmt.execute();
     stopTime = System.currentTimeMillis();
     System.out.println("拼接15000條INSERT SQL 第二次運行的耗時 Elapsed time was " + (stopTime - startTime) + " miliseconds.");

    cnn1.createStatement().execute("alter system flush shared_pool");

    System.out.print("\n alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 \n");

        startTime = System.currentTimeMillis();

        int batch=1000;

       PreparedStatement  pstmt2 = cnn1.prepareStatement("insert into insertit values(?,?,?,?)");
      ((OraclePreparedStatement)pstmt2).setExecuteBatch(batch);
       for (int z=0;z<=15000;z++)
       {
       pstmt2.setInt(1, z);
       pstmt2.setInt(2, z);
       pstmt2.setInt(3, z);
       pstmt2.setInt(4, z);

       pstmt2.executeUpdate();

       }
       ((OraclePreparedStatement)pstmt2).sendBatch();
     cnn1.commit();

     stopTime = System.currentTimeMillis();

      System.out.println("batch size= "+batch+" 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was " + (stopTime - startTime) + " miliseconds.");

      startTime = System.currentTimeMillis();

       PreparedStatement  pstmt3 = cnn1.prepareStatement("insert into insertit values(?,?,?,?)");
      ((OraclePreparedStatement)pstmt3).setExecuteBatch(batch);

       for (int z=0;z<=15000;z++)
       {
       pstmt3.setInt(1, z);
       pstmt3.setInt(2, z);
       pstmt3.setInt(3, z);
       pstmt3.setInt(4, z);

       pstmt3.executeUpdate();

       }
     ((OraclePreparedStatement)pstmt3).sendBatch();

     cnn1.commit();

     stopTime = System.currentTimeMillis();

      System.out.println("batch size= "+batch+" 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was " + (stopTime - startTime) + " miliseconds.");

      String insert = "insert into insertit values (?,?,?,?)";

      PreparedStatement  pstmt4 = cnn1.prepareStatement(insert);

       startTime = System.currentTimeMillis();

      for (int u=0;u<=15000;u++)

      {
          pstmt4.setInt(1, u);
          pstmt4.setInt(2, u);
          pstmt4.setInt(3, u);
          pstmt4.setInt(4, u);
          pstmt4.addBatch();
      }

      pstmt4.executeBatch();
       cnn1.commit();

     stopTime = System.currentTimeMillis();

       System.out.println(" BATCH update 第一次運行的耗時 Elapsed time was " + (stopTime - startTime) + " miliseconds.");

     }

    }

拼接SQL和PreparedStatement.setExecuteBatch均執行2次,第一次沒有遊標緩存,第二次有遊標緩存。PreparedStatement+addBatch()+executeBatch()只執行一次。 app

如下爲JAVA程序端的測試結果: 工具

alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第一次測試 
拼接15000條INSERT SQL 第一次運行的耗時 Elapsed time was 441299 miliseconds.
拼接15000條INSERT SQL 第二次運行的耗時 Elapsed time was 5938 miliseconds. 性能

alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 1000 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 322 miliseconds.
batch size= 1000 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 131 miliseconds.
BATCH update 第一次運行的耗時 Elapsed time was 80 miliseconds.

如下爲DB SERVER端 10046 trace的結果:

begin
  --我是一個拼接起來的SQL匿名塊
 insert into insertit values(:1 ,:2 ,:3 ,:4 );
 insert into insertit values(:5 ,:6 ,:7 ,:8 );
 insert into insertit values(:9 ,:10 ,:11 ,:12 );
 insert into insertit values(:13 ,:14 ,:15 ,:16 );
 insert into insertit values(:17 ,:18 ,:19 ,:20 );
 insert into insertit values(:21 ,:22 ,:23 ,:24 );
 insert into insertit values(:25 ,:26 ,:27 ,:28 );
 insert into insertit values(:29 ,:30 ,:31 ,:32 );
 insert into insertit values(:33 ,:34 ,:35 ,:36 );
 insert into insertit values(:37 ,:38 ,:39 ,:40 );
 insert into insertit values(:41 ,:42 ,:43 ,:44 );
 insert into insertit values(:45 ,:46 ,:47 ,:48 );
 insert into insertit values(:49 ,:50 ,:51 ,:52 );
 insert into insertit values(:53 ,:54 ,:55 ,:56 );
 insert into insertit values(:57 ,:58 ,:59 ,:60 );
 insert into insertit values(:61 ,:62 ,:63 ,:64 );
 insert into insertit values(:65 ,:66 ,:67 ,:68 );
 insert into insertit values(:69 ,:70 ,:71 ,:72 );
...................................
 insert into insertit values(:59989 ,:59990 ,:59991 ,:59992 );
 insert into insertit values(:59993 ,:59994 ,:59995 ,:59996 );
 insert into insertit values(:59997 ,:59998 ,:59999 ,:60000 );
 insert into insertit values(:60001 ,:60002 ,:60003 ,:60004 );
  commit ; end;

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1    176.10     179.33          0         97          0           0
Execute      2    150.51     155.37          2          4          0           2
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        3    326.61     334.71          2        101          0           2

===>這是拼接SQL 在DB SERVER端總耗時334秒, CPU時間 326秒

insert into insertit
values
(:1 ,:2 ,:3 ,:4 )

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        2      0.00       0.00          0          0          0           0
Execute     32      0.09       0.11          4        823       1000       30002
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total       34      0.09       0.11          4        823       1000       30002

==》這是 使用PreparedStatement.setExecuteBatch的結果, 耗時0.11秒,cpu時間 0.09秒,
由於batch size是1000,因此實際是每1000次INSERT執行一次,因此總的執行次數約爲30次

insert into insertit
values
 (:1 ,:2 ,:3 ,:4 )

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        1      0.00       0.00          0          0          0           0
Execute      1      0.03       0.04          1         93        475       15001
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        2      0.03       0.04          1         93        475       15001

==>這是使用addBatch()+executeBatch(), execute和parse均只爲一次,即15000條數據僅解析一次 執行一次

相似於PL/SQL中bulk collect INSERT的效果

以上能夠看到拼接SQL的寫法不論是對比 setExecuteBatch 仍是 executeBatch都要滿幾百倍。

拼接15000條INSERT語句到一個Pl/SQL block中的慢主要體如今:

  1. 他是一個太長的PL/SQL block,在第一次 Parse解析時Oracle 使用PL/SQL engine引擎要掃描整個block,從上面的tkprof結果能夠看到光parse就消耗了179秒,即使不用setExecuteBatch 仍是 executeBatch僅使用最普通的batch size=1的循環SQL也要比這個拼接SQL塊。
  2. 它的執行須要在Pl/SQL引擎和SQL引擎之間不斷切換,因此Execute也很是慢
  3. 它要分配60000個綁定變量,這對於PGA的壓力太大了,極可能致使SWAP
  4. 因爲變量和SQL過長,會引發一些莫名得小几率發生的BUG

實際上JDBC 原生態的 setExecuteBatch 、 executeBatch都是爲了減小 client-server之間的交互。

setExecuteBatch 是等待可執行的SQL達到必定數目後,一次性提交給Oracle,通常推薦的Batch Size是20~50,我簡單測試了不一樣batch size在本例中的狀況:

alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 1 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 4990 miliseconds.
batch size= 1 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 3778 miliseconds.
成功構建 (總時間: 9 秒)

alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 10 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 829 miliseconds.
batch size= 10 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 474 miliseconds.
alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 20 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 694 miliseconds.
batch size= 20 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 366 miliseconds.
成功構建 (總時間: 1 秒)


alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 30 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 516 miliseconds.
batch size= 30 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 204 miliseconds.
成功構建 (總時間: 1 秒)
alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 40 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 481 miliseconds.
batch size= 40 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 188 miliseconds.
成功構建 (總時間: 1 秒)


alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 50 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 456 miliseconds.
batch size= 50 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 171 miliseconds.
成功構建 (總時間: 1 秒)


alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 100 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 450 miliseconds.
batch size= 100 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 123 miliseconds.
成功構建 (總時間: 1 秒)

alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 200 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 402 miliseconds.
batch size= 200 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 92 miliseconds.
成功構建 (總時間: 1 秒)
alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 500 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 386 miliseconds.
batch size= 500 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 95 miliseconds.
成功構建 (總時間: 1 秒)
alter system flush shared_pool 已刷新共享池,避免SQL遊標緩存 影響第二次測試 
batch size= 1000 常規循環 15000 次 INSERT SQL 第一次運行的耗時 Elapsed time was 369 miliseconds.
batch size= 1000 常規循環 15000 次 INSERT SQL 第二次運行的耗時 Elapsed time was 87 miliseconds.
成功構建 (總時間: 1 秒)

能夠看到上述例子中200~500的Execute batch size得到的性能較好。

PreparedStatement+addBatch()+executeBatch()的方式很像PL/SQL中的bulk collect處理,將變量置入batch array中,執行時將array中的數據一次插入到表中,即解析一次、執行一次,雖然對PGA會有一些少許壓力,可是從上例中也能看到,得到了最快的速度。

綜上所述對於Java Jdbc減小交互提高批量處理性能,最好的辦法是 儘量多用PreparedStatement setExecuteBatch 和 executeBatch。

對於軟件開發,特別是上層軟件的開發(OS、Database、programming language除外),千萬不要想固然去本身發明一種新的」輪子」出來,必要的基礎工具 包括 「扳手」 「螺絲」 「輪胎」,他們都是現成的,也是被久經考驗 高效可用的, 本身去從新發明輪子,不免不費時費力而不討好。

相關文章
相關標籤/搜索