図2●オプティマイザが作成する実行計画の例。一つひとつの四角が内部処理単位を表します
図2●オプティマイザが作成する実行計画の例。一つひとつの四角が内部処理単位を表します
[画像のクリックで拡大表示]
図3●Oracle付属のSQL*Plusで実行計画を表示したところ。画面下部のインデントは図2のツリーの親子関係を表します
図3●Oracle付属のSQL*Plusで実行計画を表示したところ。画面下部のインデントは図2のツリーの親子関係を表します
[画像のクリックで拡大表示]
表1●Oracleのルール・ベース・アプローチにおけるアクセス・パスのランク
表1●Oracleのルール・ベース・アプローチにおけるアクセス・パスのランク
[画像のクリックで拡大表示]

効率の良い実行計画を作成する

 次は,実行計画の作成です。こちらも例を挙げて説明したほうがわかりやすいでしょう。Oracleに付属するサンプルの従業員テーブル(emp)と部署テーブル(dept)から,従業員の一覧を取り出す以下のようなSQL文を実行するとします。

SELECT ename, job, sal, dname
FROM emp, dept
WHERE emp.deptno = dept.deptno

テーブルdeptでは部署番号deptnoが主キーで,インデックスpk_deptnoが定義されています。一方テーブルempでは,deptnoが外部キー*5になりますが,これに対してインデックスは定義されていません。

 オプティマイザは,このSQL文に対して(図2[拡大表示])のような実行計画を作成します。図のそれぞれの四角は,RDBMS内部での処理単位を表し,四角同士をつなぐ線がデータの流れを示しています。処理の流れは,次のようになります。

(1)テーブルempからレコードを一つ取り出す
(2)そのレコードのdeptnoをキーとしてインデックスpk_deptを検索する
(3)検索して得られたROWID*6を使ってテーブルdeptから対応するレコードを取り出す
(4)以上(1)~(3)をテーブルempの各レコードについて繰り返し実行して従業員名enameや部署名dnameなどのフィールドを取り出して出力する

 上記のSQL文を実行する方法には,ほかにもいろいろ考えられます。テーブルempとテーブルdeptの各行を総当たり的に組み合わせて一時的なテーブルを作成してから,その中で「emp.deptno = dept.deptno」という条件を満たすレコードを取り出す,という方法もあるでしょう。しかし,総当たり的に組み合わせたテーブルのレコード数は,empのレコード数×deptのレコード数になりますから,かなりの数になります。このテーブルのすべてのレコードに対して条件を満たすかどうかを一つひとつ調べていくより,先の方法のほうが高速に実行できるのは明らかでしょう。

 テーブルempとテーブルdeptの役割を逆にして,deptの各レコードについてempから「emp.deptno = dept.deptno」の条件を満たすレコードを取り出す,というのも一つの方法です。しかし,empにはdeptnoについてのインデックスが定義されていないため,条件を満たすレコードを取り出すためにはempのすべてのレコードを一つひとつ見ていかなくてはなりません。しかも,その処理をdeptのレコードの数だけ繰り返す必要があります。これも,やはり先ほどの方法よりも遅くなってしまいます。

 オプティマイザは,こうしたさまざまな実行計画の候補の中から最も効率の良い方法を選び出して,実行計画を作成するのです。

ツールを使って実際の実行計画を見てみよう

 OracleやSQL Serverは,SQL文を実行する際の実行計画を表示することができます。例えばOracleでは,付属ツールの「SQL*Plus」上でPL/SQLのEXPLAIN PLANステートメントを利用して,実行計画をテキストとして見ることができます。SQL Serverの場合は,付属ツールの「クエリ・アナライザ」でグラフィカルに表示することも可能です。ここでは,SQL*Plusを使って,Oracleが作成した実行計画を見ることにしましょう。

 実行計画を表示するには,EXPLAIN PLAN FORの後ろに計画を表示するSQL文を記述します。ただ,毎回記述するのは面倒です。開発環境で実行する場合はSQL*Plusのプロンプトで

