図1●リレーショナル・データベース管理システム(RDBMS)が,受け取ったSQL文を実行するまでの処理の流れ
図1●リレーショナル・データベース管理システム(RDBMS)が,受け取ったSQL文を実行するまでの処理の流れ
[画像のクリックで拡大表示]

 SQL文を記述してデータベースを操作することはそれほど難しいことではありません。しかし,リレーショナル・データベース管理システム(RDBMS)が問い合わせを実行する速度は,SQL文の書き方によって大きく異なります。ちょっとした記述の違いによって,応答時間が何倍も違うことはめずらしくありません。

 では,速いSQL文を書けるようになるためには,どうすればいいのでしょうか。その答えは,「RDBMSがSQL文を内部でどのように処理しているのか」を理解することです。RDBMSは,プログラマが記述したSQL文を基にさまざまな処理を行ってから実際にデータベースにアクセスします。その過程を知ることで,アクセスのしかたをコントロールできるようになるのです。

 例えば,CUSTOMERS(顧客)テーブルからNAME(名前)を抽出する「SELECT NAME FROM CUSTOMERS」という簡単なSQL文であっても,RDBMS内部ではさまざまな処理を経て結果を返します。こうした仕組みを理解することで,ただ結果を求めるだけのSQL文を書くのではなく,「目的に応じて適切なSQL文を書く」という発想が生まれてくるのではないかと思います。

 Part1では,RDBMSがSQL文を内部でどのように処理しているのかを,最適化の話を中心に解説します。あわせて,速いSQL文を書くためのポイントも紹介しましょう。アプリケーション開発やデータベース設計など,さまざまな場面で活用していただけたら幸いです。

SQL文は実行までに三つの過程を経る

 最初に,RDBMSがSQL文をアプリケーションから受け取ってから実行するまでの流れを眺めてみましょう。大きく,(1)SQL文の解析,(2)SQL文の書き換え,(3)実行計画の作成,の三つの処理過程があります(図1[拡大表示])。順に説明していきましょう。

(1)SQL文の解析

 RDBMSは,SQL文を受け取るとまず,そのSQL文を解析します。具体的には,そのSQL文が文法的に正しいかどうかをチェックしたり,選択,射影,結合*1といった処理がそれぞれどのように実施されるかという文の構造を把握します。データベースの管理情報を基に,SQL文に指定したテーブルやフィールドが実際に存在するかどうかや,ユーザーがそれらに対するアクセス権限を持っているかどうかをチェックする処理もここで行います。

(2)SQL文の書き換え

 解析が終わると,次にRDBMSはSQL文をより高速に実行できるように書き換えます。同じ結果を返すSQL文であっても,具体的な処理内容の違いによって,実行速度は大きく異なります。そこで,処理の手順を工夫したり,演算の種類を変更するといった書き換えをします。書き換えられたSQL文は,最終的にRDBMS内部の処理命令の集まりに変換されます。

