一、什么是執行計劃
An explain plan is a representation of the access path that is taken when a query is executed within Oracle.
二、如何訪問數據
At the physical level Oracle reads blocks of data. The smallest amount of data read is a single Oracle block, the largest is constrained by operating system limits (and multiblock i/o). Logically Oracle finds the data to read by using the following methods:
Full Table Scan (FTS) --全表掃描
Index Lookup (unique & non-unique) --索引掃描(唯一和非唯一)
Rowid --物理行id
三、執行計劃層次關系
When looking at a plan, the rightmost (ie most inndented) uppermost operation is the first thing that is executed. --采用最右最上最先執行的原則看層次關系,在同一級如果某個動作沒有子ID就最先執行
1.看一個簡單的例子:
Query Plan
-----------------------------------------
SELECT STATEMENT [CHOOSE] Cost=1234
**TABLE ACCESS FULL LARGE [:Q65001] [ANALYZED] --[:Q65001]表示是并行方式,[ANALYZED]表示該對象已經分析過了
優化模式是CHOOSE的情況下,看Cost參數是否有值來決定采用CBO還是RBO:
SELECT STATEMENT [CHOOSE] Cost=1234 --Cost有值,采用CBO
SELECT STATEMENT [CHOOSE] Cost= --Cost為空,采用RBO
2.層次的父子關系,看比較復雜的例子:
PARENT1
**FIRST CHILD
****FIRST GRANDCHILD
**SECOND CHILD
Here the same principles apply, the FIRST GRANDCHILD is the initial operation then the FIRST CHILD followed by the SECOND CHILD and finally the PARENT collates the output.
四、例子解說
Execution Plan
----------------------------------------------------------
0 **SELECT STATEMENT Optimizer=CHOOSE (Cost=3 Card=8 Bytes=248)
1 0 **HASH JOIN (Cost=3 Card=8 Bytes=248)
2 1 ****TABLE ACCESS (FULL) OF 'DEPT' (Cost=1 Card=3 Bytes=36)
3 1 ****TABLE ACCESS (FULL) OF 'EMP' (Cost=1 Card=16 Bytes=304)
左側的兩排數據,前面的是序列號ID,后面的是對應的PID(父ID)。
A shortened summary of this is:
Execution starts with ID=0: SELECT STATEMENT but this is dependand on it's child objects
So it executes its first child step: ID=1 PID=0 HASH JOIN but this is dependand on it's child objects
So it executes its first child step: ID=2 PID=1 TABLE ACCESS (FULL) OF 'DEPT'
Then the second child step: ID=3 PID=2 TABLE ACCESS (FULL) OF 'EMP'
Rows are returned to the parent step(s) until finished
五、表訪問方式
1.Full Table Scan (FTS) 全表掃描
In a FTS operation, the whole table is read up to the high water mark (HWM). The HWM marks the last block in the table that has ever had data written to it. If you have deleted all the rows then you will still read up to the HWM. Truncate resets the HWM back to the start of the table. FTS uses multiblock i/o to read the blocks from disk. --全表掃描模式下會讀數據到表的高水位線(HWM即表示表曾經擴展的最后一個數據塊),讀取速度依賴于Oracle初始化參數db_block_multiblock_read_count
Query Plan
------------------------------------
SELECT STATEMENT [CHOOSE] Cost=1
**INDEX UNIQUE SCAN EMP_I1 --如果索引里就找到了所要的數據,就不會再去訪問表了
2.Index Lookup 索引掃描
There are 5 methods of index lookup:
index unique scan --索引唯一掃描
Method for looking up a single key value via a unique index. always returns a single value, You must supply AT LEAST the leading column of the index to access data via the index.
eg:
SQL> explain plan for select empno,ename from emp where empno=10;
index range scan --索引局部掃描
Index range scan is a method for accessing a range values of a particular column. AT LEAST the leading column of the index must be supplied to access data via the index. Can be used for range operations (e.g. > < <> >= <= between) .
eg:
SQL> explain plan for select mgr from emp where mgr = 5;
index full scan --索引全局掃描
Full index scans are only available in the CBO as otherwise we are unable to determine whether a full scan would be a good idea or not. We choose an index Full Scan when we have statistics that indicate that it is going to be more efficient than a Full table scan and a sort. For example we may do a Full index scan when we do an unbounded scan of an index and want the data to be ordered in the index order.
eg:
SQL> explain plan for select empno,ename from big_emp order by empno,ename;
index fast full scan --索引快速全局掃描,不帶order by情況下常發生
Scans all the block in the index, Rows are not returned in sorted order, Introduced in 7.3 and requires V733_PLANS_ENABLED=TRUE and CBO, may be hinted using INDEX_FFS hint, uses multiblock i/o, can be executed in parallel, can be used to access second column of concatenated indexes. This is because we are selecting all of the index.
eg:
SQL> explain plan for select empno,ename from big_emp;
index skip scan --索引跳躍掃描,where條件列是非索引的前導列情況下常發生
Index skip scan finds rows even if the column is not the leading column of a concatenated index. It skips the first column(s) during the search.
eg:
SQL> create index i_emp on emp(empno, ename);
SQL> select /*+ index_ss(emp i_emp)*/ job from emp where ename='SMITH';
3.Rowid 物理ID掃描
This is the quickest access method available.Oracle retrieves the specified block and extracts the rows it is interested in. --Rowid掃描是最快的訪問數據方式
六、表連接方式
有三種連接方式:
1.Sort Merge Join (SMJ) --由于sort是非常耗資源的,所以這種連接方式要避免
Rows are produced by Row Source 1 and are then sorted Rows from Row Source 2 are then produced and sorted by the same sort key as Row Source 1. Row Source 1 and 2 are NOT accessed concurrently.
SQL> explain plan for
select /*+ ordered */ e.deptno,d.deptno
from emp e,dept d
where e.deptno = d.deptno
order by e.deptno,d.deptno;
Query Plan
-------------------------------------
SELECT STATEMENT [CHOOSE] Cost=17
**MERGE JOIN
****SORT JOIN
******TABLE ACCESS FULL EMP [ANALYZED]
****SORT JOIN
******TABLE ACCESS FULL DEPT [ANALYZED]
Sorting is an expensive operation, especially with large tables. Because of this, SMJ is often not a particularly efficient join method.
2.Nested Loops (NL) --比較高效的一種連接方式
Fetches the first batch of rows from row source 1, Then we probe row source 2 once for each row returned from row source 1.
For nested loops to be efficient it is important that the first row source returns as few rows as possible as this directly controls the number of probes of the second row source. Also it helps if the access method for row source 2 is efficient as this operation is being repeated once for every row returned by row source 1.
SQL> explain plan for
select a.dname,b.sql
from dept a,emp b
where a.deptno = b.deptno;
Query Plan
-------------------------
SELECT STATEMENT [CHOOSE] Cost=5
**NESTED LOOPS
****TABLE ACCESS FULL DEPT [ANALYZED]
****TABLE ACCESS FULL EMP [ANALYZED]
3.Hash Join --最為高效的一種連接方式
New join type introduced in 7.3, More efficient in theory than NL & SMJ, Only accessible via the CBO. Smallest row source is chosen and used to build a hash table and a bitmap The second row source is hashed and checked against the hash table looking for joins. The bitmap is used as a quick lookup to check if rows are in the hash table and are especially useful when the hash table is too large to fit in memory.
SQL> explain plan for
select /*+ use_hash(emp) */ empno
from emp,dept
where emp.deptno = dept.deptno;
Query Plan
----------------------------
SELECT STATEMENT [CHOOSE] Cost=3
**HASH JOIN
****TABLE ACCESS FULL DEPT
****TABLE ACCESS FULL EMP
Hash joins are enabled by the parameter HASH_JOIN_ENABLED=TRUE in the init.ora or session. TRUE is the default in 7.3.
3.Cartesian Product --卡迪爾積,不算真正的連接方式,sql肯定寫的有問題
A Cartesian Product is done where they are no join conditions between 2 row sources and there is no alternative method of accessing the data. Not really a join as such as there is no join! Typically this is caused by a coding mistake where a join has been left out.
It can be useful in some circumstances - Star joins uses cartesian products.Notice that there is no join between the 2 tables:
SQL> explain plan for
select emp.deptno,dept,deptno
from emp,dept
Query Plan
------------------------------
SLECT STATEMENT [CHOOSE] Cost=5
**MERGE JOIN CARTESIAN
****TABLE ACCESS FULL DEPT
****SORT JOIN
******TABLE ACCESS FULL EMP
The CARTESIAN keyword indicate that we are doing a cartesian product.
七、運算符
1.sort --排序,很消耗資源
There are a number of different operations that promote sorts:
order by clauses
group by
sort merge join
2.filter --過濾,如not in、min函數等容易產生
Has a number of different meanings, used to indicate partition elimination, may also indicate an actual filter step where one row source is filtering, another, functions such as min may introduce filter steps into query plans.
3.view --視圖,大都由內聯視圖產生
When a view cannot be merged into the main query you will often see a projection view operation. This indicates that the 'view' will be selected from directly as opposed to being broken down into joins on the base tables. A number of constructs make a view non mergeable. Inline views are also non mergeable.
eg:
SQL> explain plan for
select ename,tot
from emp,(select empno,sum(empno) tot from big_emp group by empno) tmp
where emp.empno = tmp.empno;
Query Plan
------------------------
SELECT STATEMENT [CHOOSE]
**HASH JOIN
**TABLE ACCESS FULL EMP [ANALYZED]
**VIEW
****SORT GROUP BY
******INDEX FULL SCAN BE_IX
4.partition view --分區視圖
Partition views are a legacy technology that were superceded by the partitioning option. This section of the article is provided as reference for such legacy systems.
背景知識:
為了更好的進行下面的內容我們必須了解一些概念性的術語:
共享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]
從這個例子中可以看到:因為索引是已經排序了的,所以將按照索引的順序查詢出符合條件的行,因此避免了進一步排序操作。
根據索引的類型與where限制條件的不同,有4種類型的索引掃描:
索引唯一掃描(index unique scan)
索引范圍掃描(index range scan)
索引全掃描(index full scan)
索引快速掃描(index fast full scan)
(1) 索引唯一掃描(index unique scan)
通過唯一索引查找一個數值經常返回單個ROWID。如果該唯一索引有多個列組成(即組合索引),則至少要有組合索引的引導列參與到該查詢中,如創建一個索引:create index idx_test on emp(ename, deptno, loc)。則select ename from emp where ename = ‘JACK’ and deptno = ‘DEV’語句可以使用該索引。如果該語句只返回一行,則存取方法稱為索引唯一掃描。而select ename from emp where deptno = ‘DEV’語句則不會使用該索引,因為where子句種沒有引導列。如果存在UNIQUE 或PRIMARY KEY 約束(它保證了語句只存取單行)的話,Oracle經常實現唯一性掃描。
使用唯一性約束的例子:
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
(2) 索引范圍掃描(index range scan)
使用一個索引存取多行數據,同上面一樣,如果索引是組合索引,如(1)所示,而且select ename from emp where ename = ‘JACK’ and deptno = ‘DEV’語句返回多行數據,雖然該語句還是使用該組合索引進行查詢,可此時的存取方法稱為索引范圍掃描。在唯一索引上使用索引范圍掃描的典型情況下是在謂詞(where限制條件)中使用了范圍操作符(如>、<、<>、>=、<=、between)
使用索引范圍掃描的例子:
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]
在非唯一索引上,謂詞col = 5可能返回多行數據,所以在非唯一索引上都使用索引范圍掃描。
使用index rang scan的3種情況:
(a) 在唯一索引列上使用了range操作符(> < <> >= <= between)
(b) 在組合索引上,只使用部分列進行查詢,導致查詢出多行
(c) 對非唯一索引列上進行的任何查詢。
(3) 索引全掃描(index full scan)
與全表掃描對應,也有相應的全索引掃描。在某些情況下,可能進行全索引掃描而不是范圍掃描,需要注意的是全索引掃描只在CBO模式下才有效。CBO根據統計數值得知進行全索引掃描比進行全表掃描更有效時,才進行全索引掃描,而且此時查詢出的數據都必須從索引中可以直接得到。
全索引掃描的例子:
An Index full scan will not perform single block i/o's and so it may prove to be inefficient.
e.g.
Index BE_IX is a concatenated index on big_emp (empno, ename)
SQL> explain plan for select empno, ename from big_emp order by empno,ename;
Query Plan
--------------------------------------------------------------------------------
SELECT STATEMENT [
CHOOSE] Cost=26
INDEX FULL SCAN BE_IX [ANALYZED]
(4) 索引快速掃描(index fast full scan)
掃描索引中的所有的數據塊,與 index full scan很類似,但是一個顯著的區別就是它不對查詢出的數據進行排序,即數據不是以排序順序被返回。在這種存取方法中,可以使用多塊讀功能,也可以使用并行讀入,以便獲得最大吞吐量與縮短執行時間。
索引快速掃描的例子:
BE_IX索引是一個多列索引:big_emp (empno,ename)
SQL> explain plan for select empno,ename from big_emp;
Query Plan
------------------------------------------
SELECT STATEMENT [
CHOOSE] Cost=1
INDEX FAST FULL SCAN BE_IX [ANALYZED]
只選擇多列索引的第2列:
SQL> explain plan for select ename from big_emp;
Query Plan
------------------------------------------
SELECT STATEMENT [
CHOOSE] Cost=1
INDEX FAST FULL SCAN BE_IX [ANALYZED]
表之間的連接
Join是一種試圖將兩個表結合在一起的謂詞,一次只能連接2個表,表連接也可以被稱為表關聯。在后面的敘述中,我們將會使用”row source”來代替”表”,因為使用row source更嚴謹一些,并且將參與連接的2個row source分別稱為row source1和row source 2。Join過程的各個步驟經常是串行操作,即使相關的row source可以被并行訪問,即可以并行的讀取做join連接的兩個row source的數據,但是在將表中符合限制條件的數據讀入到內存形成row source后,join的其它步驟一般是串行的。有多種方法可以將2個表連接起來,當然每種方法都有自己的優缺點,每種連接類型只有在特定的條件下才會發揮出其最大優勢。
row source(表)之間的連接順序對于查詢的效率有非常大的影響。通過首先存取特定的表,即將該表作為驅動表,這樣可以先應用某些限制條件,從而得到一個較小的row source,使連接的效率較高,這也就是我們常說的要先執行限制條件的原因。一般是在將表讀入內存時,應用where子句中對該表的限制條件。
根據2個row source的連接條件的中操作符的不同,可以將連接分為等值連接(如WHERE A.COL3 = B.COL4)、非等值連接(WHERE A.COL3 > B.COL4)、外連接(WHERE A.COL3 = B.COL4(+))。上面的各個連接的連接原理都基本一樣,所以為了簡單期間,下面以等值連接為例進行介紹。在后面的介紹中,都已:
SELECT A.COL1, B.COL2
FROM A, B
WHERE A.COL3 = B.COL4;
為例進行說明,假設A表為Row Soruce1,則其對應的連接操作關聯列為COL 3;B表為Row Soruce2,則其對應的連接操作關聯列為COL 4;
連接類型:
目前為止,無論連接操作符如何,典型的連接類型共有3種:
排序 - - 合并連接(Sort Merge Join (SMJ) )
嵌套循環(Nested Loops (NL) )
哈希連接(Hash Join)
排序 - - 合并連接(Sort Merge Join, SMJ)
內部連接過程:
1) 首先生成row source1需要的數據,然后對這些數據按照連接操作關聯列(如A.col3)進行排序。
2) 隨后生成row source2需要的數據,然后對這些數據按照與sort source1對應的連接操作關聯列(如B.col4)進行排序。
3) 最后兩邊已排序的行被放在一起執行合并操作,即將2個row source按照連接條件連接起來
下面是連接步驟的圖形表示:
MERGE
/ \
SORT SORT
| |
Row Source 1 Row Source 2
如果row source已經在連接關聯列上被排序,則該連接操作就不需要再進行sort操作,這樣可以大大提高這種連接操作的連接速度,因為排序是個極其費資源的操作,特別是對于較大的表。 預先排序的row source包括已經被索引的列(如a.col3或b.col4上有索引)或row source已經在前面的步驟中被排序了。盡管合并兩個row source的過程是串行的,但是可以并行訪問這兩個row source(如并行讀入數據,并行排序).
SMJ連接的例子:
SQL> explain plan for
select /*+ ordered */ e.deptno, d.deptno
from emp e, dept d
where e.deptno = d.deptno
order by e.deptno, d.deptno;
Query Plan
-------------------------------------
SELECT STATEMENT [
CHOOSE] Cost=17
MERGE JOIN
SORT JOIN
TABLE ACCESS FULL EMP [ANALYZED]
SORT JOIN
TABLE ACCESS FULL DEPT [ANALYZED]
排序是一個費時、費資源的操作,特別對于大表。基于這個原因,SMJ經常不是一個特別有效的連接方法,但是如果2個row source都已經預先排序,則這種連接方法的效率也是蠻高的。
嵌套循環(Nested Loops, NL)
這個連接方法有驅動表(外部表)的概念。其實,該連接過程就是一個2層嵌套循環,所以外層循環的次數越少越好,這也就是我們為什么將小表或返回較小row source的表作為驅動表(用于外層循環)的理論依據。但是這個理論只是一般指導原則,因為遵循這個理論并不能總保證使語句產生的I/O次數最少。有時不遵守這個理論依據,反而會獲得更好的效率。如果使用這種方法,決定使用哪個表作為驅動表很重要。有時如果驅動表選擇不正確,將會導致語句的性能很差、很差。
內部連接過程:
Row source1的Row 1 -------------- -- Probe -> Row source 2
Row source1的Row 2 -------------- -- Probe -> Row source 2
Row source1的Row 3 -------------- -- Probe -> Row source 2
…….
Row source1的Row n -------------- -- Probe -> Row source 2
從內部連接過程來看,需要用row source1中的每一行,去匹配row source2中的所有行,所以此時保持row source1盡可能的小與高效的訪問row source2(一般通過索引實現)是影響這個連接效率的關鍵問題。這只是理論指導原則,目的是使整個連接操作產生最少的物理I/O次數,而且如果遵守這個原則,一般也會使總的物理I/O數最少。但是如果不遵從這個指導原則,反而能用更少的物理I/O實現連接操作,那盡管違反指導原則吧!因為最少的物理I /O次數才是我們應該遵從的真正的指導原則,在后面的具體案例分析中就給出這樣的例子。
在上面的連接過程中,我們稱Row source1為驅動表或外部表。Row Source2被稱為被探查表或內部表。
在NESTED LOOPS連接中,Oracle讀取row source1中的每一行,然后在row sourc2中檢查是否有匹配的行,所有被匹配的行都被放到結果集中,然后處理row source1中的下一行。這個過程一直繼續,直到row source1中的所有行都被處理。這是從連接操作中可以得到第一個匹配行的最快的方法之一,這種類型的連接可以用在需要快速響應的語句中,以響應速度為主要目標。
如果driving row source(外部表)比較小,并且在inner row source(內部表)上有唯一索引,或有高選擇性非唯一索引時,使用這種方法可以得到較好的效率。NESTED LOOPS有其它連接方法沒有的的一個優點是:可以先返回已經連接的行,而不必等待所有的連接操作處理完才返回數據,這可以實現快速的響應時間。
如果不使用并行操作,最好的驅動表是那些應用了where 限制條件后,可以返回較少行數據的的表,所以大表也可能稱為驅動表,關鍵看限制條件。對于并行查詢,我們經常選擇大表作為驅動表,因為大表可以充分利用并行功能。當然,有時對查詢使用并行操作并不一定會比查詢不使用并行操作效率高,因為最后可能每個表只有很少的行符合限制條件,而且還要看你的硬件配置是否可以支持并行(如是否有多個CPU,多個硬盤控制器),所以要具體問題具體對待。
NL連接的例子:
SQL> explain plan for
select a.dname,b.sql
from dept a,emp b
where a.deptno = b.deptno;
Query Plan
-------------------------
SELECT STATEMENT [
CHOOSE] Cost=5
NESTED LOOPS
TABLE ACCESS FULL DEPT [ANALYZED]
TABLE ACCESS FULL EMP [ANALYZED]
哈希連接(Hash Join, HJ)
這種連接是在oracle 7.3以后引入的,從理論上來說比NL與SMJ更高效,而且只用在CBO優化器中。
較小的row source被用來構建hash table與bitmap,第2個row source被用來被hansed,并與第一個row source生成的hash table進行匹配,以便進行進一步的連接。Bitmap被用來作為一種比較快的查找方法,來檢查在hash table中是否有匹配的行。特別的,當hash table比較大而不能全部容納在內存中時,這種查找方法更為有用。這種連接方法也有NL連接中所謂的驅動表的概念,被構建為hash table與bitmap的表為驅動表,當被構建的hash table與bitmap能被容納在內存中時,這種連接方式的效率極高。
HASH連接的例子:
SQL> explain plan for
select /*+ use_hash(emp) */ empno
from emp, dept
where emp.deptno = dept.deptno;
Query Plan
----------------------------
SELECT STATEMENT [
CHOOSE] Cost=3
HASH JOIN
TABLE ACCESS FULL DEPT
TABLE ACCESS FULL EMP
要使哈希連接有效,需要設置HASH_JOIN_ENABLED=TRUE,缺省情況下該參數為TRUE,另外,不要忘了還要設置 hash_area_size參數,以使哈希連接高效運行,因為哈希連接會在該參數指定大小的內存中運行,過小的參數會使哈希連接的性能比其他連接方式還要低。
總結一下,在哪種情況下用哪種連接方法比較好:
排序 - - 合并連接(Sort Merge Join, SMJ):
a) 對于非等值連接,這種連接方式的效率是比較高的。
b) 如果在關聯的列上都有索引,效果更好。
c) 對于將2個較大的row source做連接,該連接方法比NL連接要好一些。
d) 但是如果sort merge返回的row source過大,則又會導致使用過多的rowid在表中查詢數據時,數據庫性能下降,因為過多的I/O。
嵌套循環(Nested Loops, NL):
a) 如果driving row source(外部表)比較小,并且在inner row source(內部表)上
有唯一索引,或有高選擇性非唯一索引時,使用這種方法可以得到較好的效率。
b) NESTED LOOPS有其它連接方法沒有的的一個優點是:可以先返回已經
連接的行,而不必等待所有的連接操作處理完才返回數據,
這可以實現快速的響應時間。
哈希連接(Hash Join, HJ):
a) 這種方法是在oracle7后來引入的,使用了比較先進的連接理論,
一般來說,其效率應該好于其它2種連接,但是這種連接只能用在
CBO優化器中,而且需要設置合適的hash_area_size參數,
才能取得較好的性能。
b) 在2個較大的row source之間連接時會取得相對較好的效率,在一個
row source較小時則能取得更好的效率。
c) 只能用于等值連接中
笛卡兒乘積(Cartesian Product)
當兩個row source做連接,但是它們之間沒有關聯條件時,就會在兩個row source中做笛卡兒乘積,這通常由編寫代碼疏漏造成(即程序員忘了寫關聯條件)。笛卡爾乘積是一個表的每一行依次與另一個表中的所有行匹配。在特殊情況下我們可以使用笛卡兒乘積,如在星形連接中,除此之外,我們要盡量使用笛卡兒乘積,否則,自己想結果是什么吧!
注意在下面的語句中,在2個表之間沒有連接。
SQL> explain plan for
select emp.deptno,dept,deptno
from emp,dept
Query Plan
------------------------------
SLECT STATEMENT [
CHOOSE] Cost=5
MERGE JOIN CARTESIAN
TABLE ACCESS FULL DEPT
SORT JOIN
TABLE ACCESS FULL EMP
CARTESIAN關鍵字指出了在2個表之間做笛卡爾乘積。假如表emp有n行,dept表有m行,笛卡爾乘積的結果就是得到n * m行結果。
本文介紹了ORACLE執行計劃的一些基本概念,供學習應用。
一.相關的概念
Rowid的概念:rowid是一個偽列,既然是偽列,那么這個列就不是用戶定義,而是系統自己給加上的。對每個表都有一個rowid的偽列,但是表中并不物理存儲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訪問數據的存取方法
1) 全表掃描(Full Table Scans, FTS)
為實現全表掃描,Oracle讀取表中所有的行,并檢查每一行是否滿足語句的WHERE限制條件一個多塊讀操作可以使一次I/O能讀取多塊數據塊(db_block_multiblock_read_count參數設定),而不是只讀取一個數據塊,這極大的減少了I/O總次數,提高了系統的吞吐量,所以利用多塊讀的方法可以十分高效地實現全表掃描,而且只有在全表掃描的情況下才能使用多塊讀操作。在這種訪問模式下,每個數據塊只被讀一次。
使用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存取單行數據的最快方法。
這種存取方法不會用到多塊讀操作,一次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
但是如果查詢的數據能全在索引中找到,就可以避免進行第2步操作,避免了不必要的I/O,此時即使通過索引掃描取出的數據比較多,效率還是很高的
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]
從這個例子中可以看到:因為索引是已經排序了的,所以將按照索引的順序查詢出符合條件的行,因此避免了進一步排序操作。
根據索引的類型與where限制條件的不同,有4種類型的索引掃描:
索引唯一掃描(index unique scan)
索引范圍掃描(index range scan)
索引全掃描(index full scan)
索引快速掃描(index fast full scan)
(1) 索引唯一掃描(index unique scan)
通過唯一索引查找一個數值經常返回單個ROWID。如果存在UNIQUE 或PRIMARY KEY 約束(它保證了語句只存取單行)的話,Oracle經常實現唯一性掃描。
使用唯一性約束的例子:
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
(2) 索引范圍掃描(index range scan)
使用一個索引存取多行數據,在唯一索引上使用索引范圍掃描的典型情況下是在謂詞(where限制條件)中使用了范圍操作符(如>、<、<>、>=、<=、between)
使用索引范圍掃描的例子:
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]
在非唯一索引上,謂詞col = 5可能返回多行數據,所以在非唯一索引上都使用索引范圍掃描。
使用index rang scan的3種情況:
(a) 在唯一索引列上使用了range操作符(> < <> >= <= between)
(b) 在組合索引上,只使用部分列進行查詢,導致查詢出多行
(c) 對非唯一索引列上進行的任何查詢。
(3) 索引全掃描(index full scan)
與全表掃描對應,也有相應的全索引掃描。而且此時查詢出的數據都必須從索引中可以直接得到。
全索引掃描的例子:
An Index full scan will not perform single block i/o's and so it may prove to be inefficient.
e.g.
Index BE_IX is a concatenated index on big_emp (empno, ename)
SQL> explain plan for select empno, ename from big_emp order by empno,ename;
Query Plan
--------------------------------------------------------------------------------
SELECT STATEMENT[CHOOSE] Cost=26
INDEX FULL SCAN BE_IX [ANALYZED]
(4) 索引快速掃描(index fast full scan)
掃描索引中的所有的數據塊,與 index full scan很類似,但是一個顯著的區別就是它不對查詢出的數據進行排序,即數據不是以排序順序被返回。在這種存取方法中,可以使用多塊讀功能,也可以使用并行讀入,以便獲得最大吞吐量與縮短執行時間。
索引快速掃描的例子:
BE_IX索引是一個多列索引: big_emp (empno,ename)
SQL> explain plan for select empno,ename from big_emp;
Query Plan
------------------------------------------
SELECT STATEMENT[CHOOSE] Cost=1
INDEX FAST FULL SCAN BE_IX [ANALYZED]
只選擇多列索引的第2列:
SQL> explain plan for select ename from big_emp;
Query Plan
------------------------------------------
SELECT STATEMENT[CHOOSE] Cost=1
INDEX FAST FULL SCAN BE_IX [ANALYZED]
三.表之間的連接
Join是一種試圖將兩個表結合在一起的謂詞,一次只能連接2個表,表連接也可以被稱為表關聯。在后面的敘述中,我們將會使用”row source”來代替”表”,因為使用row source更嚴謹一些,并且將參與連接的2個row source分別稱為row source1和row source 2。Join過程的各個步驟經常是串行操作,即使相關的row source可以被并行訪問,即可以并行的讀取做join連接的兩個row source的數據,但是在將表中符合限制條件的數據讀入到內存形成row source后,join的其它步驟一般是串行的。有多種方法可以將2個表連接起來,當然每種方法都有自己的優缺點,每種連接類型只有在特定的條件下才會發揮出其最大優勢。
row source(表)之間的連接順序對于查詢的效率有非常大的影響。通過首先存取特定的表,即將該表作為驅動表,這樣可以先應用某些限制條件,從而得到一個較小的row source,使連接的效率較高,這也就是我們常說的要先執行限制條件的原因。一般是在將表讀入內存時,應用where子句中對該表的限制條件。
根據2個row source的連接條件的中操作符的不同,可以將連接分為等值連接(如WHERE A.COL3 = B.COL4)、非等值連接(WHERE A.COL3 > B.COL4)、外連接(WHERE A.COL3 = B.COL4(+))。上面的各個連接的連接原理都基本一樣,所以為了簡單期間,下面以等值連接為例進行介紹。
在后面的介紹中,都已:
SELECT A.COL1, B.COL2
FROM A, B
WHERE A.COL3 = B.COL4;
為例進行說明,假設A表為Row Soruce1,則其對應的連接操作關聯列為COL 3;B表為Row Soruce2,則其對應的連接操作關聯列為COL 4;
連接類型:
目前為止,無論連接操作符如何,典型的連接類型共有3種:
排序 - - 合并連接(Sort Merge Join (SMJ) )
嵌套循環(Nested Loops (NL) )
哈希連接(Hash Join)
排序 - - 合并連接(Sort Merge Join, SMJ)
內部連接過程:
1) 首先生成row source1需要的數據,然后對這些數據按照連接操作關聯列(如A.col3)進行排序。
2) 隨后生成row source2需要的數據,然后對這些數據按照與sort source1對應的連接操作關聯列(如B.col4)進行排序。
3) 最后兩邊已排序的行被放在一起執行合并操作,即將2個row source按照連接條件連接起來
下面是連接步驟的圖形表示:
MERGE
/\
SORTSORT
||
Row Source 1Row Source 2
如果row source已經在連接關聯列上被排序,則該連接操作就不需要再進行sort操作,這樣可以大大提高這種連接操作的連接速度,因為排序是個極其費資源的操作,特別是對于較大的表。預先排序的row source包括已經被索引的列(如a.col3或b.col4上有索引)或row source已經在前面的步驟中被排序了。盡管合并兩個row source的過程是串行的,但是可以并行訪問這兩個row source(如并行讀入數據,并行排序).
SMJ連接的例子:
SQL> explain plan for
select /*+ ordered */ e.deptno, d.deptno
from emp e, dept d
where e.deptno = d.deptno
order by e.deptno, d.deptno;
Query Plan
-------------------------------------
SELECT STATEMENT [CHOOSE] Cost=17
MERGE JOIN
SORT JOIN
TABLE ACCESS FULL EMP [ANALYZED]
SORT JOIN
TABLE ACCESS FULL DEPT [ANALYZED]
排序是一個費時、費資源的操作,特別對于大表。基于這個原因,SMJ經常不是一個特別有效的連接方法,但是如果2個row source都已經預先排序,則這種連接方法的效率也是蠻高的。
嵌套循環(Nested Loops, NL)
這個連接方法有驅動表(外部表)的概念。其實,該連接過程就是一個2層嵌套循環,所以外層循環的次數越少越好,這也就是我們為什么將小表或返回較小 row source的表作為驅動表(用于外層循環)的理論依據。但是這個理論只是一般指導原則,因為遵循這個理論并不能總保證使語句產生的I/O次數最少。有時不遵守這個理論依據,反而會獲得更好的效率。如果使用這種方法,決定使用哪個表作為驅動表很重要。有時如果驅動表選擇不正確,將會導致語句的性能很差、很差。
內部連接過程:
Row source1的Row 1 ---------------- Probe ->Row source 2
Row source1的Row 2 ---------------- Probe ->Row source 2
Row source1的Row 3 ---------------- Probe ->Row source 2
…….
Row source1的Row n ---------------- Probe ->Row source 2
從內部連接過程來看,需要用row source1中的每一行,去匹配row source2中的所有行,所以此時保持row source1盡可能的小與高效的訪問row source2(一般通過索引實現)是影響這個連接效率的關鍵問題。這只是理論指導原則,目的是使整個連接操作產生最少的物理I/O次數,而且如果遵守這個原則,一般也會使總的物理I/O數最少。但是如果不遵從這個指導原則,反而能用更少的物理I/O實現連接操作,那盡管違反指導原則吧!因為最少的物理 I/O次數才是我們應該遵從的真正的指導原則,在后面的具體案例分析中就給出這樣的例子。
在上面的連接過程中,我們稱Row source1為驅動表或外部表。Row Source2被稱為被探查表或內部表。
在NESTED LOOPS連接中,Oracle讀取row source1中的每一行,然后在row sourc2中檢查是否有匹配的行,所有被匹配的行都被放到結果集中,然后處理row source1中的下一行。這個過程一直繼續,直到row source1中的所有行都被處理。這是從連接操作中可以得到第一個匹配行的最快的方法之一,這種類型的連接可以用在需要快速響應的語句中,以響應速度為主要目標。
如果driving row source(外部表)比較小,并且在inner row source(內部表)上有唯一索引,或有高選擇性非唯一索引時,使用這種方法可以得到較好的效率。NESTED LOOPS有其它連接方法沒有的的一個優點是:可以先返回已經連接的行,而不必等待所有的連接操作處理完才返回數據,這可以實現快速的響應時間。
如果不使用并行操作,最好的驅動表是那些應用了where 限制條件后,可以返回較少行數據的的表,所以大表也可能稱為驅動表,關鍵看限制條件。對于并行查詢,我們經常選擇大表作為驅動表,因為大表可以充分利用并行功能。當然,有時對查詢使用并行操作并不一定會比查詢不使用并行操作效率高,因為最后可能每個表只有很少的行符合限制條件,而且還要看你的硬件配置是否可以支持并行(如是否有多個CPU,多個硬盤控制器),所以要具體問題具體對待。
NL連接的例子:
SQL> explain plan for
select a.dname,b.sql
from dept a,emp b
where a.deptno = b.deptno;
Query Plan
-------------------------
SELECT STATEMENT [CHOOSE] Cost=5
NESTED LOOPS
TABLE ACCESS FULL DEPT [ANALYZED]
TABLE ACCESS FULL EMP [ANALYZED]
哈希連接(Hash Join, HJ)
這種連接是在oracle 7.3以后引入的,從理論上來說比NL與SMJ更高效,而且只用在CBO優化器中。
較小的row source被用來構建hash table與bitmap,第2個row source被用來被hansed,并與第一個row source生成的hash table進行匹配,以便進行進一步的連接。Bitmap被用來作為一種比較快的查找方法,來檢查在hash table中是否有匹配的行。特別的,當hash table比較大而不能全部容納在內存中時,這種查找方法更為有用。這種連接方法也有NL連接中所謂的驅動表的概念,被構建為hash table與bitmap的表為驅動表,當被構建的hash table與bitmap能被容納在內存中時,這種連接方式的效率極高。
HASH連接的例子:
SQL> explain plan for
select /*+ use_hash(emp) */ empno
from emp, dept
where emp.deptno = dept.deptno;
Query Plan
----------------------------
SELECT STATEMENT[CHOOSE] Cost=3
HASH JOIN
TABLE ACCESS FULL DEPT
TABLE ACCESS FULL EMP
要使哈希連接有效,需要設置HASH_JOIN_ENABLED=TRUE,缺省情況下該參數為TRUE,另外,不要忘了還要設置 hash_area_size參數,以使哈希連接高效運行,因為哈希連接會在該參數指定大小的內存中運行,過小的參數會使哈希連接的性能比其他連接方式還要低。
總結一下,在哪種情況下用哪種連接方法比較好:
排序 - - 合并連接(Sort Merge Join, SMJ):
a) 對于非等值連接,這種連接方式的效率是比較高的。
b) 如果在關聯的列上都有索引,效果更好。
c) 對于將2個較大的row source做連接,該連接方法比NL連接要好一些。
d) 但是如果sort merge返回的row source過大,則又會導致使用過多的rowid在表中查詢數據時,數據庫性能下降,因為過多的I/O。
嵌套循環(Nested Loops, NL):
a) 如果driving row source(外部表)比較小,并且在inner row source(內部表)上有唯一索引,或有高選擇性非唯一索引時,使用這種方法可以得到較好的效率。
b) NESTED LOOPS有其它連接方法沒有的的一個優點是:可以先返回已經連接的行,而不必等待所有的連接操作處理完才返回數據,這可以實現快速的響應時間。
哈希連接(Hash Join, HJ):
a) 這種方法是在oracle7后來引入的,使用了比較先進的連接理論,一般來說,其效率應該好于其它2種連接,但是這種連接只能用在CBO優化器中,而且需要設置合適的hash_area_size參數,才能取得較好的性能。
b) 在2個較大的row source之間連接時會取得相對較好的效率,在一個row source較小時則能取得更好的效率。
c) 只能用于等值連接中
笛卡兒乘積(Cartesian Product)
當兩個row source做連接,但是它們之間沒有關聯條件時,就會在兩個row source中做笛卡兒乘積,這通常由編寫代碼疏漏造成(即程序員忘了寫關聯條件)。笛卡爾乘積是一個表的每一行依次與另一個表中的所有行匹配。在特殊情況下我們可以使用笛卡兒乘積,如在星形連接中,除此之外,我們要盡量使用笛卡兒乘積,否則,自己想結果是什么吧!
注意在下面的語句中,在2個表之間沒有連接。
SQL> explain plan for
select emp.deptno,dept,deptno
from emp,dept
Query Plan
------------------------
SLECT STATEMENT [CHOOSE] Cost=5
MERGE JOIN CARTESIAN
TABLE ACCESS FULL DEPT
SORT JOIN
TABLE ACCESS FULL EMP
CARTESIAN關鍵字指出了在2個表之間做笛卡爾乘積。假如表emp有n行,dept表有m行,笛卡爾乘積的結果就是得到n * m行結果。