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