(3)実行計画の作成

 こうして作られたRDBMS内部の処理命令の集まりを実行する方法は,一つとは限りません。例えば,一つのテーブルからデータを取り出す方法(「アクセス・パス」と呼びます)には,テーブル全体を先頭から順に検索していく方法(「全表走査」と呼びます)もあれば,インデックス*2を利用する方法もあります。テーブルを結合する場合など,複数のテーブルを扱う際には,それらを結合するアルゴリズムや結合の順序などにもさまざまな方法が考えられます。検索条件が複雑で,かつテーブルに複数のインデックスが定義されているなら,それらのどれを使うのかも問題になるでしょう。

 RDBMSは,処理命令を実行する手続きを何通りか作成したうえで,その中から最も効率の良いものを選択します。こうして出来上がった,RDBMS内部の形式で表された一連の手続きのことを「実行計画」と呼びます。

 ただ,複雑なSQL文の場合には,個々のテーブルのアクセス・パスなどの組み合わせは何千通りにもなることがあります。これらをすべて調べていたのではそのために時間がかかってしまい,本末転倒になってしまいます。そこで通常は,最速になりそうな実行計画の候補をある程度絞り込んだうえで比較検討します。

 実行計画の作成は比較的時間がかかる処理ですから,SQL文が発行されるたびに行うのでは効率がよくありません。そこで,OracleやMicrosoft SQL Server(以下,SQL Server)などのRDBMSでは,作成した実行計画をキャッシュに保存しておき,同じSQL文が発行されたときにはそのキャッシュ上の実行計画を利用するようになっています。

 ここまで来てようやく,SQL文を実行する用意ができたわけです。いかがですか。処理の多さに驚かれている人もいるかもしれませんね。

 こうして作成された実行計画は,内部処理命令ごとに実行され,ハードディスクからテーブルのデータなどがキャッシュ・バッファに読み込まれて操作されることになります。実際にハードディスクから読み込む処理の手順などについては,Part2で解説します。

 (1)~(3)の,SQL文を解析して内部形式で表した実行計画を作成するまでの処理を,SQL文の「コンパイル」と呼ぶことがあります*3。Visual BasicやC言語で作成したテキスト形式のソース・ファイルをコンパイルして,CPUが直接解釈できる機械語を生成するのと同じイメージですね。(2)と(3)のSQL文を高速に実行するための処理のことを,「最適化(optimization)」と呼びます。(1)のSQL文の解析は,RDBMSの「パーサー」と呼ばれる機能が,(2)(3)の最適化の処理は「オプティマイザ」という機能が担当します。

推論を行ってSQL文を書き換える

 では,最適化の部分で具体的にどんな処理がなされているのかをもう少し詳しく見ていきましょう。まずはSQL文の書き換えからです。

 例えば,table1とtable2という二つのテーブルから,table1のidフィールドの値が10であり,かつtable1とtable2のidフィールドの値が一致するレコードを検索する

SELECT * FROM table1, table2
WHERE table1.id = 10
AND table2.id = table1.id

というSQL文があったとしましょう。ここではWHERE句の「table1.id = 10」と「table2.id = table1.id」という二つの条件から,「table2.id = 10」という条件が導き出せますね。このような場合,オプティマイザは上記のSQL文を

SELECT * FROM table1, table2
WHERE table1.id = 10
AND table2.id = table1.id
AND table2.id = 10

のように書き換えることがあります。単に無意味な条件を追加しただけのように見えるかもしれませんが,この「table2.id = 10」のおかげで,それまで使えなかったインデックスが検索に利用できるようになる可能性が生じます。結果として,アクセス・パスを検討して実行計画を作成する際に,より高速なパスを選択できる可能性がでてくるというわけです。Oracleのオプティマイザは,等号「=」以外に比較演算子などを使った「table1.id < 10」のような式の場合にも上のような推論を行います。

 WHERE句にNOTが現れているような場合にも,書き換えを行ってできるだけ簡単になるようにします。例えば,

SELECT * FROM table
WHERE NOT (id >= 100 OR
aux IS NULL)

のようなSQL文の場合,オプティマイザは「NOT」を除去して

SELECT * FROM table
WHERE id < 100 AND aux IS NOT NULL

のように書き換えます*4。NOTが付けられた条件にはインデックスによる検索が利用できないため,無条件に全表走査が実行されてしまうからです。上のように書き換えれば,フィールドidにインデックスが定義されている場合,それを利用できるようになります。

 ただし,人間が式を変形するのと違い,変形できる対象範囲はそれほど広くありません。例えば,左辺に複数のフィールドが現れていたり,

table1.id < table3.id

のように左辺と右辺の両方にフィールドが現れているような式を受け取った場合,オプティマイザがそれ以上変形することはありません。オプティマイザが推論をしてSQL文を書き換えるといっても限度があるわけです。


布目 綾子