1. 程式人生 > 程式設計 >【Oracle效能優化】執行計劃與索引型別分析

【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

資料處理方式,測試SQL用例如下:

-- 刪除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)

empnoemp表的主鍵,是一個唯一索引,我們執行下面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)特性而已。執行計劃如下: