実行計画
Query Execution Plan
SQLをどんな手順で実行するかをデータベースが立てる作戦書。遅いクエリ調査の出発点。
概要
実行計画(query execution plan)は、SQL 文を受け取ったデータベースが「このクエリを実際にどんな手順で処理するか」を決めた作戦書です。SQL は「どんなデータが欲しいか」だけを宣言する言語で、「どうやって取ってくるか」は書きません。テーブルを端から全部読むのか、インデックスで絞り込むのか、2つのテーブルをどの順番・どの方式で結合するのか — その具体的な手順を決めるのがデータベース内部のオプティマイザ(最適化器)であり、その決定結果が実行計画です。
開発者がこの語彙に出会うのは、ほぼ間違いなく「クエリが遅い」ときです。EXPLAIN コマンドを SQL の頭に付けて実行すると、データベースは実行計画をそのまま見せてくれます。同じ SQL でも、インデックスの有無やデータ量によって選ばれる計画はまったく変わり、性能は数千倍違うこともあります。遅いクエリの調査は「SQL を眺める」のではなく「実行計画を読む」ことから始まる — これがデータベース性能改善の第一歩です。
なぜ生まれたか
SQL 以前のデータベースでは、プログラマがレコードへのアクセス経路(どのインデックスをどの順でたどるか)を自分でコードに書いていました。データの物理的な持ち方が変わればプログラムも書き直しです。1970年代のリレーショナルモデルは「欲しい結果だけを宣言し、取り方はシステムに任せる」という分離を打ち出しましたが、これが成立するには「システムが取り方を賢く決められる」ことが必須でした。同じ結果を返す手順は無数にあり、選択を誤れば宣言型の利便性は「遅くて使いものにならない」で終わってしまうからです。
この課題に答えたのが、IBM の System R(1970年代後半)で開発されたコストベースオプティマイザです。取りうる手順それぞれの処理コストを統計情報から見積もり、最も安い計画を選ぶ。この発明によって「人間は what を書き、機械が how を決める」という SQL の理想が実用になりました。実行計画とは、この自動化された意思決定を人間が覗き込むための窓なのです。
詳細
計画は木である
実行計画の実体は、演算子(オペレータ)を積み上げた木構造です。葉にあたるノードがテーブルからデータを読み出し(スキャン)、その上のノードが結合・絞り込み・集約・並べ替えを行い、根が最終結果を返します。データは葉から根へ向かって流れます。EXPLAIN の出力はこの木をインデントや矢印で表現したものなので、「一番深い(内側の)行から読む」のが読み方の基本です。
フルスキャンとインデックススキャン
葉のノードで最も重要な分岐が、テーブルの読み方です。フルスキャン(シーケンシャルスキャン)はテーブルの全行を先頭から読む方式で、行数に比例したコストがかかります。インデックススキャンはインデックスの木構造をたどって該当行の場所を特定し、そこだけを読む方式で、対象が少なければ圧倒的に速くなります。計算量の言葉で言えば、O(n) と O(log n) の違いです。
ただし「インデックスが常に速い」わけではないのが肝心な点です。テーブルの大部分の行が該当する条件では、インデックスをたどってから本体を1行ずつ拾いにいくランダムアクセスの繰り返しより、先頭から一気に読むフルスキャンのほうが速くなります。オプティマイザは「該当行がどれくらいありそうか」を見積もって読み方を選んでおり、「なぜインデックスを使ってくれないのか」の答えは多くの場合「使わないほうが安いと見積もったから」です。結合方式(少数×少数の Nested Loop、大量×大量の Hash Join など)の選択も同じで、行数の見積もりがすべての判断の土台になっています。
統計情報 — 見積もりの原料
その見積もりの原料が統計情報です。データベースはテーブルごとに行数、各列の値の分布(ヒストグラム)、値の種類数などを定期的にサンプリングして保持しており、オプティマイザは「この条件なら全体の約2%が該当するはず」といった推定(選択率)をここから導きます。
ということは、統計が実態とずれていれば計画も狂います。大量のデータを一括投入した直後や、値の分布が偏っている列で、オプティマイザが行数を1万倍違って見積もり、最悪の計画を選ぶ — というのは実務で本当によく起きる事故です。PostgreSQL の ANALYZE のような統計更新を適切に走らせることは、インデックス設計と並ぶ性能運用の基本です。また「昨日まで速かったクエリが今日突然遅くなった」という現象の多くは、データ量の変化で見積もりが閾値を越え、計画が切り替わったことが原因です。
EXPLAIN の読み方と実務の勘所
調査の実際は、EXPLAIN で計画を見るところから始まり、可能なら EXPLAIN ANALYZE(実際に実行して実測値も表示する形式)まで取ります。着目点は3つです。第一に、大きなテーブルへの意図しないフルスキャンがないか。第二に、推定行数と実際の行数が大きく乖離しているノードはないか — 乖離があれば統計の問題であり、そのノードより上の判断はすべて疑わしくなります。第三に、ソートや集約がメモリに収まらずディスクに溢れていないか。改善手段は、インデックスの追加・複合インデックスの列順見直し、クエリの書き換え、統計の更新が三本柱です。
なお、ORM が生成する SQL は開発者の目に触れにくいため、実行計画の問題(そもそもクエリが N 回飛ぶ N+1問題も含めて)に気づくのが遅れがちです。RDBMS のスロークエリログを監視し、遅いクエリが見つかったら EXPLAIN を取る、という定常的なループを回すことが、データベースを長く健康に保つ運用の型になります。
