[Oracle] バインド変数を使っても必ずしも速くなるとは限らない!? | Archive Redo Blog

Archive Redo Blog

DBエンジニアのあれこれ備忘録

リテラルのみが異なるSQLを繰り返し実行する場合、バインド変数化するとSQLが共有されてパフォーマンスが向上するということはパフォーマンスチューニングにおける定説となっていますが、そうでないケースもまれに存在するのでしょうか? 

例えば、以下のようなSQL。

SELECT A.A_NAME, B.B_NAME, C.C_NAME, A.D_ID, D.D_NAME
   FROM TABLE_A A
  INNER JOIN TABLE_B B
    ON A.B_ID = B.B_ID
  INNER JOIN TABLE_C C
    ON B.C_ID = C.C_ID
  LEFT OUTER JOIN TABLE_D D
     ON A.D_ID = D.D_ID
  WHERE A.A_ID BETWEEN 101 AND 200
  ORDER BY A.A_ID DESC

これはTABLE_AをベースにTABLE_B、TABLE_C、TABLE_Dを結合してそれぞれのテーブルから必要なデータを取得するSQLなのですが、TABLE_Aが100万件程度、TABLE_Bが50万件程度と非常にデータ量が多いため、WHERE句でTABLE_AのIDをリテラルで範囲指定することによって100件ずつ取得しようとしています。

explain planを実行してこのSQLの実行計画を事前に確認してみると、

SQL> explain plan for
  2  SELECT A.A_ID, B.B_ID, C.C_NAME, A.D_ID, D.D_NAME
  3  FROM TABLE_A A
  4  INNER JOIN TABLE_B B
  5    ON A.B_ID = B.B_ID
  6  INNER JOIN TABLE_C C
  7    ON B.C_ID = C.C_ID
  8  LEFT OUTER JOIN TABLE_D D
  9    ON A.D_ID = D.D_ID
 10  WHERE A.A_ID BETWEEN 101 AND 200
 11  ORDER BY A.A_ID DESC
 12  /

解析されました。
SQL> @@c:\oracle\ora92\rdbms\admin\utlxpls.sql

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------


-------------------------------------------------------------------------------------
| Id  | Operation                       |  Name             | Rows  | Bytes | Cost  |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |                   |     1 |   155 |     9 |
|   1 |  NESTED LOOPS OUTER             |                   |     1 |   155 |     9 |
|   2 |   NESTED LOOPS                  |                   |     1 |    93 |     7 |
|   3 |    NESTED LOOPS                 |                   |     1 |    80 |     6 |
|   4 |     TABLE ACCESS BY INDEX ROWID | TABLE_A           |     1 |    73 |     4 |
|*  5 |      INDEX RANGE SCAN DESCENDING| PK_TABLE_A        |     1 |       |     3 |
|   6 |     TABLE ACCESS BY INDEX ROWID | TABLE_B           |     1 |     7 |     2 |
|*  7 |      INDEX UNIQUE SCAN          | PK_TABLE_B        |     1 |       |     1 |
|   8 |    TABLE ACCESS BY INDEX ROWID  | TABLE_C           |     1 |    13 |     1 |
|*  9 |     INDEX UNIQUE SCAN           | PK_TABLE_C        |     1 |       |       |
|  10 |   TABLE ACCESS BY INDEX ROWID   | TABLE_D           |     1 |    62 |     2 |
|* 11 |    INDEX RANGE SCAN             | PK_TABLE_D        |     1 |       |     1 |
-------------------------------------------------------------------------------------

すべてのテーブルのアクセスにインデックスが使用されており、オプティマイザが算出した取得コストは 9 と非常に低くなっています。

ただ、この範囲指定条件は都度変わるためバインド変数化すべきです。

そこで、このSQLの範囲指定条件部分をバインド変数化してexplain planを実行して実行計画を確認してみると、

SQL> explain plan for
  2  SELECT A.A_ID, B.B_ID, C.C_NAME, A.D_ID, D.D_NAME
  3  FROM TABLE_A A
  4  INNER JOIN TABLE_B B
  5    ON A.B_ID = B.B_ID
  6  INNER JOIN TABLE_C C
  7    ON B.C_ID = C.C_ID
  8  LEFT OUTER JOIN TABLE_D D
  9    ON A.D_ID = D.D_ID
 10  WHERE A.A_ID BETWEEN :b1 AND :b2
 11  ORDER BY A.A_ID DESC
 12  /

解析されました。
SQL> @@c:\oracle\ora92\rdbms\admin\utlxpls.sql


PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------


--------------------------------------------------------------------------------------
| Id  | Operation                        |  Name             | Rows  | Bytes | Cost  |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                   |  2070 |   313K|  5521 |
|   1 |  SORT ORDER BY                   |                   |  2070 |   313K|  5521 |
|*  2 |   FILTER                         |                   |       |       |       |
|*  3 |    HASH JOIN                     |                   |  2070 |   313K|  5470 |
|   4 |     TABLE ACCESS FULL            | TABLE_C           |    11 |   143 |     2 |
|   5 |     NESTED LOOPS                 |                   |  2070 |   287K|  5467 |
|*  6 |      HASH JOIN OUTER             |                   |  2070 |   272K|  1327 |
|   7 |       TABLE ACCESS BY INDEX ROWID| TABLE_A           |  2070 |   147K|  1322 |
|*  8 |        INDEX RANGE SCAN          | PK_TABLE_A        |  3726 |       |    20 |
|   9 |       TABLE ACCESS FULL          | TABLE_D           |  1193 | 73966 |     3 |
|  10 |      TABLE ACCESS BY INDEX ROWID | TABLE_B           |     1 |     7 |     2 |
|* 11 |       INDEX UNIQUE SCAN          | PK_TABLE_B        |     1 |       |     1 |
--------------------------------------------------------------------------------------

実行計画ががらりと変わり、オプティマイザが算出した取得コストは 5521 と劇的に増えてしまいました。

インデックス列を検索条件とした場合、オプティマイザはヒストグラムを参照してアクセスパスを決定します。

そのため、リテラルでごく狭い範囲を指定した場合にはインデックスが使用され、逆に広い範囲を指定した場合には全表走査が選択されやすくなるのですが、explain planを実行した場合は、バインド変数の検証が行われないため、実際にバインド変数に値をバインドして実行した時と異なる実行計画が提示されるようです。
(上記の例だと2,000件程度を検索するのに最適なアクセスパスということになるのでしょうか。)

つまり、上記のような結果が出たとしても、それはバインド変数化しても速くならないということを示すものではないということのようです。

バインド変数に値をバインドした時の実行計画は、実際にSQLを実行した後で確認する必要があるようです。