SQL*Plusの使いにくさは異常

主にOracleネタの検証ブログ。

INDEX SCANのフィルタ述語を検証する②

テーマ

前の記事から継続。
orcl.hatenablog.com
今回は索引第一列のカーディナリティを下げてみる。

準備

SQL> create table test2(col1 number not null ,col2 number not null);

Table created.

SQL> create index idx_test2 on test2(col1 ,col2);

Index created.

SQL> insert into test2 select mod(level ,10) + 1 ,1000001 - level from dual connect by level <= 1000000;

1000000 rows created.

SQL> select col1 ,count(*) from test2 group by col1 order by 1;

      COL1   COUNT(*)
---------- ----------
         1     100000
         2     100000
         3     100000
         4     100000
         5     100000
         6     100000
         7     100000
         8     100000
         9     100000
        10     100000

10 rows selected.

INSERT文が気持ち分かりにくいが要はCOL1を1~10の範囲に限定しているだけ。

検証1

とりあえず比較用に索引の第一列(COL1)全てを検索する条件。

SQL> select /*+ index(test2 idx_test2) */ count(1) from test2 where col1 >= 1;

  COUNT(1)
----------
   1000000


Execution Plan
----------------------------------------------------------
Plan hash value: 3780423660

-------------------------------------------------------------------------------
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |           |     1 |    13 |  2717   (1)| 00:00:33 |
|   1 |  SORT AGGREGATE   |           |     1 |    13 |            |          |
|*  2 |   INDEX RANGE SCAN| IDX_TEST2 |  1004K|    12M|  2717   (1)| 00:00:33 |
-------------------------------------------------------------------------------

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

   2 - access("COL1">=1)

Note
-----
   - dynamic sampling used for this statement (level=2)


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       2652  consistent gets
          0  physical reads
          0  redo size
        526  bytes sent via SQL*Net to client
        524  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

読み取りブロック数は2652。

検証2

索引第二列(COL2)を一意に絞る条件を追加。

SQL> select /*+ index(test2 idx_test2) */ count(1) from test2 where col1 >= 1 and col2 = 1;

  COUNT(1)
----------
         1


Execution Plan
----------------------------------------------------------
Plan hash value: 3780423660

-------------------------------------------------------------------------------
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |           |     1 |    26 |  2717   (1)| 00:00:33 |
|   1 |  SORT AGGREGATE   |           |     1 |    26 |            |          |
|*  2 |   INDEX RANGE SCAN| IDX_TEST2 |    21 |   546 |  2717   (1)| 00:00:33 |
-------------------------------------------------------------------------------

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

   2 - access("COL1">=1 AND "COL2"=1)
       filter("COL2"=1)

Note
-----
   - dynamic sampling used for this statement (level=2)


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       2652  consistent gets
          0  physical reads
          0  redo size
        526  bytes sent via SQL*Net to client
        524  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

RANGE SCANになって読み取りブロック数は検証1のときと同じ。
やはり前回の記事同様に「COL2=1」はfilterしかしてないように見える。
accessとfilter両方に出ている場合はもう実質filterだと思えば良いのか?

検証3

ヒント句でSKIP SCANを指定。

SQL> select /*+ index_ss(test2 idx_test2) */ count(1) from test2 where col1 >= 1 and col2 = 1;

  COUNT(1)
----------
         1


Execution Plan
----------------------------------------------------------
Plan hash value: 2609643709

------------------------------------------------------------------------------
| Id  | Operation        | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |           |     1 |    26 |    44  (14)| 00:00:01 |
|   1 |  SORT AGGREGATE  |           |     1 |    26 |            |          |
|*  2 |   INDEX SKIP SCAN| IDX_TEST2 |    21 |   546 |    44  (14)| 00:00:01 |
------------------------------------------------------------------------------

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

   2 - access("COL1">=1 AND "COL2"=1)
       filter("COL2"=1)

Note
-----
   - dynamic sampling used for this statement (level=2)


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
         17  consistent gets
          0  physical reads
          0  redo size
        526  bytes sent via SQL*Net to client
        524  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

述語はRANGE SCANのときと変わらないが読み取りブロックが劇的に減った。
索引の前の方の列のカーディナリティが低いとある程度効率的な読み取りが出来る様子。
なんとなくSKIP SCANの動きも分かったような気がする。

まとめ

accessとfilterで重複して述語が出ている場合は

  • RANGE SCANの場合は実質filterしかしていないと思って良さそう(多分)
  • SKIP SCANの場合はカーディナリティによってはaccessできそう(表現怪しい)

そもそもCHAR(2)くらいの種別コードやフラグ列なんかの索引でもないとSKIP SCAN自体採用されないのかもしれない。
↓こういう記事もあるし。
www.shift-the-oracle.com