set autotrace traceonly exlpain;

と入力して,SQL文を実行するたびにトレース結果として実行計画を表示するようにしたほうが簡単でしょう。作業が終わったら,

set autotrace off;

としてトレースをオフにします*7

 SQL*PlusでSQL文の実行計画を表示してみたのが(図3[拡大表示])です。「実行計画」とある部分の下のインデントされた文字列が,実行計画です。図2と同じ内容が表示されていることを確認できるでしょう。インデントは,それぞれの処理がその一つ上のレベルの下位に位置する(図2だと四角がすぐ下にあること)ことを表しています。先頭の「Optimizer = CHOOSE」は,オプティマイザとしてデフォルトの機能を使ったことを意味します。

実行計画の選択基準は大きく2通りある

 オプティマイザはさまざまな実行計画の候補の中からもっとも効率の良いものを選ぶと先に書きました。ではオプティマイザは,どのような基準で最適な実行計画を選ぶのでしょうか。これには,大きく分けて二つのやり方があります。一つは,ルール・ベース・アプローチ,もう一つはコスト・ベース・アプローチです。

 ルール・ベース・アプローチは,アクセス・パスの「ランク」に基づいて実行計画を選択します。ランクは,インデックスを使用してアクセスするかどうかなど,操作の種類によって決まる効率の度合いを表す数値です。Oracleの場合,アクセス・パスのランクは15に分かれています((表1[拡大表示]))。基本的にランクが上位のアクセス・パスの方が高速です。

 ランクの最上位に位置するアクセス・パスは,ROWIDによる単一行アクセス(ランク1)で,最下位のランクは全表走査(ランク15)となっています。オプティマイザは,SQL文のWHERE句の条件とインデックスの有無などから,使用可能なアクセス・パスを求め,その中でランクがもっとも上位のものを選択します。

 SQL文を変えると,使用可能なアクセス・パスは変化します。すなわち,オプティマイザによる処理結果を直接操作することはできませんが,SQL文を変更することで間接的に実行計画を変更できるのです。

 一方,コスト・ベース・アプローチでは,使用可能なアクセス・パスやアクセスするオブジェクト(表やインデックスなど)に関する「統計情報」を使用して,アクセス・パスの「コスト」を計算します。そして,コストがもっとも低くなるものを実行計画として選択します。

 ここで言うコストとは,処理に必要なリソースの消費量のことで,最も重要視されるのが処理に必要なディスク・アクセスの回数です。ほかにCPUの負荷やメモリーの使用量なども考慮されます。統計情報は,テーブルのレコード数や,フィールドの値の最大値/最小値などで,RDBMSがテーブル定義などの情報とともに管理しています。

 ルール・ベース・アプローチでは,ランクの上位にあるアクセス・パスを選択するのが基本です。アクセス・パスのランク付けは一般的な状況を前提にしているため,場合によっては遅いほうのパスを選択してしまうことがあります。例えば,

SELECT * FROM emp WHERE eno > 500

のような範囲検索の場合,表全体の中で取り出すレコードの割合が少なければ,インデックスを使って検索したほうが高速でしょう。一方,取り出すレコードの割合が大きければ全表走査のほうが高速になります。しかし,ルール・ベース・アプローチでは,常にインデックスによる検索を選択してしまいます。

 これに対してコスト・ベース・アプローチでは,キーenoの統計情報から得たenoの値の範囲を基に「eno > 500」を満たすレコードの割合を推測します。そのうえで,高速と思われるほうを選択するので,こうした問題は発生しにくくなります。

 したがって現在では,コスト・ベース・アプローチが主流になっています。例えば日本IBMのDB2はコスト・ベースのオプティマイザだけを実装しています。OracleやSQL Serverはどちらも実装していますから,状況に応じて使い分けることが可能です。ただし,コスト・ベース・アプローチでしか使えない機能もあるので,注意が必要です。


布目 綾子