【Oracle】不要なインデックスを見つけて削除する方法|V$OBJECT_USAGE・DBA_INDEX_USAGE・重複検出・INVISIBLE化・安全な削除フローまで解説

【Oracle】不要なインデックスを見つけて削除する方法|V$OBJECT_USAGE・DBA_INDEX_USAGE・重複検出・INVISIBLE化・安全な削除フローまで解説 Oracle

インデックスは SELECT を高速化する一方で、INSERT / UPDATE / DELETE の度にメンテナンスコストが発生します。使われていないインデックスは性能を改善しないどころか、DML を遅くし、ストレージを無駄に消費し、統計収集の時間を延ばします。

本記事では、不要なインデックスを安全に特定・検証・削除するための手法を解説します。V$OBJECT_USAGE での使用監視DBA_INDEX_USAGE(12c 以降)での使用頻度分析重複インデックスの検出INVISIBLE 化による安全な無効化テスト、そして削除の判断基準と実行手順まで体系的に解説します。

この記事でわかること
・不要なインデックスが DML に与える負荷
・V$OBJECT_USAGE でインデックスの使用/未使用を監視する方法
・DBA_INDEX_USAGE(12c 以降)で使用頻度を定量的に分析する方法
・完全重複・先頭列冗長なインデックスを検出する SQL
・INVISIBLE 化して安全に「消しても大丈夫か」をテストする方法
・削除の判断基準と実行手順
・インデックス削除後の確認事項
スポンサーリンク

不要なインデックスが与える影響

影響 詳細
DML の遅延 INSERT / UPDATE / DELETE のたびにインデックスも更新される。インデックスが多いほど DML が遅くなる
ストレージの浪費 インデックスはデータの 10%〜30% 程度の領域を消費する
統計収集の遅延 DBMS_STATS がインデックスの統計も収集するため、インデックスが多いほど時間がかかる
バックアップ時間の増加 RMAN / Data Pump のバックアップ対象が増える
オプティマイザの混乱 不要なインデックスが実行計画の選択肢を増やし、最適でない計画が選ばれる可能性
「インデックスは多ければ多いほど良い」は誤解
SELECT の性能は向上しますが、DML が増えるシステムでは不要なインデックスの削除が最も効果的なチューニングになることがあります。特にバッチ INSERT が多いテーブルでは、インデックス数の見直しが重要です。

V$OBJECT_USAGE でインデックスの使用を監視する

ALTER INDEX ... MONITORING USAGE でインデックスの使用を監視し、一定期間後に V$OBJECT_USAGE で「使われたかどうか」を確認します。

SQL(インデックス使用監視の開始)
-- 特定のインデックスの監視を開始
ALTER INDEX idx_emp_dept MONITORING USAGE;

-- テーブルの全インデックスの監視を一括開始
BEGIN
    FOR rec IN (
        SELECT index_name FROM user_indexes WHERE table_name = 'EMPLOYEES'
    ) LOOP
        EXECUTE IMMEDIATE 'ALTER INDEX ' || rec.index_name || ' MONITORING USAGE';
    END LOOP;
END;
/
SQL(一定期間後に使用状況を確認)
-- 監視結果を確認
SELECT index_name, table_name,
       monitoring,     -- YES = 監視中
       used,           -- YES = 使用された / NO = 未使用
       start_monitoring,
       end_monitoring
FROM v$object_usage
ORDER BY used, index_name;

-- USED = NO のインデックスが「監視期間中に一度も使われなかった」候補
SQL(監視を停止)
-- 監視を停止
ALTER INDEX idx_emp_dept NOMONITORING USAGE;
V$OBJECT_USAGE の限界
・「使われた / 使われなかった」の 2 値しかわからない(使用頻度は不明)
・月末バッチなど低頻度で使うインデックスは見逃す可能性がある
・監視期間は1 か月以上(できれば月末処理を含む期間)を推奨
・11g 以前のメインの方法。12c 以降は DBA_INDEX_USAGE が推奨

DBA_INDEX_USAGE で使用頻度を分析する(12c 以降)

Oracle 12c R2 以降の DBA_INDEX_USAGE ビューは、インデックスの使用回数・最終使用日時を自動的に記録します。MONITORING USAGE の設定は不要で、常に情報が蓄積されます。

SQL(DBA_INDEX_USAGE: 使用頻度の分析)
-- 全インデックスの使用状況(使用回数と最終使用日時)
SELECT
    o.object_name AS index_name,
    i.table_name,
    u.total_access_count AS access_count,
    u.total_exec_count AS exec_count,
    u.total_rows_returned AS rows_returned,
    u.last_used
