@TOC
1浴滴、文章寫作前言簡介
之前曾經(jīng)拜讀過《收獲,不止sql調(diào)優(yōu)》一書蒲牧,此書是國內(nèi)DBA寫的一本很不錯的調(diào)優(yōu)類型的書撇贺,是一些很不錯的調(diào)優(yōu)經(jīng)驗的分享。雖然讀了一遍冰抢,做了下讀書筆記松嘶,覺得很有所收獲,但是到實際的實踐中覺得還是很缺實踐挎扰。剛好最近又有一次sql調(diào)優(yōu)培訓(xùn)活動翠订,去參加后巢音,重新復(fù)習(xí)Oracle執(zhí)行計劃,所以整理資料尽超,做成筆記分享出來
2官撼、什么是執(zhí)行計劃?
執(zhí)行計劃是一條查詢語句在Oracle中的執(zhí)行過程或訪問路徑的描述似谁。
執(zhí)行計劃描述了SQL引擎為執(zhí)行SQL語句進(jìn)行的操作傲绣;分析SQL語句相關(guān)的性能問題或僅僅質(zhì)疑查詢優(yōu)化器的決定時,必須知道執(zhí)行計劃巩踏;所以執(zhí)行計劃常用于sql調(diào)優(yōu)秃诵。
3、怎么查看執(zhí)行計劃塞琼?
查看Oracle執(zhí)行計劃有很多種菠净,詳情參考我之前的讀書筆記,本博客只介紹很常用的方法
oracle要使用執(zhí)行計劃一般在sqlplus執(zhí)行sql:
explain plan for select 1 from t
不過如果是使用PLSQL的話彪杉,那就可以使用PLSQL提供的查詢執(zhí)行計劃了,也就是按F5
打開PLSQL工具 -> 首選項 -> 窗口類型 -> 計劃窗口 毅往,在這里加入執(zhí)行計劃需要的參數(shù)
找個SQL,用PLSQL執(zhí)行一下派近,這是plsql的簡單使用
解釋一下這些參數(shù)的意思:
- 基數(shù)(Rows):Oracle估計的當(dāng)前步驟的返回結(jié)果集行數(shù)
- 字節(jié)(Bytes):執(zhí)行SQL對應(yīng)步驟返回的字節(jié)數(shù)
- 耗費(COST)攀唯、CPU耗費:Oracle估計的該步驟的執(zhí)行耗費和CPU耗費
- 時間(Time):Oracle估計的執(zhí)行sql對于步驟需要的時間
4、查看真實執(zhí)行計劃
之前查看執(zhí)行計劃也喜歡按F5构哺,不過最近去培訓(xùn)革答,聽一名dba說战坤,這種方法有時候不能獲取真實的執(zhí)行計劃曙强,收集的信息也不全面,然后怎么查看sql執(zhí)行過程的真實信息途茫?從培訓(xùn)中學(xué)到的經(jīng)驗做成筆記
sqlplus窗口執(zhí)行:
- step1:set statistics_level
alter session set statistics_level=ALL;
- step2:執(zhí)行業(yè)務(wù)sql
select /*+ monitor */ * from ... where ....;
- step3:為了樣式碟嘴,設(shè)置linesize
set linesize 200 pagesize 300;
- step4:查詢真實執(zhí)行計劃
select * from table(dbms_xplan.display_cursor(null, null, 'iostats last'));
sqlplus一般要數(shù)據(jù)庫管理員才可以使用,如果你不是dba囊卜,只能使用plsql developer的話娜扇,只能用下面的方法,方法是從培訓(xùn)中學(xué)到的
使用存儲過程栅组,SQL:
declare
b1 date;
begin
execute immediate 'alter session set statistics_level=ALL';
b1 := sysdate - 1;
for test in (
/*業(yè)務(wù)SQL(sql后面不需要加";")*/
select * from t) loop
null;
end loop;
for x in (select p.plan_table_output
from table(dbms_xplan.display_cursor(null,
null,
'advanced -bytes -PROJECTION allstats last')) p) loop
dbms_output.put_line(x.plan_table_output);
end loop;
rollback;
end;
/
兩種窗口:
- 1雀瓢、SQL窗口的,執(zhí)行SQL后只能去output查看玉掸;
- 2刃麸、command window的,需要先設(shè)置
set serveroutput on size unlimited
司浪,然后再執(zhí)行存儲過程
output或者命令窗口查看的真實執(zhí)行計劃和統(tǒng)計信息:
SQL_ID abk3ghv9u1tvb, child number 0
-------------------------------------
SELECT /*+ monitor */ * FROM APPR_HANDLE_INFO
Plan hash value: 885170757
------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 210 (100)| | 72059 |00:00:00.06 | 2460 |
| 1 | TABLE ACCESS FULL| APPR_HANDLE_INFO | 1 | 32752 | 210 (1)| 00:00:03 | 72059 |00:00:00.06 | 2460 |
------------------------------------------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - SEL$1 / APPR_HANDLE_INFO@SEL$1
Outline Data
-------------
/*+
BEGIN_OUTLINE_DATA
IGNORE_OPTIM_EMBEDDED_HINTS
OPTIMIZER_FEATURES_ENABLE('11.2.0.4')
DB_VERSION('11.2.0.4')
ALL_ROWS
OUTLINE_LEAF(@"SEL$1")
FULL(@"SEL$1" "APPR_HANDLE_INFO"@"SEL$1")
END_OUTLINE_DATA
*/
關(guān)鍵信息解釋:
- Starts:該SQL執(zhí)行的次數(shù)
- E-Rows:為執(zhí)行計劃預(yù)計的行數(shù)
- A-Rows:實際返回的行數(shù)泊业,E-Rows和A-Rows作比較把沼,就可以看出具體那一步執(zhí)行計劃出問題了
- A-Time:每一步實際執(zhí)行的時間,可以看出耗時的SQL
- Buffers:每一步實際執(zhí)行的邏輯讀或一致性讀
5吁伺、看懂Oracle執(zhí)行計劃
上面已經(jīng)介紹了如何查看執(zhí)行計劃饮睬,現(xiàn)在簡單介紹一下一些基本方法和相關(guān)理論知識
5.1 查看explain
找一條比較復(fù)雜的SQL,執(zhí)行:
F5方式查看:
set statistics_level=ALL方式:
SQL_ID 4qfq3t2ukm0y1, child number 0
-------------------------------------
SELECT /*+ monitor*/ A.USER_CODE, A.FULL_NAME, A.USER_PWD, C.UNIT_CODE,
C.UNIT_NAME FROM BASE_USER A LEFT JOIN (SELECT UR.USER_CODE,
UR.UNIT_CODE FROM APPR_USER_ROLE UR WHERE UR.USER_ROLE < 10) B ON
A.USER_CODE = B.USER_CODE LEFT JOIN LZCITY_APPROVE_UNIT_INFO C ON
B.UNIT_CODE = C.UNIT_CODE WHERE C.UNIT_CODE ='15803'
Plan hash value: 3288287052
------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 3 (100)| | 16 |00:00:00.01 | 38 |
| 1 | NESTED LOOPS | | 1 | 1 | 3 (0)| 00:00:01 | 16 |00:00:00.01 | 38 |
| 2 | NESTED LOOPS | | 1 | 1 | 3 (0)| 00:00:01 | 16 |00:00:00.01 | 22 |
| 3 | NESTED LOOPS | | 1 | 1 | 2 (0)| 00:00:01 | 16 |00:00:00.01 | 5 |
| 4 | TABLE ACCESS BY INDEX ROWID| LZCITY_APPROVE_UNIT_INFO | 1 | 1 | 1 (0)| 00:00:01 | 1 |00:00:00.01 | 3 |
|* 5 | INDEX UNIQUE SCAN | PK_LZCITY_APPROVE_UNIT_INFO | 1 | 1 | 0 (0)| | 1 |00:00:00.01 | 2 |
|* 6 | INDEX RANGE SCAN | PK_APPR_USER_ROLE | 1 | 1 | 1 (0)| 00:00:01 | 16 |00:00:00.01 | 2 |
|* 7 | INDEX UNIQUE SCAN | PK_BASE_USER | 16 | 1 | 0 (0)| | 16 |00:00:00.01 | 17 |
| 8 | TABLE ACCESS BY INDEX ROWID | BASE_USER | 16 | 1 | 1 (0)| 00:00:01 | 16 |00:00:00.01 | 16 |
------------------------------------------------------------------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - SEL$E3445A69
4 - SEL$E3445A69 / C@SEL$4
5 - SEL$E3445A69 / C@SEL$4
6 - SEL$E3445A69 / UR@SEL$2
7 - SEL$E3445A69 / A@SEL$3
8 - SEL$E3445A69 / A@SEL$3
Outline Data
-------------
/*+
BEGIN_OUTLINE_DATA
IGNORE_OPTIM_EMBEDDED_HINTS
OPTIMIZER_FEATURES_ENABLE('11.2.0.4')
DB_VERSION('11.2.0.4')
ALL_ROWS
OUTLINE_LEAF(@"SEL$E3445A69")
MERGE(@"SEL$2")
OUTLINE(@"SEL$A2E96217")
OUTER_JOIN_TO_INNER(@"SEL$E9F4A6F9" "B"@"SEL$1")
OUTER_JOIN_TO_INNER(@"SEL$E9F4A6F9" "C"@"SEL$4")
OUTLINE(@"SEL$2")
OUTLINE(@"SEL$E9F4A6F9")
MERGE(@"SEL$80808B20")
OUTLINE(@"SEL$6")
OUTLINE(@"SEL$80808B20")
MERGE(@"SEL$4")
MERGE(@"SEL$F1D6E378")
OUTLINE(@"SEL$5")
OUTLINE(@"SEL$4")
OUTLINE(@"SEL$F1D6E378")
MERGE(@"SEL$1")
OUTLINE(@"SEL$3")
OUTLINE(@"SEL$1")
INDEX_RS_ASC(@"SEL$E3445A69" "C"@"SEL$4" ("LZCITY_APPROVE_UNIT_INFO"."UNIT_CODE"))
INDEX(@"SEL$E3445A69" "UR"@"SEL$2" ("APPR_USER_ROLE"."UNIT_CODE" "APPR_USER_ROLE"."USER_CODE" "APPR_USER_ROLE"."AREA_SEQ"
"APPR_USER_ROLE"."USER_ROLE"))
INDEX(@"SEL$E3445A69" "A"@"SEL$3" ("BASE_USER"."USER_CODE"))
LEADING(@"SEL$E3445A69" "C"@"SEL$4" "UR"@"SEL$2" "A"@"SEL$3")
USE_NL(@"SEL$E3445A69" "UR"@"SEL$2")
USE_NL(@"SEL$E3445A69" "A"@"SEL$3")
NLJ_BATCHING(@"SEL$E3445A69" "A"@"SEL$3")
END_OUTLINE_DATA
*/
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("C"."UNIT_CODE"='15803')
6 - access("UR"."UNIT_CODE"='15803' AND "UR"."USER_ROLE"<10)
filter("UR"."USER_ROLE"<10)
7 - access("A"."USER_CODE"="UR"."USER_CODE")
5.2 explain執(zhí)行順序
所以不管是用F5方式還是set statistics_level=ALL方式篮奄,都有Operation參數(shù)捆愁,Operation表示sql執(zhí)行過程,查看怎么執(zhí)行的窟却,有兩個規(guī)則:
- 根據(jù)Operation縮進(jìn)判斷牙瓢,縮進(jìn)最多的最先執(zhí)行;
- Operation縮進(jìn)相同時间校,最上面的是最先執(zhí)行的矾克;
如圖執(zhí)行計劃,根據(jù)規(guī)則憔足,可以得出執(zhí)行順序:INDEX UNIQUE SCAN->TABLE ACCESS BY INDEX ROWID->INDEX RANGE SCAN ->NESTED LOOPS ->INDEX UNIQUE SCAN->NESTED LOOPS ->TABLE ACCESS BY INDEX ROWID->NESTED LOOPS-> SELECT STATEMENT
5.3 訪問數(shù)據(jù)的方法
Oracle訪問表中數(shù)據(jù)的方法有兩種胁附,一種是直接表中訪問數(shù)據(jù),另外一種是先訪問索引滓彰,如果索引數(shù)據(jù)不符合目標(biāo)SQL控妻,就回表,符合就不回表揭绑,直接訪問索引就可以弓候。
Oracle直接訪問表中數(shù)據(jù)的方法又分為兩種:一種是全表掃描;另一種是ROWID掃描
5.3.1 全表掃描(TABLE ACCESS FULL)
- 全表掃描他匪;(TABLE ACCESS FULL)
全表掃描是Oracle直接訪問數(shù)據(jù)的一種方法菇存,全表掃描時從第一個區(qū)(EXTENT)的第一個塊(BLOCK)開始掃描,一直掃描的到表的高水位線(High Water Mark)邦蜜,這個范圍內(nèi)的數(shù)據(jù)塊都會掃描到
全表掃描是采用多數(shù)據(jù)塊一起掃的依鸥,并不是一個個數(shù)據(jù)庫掃的,然后我們經(jīng)常說全表掃描慢是針對數(shù)據(jù)量很多的情況悼沈,數(shù)據(jù)量少的話贱迟,全表掃描并不慢的,不過隨著數(shù)據(jù)量越多絮供,高水位線也就越高衣吠,也就是說需要掃描的數(shù)據(jù)庫越多,自然掃描所需要的IO越多壤靶,時間也越多
注意:數(shù)據(jù)量越多缚俏,全表掃描所需要的時間就越多,然后直接刪了表數(shù)據(jù)呢?查詢速度會變快袍榆?其實并不會的胀屿,因為即使我們刪了數(shù)據(jù),高位水線并不會改變包雀,也就是同樣需要掃描那么多數(shù)據(jù)塊
5.3.2 ROWID掃描(TABLE ACCESS BY ROWID)
- ROWID掃描(TABLE ACCESS BY ROWID)
ROWID也就是表數(shù)據(jù)行所在的物理存儲地址宿崭,所謂的ROWID掃描是通過ROWID所在的數(shù)據(jù)行記錄去定位。ROWID是一個偽列才写,數(shù)據(jù)庫里并沒有這個列葡兑,它是數(shù)據(jù)庫查詢過程中獲取的一個物理地址,用于表示數(shù)據(jù)對應(yīng)的行數(shù)赞草。
用sql查詢:
select t.* , rowid from 表格
隨意獲取一個ROWID序列:AAAWSJAAFAAAWwUAAA讹堤,前6位表示對象編號(Data Object number),其后3位文件編號(Relative file number)厨疙,接著其后6位表示塊編號(Block number)洲守, 再其后3位表示行編號(Row number)
ROWID編碼方法是:A ~ Z表示0到25;a ~ z表示26到51沾凄;0~9表示52到61梗醇;+表示62;/表示63撒蟀;剛好64個字符叙谨。
這里隨意找張表查一下文件編號、區(qū)編號保屯、行編號手负,查詢后會返回rowid的一系列物理地址和文件編號(rowid_relative_fno(rowid))、塊編號(rowid_block_number(rowid))姑尺、行編號(rowid_row_number(rowid))
select t.seq,
rowid,
dbms_rowid.rowid_relative_fno(rowid),
dbms_rowid.rowid_block_number(rowid),
dbms_rowid.rowid_row_number(rowid)
from t_info t
SQL查詢一下表格名稱為TABLE的對象編碼
select owner,object_id,data_object_id,status from dba_objects where object_name='TABLE';
相對文件id和絕對文件編碼
相對文件id是指相對于表空間竟终,在表空間唯一;絕對文件編碼是指相當(dāng)于全局?jǐn)?shù)據(jù)庫而言的,全局唯一股缸;下面SQL查詢一下相對文件id和絕對文件編碼
select file_name,file_id,relative_fno from dba_data_files;
訪問索引(TABLE ACCESS BY INDEX SCAN)的情況就比較多了衡楞,可以分為:
- 索引唯一掃描(INDEX UNIQUE SCAN)
- 索引全掃描(INDEX FULL SCAN)
- 索引范圍掃描(INDEX RANGE SCAN)
- 索引快速全掃描(INDEX FAST FULL SCAN)
- 索引跳躍式掃描(INDEX SKIP SCAN)
5.3.3 索引唯一掃描(INDEX UNIQUE SCAN)
- 索引唯一掃描(INDEX UNIQUE SCAN)
索引唯一性掃描(INDEX UNIQUE SCAN)是針對唯一性索引(UNIQUE INDEX)來說的,也就是建立唯一性索引才能索引唯一性掃描敦姻,唯一性掃描,其結(jié)果集只會返回一條記錄歧杏。
- 索引范圍掃描(INDEX RANGE SCAN)
5.3.4 索引范圍掃描(INDEX RANGE SCAN)
- 索引范圍掃描(INDEX RANGE SCAN)索引范圍掃描(INDEX RANGE SCAN)適用于所有類型的B樹索引镰惦,一般不包括唯一性索引,因為唯一性索引走索引唯一性掃描犬绒。 當(dāng)掃描的對象是非唯一性索引的情況旺入,where謂詞條件為Between、=、<茵瘾、>等等的情況就是索引范圍掃描礼华,注意,可以是等值查詢拗秘,也可以是范圍查詢圣絮。如果where條件里有一個索引鍵值列沒限定為非空的,那就可以走索引范圍掃描雕旨,如果改索引列是非空的扮匠,那就走索引全掃描**
前面說了,同樣的SQL建的索引不同凡涩,就可能是走索引唯一性掃描棒搜,也有可能走索引范圍掃描。在同等的條件下活箕,索引范圍掃描所需要的邏輯讀和索引唯一性掃描對比力麸,邏輯讀如何?索引范圍掃描可能返回多條記錄育韩,所以優(yōu)化器為了確認(rèn)末盔,肯定會多掃描,所以在同等條件座慰,索引范圍掃描所需要的邏輯讀至少會比相應(yīng)的唯一性掃描的邏輯讀多1
5.3.5 索引全掃描(INDEX FULL SCAN)
- 索引全掃描(INDEX FULL SCAN)
索引全掃描(INDEX FULL SCAN)適用于所有類型的B樹索引(包括唯一性索引和非唯一性索引)陨舱。
索引全掃描過程簡述:索引全掃描是指掃描目標(biāo)索引所有葉子塊的索引行,但不意思著需要掃描所有的分支塊版仔,索引全掃描時只需要訪問必要的分支塊游盲,然后定位到位于改索引最左邊的葉子塊的第一行索引行,就可以利用改索引葉子塊之間的雙向指針鏈表蛮粮,從左往右依次順序掃描所有的葉子塊的索引行
5.3.6 索引快速全掃描(INDEX FAST FULL SCAN)
- 索引快速全掃描(INDEX FAST FULL SCAN)
索引快速全掃描和索引全掃描很類似益缎,也適用于所有類型的B樹索引(包括唯一性索引和非唯一性索引)。和索引全掃描類似然想,也是掃描所有葉子塊的索引行莺奔,這些都是索引快速全掃描和索引全掃描的相同點
索引快速全掃描和索引全掃描區(qū)別:
- 索引快速全掃描只適應(yīng)于CBO(基于成本的優(yōu)化器)
- 索引快速全掃描可以使用多塊讀,也可以并行執(zhí)行
- 索引全掃描會按照葉子塊排序返回变泄,而索引快速全掃描則是按照索引段內(nèi)存儲塊順序返回
- 索引快速全掃描的執(zhí)行結(jié)果不一定是有序的令哟,而索引全掃描的執(zhí)行結(jié)果是有序的,因為索引快速全掃描是根據(jù)索引行在磁盤的物理存儲順序來掃描的妨蛹,不是根據(jù)索引行的邏輯順序來掃描的
5.3.7 索引跳躍式掃描(INDEX SKIP SCAN)
- 索引跳躍式掃描(INDEX SKIP SCAN)
索引跳躍式掃描(INDEX SKIP SCAN)適用于所有類型的復(fù)合B樹索引(包括唯一性索引和非唯一性索引)屏富,索引跳躍式掃描可以使那些在where條件中沒有目標(biāo)索引的前導(dǎo)列指定查詢條件但是有索引的非前導(dǎo)列指定查詢條件的目標(biāo)SQL依然可以使用跳躍索引
如圖執(zhí)行計劃就有INDEX RANGE SCAN、 INDEX UNIQUE SCAN 等等
5.4 表連接方法
如圖蛙卤,執(zhí)行計劃中有如下NESTED LOOPS等等這些狠半,是什么噩死?這種其實就是Oracle中表連接的方法
兩個表之間的表連接方法有排序合并連接、嵌套循環(huán)連接神年、哈希連接已维、笛卡爾連接
排序合并連接(merge sort join)
merge sort join是先將關(guān)聯(lián)表的關(guān)聯(lián)列各自做排序,然后從各自的排序表中抽取數(shù)據(jù)已日,到另一個排序表中做匹配嵌套循環(huán)連接(Nested loop join)
Nested loops 工作方式是循環(huán)從一張表中讀取數(shù)據(jù)(驅(qū)動表outer table)垛耳,然后訪問另一張表(被查找表 inner table,通常有索引)。驅(qū)動表中的每一行與inner表中的相應(yīng)記錄JOIN捂敌。類似一個嵌套的循環(huán)艾扮。對于被連接的數(shù)據(jù)子集較小的情況,nested loop連接是個較好的選擇哈希連接(Hash join)
散列連接是CBO 做大數(shù)據(jù)集連接時的常用方式占婉,優(yōu)化器使用兩個表中較小的表(或數(shù)據(jù)源)利用連接鍵在內(nèi)存中建立散列表泡嘴,然后掃描較大的表并探測散列表,找出與散列表匹配的行逆济。笛卡爾連接(Cross join)
如果兩個表做表連接而沒有連接條件酌予,而會產(chǎn)生笛卡爾積,在實際工作中應(yīng)該盡可能避免笛卡爾積
對于這些連接的詳細(xì)介紹可以查看《收獲奖慌,不止sql調(diào)優(yōu)》一書抛虫,或者查看我做的讀書筆記
5.5 explain參數(shù)信息
前面的學(xué)習(xí),我們已經(jīng)知道了執(zhí)行計劃執(zhí)行的順序简僧、sql是做索引建椰,還是全表掃描,或者是rowid掃描岛马,但是如圖執(zhí)行計劃還有很多參數(shù)棉姐,如圖,比如Starts啦逆,E-Rows伞矩,Cost (%CPU)等等,這些參數(shù)表示什么含義夏志?
執(zhí)行計劃關(guān)鍵信息介紹:
- Starts:該SQL執(zhí)行的次數(shù)
- E-Rows:為執(zhí)行計劃預(yù)計的行數(shù)
- Cost (%CPU):CPU cost在整個cost中占的百分比
- A-Rows:實際返回的行數(shù)乃坤,E-Rows和A-Rows作比較,就可以看出具體那一步執(zhí)行計劃出問題了
- A-Time:每一步實際執(zhí)行的時間沟蔑,可以看出耗時的SQL
- Buffers:每一步實際執(zhí)行的邏輯讀或一致性讀