PreparedStatement的預編譯原理
preparedStatement 有三大優點:
-
代碼的可讀性和可維護性。
-
PreparedStatement盡最大可能提高性能。
-
最重要的一點是極大地提高了安全性。
其中一和三的確很易理解,關於性能的提高也是最有價值的這點,我對其原理還有些質疑。
網上一:
SQL 語句被預編譯並且存儲在 PreparedStatement 對象中,其后可以使用該對象高效地多次執行該語句。
問題:預編譯在JDBC中完成的?還是數據庫中?即便存在preparedStatement中,如果這個preparedStatement 對象不被緩存起來,其后又如何重復使用,多次執行? (一些webserver會緩存preparedStatement)
我想關於其性能的提高主要得需要數據庫的支持。
oracle 性能優化寫道:
共享SQL語句:為了不重復解析相同的SQL語句,在第一次解析之后,ORACLE將SQL語句存放在內存中。
可惜的是ORACLE只對簡單的表提供高速緩沖(cache buffering) ,這個功能並不適用於多表連接查詢。
個人理解:預編譯是利用數據庫的SQL共享來實現的,因為當使用preparedStatement時,盡管參數不同,但是在語句中用占位符"?"來替代。因此很多語句就完全相同。(這個道理和J2EE的server緩存preparedStatement 的道理應該是一樣的)
問題:但是如果是這樣的話,那么oracle的多表查詢豈不是並無預編譯效果?
問題:關於緩存:是否根據創建preparedStatement的語句來做為Key來map的?例如以下:是否檢查匹配insert into tb_name (col1,col2,col2,col4) values (?, ?, ?, ?)來確定是否用已經有的執行路徑來執行?
perstmt = con.prepareStatement("insert into tb_name (col1,col2,col2,col4) values (?,?,?,?)");
以下幾篇從網絡上搜索到的三篇文章:
CSDN 寫道
一.代碼的可讀性和可維護性.
雖然用PreparedStatement來代替Statement會使代碼多出幾行,但這樣的代碼無論從可讀性還是可維護性上來說.都比直接用Statement的代碼高很多檔次:
stmt.executeUpdate(
"insert into tb_name (col1,col2,col2,col4) values ('"+var1+"','"+var2+"',"+var3+",'"+var4+"')");
perstmt = con.prepareStatement("insert into tb_name (col1,col2,col2,col4) values (?,?,?,?)");
perstmt.setString(1,var1);
perstmt.setString(2,var2);
perstmt.setString(3,var3);
perstmt.setString(4,var4);
perstmt.executeUpdate();
不用我多說,對於第一種方法.別說其他人去讀你的代碼,就是你自己過一段時間再去讀,都會覺得傷心.
二.PreparedStatement盡最大可能提高性能.
每一種數據庫都會盡最大努力對預編譯語句提供最大的性能優化.因為預編譯語句有可能被重復調用.所以語句在被DB的編譯器編譯后的執行代碼被緩存下來,那么下次調用時只要是相同的預編譯語句就不需要編譯,只要將參數直接傳入編譯過的語句執行代碼中(相當於一個涵數)就會得到執行.這並不是說只有一個Connection中多次執行的預編譯語句被緩存,而是對於整個DB中,只要預編譯的語句語法和緩存中匹配.那么在任何時候就可以不需要再次編譯而可以直接執行.而statement的語句中,即使是相同一操作,而由於每次操作的數據不同所以使整個語句相匹配的機會極小,幾乎不太可能匹配.比如:
insert into tb_name (col1,col2) values ('11','22');
insert into tb_name (col1,col2) values ('11','23');
即使是相同操作但因為數據內容不一樣,所以整個個語句本身不能匹配,沒有緩存語句的意義.事實是沒有數據庫會對普通語句編譯后的執行代碼緩存.這樣每執行一次都要對傳入的語句編譯一次.
當然並不是所以預編譯語句都一定會被緩存,數據庫本身會用一種策略,比如使用頻度等因素來決定什么時候不再緩存已有的預編譯結果.以保存有更多的空間存儲新的預編譯語句.
三.最重要的一點是極大地提高了安全性.
即使到目前為止,仍有一些人連基本的惡義SQL語法都不知道.
String sql = "select * from tb_name where name= '"+varname+"' and passwd='"+varpasswd+"'";
如果我們把[' or '1' = '1]作為varpasswd傳入進來.用戶名隨意,看看會成為什么?
select * from tb_name = '隨意' and passwd = '' or '1' = '1';
因為'1'='1'肯定成立,所以可以任何通過驗證.更有甚者:
把[';drop table tb_name;]作為varpasswd傳入進來,則:
select * from tb_name = '隨意' and passwd = '';drop table tb_name;有些數據庫是不會讓你成功的,但也有很多數據庫就可以使這些語句得到執行.
而如果你使用預編譯語句.你傳入的任何內容就不會和原來的語句發生任何匹配的關系.(前提是數據庫本身支持預編譯,但上前可能沒有什么服務端數據庫不支持編譯了,只有少數的桌面數據庫,就是直接文件訪問的那些)只要全使用預編譯語句,你就用不着對傳入的數據做任何過慮.而如果使用普通的statement,有可能要對drop,;等做費盡心機的判斷和過慮.
論壇 寫道
-
執行效率:Statement 采取直接編譯 SQL 語句的方式,扔給數據庫去執行,而 PreparedStatement 則先將 SQL 語句預編譯一遍,再填充參數,這樣效率會高一些。JDK 文檔說:SQL 語句被預編譯並且存儲在 PreparedStatement 對象中,其后可以使用該對象高效地多次執行該語句。
-
代碼可讀性:Statement 中 SQL 語句中需要 Java 中的變量,加就得進行字符串的運算,還需要考慮一些引號、單引號的問題,參數變量越多,代碼就越難看,而且會被單引號、雙引號搞瘋掉;而 PreparedStatement,則不需要這樣,參數可以采用“?”占位符代替,接下來再進行參數的填充,這樣利於代碼的可讀性,並且符合面向對象的思想。
-
安全性:Statement 由於可能需要采取字符串與變量的拼接,很容易進行 SQL 注入攻擊,而 PreparedStatement 由於是預
編譯,再填充參數的,不存在 SQL 注入問題。
Oracle 優化 寫道
- 共享SQL語句
為了不重復解析相同的SQL語句,在第一次解析之后, ORACLE將SQL語句存放在內存中。這塊位於系統全局區域SGA(system global area)的共享池(shared buffer pool)中的內存可以被所有的數據庫用戶共享。 因此,當你執行一個SQL語句(有時被稱為一個游標)時,如果它和之前的執行過的語句完全相同, ORACLE就能很快獲得已經被解析的語句以及最好的執行路徑。 ORACLE的這個功能大大地提高了SQL的執行性能並節省了內存的使用。
可惜的是ORACLE只對簡單的表提供高速緩沖(cache buffering) ,這個功能並不適用於多表連接查詢。
問題補充:
感謝geeksun的回答,我的理解是:
問題1, PreparedStatement是jdbc 驅動包里的一個類,並不是說存儲在jdbc中。
問題2, 我認為多表查詢不支持預編這是不可能的。
問題3, 我想這樣提問可能更好一些。
java 方法如下:
protected boolean updateSalary(Connection conn,BigDecimal x,String ID) throws SQLException{
PreparedStatement pstmt = null;
try {
pstmt = conn.prepareStatement("UPDATE EMPLOYEES SET SALARY = ? WHERE ID = ?");
pstmt.setBigDecimal(1, x);
pstmt.setString(2, ID);
return true;
} finally{
if (pstmt!=null){
pstmt.close();
}
}
}
第一次調用以后,是否會緩存PreparedStatement, 這個在用weblogic這種server的時候是可以肯定的。因為從數據源的來connection是一個包裝類,不是jdbc驅動的connection,而用connection來創建的PreparedStatement也是封裝類。因此盡管pstmt.close().但是實際被封裝的PreparedStatement並未關閉。
但是是第二次調用這個方法,會重新conn.prepareStatement, 這個時候我想應該是根據語句來作為key來尋找緩存的preparedStatement,但是盡管取到緩存。 如果沒有Database的配合,效率也很難提高多少。
問題補充:
謝謝geeksun的幫忙,Sql的共享我想肯定是支持多表的。 至於高速緩存應該講的是結果的緩存。
但是你對PreparedStatement的觀點我認為是不對的。 因為這個對象肯定是在WebServer端的, 不可能跑到數據庫那端。K-V對應的K值應該就是創建這個對象的那條語句,只是需要一個精通人士的肯定。
答案補充:
1. 昨天又研究了一下PreparedStatement,這里糾正一下觀點,PreparedStatement在第一次編譯后,存放在數據庫里,類似於K-V對應的方式存儲,這樣,當下一條同樣的PreparedStatement發送到數據庫里,數據庫查找到有相應的K存在,就調用K中的方法,省卻了再去重新創建語句的過程,提高數據庫的性能。
2. 多表查詢支持不支持預編譯?這個問題,我想請教一下oracle的DBA比較好一些。
3. 在J2EE的應用服務器的PreparedStatement緩存,個人認為,這里緩存的不是真正的緩存,而是數據庫的PreparedStatement緩存的代理,不過應用服務器的PreparedStatement緩存和數據庫里的PreparedStatement緩存保持一致,這樣做,可能是應用服務器在管理PreparedStatement緩存時有它自己的比較好的實現策略,比單純的使用JDBC使用數據庫的緩存具有更好的性能或內存管理。
1. PreparedStatement是存儲在JDBC里的,初始化后,緩存到了JDBC里,初始化的開銷比Statement大,對於少量的查詢操作沒有優勢,適用於大量的查詢語句才能體現性能的優勢。
2. ORACLE只對簡單的表提供高速緩沖(cache buffering) ,這個功能並不適用於多表連接查詢,回答了第二個問題"oracle的多表查詢並無預編譯效果".
3. 問題:關於緩存:是否根據創建preparedStatement的語句來做為Key來map的?
應該是這樣的,例:
PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES
SET SALARY = ? WHERE ID = ?");
pstmt.setBigDecimal(1, 153833.00);
pstmt就是緩存的key,在緩存中存的是pstmt對象。