FROM dba_index_usage u
JOIN dba_objects o ON u.object_id = o.object_id
JOIN dba_indexes i ON o.object_name = i.index_name AND o.owner = i.owner
WHERE i.owner = USER
ORDER BY u.total_access_count;
SQL(30 日以上使われていないインデックス)
-- 最終使用日が 30 日以上前、または一度も使われていないインデックス
SELECT o.object_name AS index_name,
       i.table_name,
       NVL(u.total_access_count, 0) AS access_count,
       u.last_used
FROM dba_indexes i
JOIN dba_objects o ON i.index_name = o.object_name
                  AND i.owner = o.owner
                  AND o.object_type = 'INDEX'
LEFT JOIN dba_index_usage u ON o.object_id = u.object_id
WHERE i.owner = USER
  AND i.index_type IN ('NORMAL', 'BITMAP', 'FUNCTION-BASED NORMAL')
  AND (u.last_used IS NULL OR u.last_used < SYSDATE - 30)
ORDER BY NVL(u.total_access_count, 0);
DBA_INDEX_USAGE は 12c R2 以降の推奨ツール
V$OBJECT_USAGE と異なり、使用回数・最終使用日時がわかるため、「1 回だけ月末に使われた」インデックスも正確に判断できます。12c R2 以降であれば V$OBJECT_USAGE より DBA_INDEX_USAGE を優先してください。

重複インデックスを検出する

同じ列(または先頭列が同じ)のインデックスが複数存在する場合、片方は不要な可能性が高いです。

完全重複(全列が同じ)の検出

SQL(完全重複インデックスの検出)
-- 列の組み合わせが完全に同じインデックスのペアを検出
SELECT a.index_name AS index_a,
       b.index_name AS index_b,
       a.table_name,
       a.cols
FROM (
    SELECT index_name, table_name,
           LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_position) AS cols
    FROM user_ind_columns
    GROUP BY index_name, table_name
) a
JOIN (
    SELECT index_name, table_name,
           LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_position) AS cols
    FROM user_ind_columns
    GROUP BY index_name, table_name
) b ON a.table_name = b.table_name AND a.cols = b.cols AND a.index_name < b.index_name;

先頭列が冗長(包含関係)の検出

SQL(先頭列が冗長なインデックスの検出)
-- idx_a(A, B) と idx_b(A, B, C) がある場合、idx_a は冗長
-- idx_b だけで idx_a のカバー範囲を包含できる
SELECT a.index_name AS redundant_index,
       b.index_name AS covering_index,
       a.table_name,
       a.cols AS redundant_cols,
       b.cols AS covering_cols
FROM (
    SELECT index_name, table_name,
           LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_position) AS cols
    FROM user_ind_columns
    GROUP BY index_name, table_name
) a
JOIN (
    SELECT index_name, table_name,
           LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_position) AS cols
    FROM user_ind_columns
    GROUP BY index_name, table_name
) b ON a.table_name = b.table_name
    AND a.index_name <> b.index_name
    AND b.cols LIKE a.cols || ',%'  -- b の先頭列が a と同じ
ORDER BY a.table_name, a.index_name;

インデックス情報の詳しい取得方法は「インデックス情報を取得する方法」を参照してください。

インデックスのサイズを確認する

SQL(インデックスごとのサイズ)
-- インデックスのサイズを確認(削除による空き容量の見積もり)
SELECT index_name, table_name,
       ROUND(bytes/1024/1024, 1) AS size_mb
FROM user_segments
WHERE segment_type = 'INDEX'
ORDER BY bytes DESC;

-- テーブルごとのインデックス合計サイズ
SELECT i.table_name,
       COUNT(*) AS index_count,
       ROUND(SUM(s.bytes)/1024/1024, 1) AS total_index_mb
FROM user_indexes i
JOIN user_segments s ON i.index_name = s.segment_name AND s.segment_type = 'INDEX'
GROUP BY i.table_name
ORDER BY total_index_mb DESC;

INVISIBLE 化で安全に削除テストする

いきなり DROP INDEX するのはリスクがあります。INVISIBLE にするとオプティマイザがそのインデックスを無視するため、「削除した場合の影響」を安全にテストできます。問題があればすぐに VISIBLE に戻せます。

SQL(INVISIBLE 化 → 検証 → 判断)
-- (1) インデックスを INVISIBLE にする(オプティマイザが無視する)
ALTER INDEX idx_emp_dept INVISIBLE;

