【Oracle效能優化】執行計劃與索引型別分析
一條sql的好壞,主要來源兩個方面:
- 1、 從資料庫層面:取決於優化器所採用的資料訪問方式和資料處理的方式決定
- 2、從業務方面來講:這條sql在業務上是不是一條好的sql
我們以oracle 11g為例子進行分析。
一、資料的訪問方式
【沒有索引】
如果一張表沒有建立索引,那麼優化器採用的資料訪問方式也會截然不同,這就取決於oracle的資料訪問方式,下邊列舉兩種:
- 1、並行訪問
- 2、多資料塊訪問
【有索引】
建立索引的情況,也會有不同的資料訪問方式,主要有下面5種:
- 1、唯一索引(index unique scan)
- 2、範圍索引掃描(index range scan)
- 3、全索引掃描(index full scan)
- 4、全索引快速掃描(index fast full scan)
- 5、索引跳躍掃描(index skip scan)
二、資料的處理方式
上面列舉了幾種資料的訪問方式,其實像我們日常開發中使用到的排序order by
,分組group by
、統計count
等等操作,都是對資料的一種操作方式,但是,除了這些基本的操作方式之外,我們一般還會對錶進行連線join
處理,對於連線這種處理方式,又有下面幾種情況:
-
1、
nested loop join
(內部巢狀迴圈連線) -
2、
hash join
(雜湊連線) -
3、
sort merge join
(合併排序連線)
接下來,我們使用測試用例驗證上面3種join
-- 刪除nestedLoopTest1、nestedLoopTest2表
drop table nestedLoopTest1 ;
drop table nestedLoopTest2 ;
-- 建立nestedLoopTest表
create table nestedLoopTest1
(
id NUMBER(11)
);
commit;
create table nestedLoopTest2
(
id NUMBER(11)
);
commit;
-- 各賦值100條資料
BEGIN
FOR i IN 0..100 LOOP
INSERT INTO nestedLoopTest1(id) VALUES(i);
END LOOP;
END;
commit;
BEGIN
FOR i IN 0..100 LOOP
INSERT INTO nestedLoopTest2(id) VALUES(i);
END LOOP;
END;
commit;
-- 1、hash join 雜湊連線,因為此時兩張表是並行執行的
xxxxxx
-- 2、nested join 內部巢狀連線,此時t2中id建了索引
xxxxxx
-- 3、兩個表的id都建立了索引
複製程式碼
- 1、
hash join
(雜湊連線)
我們繼續執行下面sql:
select t1.* from nestedLoopTest1 t1,nestedLoopTest2 t2 where t1.id=t2.id;
複製程式碼
此時表nestedLoopTest1
和表nestedLoopTest2
中的id
都沒有建立索引,因此,我們會看到下面的執行計劃:
執行步驟如上圖所示,我們可以看到,此時兩張表都是全表掃描,然後再進行一次Hash join
,至於hash join
的原理,後面單獨學習介紹。hash join
會將小表load進記憶體中,然後利用大表和小表進行關聯操作
- 2、
nested loop join
(內部巢狀迴圈連線)
我們繼續執行下面sql:
create index nestedLoopTest2index on nestedLoopTest2(id);
select t1.* from nestedLoopTest1 t1,nestedLoopTest2 t2 where t1.id=t2.id;
複製程式碼
從執行計劃中可以看出,首先對錶t1
進行全表掃描,然後對索引nestedLoopTest2index
進行range
範圍掃描,為什麼是範圍掃描呢?因為表t1
中的一條記錄,可能在表t2
對應多條記錄。
對於迴圈巢狀連線方式,我們可以想象成2個for迴圈巢狀即可。
另外,當兩個表都建立索引時,我們再繼續執行下面的sql:
create index nestedLoopTest1index on nestedLoopTest1(id);
select t1.* from nestedLoopTest1 t1,nestedLoopTest2 t2 where t1.id=t2.id;
複製程式碼
相比上圖,表t1
不再是全表掃描了,而是全索引掃描。
- 3、
sort merge join
(合併排序連線)
該連結方式大概的原理就是,判斷原表是否排序,如果未排序,則針對關聯欄位進行排序;判斷關聯表是否排序,如果未排序,則進行排序,最後將兩個排序的表進行合併。
三、oracle執行計劃
我們要知道oracle資料是如何資料訪問和資料處理的,我們就要看下執行計劃,但執行計劃又僅僅告訴我們這些資訊。
我們登陸上sqlplus,就拿一條最簡單的sql進行說明,簡單說下我們應該如何看懂執行計劃:
select * from emp;
emp
表是oracle自帶的員工表,右鍵點選Explain Plan
,或者按下F5
檢視執行計劃,如下圖:
執行計劃最左邊的Description
列是比較重要的,它會列出這個sql的一些執行步驟,該列有下面幾個檢視規則:
- 1、層次不同情況下,越靠右的步驟越先執行;
- 2、層次相同情況下,越上方的結果越先執行;
上圖告訴我們,這條語句採用的資料訪問方式是:TABLE ACCESS FULL
,也就是全表掃描,我們可能會問,這個emp
表不是有建立索引嗎?其實有索引也沒有用,因為我們就是要提取整個表的資料,索引沒有意義,這也說明一個情況,有索引的表,不一定效率就高,後面會講到。
對於Cost
這個指標,這是oracle優化器用來衡量這條sql執行的代價有多大,比如需要消耗多少CPU計算資源呀之類的。
下面介紹幾種通過索引訪問方式的SQL例子
一、唯一索引(index unique scan)
empno
是emp
表的主鍵,是一個唯一索引,我們執行下面sql語句,檢視其執行計劃,如下圖:
select * from emp where empno=7782
從執行計劃中顯示的INDEX UNIQUE SCAN
可以看出,這句sql,oracle優化器會執行唯一索引掃描,掃描完索引之後,我們得到索引的值為7782
,然後oracle肯定要去資料檔案中去取這條編號對應的資料塊返回嘛,因此我們可以看到執行計劃中顯示了TABLE ACCESS BY INDEX ROWID
,因為索引存的是每一行的id,因此oracle根據rowid這個屬性去找對應的資料。
其實去訪問資料塊取資料這個步驟有時候是沒有的,也就是當你只想取其編號empno
而不是*
的時候,執行計劃就不會去資料檔案中取資料了,也就是步驟2不會有了,因為oracle直接從索引掃描到之後就直接返回索引這個值就行了,沒必要去取資料,我們又不需要,驗證如下:
select empno from emp where empno=7782
其實我們一般也不會寫這樣的sql吧,哈哈~~~
二、範圍索引掃描(index range scan)
假設我們執行下面sql語句:
select job from emp where empno>7782
其執行計劃如下:
從執行計劃中可以看出,這種型別的SQL語句,採用的執行方式為index range scan
範圍索引掃描。
三、全索引掃描(index full scan)
全索引掃描,顧名思義就是掃描整個索引區域就能確定出執行結果,比如下面sql語句:
select count(*) from emp
我們統計整個表的所有資料個數,直接讀索引資料塊的個數即可,步驟2為將步驟一的記過進行一個求和,彙總得到一個總數返回。
四、全索引快速掃描(index fast full scan)
我們先用下面語句拷貝一個表並將重新命名:
create table emp1 as (select * from emp);
truncate table emp1;
-- 插入1,000,000條資料
BEGIN
FOR I IN 0..1000000 LOOP
INSERT INTO EMP1(EMPNO,ENAME) VALUES(
I,CONCAT('TBL',I));
END LOOP;
END;
複製程式碼
表建好之後,我們看下下面sql的執行計劃,看看當資料量大的時候,這句sql會不會採用index fast full scan
index fast full scan
區別於index full scan
的地方是前者可以一次性讀取多個資料塊,類似於並行,而後者序列讀取。
使用這種執行方式的SQL語句一般是那種可以直接通過索引就能確定出執行結果,比如我們執行下面SQL:
五、索引跳躍掃描(index skip scan)
index skip scan
是oracle 9i之後才提供的索引掃描方式,主要使用來解決組合索引中,where條件使用非前導列查詢時,預設採用ACCESS TABLE FULL
全表掃描的缺點。但是使用該特性是有一些限制條件的,主要有下面幾個點:
- 1、組合索引前導列唯一值較少(重複值很多)
- 2、資料庫採用CBO優化器,且表和索引都經過分析
- 3、where查詢條件中不存在組合索引前導列
接下來我們主要驗證兩個問題:
測試相應的SQL語句如下:
--刪除表
drop table student;
commit;
-- 建立Student表
create table STUDENT
(
stuno NUMBER(11),stuname VARCHAR2(20),schoolno NUMBER(11),age NUMBER(3)
);
commit;
-- 建立組合索引
create index stucombindex on student(stuname,schoolno);
commit;
--F5檢視執行計劃,會看到是ACCESS TABLE FULL全表掃描,stuname是前導列,schoolno為非前導列
select * from student t where t.schoolno=100;
-- 賦值100萬條資料
BEGIN
FOR i IN 0..1000000 LOOP
INSERT INTO student(stuno,stuname,schoolno) VALUES(
i,'TBL',i);
END LOOP;
END;
commit;
-- 更新3條資料的前導列為不同值
update student t set t.stuname=concat('s',t.stuno) where mod(t.stuno,10000)=0;
commit;
select count(*),count(distinct stuname) from student;
-- 對錶、索引進行分析
analyze table student compute statistics for table for all columns for all indexes;
-- 此處檢視執行計劃,可看到優化器採用的是index skip scan
select * from student where schoolno = 1000;
複製程式碼
1、組合索引中,使用非前導列進行查詢時,優化器採用的是ACCESS TABLE FULL
全表掃描
首先將上述sql中,倒數第二句SQL註釋掉,將會輸出如下內容,預設採用全表掃描:
2、驗證index skip scan
經過我們的驗證,我們可以知道,當我們建立組合索引時,日常開發中,我們儘可能將 __需求經常用到、選擇性高重複值少__的列作為前導列,這樣才能最大程度減少非引導列不走索引或者只走跳躍索引的情況。另外我們什麼時候建立組合索引呢?主要考慮下面幾種情況:
- 1、當單條件查詢時,返回較多資料
- 2、當符合條件查詢時,返回資料較少
當且僅當條件1和條件2同時成立時,我們這個時候可以建立組合索引了,舉個例子,員工表employee
中,age=28
這個條件的人非常多,role=Java程式設計師
這個條件返回的資料也非常多,但是age=28 and role=Java程式設計師
返回的資料卻非常少!
單條件指:where xxx=xxx
複合條件指:where xxx=xxx and xxx1=xxx1
六、應用場景
1、oracle千萬級別大表分頁查詢
傳統oracle分頁使用如下結構:
select *
from (
select fundacco,rownum rowno from
tbl_20191231
where rownum <= #{end}) b
where
b.rowno > #{start}
複製程式碼
當時當start越來越大的時候,這個外層子查詢所需要遍歷的資料量就越多,經過實際生產驗證會很慢,500W資料量,每頁250條,當start大於200W時,平均耗時在1-2s。
如何優化呢?oracle sql層面上我們不能進行優化了,但我們可以通過新增加一個列rownos,值單調遞增且建立唯一索引。然後我們通過下面sql查詢就非常快了,平均在20ms左右。
select * from tbl_20191231 t where t.rownos > #{start} and t.rownos <= #{end}
複製程式碼
其實不難發現,我們是利用了oracle的索引範圍掃描(index range scan
)特性而已。執行計劃如下: