TT Lab
はじめる
学ぶ 学習パス コース

SIのDB運用

実行計画を読むということ

TT Labで続きを見る

一言でいうと

実行計画を読むことは、ノード名を眺めることではなく、オプティマイザーの予測と実際の結果とのずれを探すことです。

遅いという報告が届いたとき

安定化期間に最も多く受ける報告が、「画面が遅いです」です。このとき、順序があります。

1. 어느 화면/기능이 느린가 (액세스 로그의 응답시간 상위 URL)
2. 그 기능이 실행하는 SQL 은 무엇인가
3. 그 SQL 의 실행계획은 어떻게 생겼나
4. 옵티마이저의 예측과 실제 결과가 얼마나 다른가
5. 데이터가 문제인가, 인덱스가 문제인가, 쿼리가 문제인가

このコードブロックの韓国語は、調査の5項目を順に示しています。どの画面・機能が遅いか(アクセスログの応答時間上位のURL)、その機能が実行するSQLは何か、そのSQLの実行計画はどうなっているか、オプティマイザーの予測と実際の結果はどれだけ違うか、データ・インデックス・クエリのどれが問題か、です。

最初の項目を飛ばして「DBが遅いようだ」から始めると、方向がぶれます。測定箇所を先に特定することがチューニングの半分です。

実行計画はノード名を読むことではありません

実行計画を読むとは、ノード名を眺めることではありません。 オプティマイザーの予測と実際の結果とのずれを探すことです。

オプティマイザーは統計を見て、「この条件ならおおよそ何行出てくるだろう」と予測します。その予測が合っていれば、おおむね良い計画を立てます。外れていれば、見当違いの計画を立てます。だから見るべきは、予測行数と実際の行数の比較です。

예측 92,300  실제 91,188   → 오차 1.2%. 통계가 건강하다. 계획을 믿어도 된다
예측      1  실제 482,913   → 옵티마이저가 잘못된 전제 위에서 정확히 계산했다

このコードブロックの韓国語は、1行目は予測と実際の誤差が1.2%で統計が健全なので計画を信頼してよいこと、2行目は予測が1行なのに実際は482,913行で、オプティマイザーが誤った前提の上で正確に計算したことを述べています。

2つ目のケースで直すべきなのは、クエリではなく統計です。ANALYZEを実行して統計を更新すると、計画がまるごと変わります。クエリにこだわってヒントを付ける前に、統計をまず疑ってください。

costは時間ではありません

もう1つのよくある誤解です。実行計画に出てくるcostは時間の単位ではありません。「ディスクページ1つを順次読み取るコスト」を1.0として、相対的に付けたスコアです。そのため、こんなアドバイスには根拠がありません。

「costが10000を超えたら危険です」

10000が0.3秒のこともあれば、30秒のこともあります。ハードウェアとキャッシュの状態によって違います。costは同じクエリの2つの計画を比較するときに使う値であり、絶対的な基準ではありません。

親ノードの時間は累積値です

実行計画で各ノードの実際の時間を見るときに、必ず知っておくべきことがあります。

親ノードの時間は、子ノードの時間を含んだ累積値です。

Hash Join  (실제 131ms)
  ├─ Seq Scan on orders   (실제 92ms)   ← 여기가 진짜 범인
  └─ Hash on customers    (실제 12ms)

Hash Joinが131msだからといって、結合が遅いわけではありません。そのうち92msはordersのスキャンです。結合自体が使った時間は約27msです。これを知らないと、見当違いの場所をチューニングします。

インデックスが使われない代表的な理由

1. カラムに関数を適用している場合

-- 인덱스를 못 탄다
WHERE substr(ORD_DT, 1, 6) = '202608'

-- 범위 조건으로 바꾸면 탄다
WHERE ORD_DT >= '20260801' AND ORD_DT < '20260901'

インデックスはカラムの元の値で並んでいます。関数を適用するとその値ではなくなるため、インデックスの順序を使えません。日付文字列、大文字小文字の変換、型キャストでよく発生します。

これをSARGableであると表現します。検索に使える条件で書きなさい、という意味です。

2. 複合インデックスの先頭カラムが条件にない場合

(CUST_ID, ORD_DT)のインデックスに対してWHERE ORD_DT = ?だけがあると、使われません。電話帳が姓-名の順に並んでいるのに、名前だけを知っている状況と同じです。

