天天看點

第三章 循規蹈矩——如何讀懂SQL執行計劃

參考《收獲,不止SQL優化》作者: 梁敬彬 / 梁敬弘

一、什麼是SQL執行計劃

以上SQL,是先通路T1表再通路T2表,還是先通路T2表再通路T1表?這兩種通路方式就是兩個不同的執行計劃。

那Oracle會如何選擇執行計劃呢?是看哪個執行開銷更低。開銷更低,意味着性能更好,速度更快。這個過程就是Oracle的解析過程,一般1S内可以完成。然後資料庫會把開銷更低的這個執行計劃儲存下來放到SGA的Shared Pool裡,後續如果再執行同樣的SQL,可能不用再解析,隻需要在Shared Pool裡擷取到執行計劃就行。前者是硬解析(Oracle進行解析生成執行計劃),後者是軟解析(不分析直接從Shared Pool裡擷取到執行計劃)。

二、SQL執行計劃標明依據

1、如何判斷哪個執行計劃開銷更低?主要是關注統計資訊。

第三章 循規蹈矩——如何讀懂SQL執行計劃

2、這些統計資訊是如何進行收集的呢?

Step1:設定空閑的時間,進行集中收集;

Step2:當表變動量很少,未超過設定的門檻值,不重新收集;

Step3:可手動進行收集,可隻針對某個表的分區、索引進行收集。

第三章 循規蹈矩——如何讀懂SQL執行計劃

3、如何手動進行統計資訊的收集?

-- 表相關統計資訊
SELECT T.TABLE_NAME, T.NUM_ROWS, T.BLOCKS, T.LAST_ANALYZED
  FROM USER_TABLES T
 WHERE T.TABLE_NAME IN ('TTRD_WMPS_UNIT');

-- 索引相關統計資訊
SELECT T.TABLE_NAME,T.INDEX_NAME,T.blevel,T.NUM_ROWS,T.LEAF_BLOCKS,T.LAST_ANALYZED
  FROM USER_INDEXES T
 WHERE T.TABLE_NAME IN ('TTRD_WMPS_UNIT');
 
 
-- 收集表/索引統計資訊
CALL dbms_stats.gather_table_stats(ownname => 'TRD_CAMS',tabname => 'TTRD_WMPS_UNIT',estimate_percent => 10,method_opt => 'for all indexed columns');

-- 收集索引統計資訊
CALL dbms_stats.gather_index_stats(ownname => 'TRD_CAMS',indname => 'PK_WMPS_UNIT',estimate_percent => 10,degree => 4 );

-- 收集表/索引統計資訊
CALL dbms_stats.gather_table_stats(ownname => 'TRD_CAMS',tabname => 'TTRD_WMPS_UNIT',estimate_percent => 10,method_opt => 'for all indexed columns',cascade => true);-- plsql工具執行報錯
exec dbms_stats.gather_table_stats(ownname => 'TRD_CAMS',tabname => 'TTRD_WMPS_UNIT',estimate_percent => 10,method_opt => 'for all indexed columns',cascade => true);-- 指令視窗執行成功

-- 分區表
SELECT COUNT(1) FROM REPORT_LD_ASSET_MANAGEUNDER ;
CALL dbms_stats.gather_table_stats(ownname => 'REGULATORY',tabname => 'REPORT_LD_ASSET_MANAGEUNDER',partname => 'P20220701',estimate_percent => 10,method_opt => 'for all indexed columns');

SELECT T.TABLE_NAME, T.NUM_ROWS, T.BLOCKS, T.LAST_ANALYZED
  FROM USER_TABLES T
 WHERE T.PARTITIONED = 'YES'
   AND T.TABLE_NAME IN ('REPORT_LD_ASSET_MANAGEUNDER');-- 更新

SELECT T.TABLE_NAME,T.INDEX_NAME,T.blevel,T.NUM_ROWS,T.LEAF_BLOCKS,T.LAST_ANALYZED
  FROM USER_INDEXES T
 WHERE T.TABLE_NAME IN ('REPORT_LD_ASSET_MANAGEUNDER');-- 更新

-- 隻更新一個分區   
select t.table_name,t.partition_name,t.num_rows,t.last_analyzed from user_tab_partitions t where table_name in ('REPORT_LD_ASSET_MANAGEUNDER');

           
第三章 循規蹈矩——如何讀懂SQL執行計劃

“- dynamic statistics used: dynamic sampling (level=2)”

如果執行計劃裡有如上資訊,表示動态采樣(一般USER_TABLES表對應的TABLE,行數塊數最後分析時間等為空,會動态采樣)。

比如表是早上9點建立的,建完以後再插入資料,Oracle是每天晚上12點動态收集統計資訊。那麼早9點到晚12點之間執行的SQL語句中包含此表的話,統計表中該TABLE對應的統計字段為空,就會動态收集資訊。且動态收集後并不會将統計資訊記錄下來,隻能等晚12點統一收集,或手動執行語句收集。

三、擷取執行計劃的六種方法

-- 方法一、EXPLAIN PLAN FOR 
set linesize 1000 
set pagesize 2000
-- 1
EXPLAIN PLAN FOR
SELECT * FROM TTRD_WMPS_UNIT WHERE STATUS='1';
-- 2
SELECT * FROM TABLE(dbms_xplan.display());


Plan hash value: 2172943561
 
-----------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name                | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |                     |     1 |  2241 |     1   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TTRD_WMPS_UNIT      |     1 |  2241 |     1   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | TTRD_WMPS_UNIT_IDX1 |     1 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - access("STATUS"=1)
 
Note
-----
   - dynamic statistics used: dynamic sampling (level=2)


-- 方法二、 set autotrace on
set linesize 266
set timing on
set pagesize 5000
SET AUTOTRACE OFF ---------------- 不生成AUTOTRACE 報告,這是預設模式
SET AUTOTRACE ON EXPLAIN ------ AUTOTRACE隻顯示優化器執行路徑報告
SET AUTOTRACE ON STATISTICS -- 隻顯示執行統計資訊
SET AUTOTRACE ON ----------------- 包含執行計劃和統計資訊
SET AUTOTRACE TRACEONLY ------ 同set autotrace on,但是不顯示查詢輸出

在ORACLE的SQLPLUS裡用
輸入指令回車就行了
create public synonym plan_table for plan_table;
grant all on plan_table to public;
conn itsm_sac_kaifa/itsm ;


SET AUTOTRACE ON/TRACEONLY
SELECT * FROM TTRD_WMPS_UNIT WHERE STATUS='1';

-- 方法三、statistics_level=all
-- 3.1
set pagesize 0
set linesize 1000
set AUTOTRACE off 

-- (1)
alter session set statistics_level=all ;
-- (2)、執行語句
SELECT * FROM T_SAMPLE_7 WHERE OBJECT_ID=9;
-- (3)、輸出
select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

-- set serveroutput off ;
-- 3.2,用/*+ gather plan statist cs */,省略statistics_level=all
set pagesize 0
set linesize 1000
-- (1)、執行語句
SELECT /*+ gather plan statist cs */* FROM T_SAMPLE_7 WHERE OBJECT_ID=20;
-- (2)、輸出
select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
 

-- 方法四、 dbms_xplan.display_cursor 擷取
-- 找到sql_id
select * from v$sql l where l.SQL_FULLTEXT like '%T_SAMPLE_7%' ORDER BY L.LAST_ACTIVE_TIME DESC;-- sql_id=3xmv0avhrqkt3

-- 4.1
-- 查詢sql_id對應的執行計劃
select * from table(dbms_xplan.display_cursor('3xmv0avhrqkt3')) ;

-- 輸出結果
SQL_ID  3xmv0avhrqkt3, child number 0 
-------------------------------------
SELECT * FROM T_SAMPLE_7 WHERE OBJECT_ID=9
 
Plan hash value: 1512084632
 
------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name                 | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |                      |       |       |     2 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T_SAMPLE_7           |     1 |   132 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | IDX_T_SAMPLE_7_OBJID |     1 |       |     1   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
 
   2 - access("OBJECT_ID"=9)
 

-- child number 0 表示第一個執行計劃,預設0,若有多個執行計劃,加參數即可
select * from table(dbms_xplan.display_cursor('3xmv0avhrqkt3',0)) ;-- 輸出執行計劃
select * from table(dbms_xplan.display_cursor('3xmv0avhrqkt3',1)) ;-- SQL_ID: 3xmv0avhrqkt3, child number: 1 cannot be found 

-- 4.2
-- 查詢sql_id對應的執行計劃(如果有AWR性能視圖裡有的話?)
select * from table(dbms_xplan.display_awr('3xmv0avhrqkt3')) ;


-- 方法五、事件 10046 trace 跟蹤
/* 
步驟 1: alter session set events '10046 trace name context forever,level 12 '; (開啟跟蹤)
步驟 2: 執行你的語旬
步驟 3: alter session set events '10046 trace name context off '; (關閉跟蹤)
步驟 4: 找到跟蹤後産生的檔案
步驟 5: tkprof trc檔案 目标檔案 sys=no sort=prsela,exeela,fchela (格式化指令)
*/

set autotrace off 
alter session set statistics_level=typical; 

-- 步驟 1: 開啟跟蹤
alter session set events '10046 trace name context forever,level 12 '; 

-- 步驟 2: 執行你的語旬
SELECT OBJECT_ID,OWNER,DATA_OBJECT_ID FROM T_SAMPLE_7 WHERE OBJECT_ID=20;

-- 步驟 3: 關閉跟蹤
alter session set events '10046 trace name context off ';

-- 步驟 4: 找到跟蹤後産生的檔案
-- 4.1、分别找到路徑和會話的spid,拼接而成
select a.spid from v$process a,v$session b where a.addr=b.paddr and b.audsid=userenv('sessionid'); -- 會話的spid=5668

-- 從11gR1開始,Oracle引入了新的診斷結構,以參數DIAGNOSTIC_DEST控制存放trace檔案與core檔案的路徑。
show parameter diagnostic_dest;-- 我本機是19C版本,查出來的路徑是'D:\app\qiying.li\',檢視發現檔案生成在'D:\app\qiying.li\diag\rdbms\orcl\orcl\trace\'下

-- 11gR1以前,如果是使用者程序,10046 trace檔案會被生成在user_dump_dest下,如果是背景程序,trace檔案會被生成在background_dump_dest下。
SQL> show parameter user_dump_dest ;-- 我本機是19C版本,查出來的路徑是'D:\05\19C\RDBMS\TRACE',但是找不到最新的trace檔案
SQL> show parameter background_dump_dest;

-- 4.2、直接用sql語句拼接好路徑和spid并輸出(11gR1以前,檔案路徑在參數'user_dump_dest';之後的版本直接這麼擷取可能路徑不對找不到檔案)
select a.value || '/' || b.instance_name || '_ora_' || c.spid || '.trc' trace_file
  from (select value from v$parameter where name = 'user_dump_dest') a,
       (select instance_name from v$instance) b,
       (select spid
          from v$process
         where addr =
               (select paddr
                  from v$session
                 where sid = (select distinct sid from v$mystat))) c;


EXIT;

-- 步驟 5: 格式化指令(orcl_ora_spid.trc,退出sql指令界面,直接cmd中執行以下指令即可)
tkprof D:\app\qiying.li\diag\rdbms\orcl\orcl\trace\orcl_ora_2584.trc  D:\10046.txt  sys=no sort=prsela,exeela,fchela


-- 方法六、awrsqrpt.sql
-- 參考《第一章 全局在胸——用工具對SQL整體優化》

           

方法一、EXPLAIN PLAN FOR

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

方法二、SET AUTOTRACE ON

SET AUTOTRACE ON

SET AUTOTRACE TRACEONLY – 傳回結果不輸出

雖然要先執行語句,再展示執行計劃,但這個執行計劃并不是真正的執行計劃,同EXPLAINPLAN FOR

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

方法三、statistics_level=all

3.1、statistics_level=all

第三章 循規蹈矩——如何讀懂SQL執行計劃

3.2、直接

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

方法四、dbms_xplan.display_cursor 擷取

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

方法五、事件10046 TRACE 跟蹤

這種方法的關鍵是找到存放trace檔案的路徑+該會話的spid值

具體方法和解讀可參考:https://blog.csdn.net/weixin_40913898/article/details/120622200

PARSE(SQL解析),EXEC(執行),FETCH(擷取資料)

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

方法六、awrsqrpt.sql

第三章 循規蹈矩——如何讀懂SQL執行計劃

四、這六種擷取執行計劃的方法的差異

什麼時候該用何種方法?

1、EXPLAIN PLAIN FOR

這種方法沒有真正去執行SQL,是最簡單的方法。主要看下通路順序、用到了什麼索引、關聯條件,同在PLSQL中按F5擷取到的資訊差不多。

2、SET AUTOTRACE ON/TRACEONLY

會去執行SQL,但是展示的不是真正的執行計劃(SET AUTOTRACE的執行計劃仍然來自于EXPLAIN PLAN),除了沒有STARTS/E-ROWS/A-ROWS其他資訊基本都有。

3、statistics_level=all

展示的是真正執行的執行計劃,如果想看STARTS/E-ROWS/A-ROWS這些資訊,用此方法。雖沒有實體讀,但有邏輯讀(buffers),邏輯讀才是重點。也沒有調用次數等資訊。(隻有此方法有STARTS/E-ROWS/A-ROWS這些資訊)

4、dbms_xplan.display_cursor 擷取

展示的是真正執行的執行計劃,但是是已經執行過的SQL,是以需要先找到SQL_ID。展示的資訊同EXPLAIN PLAN FOR差不多,但如果同一SQL(SQL_ID)有多個執行計劃,用此方法可以分别展示。(若非分析多個執行計劃,一般不用此方法,因為相關資訊3中也有了)

5、事件10046 TRACE 跟蹤

展示的是真正執行的執行計劃,此方法操作比較複雜,如果SQL中使用的函數中還有SQL,用此方法可以清晰列出。還可以看到SQL對應的等待事件。無STARTS/E-ROWS/A-ROWS這些資訊。

6、awrsqrpt.sql

展示的是真正執行的執行計劃,某段時間某個SQL(SQL_ID)的執行頻率和時間,若有多個執行計劃,也會一一列出。主要用來分析某段時間内的整體執行情況。

7、方法三和方法五的資訊結合,完全足夠分析SQL語句的真實執行計劃了。

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

五、 如何讀懂執行計劃

第三章 循規蹈矩——如何讀懂SQL執行計劃

【單獨型】

執行順序:3->2->1,父子關系

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

【聯合型】

1、非關聯

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

2、關聯型

2.1 關聯型(NL)

情況1:

ID=2處傳回10行(A-Rows),Id=3處DEPT表被通路10次(Starts=10),驅動表和被驅動表之間有關聯關系。

第三章 循規蹈矩——如何讀懂SQL執行計劃

情況2:

以下SQL也有關聯關系,但是Id=2處傳回14886行,Id=3處為什麼表隻通路了38次?不應該是14886行?

第三章 循規蹈矩——如何讀懂SQL執行計劃

原因如下:

驅動表的P_CLASS與被驅動表關聯,而驅動表過濾後,P_CLASS隻有38條不重複的值。驅動表傳回多少條不重複記錄,被驅動表就被通路多少次。這是Oracle的一種優化。TTRD_P_CLASS表通路了38次,隻34行記錄能比對到。

網上說明:

正常的NESTED LOOPS中,被驅動表的執行次數為驅動表的結果集行數。

對于NESTED LOOPS ANTI/SEMI的反連接配接和半連接配接,從探究實驗的結果看,被驅動表的通路次數為驅動表的連接配接條件的去重後的值。

第三章 循規蹈矩——如何讀懂SQL執行計劃

情況3:

NESTED LOOPS,被驅動表的執行次數為驅動表的結果集行數。

以下第一張圖,no_unnest 好像少了一杠,是以還是NL連接配接

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

2.2 關聯型(FILTER)

書中原句“FILTER 其實對比 NESTED LOOPS 是一種優化,驅動表傳回多少條不重複記錄,被驅動表被通路多少次”。

第三章 循規蹈矩——如何讀懂SQL執行計劃

2.3、關聯型( UPDATE )

分析:

ID=2, EMP全表掃描,通路1次,得到14條記錄

ID=4, EMP全表掃描,通路3次(不同的deptno有3個),總共得到14條記錄

ID=3, ID=4處得到的結果通路三次,做三次彙總計算,得到3條記錄

ID=6, EMP全表掃描,通路1次,得到14條記錄

ID=5, ID=6處得到的結果通路1次,全部資料做一次彙總,得到1條記錄

ID=1,UPDATE

第三章 循規蹈矩——如何讀懂SQL執行計劃

2.4、關聯型(CONNECT BY WITH FLITERING)(樹形查詢)

為什麼ID=5(CONNECT BY PUMP)處,Starts=4?

因為此樹結構為4層:

第1次通路是PARENT_ID IS NULL

第2次通路是PRIOR NODE_ID(PARENT_ID IS NULL對應的NODE_ID) = PARENT_ID

……

第三章 循規蹈矩——如何讀懂SQL執行計劃

“TABLE ACCESS BY INDEX ROWID BATCHED”

網上說明:

Oracle 12c中新增通過ROWID BATCHED通路資料塊的方式,優化原來使用單個rowid進行資料塊通路方式帶來的資源消耗及對資料塊的掃描次數,即TABLE ACCESS BY INDEX ROWID BATCHED特性。該特性通過隐藏參數“ _optimizer_batch_table_access_by_rowid ”控制,預設值為 true ,即預設開啟。 資料庫将對從索引中檢索的rowid進行排序,然後按塊順序通路行,進而減少資料庫必須通路的塊的次數以降低資源的消耗。

六、 從執行計劃中判斷出問題

第三章 循規蹈矩——如何讀懂SQL執行計劃

除了預測錯外。 如果在執行計劃中有 “COUNT STOPKEY” 關鍵字 ,還可能是rownum分頁查詢的執行計劃,表示在第10行就停止前進了。

第三章 循規蹈矩——如何讀懂SQL執行計劃

七、問題

問題1:表是很早建的,索引是新加的,索引還未有統計資訊,此時執行的SQL會動态收集嗎?

加索引的時候就會統計一次目前資訊,索引有統計資訊,問題不成立。而且SQL是否會動态收集,是看表是否有統計資訊,而不是索引是否有統計資訊。(加索引是否會統計一次目前資訊?不一定,不同的庫測出來結果不同)

問題2:本地庫測試,建表建索引後,自動就搜集了統計資訊,是否跟版本或參數有關?

應該無關。

CREATE TABLE A AS SELECT * FROM TABLE B;-- 用此方法建表,表會自動搜集統計資訊。

CREATE TABLE A();

INSERT INTO A SELECT * FROM B; – 用此方法建表,表不會自動搜集統計資訊。

建索引時都會搜集一次,資料為空則統計資訊為0,資料不為空則有具體的行數等資訊。

問題3:FILTER、NESTED LOOPS ANTI/SEMI、NESTED LOOPS的差別

NESTED LOOPS:NL連接配接,驅動表的傳回行數=被驅動表的通路次數

NESTED LOOPS SEMI:半連接配接,一般用于IN , EXISTS,這種操作join時候,通常查找到一條紀錄就可以了,是以被驅動表關聯字段去重行數=被驅動表的通路次數

NESTED LOOPS ANTI:反連接配接,一般用于NOT IN ,NOT EXISTS

FILTER:filter會維護一張hash table(有緩存提高效率),也是被驅動表關聯字段去重行數=被驅動表的通路次數,更多參考:https://www.modb.pro/db/383452

NESTED LOOPS ANTI/ FILTER:差别不大,filter本身的算法和nest loop差别不太大,隻是内部構造了hash table加快查找,是以走相類似的執行計劃(NESTED LOOPS ANTI)的時候效率差别不會特别大

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

NESTED LOOPS/ FILTER:此處FILTER更優

第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃
第三章 循規蹈矩——如何讀懂SQL執行計劃

問題4:如何将NESTED LOOPS改成 FILTER?

/+no_unnest/ 但性能不一定提高

問題5:請總結一下,如何從執行計劃中判斷出問題

解題思路:先了解執行計劃有哪幾個大類的資訊,再按大類進行分析。

1、從predicate information(謂語資訊)中看:關注filter部分,這部分的條件字段用不到索引,特别需要注意是否有類型轉換函數。

2、從統計資訊看:觀察是否有排序(記憶體/磁盤),能否避免排序;是否有遞歸調用等。

3、從執行計劃清單看:

觀察A-Rows和E-Rows差别是否很大,如果很大,則執行計劃的準确性很讓人懷疑,是否統計資訊有問題(當然也不排序執行計劃沒有問題,比如用到了分頁,預計傳回行數很多,實際分頁結束就停止了,這種事正常的);

觀察A-Rows和Buffers(邏輯讀)差别是否很大,如果實際傳回是1,邏輯讀很大,考慮是否需要建索引;

觀察Starts是否很大,如果很大,看是否hash連接配接,兩大表(傳回結果均很多)關聯一般hash連接配接比nl連接配接高效。