JDBC性能優化篇

系統性能. 
少用Metadata方法 
    與其它的JDBC方法相比, 由ResultSet對象生成的metadata對象的相對來講是很慢的. 應用程序應該緩存從ResultSet返回的metadata信息,避免屢次沒必要要的執行這個操做. 
幾乎沒有哪個JDBC應用程序不用到metadata,雖然如此,你仍能夠經過少用它們來改善系統性能. 要返回JDBC規範規定的結果集的全部列信息, 一個簡單的metadata的方法調用可能會使JDBC驅動程序去執行很複雜的查詢甚至屢次查詢去取得這些數據. 這些細節上的SQL語言的操做是很是消耗性能的. 
應用程序應該緩存這些metadata信息. 例如, 程序調用一次getTypeInfo方法後就將這些程序所依賴的結果信息緩存. 而任何程序都不大可能用到這些結果信息中的全部內容,因此這些緩存信息應該是不難維護的. 
避免null參數 
在metadata的方法中使用null參數或search patterns是很耗時的. 另外, 額外的查詢會致使潛在的網絡交通的增長. 應儘量的提供一些non-null的參數給metadata方法. 
由於metadata的方法很慢, 應用程序要儘量有效的調用它們. 許多應用程序只傳遞少許的non-null參數給這些方法. 數據庫

Java代碼   收藏代碼
  1. 例如:  
  2. ResultSet WSrs = WSc.getTables (null, null, "WSTable", null);  
  3. 應該這樣:  
  4. ResultSet WSrs = WSc.getTables ("cat1", "johng", "WSTable",  "TABLE");  


在第一個getTables()的調用中, 程序可能想知道表'WSTable'是否存在. 固然, JDBC驅動程序會逐個調用它們而且會解譯不一樣的請求. JDBC驅動程序會解譯請求爲: 返回全部的表, 視圖, 系統表, synonyms, 臨時表, 或存在於任何數據庫類別任何Schema中的任何別名爲'WSTable'的對象. 
第二個getTables()的調用會獲得更正確的程序想知道的內容. JDBC驅動程序會解譯這個請求爲: 返回當前數據庫類別中全部存在於'johng'這個schema中的全部表. 
很顯然, JDBC驅動程序處理第二個請求比處理第一個請求更有效率一些. 
有時, 你所請求信息中的對象有些信息是已知的. 當調用metadata方法時, 程序能傳送到驅動程序的的任何有用信息均可以致使性能和可靠性的改善. 
使用'啞元'(dummy)查詢肯定表的特性 
要避免使用getColumns()去肯定一個表的特性. 而應該使用一個‘啞元’查詢來使用getMetadata()方法. 
請考慮這樣一個程序, 程序中要容許用戶選取一些列. 咱們是否應該使用getColumns()去返回列信息給用戶仍是以一個'啞元'查詢來調用getMetadata()方法呢? 
案例 1: GetColumns 方法 緩存

Java代碼   收藏代碼
  1. ResultSet WSrc = WSc.getColumns (... "UnknownTable" ...);  
  2. // getColumns()會發出一個查詢給數據庫系統  
  3. . . .  
  4. WSrc.next();  
  5. string Cname = getString(4);  
  6. . . .  
  7. // 用戶必須從反覆從服務器獲取N行數據  
  8. // N = UnknownTable的列數  



案例 2: GetMetadata 方法 服務器

Java代碼   收藏代碼
  1. // 準備'啞元'查詢  
  2. PreparedStatement WSps = WSc.prepareStatement  
  3.   ("SELECT * from UnknownTable WHERE 1 = 0");  
  4. // 查詢歷來沒有被執行,只是被預儲  
  5. ResultSetMetaData WSsmd=WSps.getMetaData();  
  6. int numcols = WSrsmd.getColumnCount();  
  7. ...  
  8. int ctype = WSrsmd.getColumnType(n)  
  9. ...  


