実行計画を読むということ
一言でいうと
実行計画を読むことは、ノード名を眺めることではなく、オプティマイザーの予測と実際の結果とのずれを探すことです。
遅いという報告が届いたとき
安定化期間に最も多く受ける報告が、「画面が遅いです」です。このとき、順序があります。
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
オプティマイザーの判断の根拠が統計です。そのため、統計が古くなると計画が悪くなります。
- 大量ロード/削除のあとは
ANALYZEを実行します - 移行の直後には必ず実行します。そうしないと、オプティマイザーが「このテーブルは空だ」と信じ込みます
- 定期的に実行されているか確認します。統計の自動収集がオフになっている本番DBが実際にあります
移行後に性能が急に悪くなったという報告の相当数が、統計の未更新です。クエリもインデックスもそのままなのに、計画だけが変わったのです。原因がわからないと、長い間迷います。
最後に: チューニングの結果は文書で
何をなぜ変えたのかを残す必要があります。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')か、複合インデックスの先頭カラムが条件にないか、選択度が悪いためオプティマイザーがあえて使わないことにしたか、です。最後のケースでインデックスを強制すると、かえって遅くなります。
そして、チューニングの結果は文書に残す必要があります。インデックスを増やすことはタダではなく、読み取りと書き込みを交換することなので、なぜこのインデックスを作ったのかがなければ、次の人は削除すべきか残すべきか判断できません。