JDBC性能技巧:選用JDBC對象和方法 收藏
在存儲過(guò)程中使用參數標記作為參數
當 調用存儲過(guò)程時(shí), 應總是使用參數標記(?)來(lái)代替字面上的參數. JDBC驅動(dòng)能調用數據庫的存儲過(guò)程, 也能被其它的SQL查詢(xún)執行, 或者直接通過(guò)遠程進(jìn)程調用(RPC)的方式執行. 當你將存儲過(guò)程作為一個(gè)SQL查詢(xún)執行時(shí), 數據庫要解析這個(gè)查詢(xún)語(yǔ)句, 校驗參數并將參數轉換為正確的數據類(lèi)型.
要記住, SQL語(yǔ)句總是以字符串的形式送到數據庫, 例如, “{call getCustName (12345)}”. 在這里, 即使程序中將參數作為整數賦給了getSustName, 而實(shí)現上參數還是以字符串的形式傳給了服務(wù)器. 數據庫會(huì )解析這個(gè)SQL查詢(xún), 并且根據metadata來(lái)決定存儲過(guò)程的參數類(lèi)型, 然后分解出參數"12345", 然后在最終將存儲過(guò)程作為一個(gè)SQL查詢(xún)執行之前將字串'12345’轉換為整型數.
按RPC方式調用時(shí), 之前那種SQL字符串的方式要避免使用. 取而代之, JDBC驅動(dòng)會(huì )構造一個(gè)網(wǎng)絡(luò )packet, 其中包含了本地數據類(lèi)型的參數,然后執行遠程調用.
案例 1
在這個(gè)例子中, 存儲過(guò)程不能最佳的使用RPC. 數據庫必須將這作為一個(gè)普通的語(yǔ)言來(lái)進(jìn)行解析,校驗參數類(lèi)型并將參數轉換為正確的數據類(lèi)型,最后才執行這個(gè)存儲過(guò)程.
CallableStatement cstmt = conn.prepareCall (
"{call getCustName (12345)}");
ResultSet rs = cstmt.executeQuery ();
案例 2
在這個(gè)例子中, 存儲過(guò)程能最佳的執行RPC. 因為程序避免了字面的的參數, 使用特殊的參數來(lái)調用存儲過(guò)程, JDBC驅動(dòng)能最好以RPC方式直接來(lái)執行存儲過(guò)程. SQL語(yǔ)言上的處理在這里被避免并且執行也得到很大的改善.
CallableStatement cstmt - conn.prepareCall (
"{call getCustName (?)}");
cstmt.setLong (1,12345);
ResultSet rs = cstmt.executeQuery();
使用Statement而不是PreparedStatement對象
JDBC 驅動(dòng)的最佳化是基于使用的是什么功能. 選擇PreparedStatement還是Statement取決于你要怎么使用它們. 對于只執行一次的SQL語(yǔ)句選擇Statement是最好的. 相反, 如果SQL語(yǔ)句被多次執行選用PreparedStatement是最好的.
PreparedStatement 的第一次執行消耗是很高的. 它的性能體現在后面的重復執行. 例如, 假設我使用Employee ID, 使用prepared的方式來(lái)執行一個(gè)針對Employee表的查詢(xún). JDBC驅動(dòng)會(huì )發(fā)送一個(gè)網(wǎng)絡(luò )請求到數據解析和優(yōu)化這個(gè)查詢(xún). 而執行時(shí)會(huì )產(chǎn)生另一個(gè)網(wǎng)絡(luò )請求. 在JDBC驅動(dòng)中,減少網(wǎng)絡(luò )通訊是最終的目的. 如果我的程序在運行期間只需要一次請求, 那么就使用Statement. 對于Statement, 同一個(gè)查詢(xún)只會(huì )產(chǎn)生一次網(wǎng)絡(luò )到數據庫的通訊.
對于使用 PreparedStatement池的情況下, 本指導原則有點(diǎn)復雜. 當使用PreparedStatement池時(shí), 如果一個(gè)查詢(xún)很特殊, 并且不太會(huì )再次執行到, 那么可以使用Statement. 如果一個(gè)查詢(xún)很少會(huì )被執行,但連接池中的Statement池可能被再次執行, 那么請使用PreparedStatement. 在不是Statement池的同樣情況下, 請使用Statement.
使用PreparedStatement的Batch功能
Update 大量的數據時(shí), 先Prepare一個(gè)INSERT語(yǔ)句再多次的執行, 會(huì )導致很多次的網(wǎng)絡(luò )連接. 要減少JDBC的調用次數改善性能, 你可以使用PreparedStatement的AddBatch()方法一次性發(fā)送多個(gè)查詢(xún)給數據庫. 例如, 讓我們來(lái)比較一下下面的例子.
例 1: 多次執行Prepared Statement
PreparedStatement ps = conn.prepareStatement(
"INSERT into employees values (?, ?, ?)");
for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.executeUpdate();
}
例 2: 使用Batch
PreparedStatement ps = conn.prepareStatement(
"INSERT into employees values (?, ?, ?)");
for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.addBatch();
}
ps.executeBatch();
在 例 1中, PreparedStatement被用來(lái)多次執行INSERT語(yǔ)句. 在這里, 執行了100次INSERT操作, 共有101次網(wǎng)絡(luò )往返. 其中,1次往返是預儲statement, 另外100次往返執行每個(gè)迭代. 在例2中, 當在100次INSERT操作中使用addBatch()方法時(shí), 只有兩次網(wǎng)絡(luò )往返. 1次往返是預儲statement, 另一次是執行batch命令. 雖然Batch命令會(huì )用到更多的數據庫的CPU周期, 但是通過(guò)減少網(wǎng)絡(luò )往返,性能得到提高. 記住, JDBC的性能最大的增進(jìn)是減少JDBC驅動(dòng)與數據庫之間的網(wǎng)絡(luò )通訊.
選擇合適的光標類(lèi)型
選擇適用的光標類(lèi)型以最大限度的適用你的應用程序. 本節主要討論三種光標類(lèi)型的性能問(wèn)題.
對于從一個(gè)表中順序讀取所有記錄的情況來(lái)說(shuō), Forward-Only型的光標提供了最好的性能. 獲取表中的數據時(shí), 沒(méi)有哪種方法比使用Forward-Only型的光標更快. 但不管怎樣, 當程序中必須按無(wú)次序的方式處理數據行時(shí), 這種光標就無(wú)法使用了.
對 于程序中要求與數據庫的數據同步以及要能夠在結果集中前后移動(dòng)光標, 使用JDBC的Scroll-Insensitive型光標是較理想的選擇. 此類(lèi)型的光標在第一次請求時(shí)就獲取了所有的數據(當JDBC驅動(dòng)采用'lazy'方式獲取數據時(shí)或許是很多的而不是全部的數據)并且儲存在客戶(hù)端. 因此, 第一次請求會(huì )非常慢, 特別是請求長(cháng)數據時(shí)會(huì )理嚴重. 而接下來(lái)的請求并不會(huì )造成任何網(wǎng)絡(luò )往返(當使用'lazy'方法時(shí)或許只是有限的網(wǎng)絡(luò )交通) 并且處理起來(lái)很快. 因為第一次請求速度很慢, Scroll-Insensitive型光標不應該被使用在單行數據的獲取上. 當有要返回長(cháng)數據時(shí), 開(kāi)發(fā)者也應避免使用Scroll-Insensitive型光標, 因為這樣可能會(huì )造成內存耗盡. 有些Scroll-Insensitive型光標的實(shí)現方式是在數據庫的臨時(shí)表中緩存數據來(lái)避免性能問(wèn)題, 但多數還是將數據緩存在應用程序中.
Scroll -Sensitive型光標, 有時(shí)也稱(chēng)為Keyset-Driven光標, 使用標識符, 像數據庫的ROWID之類(lèi). 當每次在結果集移動(dòng)光標時(shí), 會(huì )重新該標識符的數據. 因為每次請求都會(huì )有網(wǎng)絡(luò )往返, 性能可能會(huì )很慢. 無(wú)論怎樣, 用無(wú)序方式的返回結果行對性能的改善是沒(méi)有幫助的.
現 在來(lái)解釋一下這個(gè), 來(lái)看這種情況. 一個(gè)程序要正常的返回1000行數據到程序中. 在執行時(shí)或者第一行被請求時(shí), JDBC驅動(dòng)不會(huì )執行程序提供的SELECT語(yǔ)句. 相反, 它會(huì )用鍵標識符來(lái)替換SELECT查詢(xún), 例如, ROWID. 然后修改過(guò)的查詢(xún)都會(huì )被驅動(dòng)程序執行,跟著(zhù)會(huì )從數據庫獲取所有1000個(gè)鍵值. 每一次對一行結果的請求都會(huì )使JDBC驅動(dòng)直接從本地緩存中找到相應的鍵值, 然后構造一個(gè)包含了'WHERE ROWID=?'子句的最佳化查詢(xún), 再接著(zhù)執行這個(gè)修改過(guò)的查詢(xún), 最后從服務(wù)器取得該數據行.
當程序無(wú)法像Scroll-Insensitive型光標一樣提供足夠緩存時(shí), Scroll-Sensitive型光標可以被替代用來(lái)作為動(dòng)態(tài)的可滾動(dòng)的光標.
使用有效的getter方法
JDBC 提供多種方法從ResultSet中取得數據, 像getInt(), getString(), 和getObject()等等. 而getObject()方法是最泛化了的, 提供了最差的性能。 這是因為JDBC驅動(dòng)必須對要取得的值的類(lèi)型作額外的處理以映射為特定的對象. 所以就對特定的數據類(lèi)型使用相應的方法.
要更進(jìn)一步的改善性能, 應在取得數據時(shí)提供字段的索引號, 例如, getString(1), getLong(2), 和getInt(3)等來(lái)替代字段名. 如果沒(méi)有指定字段索引號, 網(wǎng)絡(luò )交通不會(huì )受影響, 但會(huì )使轉換和查找的成本增加. 例如, 假設你使用getString("foo") ... JDBC驅動(dòng)可能會(huì )將字段名轉為大寫(xiě)(如果需要), 并且在到字段名列表中逐個(gè)比較來(lái)找到"foo"字段. 如果可以, 直接使用字段索引, 將為你節省大量的處理時(shí)間.
例如, 假設你有一個(gè)100行15列的ResultSet, 字段名不包含在其中. 你感興趣的是三個(gè)字段 EMPLOYEENAME (字串型), EMPLOYEENUMBER (長(cháng)整型), 和SALARY (整型). 如果你指定getString(“EmployeeName”), getLong(“EmployeeNumber”), 和getInt(“Salary”), 查詢(xún)旱每個(gè)字段名必須被轉換為metadata中相對應的大小寫(xiě), 然后才進(jìn)行查找. 如果你使用getString(1), getLong(2), 和getInt(15). 性能就會(huì )有顯著(zhù)改善.
獲取自動(dòng)生成的鍵值
有許多數據庫提供了隱藏列為表中的每行記錄分配一 個(gè)唯一鍵值. 很典型, 在查詢(xún)中使用這些字段類(lèi)型是取得記錄值的最快的方式, 因為這些隱含列通常反應了數據在磁盤(pán)上的物理位置. 在JDBC3.0之前, 應用程序只可在插入數據后通過(guò)立即執行一個(gè)SELECT語(yǔ)句來(lái)取得隱含列的值.
例如:
//插入行
int rowcount = stmt.executeUpdate (
"insert into LocalGeniusList (name) values ('Karen')");
// 現在為新插入的行取得磁盤(pán)位置 - rowid
ResultSet rs = stmt.executeQuery (
"select rowid from LocalGeniusList where name = 'Karen'");
這 種取得隱含列的方式有兩個(gè)主要缺點(diǎn). 第一, 取得隱含列是在一個(gè)獨立的查詢(xún)中, 它要透過(guò)網(wǎng)絡(luò )送到服務(wù)器后再執行. 第二, 因為不是主鍵, 查詢(xún)條件可能不是表中的唯一性ID. 在后面一個(gè)例子中, 可能返回了多個(gè)隱含列的值, 程序無(wú)法知道哪個(gè)是最后插入的行的值.
(譯者:由于不同的數據庫支持的程度不同,返回rowid的方式各有差異。在SQL Server中,返回最后插入的記錄的id可以用這樣的查詢(xún)語(yǔ)句:SELECT @IDENTITY )
JDBC3.0規范中的一個(gè)可選特性提供了一種能力, 可以取得剛剛插入到表中的記錄的自動(dòng)生成的鍵值.
例如:
int rowcount = stmt.executeUpdate (
"insert into LocalGeniusList (name) values ('Karen')",
// 插入行并返回鍵值
Statement.RETURN_GENERATED_KEYS);
ResultSet rs = stmt.getGeneratedKeys ();
// 得到生成的鍵值
現在, 程序中包含了一個(gè)唯一性ID, 可以用來(lái)作為查詢(xún)條件來(lái)快速的存取數據行, 甚至于表中沒(méi)有主鍵的情況也可以.
這種取得自動(dòng)生成的鍵值的方式給JDBC的開(kāi)發(fā)者提供了靈活性, 并且使存取數據的性能得到提升.
選擇合適的數據類(lèi)型
接 收和發(fā)送某些數據可能代價(jià)昂貴. 當你設計一個(gè)schema時(shí), 應選擇能被最有效地處理的數據類(lèi)型. 例如, 整型數就比浮點(diǎn)數或實(shí)數處理起來(lái)要快一些. 浮點(diǎn)數的定義是按照數據庫的內部規定的格式, 通常是一種壓縮格式. 數據必須被 解壓和轉換到另外種格式, 這樣它才能被數據的協(xié)議處理.
獲取ResultSet
由于數據庫系統對可滾動(dòng)光標的支持有 限, 許多JDBC驅動(dòng)程序并沒(méi)有實(shí)現可滾動(dòng)光標. 除非你確信數據庫支持可滾動(dòng)光標的結果集, 否則不要調用rs.last()和rs.getRow()方法去找出數據集的最大行數. 因為JDBC驅動(dòng)程序模擬了可滾動(dòng)光標, 調用rs.last()導致了驅動(dòng)程序透過(guò)網(wǎng)絡(luò )移到了數據集的最后一行. 取而代之, 你可以用ResultSet遍歷一次計數或者用SELECT查詢(xún)的COUNT函數來(lái)得到數據行數.
通常情況下,請不要寫(xiě)那種依賴(lài)于結果集行數的代碼, 因為驅動(dòng)程序必須獲取所有的數據集以便知道查詢(xún)會(huì )返回多少行數據.