忘れかけのIT備忘録

今まで学んできた知識や小技、なるほど!と思ったことをメモするブログです。

Oracleの実行計画(全表スキャンと索引スキャン)

今回はOracleの実行計画について調査、検証しました。

実行計画とは
SQLの実行手順(データへのアクセスパス、テーブルの結合方法、テーブルの結合順序の組み合わせから構成される)
オプティマイザ(CBO)が実行コストを基に作成する

【補足】
CBO(コストベースオプティマイザ)
統計情報(オプティマイザ統計)のデータを元にSQLの実行コストが最も低くなるように実行計画を作成する機能(アルゴリズム)
SQLの実行手順が複数存在する場合、複数の実行コストを比較して最適な実行手順を選択する
※統計情報が古いと適切な実行計画が作成・選択されない可能性があるため、統計情報は定期的に取得することをお勧めします
※実行コスト:処理に必要な見積もり時間、見積もりリソース使用量など
なお、10gまではRBO(ルールベースオプティマイザ)が標準でした
私は未経験ですが、RBOはあらかじめ設定されているルールを元にSQLの実行手順を作成・選択する機能(アルゴリズム)です
あらかじめアクセスパスをランキング形式で用意し、アクセスパスが複数候補に挙がった場合、ランクの高い方を採用する方法です
詳細はコチラ(https://www.oracle.com/jp/a/tech/docs/technical-resources/index-tuning1.pdf)※22ページ参照

アクセスパス
データベースからデータを取得するルート
・表の全レコードを取得する場合(フルスキャン)
・索引からレコードを特定して、レコードを取得する場合(索引スキャン)
など

テーブルの結合方法
複数の表を結合する方法
主に4種類ある
ネステッドループ結合、ソートマージ結合、ハッシュ結合、直積結合

テーブルの結合順序
複数の表を結合するときの表の結合順序
3つ以上の表を結合する場合、先ず2つの表を結合し、その結果と3つ目の表を結合する
すべての表が結合されて結果が生成されるまでこれを繰り返す

実行計画を読んでみる
実行計画はツリー構造になっており、各行には親子関係があります。
ステップの一番上から親ステップ→子ステップへ遷移し、子ステップの処理の実行が完了したら親ステップへ遷移し、親ステップの処理を実行する、というのが基本動作になります。
ただし、同じインデントの子ステップが複数ある場合は注意が必要です。
とりあえずサンプルを読んでみます。

SQLに付与される識別子。13桁の文字列で管理される。MD5で算出されたハッシュ値の末尾8バイト
② 選択された子カーソル(実行計画)番号
③ 実行SQL
④ 実行計画の識別子
⑤ 実行手順。1行1行をステップと呼ぶ(赤枠)。ステップには親子関係がある
   Id:オペレーションID
   Operation:オペレーション内容(行ソース操作)
   Name:オペレーション対象のオブジェクト名
   Rows:オペレーションでアクセスされる行数
   Bytes:オペレーションでアクセスされるサイズ(バイト数)
   Cost (%CPU):オペレーションに対するオプティマイザが見積もった実行コスト
   Time:オペレーションに要するオプティマイザが見積もった時間
⑥ 述語(補足情報)。オペレーションで適用された検索条件など

サンプルだと実行順序は「3 → 2 → 5 → 4 → 1 → 0」となります。
実行順序① Id3(INDEX UNIQUE SCAN):検索条件(DEPT表のDEPTNO=10)にヒットする1行を特定
実行順序② Id2(TABLE ACCESS BY INDEX ROWID):インデックス(PK_DEPT)で特定した列データを基にテーブルからデータを取得
実行順序③ Id5(INDEX RANGE SCAN):検索条件(EMP表のDEPTNO=10)にヒットする複数行(サンプルでは3行)を特定
実行順序④ Id4(TABLE ACCESS BY INDEX ROWID):インデックス(IDX1_EMP_DEPTNO)で特定した列データを基にテーブルからデータをまとめて取得
実行順序⑤ Id1(NESTED LOOPS):DEPT表とEMP表から取得したデータを結合
実行順序⑥ Id0(SELECT STATEMENT):検索結果を返す

整理すると・・・
ポイント① 親ステップに同じインデントの子ステップが複数ある場合、一番上の子ステップへ遷移する(サンプルだとId2とId4が同じインデントの子ステップに該当するが、一番上のId2へ遷移する)
ポイント② その子ステップにさらに子ステップがある場合、現在到達している子ステップが親ステップになり、子ステップへ遷移する(サンプルだとId2が親ステップになり、Id3が子ステップになる。Id2へ遷移する)
ポイント③ 遷移した子ステップに子ステップがない場合(一番下の子ステップに到達した場合)、処理を実行する(サンプルだとId3)
ポイント④ 子ステップの処理完了後、親ステップへ遷移し、親ステップの処理を実行する(サンプルだとId2)
ポイント⑤ 親ステップの処理完了後、他に子ステップがある場合、その子ステップへ遷移後、③へ(Id2に他の子ステップがある場合、その子ステップへ遷移するがサンプルだとId2に他の子ステップはない)
           他に子ステップがない場合、その親ステップへ遷移する(サンプルだとId2からId1へ遷移する)
           他に子ステップがないが同じインデントの子ステップが残っている場合、その子ステップへ遷移後、②へ(サンプルだとId4へ遷移し、さらに子ステップのId5へ遷移・実行)
           他に子ステップがない・同じインデントの子ステップが残っていない場合、その親ステップへ遷移し、処理を実行する。これをツリーの一番上まで繰り返す(サンプルだとId1実行後、Id0実行)

【補足】
サンプルの実行計画はDBMS_XPLAN.DISPLAY_CURSOR()で出力したため、Rows、Bytes、Cost (%CPU)、Timeは実績ではなく、オプティマイザが見積もった統計値
DBMS_XPLAN.DISPLAY_CURSOR()で実行計画を出力した場合、実行計画は過去に実際に使用されたものが出力されるが、各統計値はオプティマイザが見積もったものになる
DBMS_XPLAN.DISPLAY_CURSOR()のformatパラメータに「'ALL ALLSTATS LAST'」を指定した場合、実行計画は実績、統計値は見積もりと実績の両方が出力される
※STATISTICS_LEVEL初期化パラメータを「ALL」に設定する必要がある
(例)'ALL ALLSTATS LAST'ありの場合

データへアクセスする際、よく見るオペレーションのパターンを検証しました。

■検証環境
OS:Oracle Linux 6.5
DB/GI:Oracle Database 12c Release 1 (12.1.0.2.0) Enterprise Edition
※2ノードRAC(管理者管理型DB)

■前提
・索引はBtreeインデックス
・実行計画はDBMS_XPLAN.DISPLAY_CURSOR()のformatパラメータに「'ALL ALLSTATS LAST'」は指定せずに取得

■検証パターン
①全表スキャン(TABLE ACCESS FULL)
②ROWIDスキャン(TABLE ACCESS BY USER ROWID)
③索引一意スキャン(INDEX UNIQUE SCAN)
④索引範囲スキャン(INDEX RANGE SCAN)
⑤索引スキップスキャン(INDEX SKIP SCAN)
⑥索引フルスキャン(INDEX FULL SCAN)
⑦索引高速フルスキャン(INDEX FAST FULL SCAN)

■検証
①全表スキャン(TABLE ACCESS FULL)
索引を使用せず、表(表ブロック)全体を読み込む(表フルスキャン)
表を読み込む際、HWM(High Water Mark)まで読み込む
db_file_multiblock_read_count初期化パラメータで設定された値のブロックをまとめて読み込む(マルチブロックリード
※表がパーティション化されている場合、オペレーションが「TABLE ACCESS FULL」となる場合があるが表全体ではなく、特定のパーティションを読み込んでいる可能性もある

【発生条件】
・検索条件を指定しない
・検索条件で指定する列に索引がない

【ヒント句】
/*+ FULL(表別名) */

【検証手順】
1. SELECT(全表スキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(全表スキャン)を実行
SQL> SELECT /* XPLAN_TEST1 */ * FROM EMP WHERE JOB = 'CLERK';

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
      7876 ADAMS           CLERK                 7788 1987-05-23 00:00:00       1100                    20
      7900 JAMES           CLERK                 7698 1981-12-03 00:00:00        950                    30
      7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    10

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('1gq2u2fah1sgg'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  1gq2u2fah1sgg, child number 0
-------------------------------------
SELECT /* XPLAN_TEST1 */ * FROM EMP WHERE JOB = 'CLERK'

Plan hash value: 3956160932

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |       |       |     2 (100)|          |
|*  1 |  TABLE ACCESS FULL| EMP  |     4 |   348 |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("JOB"='CLERK')

 

【実行順序】
1→0

②ROWIDスキャン(TABLE ACCESS BY USER ROWID)
索引は使用せず、レコードのROWIDを指定して、1件のレコードを読み込む
ROWIDを指定することでデータファイル、ブロック、レコードの位置が分かるため、1件のレコードを最も高速に読み込める

【発生条件】
・検索条件でROWIDを指定する

【ヒント句】
なし

【検証手順】
1. SELECT(ROWIDスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(ROWIDスキャン)を実行
SQL> --各レコードのROWID確認
SQL> SELECT /* XPLAN_TEST2 */ ROWID, EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO FROM EMP;

ROWID                   EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
------------------ ---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
AAAW4+AABAAAYTpAAA       7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
AAAW4+AABAAAYTpAAB       7499 ALLEN           SALESMAN              7698 1981-02-20 00:00:00       1600        300         30
AAAW4+AABAAAYTpAAC       7521 WARD            SALESMAN              7698 1981-02-22 00:00:00       1250        500         30
AAAW4+AABAAAYTpAAD       7566 JONES           MANAGER               7839 1981-04-02 00:00:00       2975                    20
AAAW4+AABAAAYTpAAE       7654 MARTIN          SALESMAN              7698 1981-09-28 00:00:00       1250       1400         30
AAAW4+AABAAAYTpAAF       7698 BLAKE           MANAGER               7839 1981-05-01 00:00:00       2850                    30
AAAW4+AABAAAYTpAAG       7782 CLARK           MANAGER               7839 1981-06-09 00:00:00       2450                    10
AAAW4+AABAAAYTpAAH       7788 SCOTT           ANALYST               7566 1987-04-19 00:00:00       3000                    20
AAAW4+AABAAAYTpAAI       7839 KING            PRESIDENT                  1981-11-17 00:00:00       5000                    10
AAAW4+AABAAAYTpAAJ       7844 TURNER          SALESMAN              7698 1981-09-08 00:00:00       1500          0         30
AAAW4+AABAAAYTpAAK       7876 ADAMS           CLERK                 7788 1987-05-23 00:00:00       1100                    20
AAAW4+AABAAAYTpAAL       7900 JAMES           CLERK                 7698 1981-12-03 00:00:00        950                    30
AAAW4+AABAAAYTpAAM       7902 FORD            ANALYST               7566 1981-12-03 00:00:00       3000                    20
AAAW4+AABAAAYTpAAN       7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    10

14行が選択されました。

SQL> --ROWIDスキャン
SQL> SELECT /* XPLAN_TEST2 */ * FROM EMP where rowid ='AAAW4+AABAAAYTpAAA';

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('c1f0h5kfg9vgx'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  c1f0h5kfg9vgx, child number 0
-------------------------------------
SELECT /* XPLAN_TEST2 */ * FROM EMP where rowid ='AAAW4+AABAAAYTpAAA'

Plan hash value: 1116584662

-----------------------------------------------------------------------------------
| Id  | Operation                  | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |      |       |       |     1 (100)|          |
|   1 |  TABLE ACCESS BY USER ROWID| EMP  |     1 |    99 |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

 

【実行順序】
1→0

③索引一意スキャン(INDEX UNIQUE SCAN)
検索条件に指定した列に一意制約または主キー制約が設定された索引を使用して、データを読み込む
リーフブロック内の1つのROWIDをもとにデータを参照するため、検索結果は常に「検索結果 ≦ 1 」になる(検索条件に該当する1or0エントリを返す)
なお、SQLが索引列のみ参照している場合、表データは参照せず、リーフブロック内の列データを返す場合もある(カバーリングインデックス)

【補足】
カバーリングインデックス
インデックスに参照する列データがすべて含まれている状態。インデックスブロック内で情報取得が完結するため、テーブルアクセス不要

【発生条件】
・検索条件で指定する列に索引(一意制約または主キー制約)が設定されている

【ヒント句】
なし
/*+ INDEX(表別名 索引名) */ で索引を指定しても良いと思います

【検証手順】
1. SELECT(索引一意スキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引一意スキャン)を実行
SQL> SELECT /* XPLAN_TEST3 */ * FROM EMP WHERE EMPNO = 7369;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('91kz6rf00jf72'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  91kz6rf00jf72, child number 0
-------------------------------------
SELECT /* XPLAN_TEST3 */ * FROM EMP WHERE EMPNO = 7369

Plan hash value: 2949544139

--------------------------------------------------------------------------------------
| Id  | Operation                   | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |        |       |       |     1 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID| EMP    |     1 |    87 |     1   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | PK_EMP |     1 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("EMPNO"=7369)

※TABLE ACCESS BY INDEX ROWIDはテーブルのROWIDを使用して表のレコードへアクセスするオペレーション

 

【実行順序】
2→1→0

④索引範囲スキャン(INDEX RANGE SCAN)
検索条件に指定した列に一意制約または主キー制約が設定されていない索引を使用して、データを読み込む
リーフブロック内の1つ以上のROWIDをもとにデータを参照するため、検索結果は常に「検索結果 ≧ 0 」になる(キー値の範囲でリーフブロックをスキャンして、検索条件に該当する複数エントリを返す)
③同様、SQLが索引列のみ参照している場合、表データは参照せず、リーフブロック内の列データを返す場合もある

【発生条件】
・検索条件で指定する列に索引(一意制約または主キー制約以外)が設定されている

【ヒント句】
なし
/*+ INDEX(表別名 索引名) */ でインデックスを指定しても良いと思います

【検証手順】
1. SELECT(索引範囲スキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引範囲スキャン)を実行
SQL> SELECT /* XPLAN_TEST4 */ * FROM EMP WHERE DEPTNO = 10;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7782 CLARK           MANAGER               7839 1981-06-09 00:00:00       2450                    10
      7839 KING            PRESIDENT                  1981-11-17 00:00:00       5000                    10
      7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    10

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('dmpqvjxhsp6y2'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  dmpqvjxhsp6y2, child number 0
-------------------------------------
SELECT /* XPLAN_TEST4 */ * FROM EMP WHERE DEPTNO = 10

Plan hash value: 746399115

----------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |              |       |       |     1 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| EMP          |     3 |   261 |     1   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | IDX1_EMP_DNO |     3 |       |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("DEPTNO"=10)

※TABLE ACCESS BY INDEX ROWID BATCHEDはテーブルのROWIDを使用して表のレコードへアクセスする際、複数ブロックをまとめて読み込むオペレーション(12c~)
読み込む複数ブロックが連接している場合:マルチブロックリード(db file scattered read)
読み込む複数ブロックが連接していない:パラレルリード(db file parallel read)

 

【実行順序】
2→1→0

⑤索引スキップスキャン(INDEX SKIP SCAN)
検索条件に指定した列に複合索引(コンポジット索引)が設定されているが第1キー以外の索引を使用して、データを読み込む
複合索引の第1キーのNDVが小さい(第1列は「男性」・「女性」の2種類など)場合、採用される可能性が高くなる
Oracle内部では第1キーの使用をスキップした上で第2キー以降を使用する(INDEX RANGE SCAN)となるため、最初からINDEX RANGE SCANを使用するパターンと比べると効率が悪いそうです。

【発生条件】
・検索条件で指定する列に複合索引が設定されている(WHERE句は第1キー以外の列を指定する)
・検索条件で指定する列に複合索引の第1キーの列が表の先頭列にない

【ヒント句】
/*+ INDEX_SS(表別名) */
/*+ INDEX_SS(表別名 索引名) */

【検証手順】
1. SELECT(索引スキップスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引スキップスキャン)を実行
SQL> SELECT /* XPLAN_TEST5 */ * FROM EMP WHERE EMPNO = 7369;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    20

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('4vtjmw4fv7q32'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  4vtjmw4fv7q32, child number 0
-------------------------------------
SELECT /* XPLAN_TEST5 */ * FROM EMP WHERE EMPNO = 7369

Plan hash value: 178841609

-----------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |               |       |       |     2 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| EMP           |     3 |   114 |     2   (0)| 00:00:01 |
|*  2 |   INDEX SKIP SCAN                   | IDX1_EMP_COMP |     3 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("EMPNO"=7369)
       filter("EMPNO"=7369)

 

【実行順序】
2→1→0

⑥索引フルスキャン(INDEX FULL SCAN)
リーフブロック全体をフルスキャン(シングルブロックリード)して索引の順番どおりに読み込む
SQLで参照する列がすべて索引に含まれている必要があるが、検索条件に索引列を指定する必要はない
SQLで参照する列がすべて索引列でソート処理(ORDER BY句など)が必要な場合、採用される可能性が高くなる
リーフブロック内の索引キー値でソートされた順にエントリを返すため、索引ブロックへのアクセスだけで要件が満たせる(SQLで参照する列が取得できる)場合、表ブロックアクセスおよびソート処理は省略できる可能性がある

【発生条件】
SQLで参照される列がすべて索引に含まれている
SQLで参照されるいずれかの列にNOT NULL制約がある
・ソート処理(ORDER BY句など)のキーにNOT NULL制約がある列を指定している

【ヒント句】
なし
/*+ INDEX(表別名 索引名) */ でインデックスを指定しても良いと思います

【検証手順】
1. SELECT(索引フルスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引フルスキャン)を実行
SQL> SELECT /* XPLAN_TEST6 */ EMPNO, DEPTNO FROM EMP order by EMPNO, DEPTNO;

     EMPNO     DEPTNO
---------- ----------
      7369         20
      7499         30
      7521         30
      7566         20
      7654         30
      7698         30
      7782         10
      7788         20
      7839         10
      7844         30
      7876         20
      7900         30
      7902         20
      7934         10

14行が選択されました。

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('9kxmsjcmgrvc3'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  9kxmsjcmgrvc3, child number 0
-------------------------------------
SELECT /* XPLAN_TEST6 */ EMPNO, DEPTNO FROM EMP order by EMPNO, DEPTNO

Plan hash value: 150391907

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       |     3 (100)|          |
|   1 |  SORT ORDER BY     |      |    14 |   364 |     3  (34)| 00:00:01 |
|   2 |   TABLE ACCESS FULL| EMP  |    14 |   364 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------

 

【実行順序】
1→0

⑦索引高速フルスキャン(INDEX FAST FULL SCAN)
索引ブロック全体をフルスキャン(マルチブロックリード)してディスクに保存されている順番どおりに読み込む
SQLで参照する列がすべて索引に含まれている必要があるが、検索条件に索引列を指定する必要はない(索引フルスキャンと同じ)
索引フルスキャンと似ているが、索引高速フルスキャンはソート処理を回避できない
索引フルスキャンは索引キーがソートされた状態のリーフブロックをシングルブロックリードするためソート処理を回避できるが、索引高速フルスキャンはツリー構造を意識せず索引ブロックをセグメントヘッダからマルチブロックリードするため、取得するデータを索引キーでソートできない
索引高速フルスキャンはマルチブロックリードやパラレル処理も可能なため、一般的に索引フルスキャンに比べて高速

【発生条件】
SQLで参照される列がすべて索引に含まれている
SQLで参照されるいずれかの列にNOT NULL制約がある

【ヒント句】
/*+ INDEX_FFS(表別名 索引名) */

【検証手順】
1. SELECT(索引高速フルスキャン)を実行
2. 実行計画を確認

【作業ログ】

1. SELECT(索引高速フルスキャン)を実行
SQL> SELECT /* XPLAN_TEST7 */ EMPNO FROM EMP;

     EMPNO
----------
      7369
      7369
      7369
(略)
      7934
      7934
      7934

1400行が選択されました。

2. 実行計画を確認
SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('can7v3mzd0sqd'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  can7v3mzd0sqd, child number 0
-------------------------------------
SELECT /* XPLAN_TEST7 */ EMPNO FROM EMP

Plan hash value: 3993716296

---------------------------------------------------------------------------------
| Id  | Operation            | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |          |       |       |     4 (100)|          |
|   1 |  INDEX FAST FULL SCAN| IDX1_EMP |  1400 | 18200 |     4   (0)| 00:00:01 |
---------------------------------------------------------------------------------

 

【実行順序】
1→0

■参考資料
実行計画の生成と表示
オプティマイザのアクセス・パス
コメント
https://blogs.oracle.com/otnjp/post/tsushima-hakushi-4
https://blogs.oracle.com/otnjp/post/tsushima-hakushi-21
https://www.oracle.com/webfolder/technetwork/jp/ondemand/ddd2013/A-1.pdf
https://www.oracle.com/jp/a/tech/docs/technical-resources/100811-sql-tuning.pdf
https://www.oracle.com/jp/a/tech/docs/technical-resources/index-tuning1.pdf
Oracle SQL実行計画の読み方 | コーソルDatabaseエンジニアのBlog
パフォーマンス改善と事前対策に役立つ Oracle SQLチューニング 本気で学ぶ実践的な考え方とテクニック
オラクルマスター教科書 ORACLE MASTER Expert パフォーマンス・チューニング編

■おわりに
記事内の各種オペレーションの発生条件はあくまでも書籍やサイト、実機検証で確認した条件ですが、必ず該当オペレーションが発生するわけではありません(どのオペレーションを採用するかは最終的にオプティマイザが判断します)
ヒント句を使用すれば採用されるオペレーションを狙いやすくなりますが、今回はヒント句を使用しない状態で検証したため、索引スキップスキャンや索引フルスキャン、索引高速スキャンが出たり出なかったりで苦労しました。