// 得到了列的完整信息 
在這兩個案例中, 一個查詢被傳送到服務器. 但在案例1中, 查詢必須被預儲和執行, 結果的描述信息必須肯定(以傳給getColumns()方法), 而且客戶端必須接收一個包含列信息的結果集. 在案例2中, 只要準備一個簡單的查詢而且只用肯定結果描述信息. 很顯然, 案例2執行方式更好一些. 
這個討論有點複雜, 讓咱們考慮一個沒有本地化支持prepared statement的DBMS服務器. 案例1的性能沒有改變, 但案例2中, 由於'啞元'查詢必須被執行而不是被預儲使得它的性能加強了一些. 由於查詢中的WHERE子句老是爲FALSE, 查詢在不用存取表的數據狀況的下會生成沒有數據的結果集. 在這種狀況下,第二種方式固然比第一種方式好一些. 
總而言之,老是使用ResultSet的metadata方法去獲取列信息,像列名,列的數據類型,列的數據精度和長度等. 當要求的信息沒法從ResultSet的metadata中獲取時纔去用getColumns()方法(像列的缺省值這些信息等). 




獲取數據 
要有效的獲取數據,就只需返回你須要的數據, 以及不少用效的方法. 本節的指導原則將幫助你使用JDB獲取數據時優化系統性能. 
獲取長數據 
如非必要, 應用程序不該請求長的數據, 由於長的數據經過網絡傳輸會很是慢和消耗資源. 
大多數用戶並不想看到大堆的數據. 若是用戶不想處理這些長數據, 那麼程序應可以再次查詢數據庫, 在SELECT子句中指定須要的列名. 這種方式容許通常用戶獲取結果集而不用消耗昂貴的網絡流量. 
雖然最好的方式是不要將長數據包括在SELECT子句的列名中,但仍是有一些應用程序在發送查詢給JDBC驅動程序時並無在SELECT子句中明確指出列名 (確切一點, 有些程序發送這樣的查詢: select * from 
...). 若是SELECT子句的列名中包含長數據, 許多JDBC驅動程序必須在查詢時從新獲取數據, 甚至在ResultSet中這些長數據並無被程序用到. 在可能狀況下,開發者應該試着去實現一種不需獲取全部列數據的方法. 

例如,看如下的JDBC代碼: 網絡

Java代碼   收藏代碼
  1. ResultSet rs = stmt.executeQuery (  
  2.    "select * from Employees where SSID = '999-99-2222'");  
  3. rs.next();  
  4. string name = rs.getString (4);  


要記住JDBC驅動程序沒有知覺. 當查詢被執行時它不知道哪些列是程序所要的. 驅動程序只知道應用程序能請求任意的列. 當JDBC驅動程序處理 rs.next() 請求時, 它可能會跨過網絡從數據庫服務器至少返回一行結果. 在這種狀況下, 每一個結果行會包含全部的列數據– 若是Employees表有一列包含員工相片的話它也會被包含在結果裏面. 限制SELECT子句的列名列表而且只包含有用的列名,會減小網絡流量及更快的查詢性能. 
另外,雖然getClob()和getBlob()方法能夠容許應用程序去如何控制獲取長數據, 但開發者必須認識到在許多狀況下, JDBC驅動程序缺乏真正的LOB定位器的支持. 像這種狀況下,在暴露getClob和getBlob方法給開發者以前驅動程序必須通過網絡獲取全部的長數據. 
減小獲取的數據量 
有時必需要獲取長數據. 這時, 要注意的是大多數用戶並不想在屏幕上看到100k甚至更多的文字. 
要減小網絡交通和改善性能, 經過調用setMaxRows(), SetMaxFieldSize及SetFetchSize()方法, 你能夠減小取獲取的數據量. 另外一種方式是減小數據的列數. 若是驅動程序容許你定義packet的大小, 使用最小的packet尺寸會適合你的須要. 
記住: 要當心的返回只有你須要的行和列數據. 當你只須要2列數據而你卻返回的5列數據時,性能會下降 – 特別是不須要的行中包含有長數據時. 
選擇合適的數據類型 
接收和發送某些數據可能代價昂貴. 當你設計一個schema時, 應選擇能被最有效地處理的數據類型. 例如, 整型數就比浮點數或實數處理起來要快一些. 浮點數的定義是按照數據庫的內部規定的格式, 一般是一種壓縮格式. 數據必須被 解壓和轉換到另外種格式, 這樣它才能被數據的協議處理. 
獲取ResultSet 
因爲數據庫系統對可滾動光標的支持有限, 許多JDBC驅動程序並無實現可滾動光標. 除非你確信數據庫支持可滾動光標的結果集, 不然不要調用rs.last()和rs.getRow()方法去找出數據集的最大行數. 由於JDBC驅動程序模擬了可滾動光標, 調用rs.last()致使了驅動程序透過網絡移到了數據集的最後一行. 取而代之, 你能夠用ResultSet遍歷一次計數或者用SELECT查詢的COUNT函數來獲得數據行數. 
一般狀況下,請不要寫那種依賴於結果集行數的代碼, 由於驅動程序必須獲取全部的數據集以便知道查詢會返回多少行數據. 



選用JDBC對象和方法 
本節的指導原則將幫助你在選用JDBC的對象和方法時哪些會有最好的性能. 
在存儲過程當中使用參數標記做爲參數 
當調用存儲過程時, 應老是使用參數標記(?)來代替字面上的參數. JDBC驅動能調用數據庫的存儲過程, 也能被其它的SQL查詢執行, 或者直接經過遠程進程調用(RPC)的方式執行. 當你將存儲過程做爲一個SQL查詢執行時, 數據庫要解析這個查詢語句, 校驗參數並將參數轉換爲正確的數據類型. 
要記住, SQL語句老是以字符串的形式送到數據庫, 例如, 「{call getCustName (12345)}」. 在這裏, 即便程序中將參數做爲整數賦給了getSustName, 而實現上參數仍是以字符串的形式傳給了服務器. 數據庫會解析這個SQL查詢, 而且根據metadata來決定存儲過程的參數類型, 而後分解出參數"12345", 而後在最終將存儲過程做爲一個SQL查詢執行以前將字串'12345’轉換爲整型數. 
按RPC方式調用時, 以前那種SQL字符串的方式要避免使用. 取而代之, JDBC驅動會構造一個網絡packet, 其中包含了本地數據類型的參數,而後執行遠程調用. 
案例 1 函數

Java代碼   收藏代碼
  1. 在這個例子中, 存儲過程不能最佳的使用RPC. 數據庫必須將這做爲一個普通的語言來進行解析,校驗參數類型並將參數轉換爲正確的數據類型,最後才執行這個存儲過程.  
  2. CallableStatement cstmt = conn.prepareCall (  
  3.    "{call getCustName (12345)}");   
  4. ResultSet rs = cstmt.executeQuery ();  


案例 2 性能

Java代碼   收藏代碼
  1. 在這個例子中, 存儲過程能最佳的執行RPC. 由於程序避免了字面的的參數, 使用特殊的參數來調用存儲過程, JDBC驅動能最好以RPC方式直接來執行存儲過程. SQL語言上的處理在這裏被避免而且執行也獲得很大的改善.  
  2. CallableStatement cstmt - conn.prepareCall (  
  3.    "{call getCustName (?)}");  
  4. cstmt.setLong (1,12345);  
  5. ResultSet rs = cstmt.executeQuery();  


使用Statement而不是PreparedStatement對象 
JDBC驅動的最佳化是基於使用的是什麼功能. 選擇PreparedStatement仍是Statement取決於你要怎麼使用它們. 對於只執行一次的SQL語句選擇Statement是最好的. 相反, 若是SQL語句被屢次執行選用PreparedStatement是最好的. 
PreparedStatement的第一次執行消耗是很高的. 它的性能體如今後面的重複執行. 例如, 假設我使用Employee ID, 使用prepared的方式來執行一個針對Employee表的查詢. JDBC驅動會發送一個網絡請求到數據解析和優化這個查詢. 而執行時會產生另外一個網絡請求. 在JDBC驅動中,減小網絡通信是最終的目的. 若是個人程序在運行期間只須要一次請求, 那麼就使用Statement. 對於Statement, 同一個查詢只會產生一次網絡到數據庫的通信. 
對於使用PreparedStatement池的狀況下, 本指導原則有點複雜. 當使用PreparedStatement池時, 若是一個查詢很特殊, 而且不太會再次執行到, 那麼可使用Statement. 若是一個查詢不多會被執行,但鏈接池中的Statement池可能被再次執行, 那麼請使用PreparedStatement. 在不是Statement池的一樣狀況下, 請使用Statement. 
使用PreparedStatement的Batch功能 
Update大量的數據時, 先Prepare一個INSERT語句再屢次的執行, 會致使不少次的網絡鏈接. 要減小JDBC的調用次數改善性能, 你可使用PreparedStatement的AddBatch()方法一次性發送多個查詢給數據庫. 例如, 讓咱們來比較一下下面的例子. 
例 1: 屢次執行Prepared Statement 優化

