国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 開發 > 綜合 > 正文

通過分析SQL語句的執行計劃優化SQL(二)

2024-07-21 02:09:42
字體:
來源:轉載
供稿:網友

第5章 oracle的執行計劃

  背景知識:

       
  為了更好的進行下面的內容我們必須了解一些概念性的術語:

  共享sql語句

     為了不重復解析相同的sql語句(因為解析操作比較費資源,會導致性能下降),在第一次解析之后,oracle將sql語句及解析后得到的執行計劃存放在內存中。這塊位于系統全局區域sga(system global area)的共享池(shared buffer pool)中的內存可以被所有的數據庫用戶共享。因此,當你執行一個sql語句(有時被稱為一個游標)時,如果該語句和之前的執行過的某一語句完全相同,并且之前執行的該語句與其執行計劃仍然在內存中存在,則oracle就不需要再進行分析,直接得到該語句的執行路徑。oracle的這個功能大大地提高了sql的執行性能并大大節省了內存的使用。使用這個功能的關鍵是將執行過的語句盡可能放到內存中,所以這要求有大的共享池(通過設置shared buffer pool參數值)和盡可能的使用綁定變量的方法執行sql語句。

     當你向oracle 提交一個sql語句,oracle會首先在共享內存中查找是否有相同的語句。這里需要注明的是,oracle對兩者采取的是一種嚴格匹配,要達成共享,sql語句必須完全相同(包括空格,換行等)。

     下面是判斷sql語句是否與共享內存中某一sql相同的步驟:
  1). 對所發出語句的文本串進行hashed。如果hash值與已在共享池中sql語句的hash值相同,則進行第2步:
        2)         將所發出語句的文本串(包括大小寫、空白和注釋)與在第1步中識別的所有
        已存在的sql語句相比較。
        例如:
        select * from emp where empno = 1000;
        和下列每一個都不同
        select * from emp where empno = 1000;
        select * from emp where empno = 1000;
        select * from emp where empno = 2000;
        在上面的語句中列值都是直接sql語句中的,今后我們將這類sql成為硬編碼sql或字面值sql
       
        使用綁定變量的sql語句中必須使用相同的名字的綁定變量(bind variables) ,
例如:
        a. 該2個sql語句被認為相同
        select pin , name from people where pin = :blk1.pin;
        select pin , name from people where pin = :blk1.pin;
        b. 該2個sql語句被認為不相同
        select pin , name from people where pin = :blk1.ot_ind;
        select pin , name from people where pin = :blk1.ov_ind;
        今后我們將上面的這類語句稱為綁定變量sql。

        3). 將所發出語句中涉及的對象與第2步中識別的已存在語句所涉及對象相比較。
           例如:
           如用戶user1與用戶user2下都有emp表,則
           用戶user1發出的語句:select * from emp; 與
           用戶user2發出的語句:select * from emp; 被認為是不相同的語句,
           因為兩個語句中引用的emp不是指同一個表。
   
        4). 在sql語句中使用的捆綁變量的捆綁類型必須一致。

        如果語句與當前在共享池中的另一個語句是等同的話,oracle并不對它進行語法分析。而直接執行該語句,提高了執行效率,因為語法分析比較耗費資源。

        注意的是,從oracle 8i開始,新引入了一個cursor_sharing參數,該參數的主要目的就是為了解決在編程過程中已大量使用的硬編碼sql問題。因為在實際開發中,很多程序人員為了提高開發速度,而采用類似下面的開發方法:
str_sql string;
int_empno int;
int_empno = 2000;
str_sql = ‘select * from emp where empno = ‘ + int_empno;
…………
int_empno = 1000;
str_sql = ‘select * from emp where empno = ‘ + int_empno;

        上面的代碼實際上使用了硬編碼sql,使我們不能使用共享sql的功能,結果是數據庫效率不高。但是從上面的2個語句來看,產生的硬編碼sql只是列值不同,其它部分都是相同的,如果僅僅因為列值不同而導致這2個語句不能共享是很可惜的,為了解決這個問題,引入了cursor_sharing參數,使這類問題也可以使用共享sql,從而使這樣的開發也可以利用共享sql功能。聽起來不錯,oracle真為用戶著想,使用戶在不改變代碼的情況下還可以利用共享sql的功能。真的如此嗎?天上不會無緣無故的掉一個餡餅的,oracle對該參數的使用做了說明,建議在經過實際測試后再改該參數的值(缺省情況下,該參數的值為exact,語句完全一致才使用共享sql)。因為有可能該變該值后,你的硬編碼sql是可以使用共享sql了,但數據庫的性能反而會下降。 我在實際應用中已經遇到這種情況。所以建議編寫需要穩定運行程序的開發人員最好還是一開始就使用綁定變量的sql。

  rowid的概念:
      
  rowid是一個偽列,既然是偽列,那么這個列就不是用戶定義,而是系統自己給加上的。對每個表都有一個rowid的偽列,但是表中并不物理存儲rowid列的值。不過你可以像使用其它列那樣使用它,但是不能刪除改列,也不能對該列的值進行修改、插入。一旦一行數據插入數據庫,則rowid在該行的生命周期內是唯一的,即即使該行產生行遷移,行的rowid也不會改變。

  為什么使用rowid
     
  rowid對訪問一個表中的給定的行提供了最快的訪問方法,通過rowid可以直接定位到相應的數據塊上,然后將其讀到內存。我們創建一個索引時,該索引不但存儲索引列的值,而且也存儲索引值所對應的行的rowid,這樣我們通過索引快速找到相應行的rowid后,通過該rowid,就可以迅速將數據查詢出來。這也就是我們使用索引查詢時,速度比較快的原因。

  在oracle8以前的版本中,rowid由file 、block、row number構成。隨著oracle8中對象概念的擴展,rowid發生了變化,rowid由object、file、block、row number構成。利用dbms_rowid可以將rowid分解成上述的各部分,也可以將上述的各部分組成一個有效的rowid。

  recursive sql概念
       
  有時為了執行用戶發出的一個sql語句,oracle必須執行一些額外的語句,我們將這些額外的語句稱之為'recursive calls'或'recursive sql statements'。如當一個ddl語句發出后,oracle總是隱含的發出一些recursive sql語句,來修改數據字典信息,以便用戶可以成功的執行該ddl語句。當需要的數據字典信息沒有在共享內存中時,經常會發生recursive calls,這些recursive calls會將數據字典信息從硬盤讀入內存中。用戶不比關心這些recursive sql語句的執行情況,在需要的時候,oracle會自動的在內部執行這些語句。當然dml語句與select都可能引起recursive sql。簡單的說,我們可以將觸發器視為recursive sql。

  row source(行源)
       
  用在查詢中,由上一操作返回的符合條件的行的集合,即可以是表的全部行數據的集合;也可以是表的部分行數據的集合;也可以為對上2個row source進行連接操作(如join連接)后得到的行數據集合。

  predicate(謂詞)
       
  一個查詢中的where限制條件

  driving table(驅動表)
       
  該表又稱為外層表(outer table)。這個概念用于嵌套與hash連接中。如果該row source返回較多的行數據,則對所有的后續操作有負面影響。注意此處雖然翻譯為驅動表,但實際上翻譯為驅動行源(driving row source)更為確切。一般說來,是應用查詢的限制條件后,返回較少行源的表作為驅動表,所以如果一個大表在where條件有有限制條件(如等值限制),則該大表作為驅動表也是合適的,所以并不是只有較小的表可以作為驅動表,正確說法應該為應用查詢的限制條件后,返回較少行源的表作為驅動表。在執行計劃中,應該為靠上的那個row source,后面會給出具體說明。在我們后面的描述中,一般將該表稱為連接操作的row source 1。

  probed table(被探查表)
       
  該表又稱為內層表(inner table)。在我們從驅動表中得到具體一行的數據后,在該表中尋找符合連接條件的行。所以該表應當為大表(實際上應該為返回較大row source的表)且相應的列上應該有索引。在我們后面的描述中,一般將該表稱為連接操作的row source 2。

  組合索引(concatenated index)
       
  由多個列構成的索引,如create index idx_emp on emp(col1, col2, col3, ……),則我們稱idx_emp索引為組合索引。在組合索引中有一個重要的概念:引導列(leading column),在上面的例子中,col1列為引導列。當我們進行查詢時可以使用”where col1 = ? ”,也可以使用”where col1 = ? and col2 = ?”,這樣的限制條件都會使用索引,但是”where col2 = ? ”查詢就不會使用該索引。所以限制條件中包含先導列時,該限制條件才會使用該組合索引。

  可選擇性(selectivity):
       
  比較一下列中唯一鍵的數量和表中的行數,就可以判斷該列的可選擇性。如果該列的”唯一鍵的數量/表中的行數”的比值越接近1,則該列的可選擇性越高,該列就越適合創建索引,同樣索引的可選擇性也越高。在可選擇性高的列上進行查詢時,返回的數據就較少,比較適合使用索引查詢。


        有了這些背景知識后就開始介紹執行計劃。為了執行語句,oracle可能必須實現許多步驟。這些步驟中的每一步可能是從數據庫中物理檢索數據行,或者用某種方法準備數據行,供發出語句的用戶使用。oracle用來執行語句的這些步驟的組合被稱之為執行計劃。執行計劃是sql優化中最為復雜也是最為關鍵的部分,只有知道了oracle在內部到底是如何執行該sql語句后,我們才能知道優化器選擇的執行計劃是否為最優的。執行計劃對于dba來說,就象財務報表對于財務人員一樣重要。所以我們面臨的問題主要是:如何得到執行計劃;如何分析執行計劃,從而找出影響性能的主要問題。下面先從分析樹型執行計劃開始介紹,然后介紹如何得到執行計劃,再介紹如何分析執行計劃。
       
  舉例:這個例子顯示關于下面sql語句的執行計劃。
