我們暫時先不說其他的,我們先做一個簡單的實驗來證明來看出一些問題,最後通過為什麼來說明實驗的結論,并介紹原理和正常檢視方式,好了,我們先看看下面三段代碼分别執行的結果。
首先為了測試,我們需要建立一張表:
create table pre_test_table(
c1 number,
c2 varchar2(100)
);
好了,我們做一個插入操作的對比:
代碼段1:
begin
for i in 1..20000 loop
execute immediate 'insert into pre_test_table values('||i||',''test'')';
end loop;
commit;
end;
代碼片段2:
execute immediate 'insert into pre_test_table values(:v1,:v2)' using i,'test';
commit;
代碼片段3:
insert into pre_test_table values(i,'test');
三段代碼執行效率你可以清晰的對比出來,代碼段1是最慢的,而且比後兩者慢很多倍,而代碼片段2和代碼片段3執行效率基本是一樣的,為什麼會有這樣的效果呢?看了下面的推敲,我們就清楚了,我們先把資料清理掉,共享池清理一下(下面我們再說),在做操作比較好。
現在我需要做的sql語句操作是對表的插入pre_test_table values表的操作:
insert into pre_test_table values....
至于參數如何,我們不一定,首先查詢一下共享池内部做此操作的記錄:
select * from v$sqlarea
where sql_text like 'insert into pre_test_table values%';
發現資料太多,有多少不好說了,暫時不數了,因為對得出結論沒有意義,需要清理下共享池友善試驗。
truncate table pre_test_table;//清空表
alter system flush shared_pool;//清空緩沖區
查詢共享池:
發現沒有任何資料。。。。我們開始比較幹淨的做實驗了。。。
開始試驗:
我們将上述試驗的循環次數降低為3次
首先執行代碼段1:
for i in 1..3 loop
查詢共享池(發現多了3條記錄):
sql> select sql_text,hash_value,parse_calls from v$sqlarea
2 where sql_text like 'insert into pre_test_table values%';
sql_text hash_value parse_calls
--------------------------------------------------------------- -------------- ---------------------
insert into pre_test_table values(1,'test') 2868385396 1
insert into pre_test_table values(2,'test') 2513099158 1
insert into pre_test_table values(3,'test') 455953479 1
再執行第二段代碼:
再查詢共享池(發現隻多了一條sql):
sql_text hash_value parse_calls
--------------------------------------------------------------- ---------------- ------------
insert into pre_test_table values(1,'test') 2868385396 1
insert into pre_test_table values(:v1,:v2) 357326048 3
insert into pre_test_table values(2,'test') 2513099158 1
insert into pre_test_table values(3,'test') 455953479 1
再執行第三段代碼:
insert into pre_test_table values(i,'test');
再查詢共享池(發現也隻多了一條sql):
sql_text hash_value parse_calls
------------------------------------------------------------- ------------------ ------------
insert into pre_test_table values(1,'test') 2868385396 1
insert into pre_test_table values(:b1 ,'test') 2239119514 3
insert into pre_test_table values(:v1,:v2) 357326048 3
insert into pre_test_table values(2,'test') 2513099158 1
insert into pre_test_table values(3,'test') 455953479 1
從這裡可能大家基本可以得出初步的結論,就是第一段代碼每條sql都會占用共享池,并征用共享池,而且用過一次後就不會在使用了,這個答案是正确的,我首先給出預編譯和拼sql對oracle的四個不同影響進行定義,下面再說明為什麼:
1、拼sql會導緻共享池的長期征用狀态
2、拼sql會導緻共享池中存在一堆的垃圾sql。
3、拼sql會硬解析(hard parse),而預編譯可以充分利用軟解析。
4、拼sql會導緻注入型錯誤。
疑問:
為什麼第二段代碼和第三段代碼是一個效果呢,這裡說明一下,在存儲過程提高效率的基本因素就是你如果按照過程化去寫sql,存儲過程預設就會按照預編譯方式去執行,因為這是oracle優化的基本原則,而即使在過程中使用execute immediate sql_str去執行,請注意,execute immediate不是sql語句,而是oracle在過程中支援的指令,即直接發送執行指令給oracle的分析器,是以這個是否進行編譯完全看你的sql是什麼樣的了,而不是oracle自己能決定的。
過程說明:
sql從終端通過1521 tcp服務端口以字元串方式傳送至oracle後(包含java程式也是這樣,其他應用程式,如出現string sql = "select * from a where id="+id;也會出現和代碼段1一樣的結果),oracle通過hash算法對其sql轉換,并在共享池中查找是否存在同樣hash值的sql(即:sql即使是參數或者大小寫不同,也會導緻找不到一樣的hash值),如果找到了,直接執行已經編譯完的sql,并修改使用率(這個有用);若沒有,則首先通過硬解析工具對其進行各項文法分析和性能名額分析等等,然後開始征用共享池(此為共享資源),并注冊到共享池中,标志調用次數為1,然後再執行,當在大量征用共享資源時候,并且在硬解析過程中,高并發将導緻阻塞。可以将上述第一段代碼的循環次數增加為10萬,基本機器可以弄死,呵呵。。。。
共享池的大小肯定是有限制的,是以oracle在共享池不夠用的時候,采用基于lru為核心的算法進行替換(上述的parse_calls字段可以基本看出sql被調用的次數,但是不完全依賴于它),我們最希望的就是不要做這樣的操作,因為這樣的操作必然面臨磁盤讀取,在記憶體中擷取我們稱為命中,命中率高才能提高使用率,系統的整體性能才能得到保證。
第四點中提到的至于注入型錯誤或攻擊就是傳送特殊字元串,導緻sql執行sql的修改,為什麼,因為你的sql是拼出來的,我舉個簡單例子:
你的程式中:
string sql = "select * from a where a.name = '"+name+"'";
此name假如為查詢條件傳入,那麼此時我在查詢條件文本框中這樣輸入:
第一種輸入:
' or 1=1--
此時你的sql變成:
select * from a where a.name ='' or 1=1 --'
後面兩杠是屏蔽你後面的sql,用一個or 1=1前面的東東不論是啥就永遠成立了(注意:oracle的sql執行,優先級是and 大于 or的,是以隻要or 1=1,無論有多少個and,最後會和這個or 1=1去比對,所有的資料都會提取出來),此時分頁或者說導出控制天數或者資料量,都控制不了,有多少資料就會出來多少資料,首先資料權限沒有了,然後開幾個浏覽器系統就能挂掉,呵呵!
另一種更加損的輸入招數是:
';drop table dual--
在我以前用sql server的jdbc時,這種方式是支援的,這樣是很損的一種辦法。其實輸入的方式有些通過url有些通過條件,不一定,而且千奇百怪,有些想都想不到,至少他可能會導緻你的sql執行不是那麼順利篡改了sql執行的原有意義。
是以oltp系統使用這樣的sql,尤其對于oracle資料庫(其餘資料庫請自己研究下),是非常惡心的,是以我們在oltp系統是封殺這樣的sql存在。
簡要共享池的使用介紹:
談到共享池我們大緻介紹一些sga的内容,oracle資料庫我們操作主要對象是執行個體,而非資料庫本身,主要原因為:性能、安全性。而執行個體大緻分:sga、pga,本文隻是由共享池介紹一下sga,細節說下其共享池部分,而pga後續讨論。
sga内部主要包含:資料緩沖區、共享池、java池、大池、stream池、重做日志緩沖區;
pga内部包含:使用者session資訊、排序資訊、hash area、堆棧。這些資訊被背景程序所控制,版本遞增的背景程序也在不斷增加,細節的資訊後續讨論。
其實今天所謂查詢共享池,也是查詢共享池内部的library cache。我們在sqlplus中最常用的指令就是:
sql> show sga;
total system global area 1.7062e+10 bytes
fixed size 2102776 bytes
variable size 4613736968 bytes
database buffers 1.2432e+10 bytes
redo buffers 14671872 bytes
也可以使用一下方式查詢和上述一樣的sga資訊:
sql> select * from v$sga;
name value
-------------------- ----------
fixed size 2102776
variable size 4613736968
database buffers 1.2432e+10
redo buffers 14671872
分别解釋下幾個字段的意義:
total system global area:代表sga的總體大小,包含下面幾者之和,都是以byte為機關,即位元組;
fixed size:字典資訊、控制資訊、狀态資訊。
variable size:共享池(shared pool)、java池(java pool)、大池(large pool)、stream pool;
database buffers:為資料緩沖區,oltp系統要求這塊設定較大。
redo buffer:重做日志緩沖區,适當提高緩沖區,減少檔案組切換,可以提高效率。
通過一下sql可以得到sga内部詳細的元件配置設定情況:
sql> select * from v$sgastat;
pool name bytes
------------ -------------------------- ----------
fixed_sga 2102776
buffer_cache 1.2432e+10
log_buffer 14671872
shared pool transaction 8062512
shared pool table definiti 80336
shared pool kgski scheduler heap 2 de 232
shared pool kspd run-time context 16
shared pool px subheap 61344
shared pool partitioning d 455480
shared pool message pool freequeue 757568
shared pool qesblgf:bfm 728
等等資料。。。。。。。自己檢視一下即可,我這由于篇幅所限,就輸出這麼多了。
--下面sql用于檢視sga中可進行手工調配參數的清單:
sql> select * from v$sga_dynamic_components;
component current_size min_size max_size user_specified_size oper_count last_oper_typ last_
---------------------------------------------------------------- ------------ ---------- ----------
shared pool 4194304000 4194304000 0 4194304000 0 static
large pool 134217728 134217728 0 134217728 0 static
java pool 134217728 134217728 0 134217728 0 static
streams pool 117440512 117440512 0 117440512 0 static
default buffer cache 1.2264e+10 1.2264e+10 0 1.2264e+10 2 shrink manual 07-4月
keep buffer cache 100663296 0 0 100663296 8 grow manual 07-4月 -10
recycle buffer cache 67108864 0 0 67108864 1 grow manual 07-4月 -10
default 2k buffer cache 0 0 0 0 0 static
default 4k buffer cache 0 0 0 0 0 static
default 8k buffer cache 0 0 0 0 0 static
default 16k buffer cache 0 0 0 0 0 static
default 32k buffer cache 0 0 0 0 0 static
asm buffer cache 0 0 0 1.2465e+10 0 static
查詢共享池大小:
sql> show parameter shared_pool_size;
name type value
------------------------------------ ----------- ------------------------------
shared_pool_size big integer 4000m
sql> select name,type,value
2 from v$parameter a
3 where a.name='shared_pool_size';
name type value
-------------------- ----------------------------- ----------------------------
shared_pool_size 6 4194304000
這裡順便說下,很多時候大家不知道資料字典是什麼,很多時候資料字典的名字很長,而且有些後面又s,有些後面沒有s,大家記錄不下來,此時大家隻需要知道大緻是什麼就ok了,然後用下面的基于視圖的總視圖去查詢視圖的實際名稱(我們以不知道共享池的視圖是什麼):
sql> select * from dict t
2 where t.table_name like '%v$shared%';
table_name comments
------------------------------ -------------------------------------------------------------------
v$shared_server_monitor synonym for v_$shared_server_monitor
v$shared_server synonym for v_$shared_server
v$shared_pool_reserved synonym for v_$shared_pool_reserved
v$shared_pool_advice synonym for v_$shared_pool_advice
gv$shared_server_monitor synonym for gv_$shared_server_monitor
gv$shared_server synonym for gv_$shared_server
gv$shared_pool_reserved synonym for gv_$shared_pool_reserved
gv$shared_pool_advice synonym for gv_$shared_pool_advice
其中gv$開頭的視圖是用于叢集中的,我們一般隻關心v$開頭的資訊;可能你連表的大緻意思都不清楚,你隻大緻記得有一個字段大緻的名稱,如我們知道一個視圖内部字段的名稱有一個以scn開頭的列,那麼我們這樣也可以反向給它定位:
sql> select t.table_name, t.column_name
2 from dict_columns t
3 where t.column_name like 'scn%';
table_name column_name
------------------------------ ------------------------------
all_sumdelta scn
dba_audit_trail scn
user_audit_trail scn
dba_audit_statement scn
user_audit_statement scn
dba_audit_object scn
user_audit_object scn
dba_audit_exists scn
dba_fga_audit_trail scn
dba_common_audit_trail scn
dba_capture_prepared_tables scn
all_capture_prepared_tables scn
dba_file_group_tables scn
all_file_group_tables scn
user_file_group_tables scn
v$restore_point scn
v$recovery_status scn_needed
v$logmnr_contents scn
v$xml_audit_trail scn
gv$restore_point scn
這裡回到正題:清空共享池(oltp系統運作時不要去操作,這個過程很影響整體運作):
sql>alter system flush shared_pool;
如果要查詢某過程或包的源碼,可以看一下系統的資源包:
sql>select * from user_source t where t.name = '過程或包的名字' order by line;
這些源碼資訊在首次是不會裝入記憶體的,因為共享池的大小有限,調用時再裝入記憶體,而且也不會逃脫lru的命運,若一些寫的很爛的sql,就有可能把它替換出去,這個時候我們想做到的是啟動時直接裝入記憶體并不會被替換,oracle給我們一個keep方法,但是并非預設的,也就是安裝oracle後并不是預設就提供的這個包,如果你用具有dba權限的人進去,不能使用dbms_shared_pool這個包(報:這個包不存在),說明還沒有建立,此時需要做一下操作,才能建立:
1、首先定位oracle_home的位置,我們沒有直接定位oracle_home的方式,除非是你自己安裝的,如果不知道,用下面一個辦法:
select * from v$parameter p1
where p1.name = 'spfile';
2、若沒有該目錄,使用create spfile from pfile;執行一下重新開機ok就有了,得到該目錄後,假如得到如下:
d:/oracle10/product/10.2.0/db_1/database/spfileorcl102.ora
那麼oracle_home上相推兩層得到:
d:/oracle10/product/10.2.0/db_1/
那麼要得到那個包的建立腳本就在:
d:/oracle10/product/10.2.0/db_1/rdbms/admin/dbmspool.sql
此時需要到安裝資料庫的機器上去執行,如果你本地有腳本當然也可以執行,但是注意:這個執行必須是在sqlplus中,pl/sql中執行該腳本不好用。
3、執行方式:進入到安裝該資料庫的sqlplus下用sys使用者登入,該包需要建立在sys使用者下。
sql> @d:/oracle10/product/10.2.0/db_1/rdbms/admin/dbmspool.sql
程式包已建立。
授權成功。
視圖已建立。
程式包體已建立。
4、對于系統的大過程,可能第一次裝載比較緩慢,而且如果使用頻率較高,可以将其脫離lru算法,并直接裝入記憶體,如果可以的話,做oracle啟動時觸發器,如果不行,就手動執行一下代碼:
sys.dbms_shared_pool.keep('存儲過程或包的名字');
若想将某過程從記憶體中去除掉:
sys.dbms_shared_pool.unkeep('存儲過程或包的名字');
5、此時檢視緩沖池中是否裝載改對象:
select name,owner,type
from v$db_object_cache where kept = 'yes'
and name ='過程或包的名字';//這個地方也可以用sql片段來like
檢視共享池中執行的一些sql標頭:
select * from v$sqlarea;
通不過上述的sql得到hash_value或者addredss或者sql_id都可以通過以下視圖得到對應執行sql的全部内容(當sql較長的時候,v$sqlarea隻儲存前面一部分,全部内容在該視圖中):
select * from v$sqltext_with_newlines;
得到sql的執行計劃:
select * from v$sql_plan;
得到對共享池設定的建議值,oracle根據實際運作情況,推薦值:
select * from v$shared_pool_advance;
還有些不是很常用的:
得到sql綁定變量資訊:
select * from v$sql_bind_capture;
sql占用共享池記憶體:
select * from v$sql_shared_memory;
sql消耗調用的統計資訊:
select * from v$sqlstats;
這裡隻是由預編譯->共享池->sga的過程,對于sga的核心隻是闡述了共享池的部分,下次說明sga的另一大塊,data buffer,資料緩沖區,該區域在oltp系統中非常重要。
最後補充話題,本來這個想在後面說的,因為涉及一些其他内容,不過既然說到,就提一下吧,我們在oltp要求使用綁定參數方式執行sql如:
用應用程式的sql應當是:
string sql = “select * from a where id=?”;
而不是:
string sql = “select * from a where id=”+id;
那麼這樣的情況我們該怎麼辦呢?當要查詢多個id,使用in的情況,或者同時修改多條記錄的操作,我們無疑想出最正常的三種辦法(我們先介紹正常方法,再介紹解決問題的方法):
方法1(拼串,放棄預編譯):
string sql = “select * from a where id in(”+keys+”)”;
付:該方法放棄預編譯,但是也是正常方法中的無奈之舉。
方法2(将參數個數動态化去預編譯)
stringbuffer sql = new stringbuffer(256);
sql.append(“seelct * from a where id in(”);
for(….) {
sql.append(“?”).append(“,”);
}
sql.deletecharat(sql.length()-1);
付:該方法比上一種稍微好一點,oltp下一般情況下,我們正常方法中最少要這樣去完成,大家可以把ibatis的執行sql日志拿出來看下即可發現,ibatis對于動态參數個數也是這樣去完成的,對于并發度不算高的代碼段我們可以這樣使用,如果并發度高的代碼段,這樣使用我們也不會考慮。
方法3(循環提取。循環修改)
ptmt.setint(1,id[i]);
ptmt.executequery(“select * from a where id=?”);
付:這在執行過程中往往是最“不應該使用”的辦法了;如果執行插入操作,我們會适當考慮攜帶批處理這樣去完成也是可以的,不過對于update和select這類操作我們不該這樣使用的。
在這裡上述三種辦法,隻有第二種方法在oltp中并發量不大的情況下可以使用,若并發量較大,且參數個數的動态性比較大,也應該考慮使用其他方法去實作,因為大家通過上述試驗和反向查詢後發現,問号個數的變化也會産生不同的sql,共享池中仍然會造成很多的垃圾,隻是相對第一種方法機率降低了很多,而第三種方法基本是我們不考慮的。
我們說一下如果對于這樣的情況,批量執行過程中,我們該如何轉換,利用oracle的數組進行轉換,為此我們先在oracle内部提供一個函數,和數組類型,前序工作:
步驟1:
建立資料類型(表格類型,也類似數組):
create or replace type my_table_type is table of varchar2(8000);
步驟2:
建立轉換函數(在網上很多地方可以找到類似代碼,我這隻是一個參考):
create or replace function split(src_str in varchar2,
split_str varchar2) return my_table_type is
v_table_str my_table_type := my_table_type();
v_temp_str varchar2(8000) := src_str;
v_split_str varchar2(20) := split_str;
i number := 1;
j number := 1;
if v_split_str is null then
v_split_str := ',';--我們預設用逗号分隔
end if;
if src_str is null or src_str = v_split_str then
return v_table_str;
v_temp_str := ltrim(v_temp_str, v_split_str);
loop
i := instr(v_temp_str, v_split_str, j);
exit when i = 0 or j > length(v_temp_str);
v_table_str.extend;
v_table_str(v_table_str.count) := substr(v_temp_str, j, i - j);
j := i + length(v_split_str);
end loop;
if j < length(v_temp_str) then
v_table_str(v_table_str.count) := substr(v_temp_str, j, length(v_temp_str) - j + 1);
return v_table_str;
end split;
步驟3:(測試可用性,這裡假如資料都是按照逗号分隔的)
較高版本支援這樣的寫法:
sql> select * from table(split('123,321',','));
column_value
--------------------------------------------------------------------------------
123
321
較低版本可以這樣寫:
sql> select * from table(cast(split('123,321,456', ',') as my_table_type));
456
步驟4(程式應用):
string sql = “select * from a where id in(select * from table(split(?,',')))”;//當然對于低版本的資料庫,相應修改即可。
但是oracle有些時候會很傻的去使用hash join,因為他們他不知道你裡面傳回多少資料,而我們通過轉換回來的id往往資料量很少,最多就是幾十行上百行,若目标表為一個大表,使用hash join的确是一件很浪費的事情,此時我們不願意這樣去做,因為很浪費cpu和臨時表空間(這其實是後面要說的),我們一般需要強制指定查詢的方式來控制他走嵌套循環,讓大表根據小表去走索引,使用oracle的hint來強制告訴它應該由小表引導大表執行,來保證sql執行計劃的穩定性:
string sql = “select /*+ordered use_nl(a2,a1)*/a2.* from a a1,(select column_value from table(split(?,',')) a2 where a1.id = a2.column_value”;
此時可能會問,這樣轉一次會不會很慢,是的,這不難會想想一個拆開字元串的過程,我們必然會消耗一點,不過要想到一次執行就是拆開一個字元串而已,而且我們傳入的字元串也不會太長,這個解析過程對于oracle來說還是沒啥問題的,不必擔心這個,而更加重要的提高了共享池的應用。