Java代碼   收藏代碼
  1. PreparedStatement ps = conn.prepareStatement(  
  2.    "INSERT into employees values (?, ?, ?)");  
  3. for (n = 0; n < 100; n++) {  
  4.   ps.setString(name[n]);  
  5.   ps.setLong(id[n]);  
  6.   ps.setInt(salary[n]);  
  7.   ps.executeUpdate();  
  8. }  


例 2: 使用Batch spa

Java代碼   收藏代碼
  1. PreparedStatement ps = conn.prepareStatement(  
  2.    "INSERT into employees values (?, ?, ?)");  
  3. for (n = 0; n < 100; n++) {  
  4.   ps.setString(name[n]);  
  5.   ps.setLong(id[n]);  
  6.   ps.setInt(salary[n]);  
  7.   ps.addBatch();  
  8. }  


ps.executeBatch(); 
在例 1中, PreparedStatement被用來屢次執行INSERT語句. 在這裏, 執行了100次INSERT操做, 共有101次網絡往返. 其中,1次往返是預儲statement, 另外100次往返執行每一個迭代. 在例2中, 當在100次INSERT操做中使用addBatch()方法時, 只有兩次網絡往返. 1次往返是預儲statement, 另外一次是執行batch命令. 雖然Batch命令會用到更多的數據庫的CPU週期, 可是經過減小網絡往返,性能獲得提升. 記住, JDBC的性能最大的增進是減小JDBC驅動與數據庫之間的網絡通信. 
選擇合適的光標類型 
選擇適用的光標類型以最大限度的適用你的應用程序. 本節主要討論三種光標類型的性能問題. 
對於從一個表中順序讀取全部記錄的狀況來講, Forward-Only型的光標提供了最好的性能. 獲取表中的數據時, 沒有哪一種方法比使用Forward-Only型的光標更快. 但無論怎樣, 當程序中必須按無次序的方式處理數據行時, 這種光標就沒法使用了. 
對於程序中要求與數據庫的數據同步以及要可以在結果集中先後移動光標, 使用JDBC的Scroll-Insensitive型光標是較理想的選擇. 此類型的光標在第一次請求時就獲取了全部的數據(當JDBC驅動採用'lazy'方式獲取數據時或許是不少的而不是所有的數據)而且儲存在客戶端. 所以, 第一次請求會很是慢, 特別是請求長數據時會理嚴重. 而接下來的請求並不會形成任何網絡往返(當使用'lazy'方法時或許只是有限的網絡交通) 而且處理起來很快. 由於第一次請求速度很慢, Scroll-Insensitive型光標不該該被使用在單行數據的獲取上. 當有要返回長數據時, 開發者也應避免使用Scroll-Insensitive型光標, 由於這樣可能會形成內存耗盡. 有些Scroll-Insensitive型光標的實現方式是在數據庫的臨時表中緩存數據來避免性能問題, 但多數仍是將數據緩存在應用程序中. 
Scroll-Sensitive型光標, 有時也稱爲Keyset-Driven光標, 使用標識符, 像數據庫的ROWID之類. 當每次在結果集移動光標時, 會從新該標識符的數據. 由於每次請求都會有網絡往返, 性能可能會很慢. 不管怎樣, 用無序方式的返回結果行對性能的改善是沒有幫助的. 
如今來解釋一下這個, 來看這種狀況. 一個程序要正常的返回1000行數據到程序中. 在執行時或者第一行被請求時, JDBC驅動不會執行程序提供的SELECT語句. 相反, 它會用鍵標識符來替換SELECT查詢, 例如, ROWID. 而後修改過的查詢都會被驅動程序執行,跟着會從數據庫獲取全部1000個鍵值. 每一次對一行結果的請求都會使JDBC驅動直接從本地緩存中找到相應的鍵值, 而後構造一個包含了'WHERE ROWID=?'子句的最佳化查詢, 再接着執行這個修改過的查詢, 最後從服務器取得該數據行. 
當程序沒法像Scroll-Insensitive型光標同樣提供足夠緩存時, Scroll-Sensitive型光標能夠被替代用來做爲動態的可滾動的光標. 
使用有效的getter方法 
JDBC提供多種方法從ResultSet中取得數據, 像getInt(), getString(), 和getObject()等等. 而getObject()方法是最泛化了的, 提供了最差的性能。 這是由於JDBC驅動必須對要取得的值的類型做額外的處理以映射爲特定的對象. 因此就對特定的數據類型使用相應的方法. 
要更進一步的改善性能, 應在取得數據時提供字段的索引號, 例如, getString(1), getLong(2), 和getInt(3)等來替代字段名. 若是沒有指定字段索引號, 網絡交通不會受影響, 但會使轉換和查找的成本增長. 例如, 假設你使用getString("foo") ... JDBC驅動可能會將字段名轉爲大寫(若是須要), 而且在到字段名列表中逐個比較來找到"foo"字段. 若是能夠, 直接使用字段索引, 將爲你節省大量的處理時間. 
例如, 假設你有一個100行15列的ResultSet, 字段名不包含在其中. 你感興趣的是三個字段 EMPLOYEENAME (字串型), EMPLOYEENUMBER (長整型), 和SALARY (整型). 若是你指定getString(「EmployeeName」), getLong(「EmployeeNumber」), 和getInt(「Salary」), 查詢旱每一個字段名必須被轉換爲metadata中相對應的大小寫, 而後才進行查找. 若是你使用getString(1), getLong(2), 和getInt(15). 性能就會有顯著改善. 
獲取自動生成的鍵值 
有許多數據庫提供了隱藏列爲表中的每行記錄分配一個惟一鍵值. 很典型, 在查詢中使用這些字段類型是取得記錄值的最快的方式, 由於這些隱含列一般反應了數據在磁盤上的物理位置. 在JDBC3.0以前, 應用程序只可在插入數據後經過當即執行一個SELECT語句來取得隱含列的值. 
例如: 
//插入行 設計

Java代碼   收藏代碼
  1. int rowcount = stmt.executeUpdate (  
  2.    "insert into LocalGeniusList (name) values ('Karen')");   
  3. // 如今爲新插入的行取得磁盤位置 - rowid  
  4. ResultSet rs = stmt.executeQuery (  
  5.    "select rowid from LocalGeniusList where name = 'Karen'");  


這種取得隱含列的方式有兩個主要缺點. 第一, 取得隱含列是在一個獨立的查詢中, 它要透過網絡送到服務器後再執行. 第二, 由於不是主鍵, 查詢條件可能不是表中的惟一性ID. 在後面一個例子中, 可能返回了多個隱含列的值, 程序沒法知道哪一個是最後插入的行的值. 
(譯者:因爲不一樣的數據庫支持的程度不一樣,返回rowid的方式各有差別。在SQL Server中,返回最後插入的記錄的id能夠用這樣的查詢語句:SELECT @IDENTITY ) 
JDBC3.0規範中的一個可選特性提供了一種能力, 能夠取得剛剛插入到表中的記錄的自動生成的鍵值. 
例如: 對象

Java代碼   收藏代碼
  1. int rowcount = stmt.executeUpdate (  
  2.    "insert into LocalGeniusList (name) values ('Karen')",  
  3. // 插入行並返回鍵值  
  4. Statement.RETURN_GENERATED_KEYS);   
  5. ResultSet rs = stmt.getGeneratedKeys ();   

// 獲得生成的鍵值 如今, 程序中包含了一個惟一性ID, 能夠用來做爲查詢條件來快速的存取數據行, 甚至於表中沒有主鍵的狀況也能夠.這種取得自動生成的鍵值的方式給JDBC的開發者提供了靈活性, 而且使存取數據的性能獲得提高. 選擇合適的數據類型 接收和發送某些數據可能代價昂貴. 當你設計一個schema時, 應選擇能被最有效地處理的數據類型. 例如, 整型數就比浮點數或實數處理起來要快一些. 浮點數的定義是按照數據庫的內部規定的格式, 一般是一種壓縮格式. 數據必須被 解壓和轉換到另外種格式, 這樣它才能被數據的協議處理. 獲取ResultSet 因爲數據庫系統對可滾動光標的支持有限, 許多JDBC驅動程序並無實現可滾動光標. 除非你確信數據庫支持可滾動光標的結果集, 不然不要調用rs.last()和rs.getRow()方法去找出數據集的最大行數. 由於JDBC驅動程序模擬了可滾動光標, 調用rs.last()致使了驅動程序透過網絡移到了數據集的最後一行. 取而代之, 你能夠用ResultSet遍歷一次計數或者用SELECT查詢的COUNT函數來獲得數據行數. 一般狀況下,請不要寫那種依賴於結果集行數的代碼, 由於驅動程序必須獲取全部的數據集以便知道查詢會返回多少行數據.

相關文章
相關標籤/搜索