select ename, job, sal, dname
   from emp, dept
where emp.deptno = derpt.deptno
   and not exists
     ( select *
from salgrade
where emp.sal between losal and hisal );
       
  此語句查詢薪水不在任何建議薪水范圍內的所有雇員的名字,工作,薪水和部門名。下圖5-1顯示了一個執行計劃的圖形表示:

  執行計劃的步驟
         
  執行計劃的每一步返回一組行,它們或者為下一步所使用,或者在最后一步時返回給發出sql語句的用戶或應用。由每一步返回的一組行叫做行源(row source)。圖5-1樹狀圖顯示了從一步到另一步行數據的流動情況。每步的編號反映了在你觀察執行計劃時所示步驟的順序(如何觀察執行計劃將被簡短地說明)。一般來說這并不是每一步被執行的先后順序。執行計劃的每一步或者從數據庫中檢索行,或者接收來自一個或多個行源的行數據作為輸入:由紅色字框指出的步驟從數據庫中的數據文件中物理檢索數據。這種步驟被稱之為存取路徑,后面會詳細介紹在oracle可以使用的存取路徑:
l        第3步和第6步分別的從emp表和salgrade表讀所有的行。
l        第5步在pk_deptno索引中查找由步驟3返回的每個deptno值。它找出與dept表中相關聯的那些行的rowid。
l        第4步從dept表中檢索出rowid為第5步返回的那些行。
由黑色字框指出的步驟在行源上操作,如做2表之間的關聯,排序,或過濾等操作,后面也會給出詳細的介紹:
l        第2步實現嵌套的循環操作(相當于c語句中的嵌套循環),接收從第3步和第4步來的行源,把來自第3步源的每一行與它第4步中相應的行連接在一起,返回結果行到第1步。
l        第1步完成一個過濾器操作。它接收來自第2步和第6步的行源,消除掉第2步中來的,在第6步有相應行的那些行,并將來自第2步的剩下的行返回給發出語句的用戶或應用。

  實現執行計劃步驟的順序

  執行計劃中的步驟不是按照它們編號的順序來實現的:oracle首先實現圖5-1樹結構圖形里作為葉子出現的那些步驟(例如步驟3、5、6)。由每一步返回的行稱為它下一步驟的行源。然后oracle實現父步驟。

  舉例來說,為了執行圖5-1中的語句,oracle以下列順序實現這些步驟:
l        首先,oracle實現步驟3,并一行一行地將結果行返回給第2步。
l        對第3步返回的每一行,oracle實現這些步驟:
-- oracle實現步驟5,并將結果rowid返回給第4步。
-- oracle實現步驟4,并將結果行返回給第2步。
-- oracle實現步驟2,將接受來自第3步的一行和來自第4步的一行,并返回給第1步一行。
-- oracle實現步驟6,如果有結果行的話,將它返回給第1步。
-- oracle實現步驟1,如果從步驟6返回行,oracle將來自第2步的行返回給發出sql語句的用戶。

  注意oracle對由第3步返回的每一行實現步驟5,4,2,6一次。許多父步驟在它們能執行之前只需要來自它們子步驟的單一行。對這樣的父步驟來說,只要從子步驟已返回單一行時立即實現父步驟(可能還有執行計劃的其余部分)。如果該父步驟的父步驟同樣可以通過單一行返回激活的話,那么它也同樣被執行。所以,執行可以在樹上串聯上去,可能包含執行計劃的余下部分。對于這樣的操作,可以使用first_rows作為優化目標以便于實現快速響應用戶的請求。
對每個由子步驟依次檢索出來的每一行,oracle就實現父步驟及所有串聯在一起的步驟一次。對由子步驟返回的每一行所觸發的父步驟包括表存取,索引存取,嵌套的循環連接和過濾器。

        有些父步驟在它們被實現之前需要來自子步驟的所有行。對這樣的父步驟,直到所有行從子步驟返回之前oracle不能實現該父步驟。這樣的父步驟包括排序,排序一合并的連接,組功能和總計。對于這樣的操作,不能使用first_rows作為優化目標,而可以用all_rows作為優化目標,使該中類型的操作耗費的資源最少。

  有時語句執行時,并不是象上面說的那樣一步一步有先有后的進行,而是可能并行運行,如在實際環境中,3、5、4步可能并行運行,以便取得更好的效率。從上面的樹型圖上,是很難看出各個操作執行的先后順序,而通過oracle生成的另一種形式的執行計劃,則可以很容易的看出哪個操作先執行,哪個后執行,這樣的執行計劃是我們真正需要的,后面會給出詳細說明。現在先來看一些預備知識。

  訪問路徑(方法) -- access path
      
  優化器在形成執行計劃時需要做的一個重要選擇是如何從數據庫查詢出需要的數據。對于sql語句存取的任何表中的任何行,可能存在許多存取路徑(存取方法),通過它們可以定位和查詢出需要的數據。優化器選擇其中自認為是最優化的路徑。
       
  在物理層,oracle讀取數據,一次讀取的最小單位為數據庫塊(由多個連續的操作系統塊組成),一次讀取的最大值由操作系統一次i/o的最大值與multiblock參數共同決定,所以即使只需要一行數據,也是將該行所在的數據庫塊讀入內存。邏輯上,oracle用如下存取方法訪問數據:

  1) 全表掃描(full table scans, fts)
        
  為實現全表掃描,oracle讀取表中所有的行,并檢查每一行是否滿足語句的where限制條件。oracle順序地讀取分配給表的每個數據塊,直到讀到表的最高水線處(high water mark, hwm,標識表的最后一個數據塊)。一個多塊讀操作可以使一次i/o能讀取多塊數據塊(db_block_multiblock_read_count參數設定),而不是只讀取一個數據塊,這極大的減少了i/o總次數,提高了系統的吞吐量,所以利用多塊讀的方法可以十分高效地實現全表掃描,而且只有在全表掃描的情況下才能使用多塊讀操作。在這種訪問模式下,每個數據塊只被讀一次。由于hwm標識最后一塊被讀入的數據,而delete操作不影響hwm值,所以一個表的所有數據被delete后,其全表掃描的時間不會有改善,一般我們需要使用truncate命令來使hwm值歸為0。幸運的是oracle 10g后,可以人工收縮hwm的值。

        由fts模式讀入的數據被放到高速緩存的least recently used (lru)列表的尾部,這樣可以使其快速交換出內存,從而不使內存重要的數據被交換出內存。使用fts的前提條件:在較大的表上不建議使用全表掃描,除非取出數據的比較多,超過總量的5% -- 10%,或你想使用并行查詢功能時。
        使用全表掃描的例子:
        ~~~~~~~~~~~~~~~~~~~~~~~~
        sql> explain plan for select * from dual;
        query plan
        -----------------------------------------
        select statement     [choose] cost=
          table access full dual

  2) 通過rowid的表存取(table access by rowid或rowid lookup)
      
  行的rowid指出了該行所在的數據文件、數據塊以及行在該塊中的位置,所以通過rowid來存取數據可以快速定位到目標數據上,是oracle存取單行數據的最快方法。為了通過rowid存取表,oracle 首先要獲取被選擇行的rowid,或者從語句的where子句中得到,或者通過表的一個或多個索引的索引掃描得到。oracle然后以得到的rowid為依據定位每個被選擇的行。
    
  這種存取方法不會用到多塊讀操作,一次i/o只能讀取一個數據塊。我們會經常在執行計劃中看到該存取方法,如通過索引查詢數據。

  使用rowid存取的方法:
sql> explain plan for select * from dept where rowid = 'aaaaygaadaaaaataaf';
query plan
------------------------------------
select statement [choose] cost=1
table access by rowid dept [analyzed]

  3)索引掃描(index scan或index lookup)
      
  我們先通過index查找到數據對應的rowid值(對于非唯一索引可能返回多個rowid值),然后根據rowid直接從表中得到具體的數據,這種查找方式稱為索引掃描或索引查找(index lookup)。一個rowid唯一的表示一行數據,該行對應的數據塊是通過一次i/o得到的,在此情況下該次i/o只會讀取一個數據庫塊。

  在索引中,除了存儲每個索引的值外,索引還存儲具有此值的行對應的rowid值。索引掃描可以由2步組成:(1) 掃描索引得到對應的rowid值。 (2) 通過找到的rowid從表中讀出具體的數據。每步都是單獨的一次i/o,但是對于索引,由于經常使用,絕大多數都已經cache到內存中,所以第1步的i/o經常是邏輯i/o,即數據可以從內存中得到。但是對于第2步來說,如果表比較大,則其數據不可能全在內存中,所以其i/o很有可能是物理i/o,這是一個機械操作,相對邏輯i/o來說,是極其費時間的。所以如果多大表進行索引掃描,取出的數據如果大于總量的5% -- 10%,使用索引掃描會效率下降很多。
如下列所示:
sql> explain plan for select empno, ename from emp where empno=10;
query plan
------------------------------------
select statement [choose] cost=1
table access by rowid emp [analyzed]
    index unique scan emp_i1
       
  注意table access by rowid emp部分,這表明這不是通過fts存取路徑訪問數據,而是通過rowid lookup存取路徑訪問數據的。在此例中,所需要的rowid是由于在索引查找empno列的值得到的,這種方式是index unique scan查找,后面給予介紹,emp_i1為使用的進行索引查找的索引名字。

        但是如果查詢的數據能全在索引中找到,就可以避免進行第2步操作,避免了不必要的i/o,此時即使通過索引掃描取出的數據比較多,效率還是很高的,因為這只會在索引中讀取。所以上面我在介紹基于規則的優化器時,使用了select count(id) from swd_billdetail where cn <'6',而沒有使用select count(cn) from swd_billdetail where cn <'6'。因為在實際情況中,只查詢被索引列的值的情況極為少,所以,如果我在查詢中使用count(cn),則不具有代表性。

sql> explain plan for select empno from emp where empno=10;  -- 只查詢empno列值
query plan
------------------------------------
select statement [choose] cost=1
  index unique scan emp_i1

        進一步講,如果sql語句中對索引列進行排序,因為索引已經預先排序好了,所以在執行計劃中不需要再對索引列進行排序
sql> explain plan for select empno, ename from emp
where empno > 7876 order by empno;
query plan
--------------------------------------------------------------------------------
select statement   [choose] cost=1   
table access by rowid emp [analyzed]
  index range scan emp_i1 [analyzed]
       
  從這個例子中可以看到:因為索引是已經排序了的,所以將按照索引的順序查詢出符合條件的行,因此避免了進一步排序操作。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 天全县| 婺源县| 巍山| 东源县| 洪泽县| 齐河县| 平罗县| 武乡县| 泾源县| 迁安市| 洛隆县| 彰化县| 元阳县| 缙云县| 莎车县| 莲花县| 华池县| 科技| 湖北省| 浦东新区| 社会| 内丘县| 吴桥县| 酉阳| 蒙自县| 乌兰县| 华蓥市| 监利县| 新和县| 泗阳县| 凤山县| 庄河市| 田东县| 怀来县| 邹城市| 西吉县| 余干县| 青浦区| 略阳县| 宁都县| 万荣县|