3. 選択度が低い場合

WHERE USE_YN = 'Y'で99%が'Y'なら、インデックスで探してテーブルを再度読むより、そのまま全体を走査するほうが速いです。オプティマイザーがインデックスを使わないのが正しい判断かもしれません。

ここで重要な姿勢です。Seq Scan(フルスキャン)が常に悪いわけではありません。小さなテーブルや、ほとんどの行を読む必要があるクエリでは、フルスキャンが最善です。「スキャンが見えるからインデックスを作ろう」という反射的な反応は、インデックスを増やすだけで、書き込みを遅くします。

カバリングインデックス

クエリが必要とするカラムがすべてインデックスにあれば、テーブルを読まなくて済みます。

-- 인덱스: (CUST_ID, ORD_DT, ORD_AMT)
SELECT ORD_DT, ORD_AMT FROM ORDERS WHERE CUST_ID = ?;
-- → 인덱스만 읽고 끝. 테이블 접근이 없다

実行計画にCOVERING INDEXまたはIndex Only Scanが見えたら、この状態です。よく使う照会に、カラムを1つか2つインデックスに追加してカバリングにするのは、かなり効果が大きいです。その代わり、インデックスが大きくなり、書き込みが遅くなります。トレードオフです。

N+1問題

アプリケーションで生まれる代表的な性能問題です。

주문 100건 조회       → 쿼리 1회
각 주문의 고객명 조회 → 쿼리 100회
                       총 101회

このコードブロックの韓国語は、注文100件の取得でクエリ1回、各注文の顧客名の取得でクエリ100回、合計101回という意味です。

クエリ1つ1つは1msなので、ログだけ見ればどれも速いです。ところが往復が101回あると、ネットワークの遅延だけで数百msになります。そしてデータが増えるほど線形に悪化します。

解決策は、結合1回またはIN句1回です。ORMを使うとこの問題が黙って発生するので、開発中にSQLログを有効にして、画面を1回開くのにいくつのクエリが出るかを数える習慣が必要です。

統計とANALYZE

オプティマイザーの判断の根拠が統計です。そのため、統計が古くなると計画が悪くなります。

移行後に性能が急に悪くなったという報告の相当数が、統計の未更新です。クエリもインデックスもそのままなのに、計画だけが変わったのです。原因がわからないと、長い間迷います。

最後に: チューニングの結果は文書で

何をなぜ変えたのかを残す必要があります。6か月後に誰かが「この変なインデックスは何だろう」と言って削除しようとするときに必要になります。

대상: SCR-021 주문조회
증상: p95 4.2초
원인: (ORD_DT, CUST_ID) 인덱스가 조회 패턴과 순서가 반대
조치: IX_ORDERS_01 (CUST_ID, ORD_DT) 추가, 기존 인덱스 유지(배치가 사용)
결과: p95 0.3초. 실행계획이 스캔에서 인덱스 탐색으로 전환

このコードブロックの韓国語は、対象(SCR-021の注文照会画面)・症状(p95が4.2秒)・原因((ORD_DT, CUST_ID)のインデックスが照会パターンと順序が逆)・措置(IX_ORDERS_01 (CUST_ID, ORD_DT)を追加し、既存のインデックスは維持。バッチが使用)・結果(p95が0.3秒になり、実行計画がスキャンからインデックス探索に切り替わった)の記録例です。

現場での姿

「画面が遅いです」という報告で最も抜け落ちやすいのは、最初の段階です。測定箇所を特定せずに「DBが遅いようだ」から始めると、そこから行うすべての作業が推測の上に積み上がります。アクセスログから応答時間の上位URLを取り出すのにかかる時間は1分で、その1分が方向を決めます。

次によくあるのは、インデックスを作ったのに、なぜ使われないのかわからない状況です。原因はたいてい3つのうちのどれかです。カラムに関数を適用している(substr(dt,1,6)='202603')か、複合インデックスの先頭カラムが条件にないか、選択度が悪いためオプティマイザーがあえて使わないことにしたか、です。最後のケースでインデックスを強制すると、かえって遅くなります。

そして、チューニングの結果は文書に残す必要があります。インデックスを増やすことはタダではなく、読み取りと書き込みを交換することなので、なぜこのインデックスを作ったのかがなければ、次の人は削除すべきか残すべきか判断できません。