さて、前置きが終わって連載の第1回です。今回は、いちばん基本になる“利用者がSQLを与えた時のRDBMS側の処理”についてお話ししましょう。これは人間から見える部分においては、「SQLを投げる」→「結果が投げ返される」というシンプルなことしかありません。しかし、もちろん実際には様々な処理がされているのです。
これをより細かくひもといていきますと、「サーバ内でSQLがどのように処理されるか?」という流れを理解することができます。流れを理解すれば、SQLが実行される時に起こることについても理解が深まるでしょう。
RDBMSでの処理の流れ
RDBMSにSQLが渡されたときに、実際にどのような処理がされているのかを示したのが次の図「問い合わせ実行の流れ」です。この図のように、SQLは、
- パーザで構文解釈され、
- プランナで実行計画を立て、
- エグゼキュータで実行する。
という3段階の流れによって内部的に実行されます。まずは、この大きな流れに沿って追っていき、実際にSQL文がどのように解釈され、実行されるか、大まかなところを読み解いていきます。
1:Parser(パーザ)
まず最初の処理として、パーザで処理がなされます。これは文字列表現のSQL文を、コンピューターに理解可能な形に解析します。この作業は構文解析と呼ばれ、文字列で言語を記述して実行するには必須です。人間的に言うと、命令を理解する部分と考えてよいでしょう。
構文解析によって、文字列であったSQL文は単語に分けられ、木構造(パーズツリーと呼ばれます)に変換され、コンピューターによって理解されます。
パーザは一般的に次の2ステージに分かれています。
- 単に文法を解釈する
- この段階ではテーブル名などは展開されず、文字だけで解釈します。
- 解釈結果を基に、テーブル構造、カラム名などを加味して内部表現に置き換える
- PostgreSQLの場合、この内部表現はOIDという値で管理されています。
- 内部的に、文字列で持っていたテーブル名やカラム名がIDに書き換えられ、情報参照を容易にします。
パーザを経たこの段階では、SQL文はようやくプログラムから読めるようになったに過ぎません。しかしこの解釈結果を基にその先のステージでの行動を決定するわけですから、最初の段階としては欠くべからざるものといえましょう。
1.5:ユーティリティコマンド実行
パーザにより構文解釈が終了した後、問い合わせによって2種類に分岐します。
- SELECT, INSERT, UPDATE等の場合、次のプランナへ進む。
- CREATE TABLE等の場合、そのままユーティリティコマンド実行に進み、結果を返す。
ユーティリティコマンドの実行内容は、それぞれのコマンドに依存します。プランを作成する必要があるかないかで分けられていると理解すればよいでしょう。
2:Planner(プランナ)
さて、ユーティリティコマンドでない、通常のSELECT文等の場合はプランナに進みます。構文解釈された結果は、「何を取ってくるか」に相当する部分であり、「どうやって取ってくるか」ということは一切書かれていません。ですから、それを「どうやって取ってくるか」に変換しないと、実行することは出来ません。これを行うのがプランナになります。これも人間的に言い換えると「命令を理解した後、作戦を立てる」と言えばいいでしょう。
プランナでは、構文木から、相当する情報を実際に取得するための計画である、プランを作り上げます。これは、
- 構文木を読んで、実行すれば構文通りの検索結果を取ってくるようなプランを立て、木構造(プラン木)に作り上げる
- 検索条件や、それに対するテーブル名、あるいは各種構文等を読み取ってプランに仕立てます。
- プランが複数個存在した場合は、その中から一番良いものを選ぶ
- SQLには結合条件がどうとかというのは一切書かれないのは前述の通りです。ですから、まじめに構文木からプランを起こした場合、複数のプランが存在し得ます。そのような場合、そのなかから一番良さそうなものを基準を作って選び、それを選択とします。
- もちろんたまたま1つしかプランが存在し得なかった場合は、それをそのまま選べばすみます。
という2つの段階を経ます。
PostgreSQLでは、
- スキャンしなければいけないテーブルを調べ、可能なスキャン方法をすべて列挙します。
このとき、インデックス定義などを参照して、必要なものをすべて選びます。
- テーブルが複数の場合、複数回のスキャンを組み合わせないと結果が得られません。
そのため、スキャンを組み合わせた結合方法を列挙します。
- 列挙されたプランに、あらかじめ収集してあった統計情報を加味して実行コストを算出します。
- プランごとに算出されたコストを比較し、最も小さいコストで検索可能なものを選びます。
という動作で、実際にプランを選択します。これは実行コストを元にプランを選ぶことから「コストベース」という分類になります。
プラン作成は、データベース実装での腕の見せ所の一つです。ここで最適なプランを選ぶことが出来れば、実行時間は非常に短くなる可能性が高くなります。一方で間違った、最適でないプランを選んでしまった場合には、非常に遅くなってしまう可能性もはらんでいます。
3:Executor (エグゼキュータ)
最後に実行されるのがエグゼキュータです。これは、作成されたプランを実行して結果を返す部分となります。これも人間的に言い換えれば「立てた作戦を実行する」にあたります。
プランは、前述の通り、作成された段階でその通りに実行すればSQL通りのデータが取得出来るようになっています。その計画に沿って、実際にテーブルやインデックスの物理的な内容にアクセスしたり、情報のフィルタリング・ソート・結合などを実施します。プランは多くの場合複数の実行内容を含んでいますから、これはそのまま指定した順に適用していきます。
最後まで適用が終了したら、望んでいた結果が得られますのでそれをクライアントに通知します。これですべての処理が終了しました!
エグゼキュータでは単にこれらの処理を実行するだけですので、プランやそのほかの条件が同じ場合にはほとんど同じ時間で実行出来ることが期待されます。しかし、実際にディスクなどにアクセスしてデータの読み書きをするのもこの部分です。このようなところでは、たとえば同時に更新が発生しないようにするとか、ディスク読み込みを最小限にするとか、プランナとは別の能力が問われます。
一連のサイクルを通して
SQL文の問い合わせ実行というのは、上記の3ステージ式となっています。これによりユーザーから速度チューニング等を見せないようにするなど、非常に意欲的な作りといえるでしょう。しかし一方で、チューニング等を行うときに問題となっている部分を特定することが難しいです。例えば「遅い」というだけでも、前述の内容を頭に浮かべながら考えると、
- プランを作成するのに時間がかかる
- 作成されたプランが最適でなく、実行に時間がかかる
- プランは最適だが、エグゼキュータで時間がかかっている
といったケースが思いつきます。これらは、現象こそ共通していますが、解決のためには異なるアプローチが必要となります。それが分かり難いからこそ「データベースは何を考えているのか分からない」と言いたくなるのです。しかし、この流れを理解していれば、そういう事情を理解する最初のとっかかりになるでしょう。
もちろん次のステージとしては、もっと中を掘り下げて、具体的なSQLに対して、どう具体的な処理がされるのか? というところを追っていくことになります。次回以降では、この一連のサイクルを具体的に掘り下げていくことにします。
- プランナはどのようなプランを実際に立てるのか?
- エグゼキュータでの実行時には、どのようなことが起こるのか? またどの程度時間がかかるのか?
という項目を一つ一つ見ていきましょう。そうすれば徐々にデータベースの心に迫れるはずですね。