業務システム開発・保守の実務メモを発信中

Oracleの遅いSQLはどこを見る?実行計画とインデックスの確認ポイント

業務システムの保守で「この画面が遅い」「この検索だけ返ってこない」と言われると、まずSQLを触りたくなります。 WHERE句を足す、SELECT句を削る、インデックスを足す。 どれも候補にはなりますが、順番を間違えると、直したSQLの裏で別の検索条件を遅くします。

最初に見るのは、SQLの形だけではありません。 対象データの件数、検索条件の広さ、インデックスの有無、そしてOracleがどう読もうとしているかです。

Oracleで遅いSQLを調べるなら、EXPLAIN PLAN FORDBMS_XPLAN.DISPLAY() で実行計画を取ります。 そこから TABLE ACCESS FULLINDEX RANGE SCANRowsPredicate Information を順番に見ます。 実行計画は、SQLを書き換える前に「どこで多く読んでいるか」を見るための確認材料です。

遅いSQLの調査では、まず読む量と条件の効き方を見ます。件数、アクセス方法、Predicate Informationを並べると、SQLを書き換える理由が説明しやすくなります。

題材は、注文テーブルから顧客と注文日の条件で注文を検索するSQLです。 テーブル名や列名が違っても、最初に見る順番はあまり変わりません。

目次

実行計画を見る前の切り分け

遅いのがSQL単体なのか処理全体なのかを分ける

画面が遅いと聞いて、すぐ対象SQLだけを見ると外します。 画面側の初期表示、APIの待ち時間、ファイル出力、ネットワーク、別SQLのループ実行が混ざるためです。

まず、遅いSQLを1本に絞ります。 画面検索なら、どの検索条件で遅いのか。 帳票なら、出力前の集計SQLなのか、明細取得SQLなのか。 バッチなら、何件目から遅くなるのか。

ここを曖昧にしたまま実行計画を見ると、違うSQLの計画を読んでしまいます。 確認では、実行したSQL、バインド値、実行した日時、対象環境をセットで残します。

「検索が遅い」だけでは、実行計画を見る対象が決まりません まず、遅いSQLを1本に絞るところから始めます。

返す件数と読む件数の差を見る

次は、条件でどのくらい絞れているかです。 検索結果が20件でも、裏側で10万件読んで20件にしているなら遅くなります。

たとえば、注文テーブルを顧客IDと注文日で検索しているとします。

SQL
SELECT
    order_id,
    customer_id,
    order_date,
    total_amount
FROM
    orders
WHERE
    customer_id = 1001
    AND order_date >= DATE '2026-07-01';

このSQLを見る前に、対象テーブルの件数と条件に合う件数を確認します。

SQL
SELECT COUNT(*) AS total_count
FROM orders;

SELECT COUNT(*) AS matched_count
FROM orders
WHERE customer_id = 1001
  AND order_date >= DATE '2026-07-01';

全体が200万件で、条件に合う行が120件なら、インデックスで絞る価値があります。 反対に、条件に合う行が150万件なら、インデックスを使っても読み取り量は大きいままです。

遅いSQLでは、返す件数だけでなく、読む可能性がある件数を見るのが先です。 画面に20件しか出ていなくても、内部で大量に読んでいるなら、SQLの見え方はまったく変わります。

バインド値や日付範囲も同じ条件で見る

同じSQLでも、指定する値で実行計画は変わります。 顧客ID、部門、ステータス、日付範囲の広さによって、対象件数が変わるからです。

「昨日は遅かったが今日は速い」という相談では、SQL文だけを見ても足りません。 昨日指定した日付範囲、顧客ID、ステータスを確認します。

tomo

SQL本文だけを残して、バインド値を残していない調査メモは困ります。同じSQLでも、値が違えば件数も実行計画も変わります。

確認メモには、SQL本文と一緒に実行条件を書きます。 日付範囲が1日なのか1年なのか、ステータスが1つなのか全件相当なのかで、判断が変わります。

Oracleで実行計画を取得する

EXPLAIN PLAN FORで確認対象のSQLを指定する

Oracleでは、確認したいSQLの前に EXPLAIN PLAN FOR を付けて実行計画を作成できます。 この処理で、実行計画の情報が PLAN_TABLE に入ります。

SQL
EXPLAIN PLAN FOR
SELECT
    order_id,
    customer_id,
    order_date,
    total_amount
FROM
    orders
WHERE
    customer_id = 1001
    AND order_date >= DATE '2026-07-01';

このSQLは、注文テーブルから特定顧客の注文を日付条件で絞る例です。 実務では、画面やバッチから実際に投げられているSQLに置き換えます。

EXPLAIN PLAN FOR は、SQLの結果を取得するための実行ではありません。 OracleがそのSQLをどう実行しようとするかを確認するための操作です。

DBMS_XPLAN.DISPLAYで結果を表示する

EXPLAIN PLAN FOR のあと、DBMS_XPLAN.DISPLAY() で直近の実行計画を表示します。

SQL
SELECT
    PLAN_TABLE_OUTPUT
FROM
    TABLE(DBMS_XPLAN.DISPLAY());

SQL*PlusやSQL Developerなどで実行すると、実行計画がテキストとして出ます。 出力は、たとえば次のようになります。

Markdown
--------------------------------------------------------------------------------
| Id  | Operation                   | Name          | Rows | Bytes | Cost |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |               |  120 |  7200 |   18 |
|   1 | TABLE ACCESS BY INDEX ROWID | ORDERS        |  120 |  7200 |   18 |
|   2 | INDEX RANGE SCAN            | IDX_ORDERS_01 |  120 |       |    4 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("CUSTOMER_ID"=1001 AND "ORDER_DATE">=DATE '2026-07-01')

この例では、IDX_ORDERS_01 を使って条件に合う行を探し、そのあとテーブルから行を取得しています。 確認の入口は、OperationNameRowsCostPredicate Information です。

実行計画の出力は、表全体を暗記しなくても読めます。最初は、どの表をどう読んでいるか、何件くらい読む見込みか、条件がどこで効いているかに絞ります。

EXPLAIN PLANは実行時そのものとは限らない

EXPLAIN PLAN は最初の確認に使えますが、実際にSQLを流したときの情報と必ず一致するわけではありません。 Oracleの資料でも、説明時の計画と実際に使われる計画が異なる場合があることが示されています。

違いが出る理由はいくつかあります。 スキーマ、統計情報、バインド値、セッション設定、インデックス変更、実行環境が変わると、選ばれる計画も変わります。

そのため、EXPLAIN PLAN だけで最終判断にしません。 本当に遅いSQLでは、実際に実行したカーソルの計画や実行統計まで見ます。

EXPLAIN PLAN は最初の当たりを付けるには便利です。ただし、実行時の実績値ではありません。修正前後の判断では、実行時間、対象件数、実際の実行条件も一緒に残します。

保守作業の最初の確認では、EXPLAIN PLAN FORDBMS_XPLAN.DISPLAY() から始めます。 AWR、SQL Trace、DISPLAY_CURSOR の詳細までは広げません。

実行計画で最初に見る行

TABLE ACCESS FULLなら全件走査になっているかを見る

遅いSQLで最初に目に付くのは、TABLE ACCESS FULL です。 これはテーブル全体を読む操作です。

たとえば、次のような出力なら、ORDERS を全表走査しています。

Markdown
-------------------------------------------------------------------
| Id  | Operation          | Name   | Rows   | Bytes    | Cost |
-------------------------------------------------------------------
|   0 | SELECT STATEMENT   |        | 150000 | 9000000  | 9800 |
|   1 | TABLE ACCESS FULL  | ORDERS | 150000 | 9000000  | 9800 |
-------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("CUSTOMER_ID"=1001 AND "ORDER_DATE">=DATE '2026-07-01')

この例では、ORDERS を広く読んだあと、条件で絞っている形です。 対象テーブルが小さいなら問題にならない場合もありますが、数百万件あるテーブルで毎回これが出ると遅くなりやすいです。

TABLE ACCESS FULLは常に悪ではありません ただし、少ない件数を返す検索なのに大きなテーブルを全件読んでいるなら、条件やインデックスを疑います。

INDEX RANGE SCANなら検索条件とインデックス名を見る

INDEX RANGE SCAN が出ている場合は、インデックスを使って範囲または複数候補を探しています。 ここで見るのは、どのインデックスを使っているかです。

Markdown
--------------------------------------------------------------------------------
| Id  | Operation                   | Name          | Rows | Bytes | Cost |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |               |  120 |  7200 |   18 |
|   1 | TABLE ACCESS BY INDEX ROWID | ORDERS        |  120 |  7200 |   18 |
|   2 | INDEX RANGE SCAN            | IDX_ORDERS_01 |  120 |       |    4 |
--------------------------------------------------------------------------------

この例では、IDX_ORDERS_01 を使っています。 次に確認するのは、そのインデックスがどの列で作られているかです。

SQL
SELECT
    index_name,
    column_name,
    column_position
FROM
    user_ind_columns
WHERE
    table_name = 'ORDERS'
ORDER BY
    index_name,
    column_position;

IDX_ORDERS_01CUSTOMER_IDORDER_DATE の複合インデックスなら、今回の条件に合っています。 別の列のインデックスを使っているなら、なぜその計画になったのかを見ます。

実行計画にインデックス名が出たら、そのインデックスの列構成まで確認します。 名前だけ見て「インデックスを使っているから大丈夫」と判断すると、効き方を見落とします。

RowsとCostは単独で善悪を決めない

Rows は、各ステップで処理する行数の見積りです。 Cost は、Oracleのオプティマイザが比較に使うコストです。

この2つは使いますが、数字だけで善悪を決めません。 Cost が大きいから必ず遅い、Rows が小さいから必ず速い、とは言い切れません。

確認では、想定件数とずれていないかを見ます。 検索条件では120件程度のつもりなのに、実行計画では150000件になっている。 こういうズレがあれば、統計情報、条件の書き方、バインド値、インデックスの選ばれ方を疑います。

tomo

実行計画の数字は、点数表というより読み取り量のメモです。どのステップで大きく読んでいるかを見ると、次に疑う場所を絞れます。

Predicate Informationと条件の効き方

accessとfilterの違いを見る

Predicate Information には、WHERE条件がどの操作で使われたかが出ます。 遅いSQLの調査では、ここで「条件を書いているだけ」なのか、「条件で探しに行けている」のかを分けます。

次の例では、条件がインデックスアクセスに使われています。

Markdown
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("CUSTOMER_ID"=1001 AND "ORDER_DATE">=DATE '2026-07-01')

一方、次の例では、テーブルを読んだあとにフィルタしています。

Markdown
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("CUSTOMER_ID"=1001 AND "ORDER_DATE">=DATE '2026-07-01')

access に出ている条件は、行を探しに行く段階で使われています。 filter に出ている条件は、取ってきた後の絞り込みで使われています。

filter が出たら全部ダメ、ではありません。 ただ、顧客IDや注文日など絞り込みの中心になる列が filter だけに出ていて、Rows も大きい。 この組み合わせなら、条件の書き方かインデックスの効き方を疑います。

関数や型変換でインデックスが効きにくくなる

WHERE句で列に関数をかけると、インデックスが効きにくくなります。 よくあるのは、日付列を文字列に変換して比較する書き方です。

SQL
SELECT
    order_id,
    customer_id,
    order_date
FROM
    orders
WHERE
    customer_id = 1001
    AND TO_CHAR(order_date, 'YYYY-MM-DD') = '2026-07-01';

この書き方では、order_date に関数をかけてから比較しています。 日付列のインデックスを期待しているなら、まず疑う場所です。

日付の当日分を見たいなら、範囲条件で書く方が確認しやすいです。

SQL
SELECT
    order_id,
    customer_id,
    order_date
FROM
    orders
WHERE
    customer_id = 1001
    AND order_date >= DATE '2026-07-01'
    AND order_date <  DATE '2026-07-02';

列に関数をかけた条件は、実行計画のPredicate Informationで必ず確認します インデックスがあるのに使われない相談では、ここで止まることが多いです。

複合インデックスは列の順番も見る

複合インデックスでは、列の順番も確認します。 CUSTOMER_ID, ORDER_DATE のインデックスと、ORDER_DATE, CUSTOMER_ID のインデックスは同じではありません。

注文検索で、いつも顧客IDを指定してから注文日で絞るなら、CUSTOMER_ID が先頭のインデックスが合いやすいです。 日付範囲だけで広く検索する画面なら、別の考え方になります。

インデックス列は、USER_IND_COLUMNS で順番まで確認します。

SQL
SELECT
    index_name,
    column_name,
    column_position
FROM
    user_ind_columns
WHERE
    table_name = 'ORDERS'
    AND index_name = 'IDX_ORDERS_01'
ORDER BY
    column_position;

結果は次のように見ます。

Markdown
INDEX_NAME      COLUMN_NAME   COLUMN_POSITION
--------------- ------------- ---------------
IDX_ORDERS_01   CUSTOMER_ID                 1
IDX_ORDERS_01   ORDER_DATE                  2

複合インデックスは、列が含まれているかだけでなく順番を見る必要があります。 WHERE条件とインデックス定義を横に並べると、判断しやすくなります。

SQLを書き換える前の確認順

条件を狭められるか業務条件から見る

SQLの修正では、技術的に速くする前に、条件を狭められるかを確認します。 日付範囲、ステータス、部門、顧客区分は、業務側の条件で絞れることがよくあります。

たとえば、画面では直近1年分だけ見ればよいのに、SQLでは全期間を検索している。 終了済みデータを含める必要がないのに、全ステータスを対象にしている。 こういう場合、SQLの形より条件の決め方を直した方が効きます。

SQL
-- 範囲が広すぎる例
WHERE customer_id = 1001

-- 画面要件に合わせて期間も指定する例
WHERE customer_id = 1001
  AND order_date >= ADD_MONTHS(TRUNC(SYSDATE), -12)

ここは開発者だけで決めない方がよいです。 画面や帳票で本当に必要な範囲を、利用者や仕様と照らして確認します。

tomo

遅いSQLが、実は「必要以上に広く探していただけ」だったことは珍しくありません。WHERE条件を1つ足すだけで、SQLの書き換えより効く場面があります。

SELECT句は必要な列だけにする

SELECT * は手早いですが、保守で見るSQLでは注意します。 画面や帳票で使わない列まで取っていると、読み取り量や通信量が増えます。

SQL
SELECT *
FROM orders
WHERE customer_id = 1001
  AND order_date >= DATE '2026-07-01';

必要な列が決まっているなら、列を明示します。

SQL
SELECT
    order_id,
    customer_id,
    order_date,
    total_amount
FROM
    orders
WHERE
    customer_id = 1001
    AND order_date >= DATE '2026-07-01';

ただし、SELECT * を直せば必ず速くなる、という話ではありません。 主な遅さが全件走査や結合条件にあるなら、列を減らしても効果は限定的です。

確認では、実行計画と画面仕様を合わせて見ます。 使わない列を大量に取っているなら直す。 先にアクセス方法や件数の問題が大きいなら、そちらを優先します。

インデックス追加は更新処理への影響も見る

インデックスを足すと検索が速くなる場面はあります。 ただし、追加すれば勝ちではありません。

インデックスは、INSERT、UPDATE、DELETEにも影響します。 注文テーブルの検索を速くするためにインデックスを増やした結果、夜間バッチの登録処理が遅くなる。 この手の副作用は、あとから効いてきます。

確認するときは、次の内容を残します。

確認項目見る理由残す内容
対象SQLどのSQLを速くしたいかを明確にするためSQL本文、バインド値、実行条件
対象件数インデックスで絞れる検索かを見るため全体件数、条件一致件数
既存インデックス追加前に使えるものがないかを見るためインデックス名、列、順番
更新処理追加したインデックスが書き込み処理へ影響するため登録、更新、削除、夜間バッチの有無
修正前後の計画効果を説明するためOperation、Rows、Cost、Predicate Information

インデックス追加は、検索SQLだけではなく更新処理まで見て判断します。 保守作業では、速くしたSQLの裏で別処理を遅くしないところまで見ます。

調査結果を残す形

SQL、バインド値、実行計画をセットで残す

遅いSQLの調査では、後から同じ状態を再現できる形で残します。 SQL本文だけでは足りません。

最低限、SQL、バインド値、実行日時、対象環境、実行計画をセットにします。 画面から実行したなら、画面の検索条件も残します。

残すもの理由
SQL本文SELECT … FROM orders …対象SQLを特定するため
バインド値customer_id=1001、order_date=2026-07-01件数と実行計画が値で変わるため
対象環境検証DB、本番相当データデータ量や統計情報が違うため
実行計画DBMS_XPLAN.DISPLAYの出力アクセス方法と条件の効き方を見るため
件数全体200万件、条件一致120件Rowsの妥当性を見るため
tomo

「インデックスを追加したら速くなった」だけだと、次に同じ問題が起きたときに使えません。どの条件で、何件を対象にし、実行計画がどう変わったかまで残します。

修正前後で見る項目をそろえる

修正前は画面で測り、修正後はSQL単体で測る。 修正前は本番相当データで見て、修正後は少ない検証データで見る。 この比較では、直ったかどうかを説明できません。

修正前後では、条件をそろえます。 同じSQL、同じバインド値、同じ対象データ、同じ取得方法で比べます。

Markdown
修正前:
Operation: TABLE ACCESS FULL
Rows:      150000
Cost:      9800
Predicate: filter("CUSTOMER_ID"=1001 AND "ORDER_DATE">=DATE '2026-07-01')

修正後:
Operation: INDEX RANGE SCAN + TABLE ACCESS BY INDEX ROWID
Rows:      120
Cost:      18
Predicate: access("CUSTOMER_ID"=1001 AND "ORDER_DATE">=DATE '2026-07-01')

このように並べると、単に速くなっただけでなく、どこが変わったかを説明できます。 実行時間だけを残すより、レビューやリリース判断で使いやすくなります。

修正前後の比較では、実行時間、Rows、Operation、Predicate Informationを並べます。 数字だけではなく、読み方もセットで残します。

本番反映前にデータ量と時間帯を確認する

検証環境で速くても、本番で同じように動くとは限りません。 データ量、統計情報、同時実行、時間帯が違うからです。

特に、夜間バッチや月末処理では、本番だけ件数が跳ねます。 画面検索では問題なくても、月末締めの集計では遅くなる。 検証データが薄いと、ここを見落とします。

本番反映前には、次の観点を見ます。

観点確認内容
データ量検証環境と本番相当で対象テーブルの件数差が大きくないか
統計情報実行計画の前提になる統計情報が極端に古くないか
時間帯同時に走るバッチや集計処理がないか
更新処理追加したインデックスが登録や更新を遅くしないか
再現条件遅かった条件と同じバインド値で確認したか

この確認を飛ばすと、検証では速かったのに本番で戻る、という話になります。 保守では、SQLそのものだけでなく、動く時間とデータ量まで含めて判断します。

遅いSQL調査は書き換えより先に当たりを付ける

遅いSQLを見たとき、最初にやることは魔法の書き換えではありません。 どこで多く読んでいるか、条件が効いているか、インデックスをどう使っているかを先に押さえます。

そのために、実行計画を取ります。 EXPLAIN PLAN FOR で確認対象のSQLを指定し、DBMS_XPLAN.DISPLAY() で表示する。 TABLE ACCESS FULLINDEX RANGE SCANRowsCostPredicate Information を順番に見る。

最後に、業務条件へ戻します。 本当にその期間を検索する必要があるのか。 そのステータスを含める必要があるのか。 既存インデックスで足りるのか。 インデックスを足すなら更新処理に影響しないか。

SQLの修正理由を説明できないまま書き換えると、次の調査でまた迷います 実行計画、件数、条件、修正前後の差を残しておくと、同じ種類の遅さに対応しやすくなります。

Oracleの実行計画は、全部を一度に読もうとするとしんどいです。 最初は、アクセス方法、行数、条件の効き方だけで十分です。 そこまで見れば、少なくとも「なんとなくインデックスを足す」からは抜けられます。

遅いSQL調査では、書き換える前に、読む量と条件の効き方を見ます。 この順番を固定しておくと、保守作業の説明が楽になります。

よかったらシェアしてね!
  • URLをコピーしました!
目次