-- (2) 1〜2 週間、通常の業務を実行して影響を観察
-- パフォーマンスの悪化がないか監視
-- AWR レポートや V$SQL で実行計画の変化を確認

-- (3a) 問題なし → 削除
DROP INDEX idx_emp_dept;

-- (3b) 問題あり → VISIBLE に戻す(即座に復旧)
ALTER INDEX idx_emp_dept VISIBLE;
INVISIBLE が最も安全な削除手法
DROP INDEX すると再作成に時間がかかる(大テーブルでは数時間)
INVISIBLE なら VISIBLE に戻すだけで即復旧(数秒)
・データもインデックス自体も消えないため、リスクがほぼゼロ
本番環境での削除は必ず INVISIBLE → 検証 → DROP の順序で行ってください。
INVISIBLE でもインデックスの DML メンテナンスは続く
INVISIBLE にしてもインデックスは存在し続け、INSERT/UPDATE/DELETE 時の更新コストは発生します。INVISIBLE は「オプティマイザの選択肢から外す」だけであり、DML 負荷の軽減には最終的な DROP が必要です。

不要インデックス削除の安全なワークフロー

手順 作業 期間
(1) 候補の特定 V$OBJECT_USAGE / DBA_INDEX_USAGE / 重複検出 SQL で未使用・重複を洗い出し 1 日
(2) サイズ確認 USER_SEGMENTS で削除効果(空き容量)を見積もり 1 日
(3) DDL のバックアップ DBMS_METADATA.GET_DDL でインデックスの DDL を保存(再作成用) 1 日
(4) INVISIBLE 化 ALTER INDEX … INVISIBLE で無効化 即時
(5) 影響監視 AWR / V$SQL で実行計画の変化・パフォーマンス悪化を監視 1〜4 週間
(6a) 問題なし → DROP DROP INDEX で削除 即時
(6b) 問題あり → VISIBLE ALTER INDEX … VISIBLE で復旧 即時
SQL(DDL のバックアップ: 再作成に備える)
-- 削除前にインデックスの DDL を保存
SELECT DBMS_METADATA.GET_DDL('INDEX', index_name, USER) AS ddl
FROM user_indexes
WHERE index_name = 'IDX_EMP_DEPT';

-- 結果を SQL ファイルに保存しておく
-- 万が一再作成が必要になった場合にそのまま実行できる

削除してはいけないインデックス

種類 理由 確認方法
PRIMARY KEY / UNIQUE 制約のインデックス 制約と連動しており、インデックスだけ削除すると制約も無効化 USER_CONSTRAINTS の index_name を確認
外部キー参照元のインデックス 削除するとデッドロックの原因になることがある USER_CONS_COLUMNS で FK の列を確認
アプリケーションがヒントで指定しているインデックス /*+ INDEX(table idx) */ で直接指定されている アプリケーションの SQL を grep で検索
月末 / 年次バッチでのみ使うインデックス 普段は未使用だが特定タイミングで必須 業務担当者に確認
SQL(制約に紐づくインデックスの確認)
-- PRIMARY KEY / UNIQUE 制約のインデックスを確認
SELECT c.constraint_name, c.constraint_type, c.index_name, c.table_name
FROM user_constraints c
WHERE c.index_name IS NOT NULL
ORDER BY c.table_name;

-- これらのインデックスは DROP INDEX ではなく
-- ALTER TABLE ... DROP CONSTRAINT で制約ごと削除する

実務パターン集

パターン(1): 全テーブルの未使用インデックスを一括検出

SQL(12c+: DBA_INDEX_USAGE ベース)
-- 作成から 60 日以上経過し、一度も使われていないインデックス
SELECT i.index_name, i.table_name,
       ROUND(s.bytes/1024/1024, 1) AS size_mb,
       o.created
FROM user_indexes i
JOIN user_objects o ON i.index_name = o.object_name AND o.object_type = 'INDEX'
JOIN user_segments s ON i.index_name = s.segment_name AND s.segment_type = 'INDEX'
LEFT JOIN dba_index_usage u
    ON o.object_id = u.object_id
WHERE i.index_type IN ('NORMAL', 'BITMAP', 'FUNCTION-BASED NORMAL')
  AND o.created < SYSDATE - 60
  AND NVL(u.total_access_count, 0) = 0
  AND i.index_name NOT IN (
      SELECT index_name FROM user_constraints WHERE index_name IS NOT NULL
  )
ORDER BY s.bytes DESC;

パターン(2): インデックス過多のテーブルを検出

SQL
-- インデックスが 5 個以上あるテーブル(見直し候補)
SELECT table_name, COUNT(*) AS index_count
FROM user_indexes
GROUP BY table_name
HAVING COUNT(*) >= 5
ORDER BY index_count DESC;

パターン(3): 一括 INVISIBLE 化スクリプト

SQL(候補インデックスを一括 INVISIBLE)
-- 未使用インデックスを一括で INVISIBLE にする
BEGIN
    FOR rec IN (
        SELECT i.index_name
        FROM user_indexes i
        LEFT JOIN dba_index_usage u
            ON i.index_name = (SELECT object_name FROM dba_objects WHERE object_id = u.object_id)
        WHERE NVL(u.total_access_count, 0) = 0
          AND i.index_name NOT IN (
              SELECT index_name FROM user_constraints WHERE index_name IS NOT NULL
          )
          AND i.visibility = 'VISIBLE'
    ) LOOP
        EXECUTE IMMEDIATE 'ALTER INDEX ' || rec.index_name || ' INVISIBLE';
        DBMS_OUTPUT.PUT_LINE('INVISIBLE: ' || rec.index_name);
    END LOOP;
END;
/

よくある質問

QV$OBJECT_USAGE と DBA_INDEX_USAGE はどちらを使うべきですか?
AOracle 12c R2 以降であれば DBA_INDEX_USAGE を推奨します。使用回数と最終使用日時がわかるため、V$OBJECT_USAGE(使った/使っていないの 2 値)より正確です。11g 以前では V$OBJECT_USAGE が唯一の方法です。
QINVISIBLE にしたインデックスは DML で更新されますか?
A更新されます。INVISIBLE はオプティマイザが SELECT で使わなくなるだけで、INSERT / UPDATE / DELETE 時のインデックス更新は継続します。DML 負荷を軽減するには最終的に DROP INDEX が必要です。
QPRIMARY KEY のインデックスを DROP できますか?
APRIMARY KEY 制約に紐づくインデックスは DROP INDEX 単独では削除できません。ALTER TABLE ... DROP CONSTRAINT pk_name で制約ごと削除するか、ALTER TABLE ... DROP PRIMARY KEY で削除します。ただし PRIMARY KEY の削除は通常推奨されません。
Q監視期間はどのくらい必要ですか?
A最低 1 か月、できれば 2〜3 か月 を推奨します。月末処理・四半期処理・年次処理でのみ使うインデックスを見逃さないためです。監視期間中に全業務サイクルが含まれることが重要です。
QDROP INDEX した後にパフォーマンスが悪化しました
AINVISIBLE 化をスキップして直接 DROP した場合、再作成が必要です。
DBMS_METADATA.GET_DDL で事前に保存した DDL を実行してインデックスを再作成してください。
大テーブルでは再作成に時間がかかるため、ONLINE オプションを使うとテーブルへのアクセスをブロックせずに作成できます。
CREATE INDEX idx ON table(col) ONLINE;
Q不要インデックスの削除でどのくらい性能が改善しますか?
Aテーブルのインデックスが 10 個あり、そのうち 5 個を削除した場合、INSERT の速度が 30〜50% 向上するケースもあります。UPDATE / DELETE も同様に改善されます。効果はテーブルの DML 頻度とインデックスのサイズに依存します。

まとめ

不要インデックスの発見と削除の要点をまとめます。

やりたいこと 方法
使用/未使用を監視(11g) ALTER INDEX … MONITORING USAGE → V$OBJECT_USAGE
使用頻度を分析(12c+) DBA_INDEX_USAGE(total_access_count / last_used)
完全重複を検出 USER_IND_COLUMNS を LISTAGG で結合して一致比較
先頭列冗長を検出 LISTAGG 結果の LIKE 比較(b.cols LIKE a.cols || ‘,%’)
インデックスサイズ確認 USER_SEGMENTS WHERE segment_type = ‘INDEX’
安全に無効化テスト ALTER INDEX … INVISIBLE(1〜4 週間監視 → DROP or VISIBLE)
DDL をバックアップ DBMS_METADATA.GET_DDL(‘INDEX’, index_name)
削除 DROP INDEX index_name(制約付きは ALTER TABLE … DROP CONSTRAINT)

インデックスの作成・再構築・削除の基本は「インデックスの作成・再構築・削除完全ガイド」、インデックス情報の取得は「インデックス情報を取得する方法」も併せて参照してください。