インデックスは 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 で「使われたかどうか」を確認します。
-- 特定のインデックスの監視を開始
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;
/
-- 監視結果を確認
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 のインデックスが「監視期間中に一度も使われなかった」候補
-- 監視を停止 ALTER INDEX idx_emp_dept NOMONITORING USAGE;
・「使われた / 使われなかった」の 2 値しかわからない(使用頻度は不明)
・月末バッチなど低頻度で使うインデックスは見逃す可能性がある
・監視期間は1 か月以上(できれば月末処理を含む期間)を推奨
・11g 以前のメインの方法。12c 以降は DBA_INDEX_USAGE が推奨
DBA_INDEX_USAGE で使用頻度を分析する(12c 以降)
Oracle 12c R2 以降の DBA_INDEX_USAGE ビューは、インデックスの使用回数・最終使用日時を自動的に記録します。MONITORING 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;
-- 最終使用日が 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);
V$OBJECT_USAGE と異なり、使用回数・最終使用日時がわかるため、「1 回だけ月末に使われた」インデックスも正確に判断できます。12c R2 以降であれば V$OBJECT_USAGE より DBA_INDEX_USAGE を優先してください。
重複インデックスを検出する
同じ列(または先頭列が同じ)のインデックスが複数存在する場合、片方は不要な可能性が高いです。
完全重複(全列が同じ)の検出
-- 列の組み合わせが完全に同じインデックスのペアを検出
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;
先頭列が冗長(包含関係)の検出
-- 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;
インデックス情報の詳しい取得方法は「インデックス情報を取得する方法」を参照してください。
インデックスのサイズを確認する
-- インデックスのサイズを確認(削除による空き容量の見積もり)
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 に戻せます。
-- (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;
・
DROP INDEX すると再作成に時間がかかる(大テーブルでは数時間)・
INVISIBLE なら VISIBLE に戻すだけで即復旧(数秒)・データもインデックス自体も消えないため、リスクがほぼゼロ
本番環境での削除は必ず INVISIBLE → 検証 → DROP の順序で行ってください。
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 で復旧 | 即時 |
-- 削除前にインデックスの 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 で検索 |
| 月末 / 年次バッチでのみ使うインデックス | 普段は未使用だが特定タイミングで必須 | 業務担当者に確認 |
-- 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): 全テーブルの未使用インデックスを一括検出
-- 作成から 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): インデックス過多のテーブルを検出
-- インデックスが 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 化スクリプト
-- 未使用インデックスを一括で 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;
/
よくある質問
DROP INDEX 単独では削除できません。ALTER TABLE ... DROP CONSTRAINT pk_name で制約ごと削除するか、ALTER TABLE ... DROP PRIMARY KEY で削除します。ただし PRIMARY KEY の削除は通常推奨されません。DBMS_METADATA.GET_DDL で事前に保存した DDL を実行してインデックスを再作成してください。大テーブルでは再作成に時間がかかるため、ONLINE オプションを使うとテーブルへのアクセスをブロックせずに作成できます。
CREATE INDEX idx ON table(col) ONLINE;まとめ
不要インデックスの発見と削除の要点をまとめます。
| やりたいこと | 方法 |
|---|---|
| 使用/未使用を監視(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) |
インデックスの作成・再構築・削除の基本は「インデックスの作成・再構築・削除完全ガイド」、インデックス情報の取得は「インデックス情報を取得する方法」も併せて参照してください。

