PostgreSQL インデックス削除の完全ガイド:不要なインデックスを安全に削除してDB性能を向上させる方法

データベース・SQL

「インデックスが増えすぎて、どれが必要か分からない…」 「削除したいけど、本番環境で問題が起きたらどうしよう」 「インデックスのメンテナンスでディスク容量が圧迫されている」

PostgreSQLを長期運用していると、こんな悩みが必ず出てきますよね。

実は、不要なインデックスは百害あって一利なしです。ディスク容量を無駄に消費し、INSERT/UPDATE/DELETEの性能を低下させ、バックアップ時間も長くなります。適切にインデックスを削除することで、データベースの性能を20-30%向上させることも可能です。

この記事では、インデックスを安全に削除する方法から、削除前の影響分析、本番環境での実践手順まで、実例とトラブル回避策を交えながら徹底的に解説していきます。


スポンサーリンク

🔍 削除すべきインデックスの特定

未使用インデックスの発見

一度も使われていないインデックス

完全未使用のインデックスを特定:

-- 作成後一度も使われていないインデックス
SELECT 
    schemaname,
    tablename,
    indexrelname as index_name,
    pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
    pg_stat_get_create_time(indexrelid) as created_time,
    idx_scan as total_scans,
    'DROP INDEX ' || schemaname || '.' || indexrelname || ';' as drop_command
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelname NOT LIKE '%_pkey'  -- プライマリキーは除外
ORDER BY pg_relation_size(indexrelid) DESC;

-- 実行結果例
schemaname | tablename | index_name          | index_size | total_scans | drop_command
-----------|-----------|--------------------|-----------:|------------:|------------------
public     | logs      | idx_logs_old       | 458 MB     | 0           | DROP INDEX public.idx_logs_old;
public     | archive   | idx_archive_temp   | 234 MB     | 0           | DROP INDEX public.idx_archive_temp;
public     | users     | idx_users_test     | 12 MB      | 0           | DROP INDEX public.idx_users_test;

使用頻度が極端に低いインデックス

削除候補の優先順位付け:

-- コスト対効果の悪いインデックスを発見
WITH index_stats AS (
    SELECT 
        schemaname,
        tablename,
        indexrelname,
        idx_scan,
        pg_relation_size(indexrelid) as size_bytes,
        pg_stat_get_last_scan_time(indexrelid) as last_used,
        EXTRACT(EPOCH FROM (NOW() - pg_stat_get_last_scan_time(indexrelid)))/86400 as days_since_last_use
    FROM pg_stat_user_indexes
    WHERE schemaname = 'public'
)
SELECT 
    schemaname,
    tablename,
    indexrelname,
    idx_scan as total_scans,
    pg_size_pretty(size_bytes) as index_size,
    ROUND(days_since_last_use::numeric, 1) as days_unused,
    CASE 
        WHEN idx_scan = 0 THEN 'NEVER_USED'
        WHEN days_since_last_use > 90 THEN 'OBSOLETE'
        WHEN days_since_last_use > 30 THEN 'RARELY_USED'
        WHEN idx_scan < 10 AND size_bytes > 10485760 THEN 'INEFFICIENT'
        ELSE 'ACTIVE'
    END as recommendation,
    CASE 
        WHEN idx_scan = 0 OR days_since_last_use > 90 THEN 1
        WHEN days_since_last_use > 30 THEN 2
        WHEN idx_scan < 10 AND size_bytes > 10485760 THEN 3
        ELSE 4
    END as priority
FROM index_stats
WHERE idx_scan < 100  -- 100回未満の使用
   OR days_since_last_use > 30  -- 30日以上未使用
ORDER BY priority, size_bytes DESC;

重複インデックスの検出

完全に重複しているインデックス

同じカラムに複数のインデックス:

-- 重複インデックスを検出して削除候補を提案
WITH index_info AS (
    SELECT 
        n.nspname as schema_name,
        t.relname as table_name,
        i.relname as index_name,
        array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
        ix.indisunique as is_unique,
        ix.indisprimary as is_primary,
        pg_relation_size(i.oid) as size_bytes,
        idx_scan as scan_count
    FROM pg_index ix
    JOIN pg_class t ON t.oid = ix.indrelid
    JOIN pg_class i ON i.oid = ix.indexrelid
    JOIN pg_namespace n ON n.oid = t.relnamespace
    JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
    LEFT JOIN pg_stat_user_indexes si ON si.indexrelid = i.oid
    WHERE n.nspname = 'public'
    GROUP BY n.nspname, t.relname, i.relname, ix.indisunique, ix.indisprimary, i.oid, idx_scan
)
SELECT 
    ii1.table_name,
    ii1.index_name as keep_index,
    ii2.index_name as drop_index,
    ii1.columns as index_columns,
    pg_size_pretty(ii2.size_bytes) as size_to_free,
    ii2.scan_count as drop_index_scans,
    'DROP INDEX ' || ii2.index_name || '; -- Duplicate of ' || ii1.index_name as drop_command
FROM index_info ii1
JOIN index_info ii2 ON 
    ii1.schema_name = ii2.schema_name 
    AND ii1.table_name = ii2.table_name
    AND ii1.columns = ii2.columns
    AND ii1.index_name < ii2.index_name
WHERE NOT ii2.is_primary  -- プライマリキーは削除対象外
ORDER BY ii2.size_bytes DESC;

非効率なインデックスの特定

選択性が低いインデックス

カーディナリティが低いカラムのインデックス:

-- 選択性の低いインデックスを発見
WITH column_stats AS (
    SELECT 
        schemaname,
        tablename,
        attname,
        n_distinct,
        null_frac,
        avg_width,
        correlation
    FROM pg_stats
    WHERE schemaname = 'public'
),
index_columns AS (
    SELECT 
        n.nspname as schema_name,
        t.relname as table_name,
        i.relname as index_name,
        a.attname as column_name,
        pg_relation_size(i.oid) as index_size,
        t.reltuples as table_rows
    FROM pg_index ix
    JOIN pg_class t ON t.oid = ix.indrelid
    JOIN pg_class i ON i.oid = ix.indexrelid
    JOIN pg_namespace n ON n.oid = t.relnamespace
    JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
    WHERE n.nspname = 'public'
    AND array_length(ix.indkey, 1) = 1  -- 単一カラムインデックスのみ
)
SELECT 
    ic.table_name,
    ic.index_name,
    ic.column_name,
    cs.n_distinct,
    CASE 
        WHEN cs.n_distinct > 0 THEN cs.n_distinct
        WHEN cs.n_distinct = -1 THEN ic.table_rows
        ELSE ic.table_rows * (-cs.n_distinct)
    END::BIGINT as estimated_distinct_values,
    pg_size_pretty(ic.index_size) as index_size,
    ROUND((
        CASE 
            WHEN cs.n_distinct > 0 THEN cs.n_distinct
            WHEN cs.n_distinct = -1 THEN ic.table_rows
            ELSE ic.table_rows * (-cs.n_distinct)
        END / GREATEST(ic.table_rows, 1)
    )::numeric * 100, 2) as selectivity_percent,
    CASE 
        WHEN cs.n_distinct BETWEEN 1 AND 10 THEN 'DROP - Very Low Selectivity'
        WHEN (cs.n_distinct > 0 AND cs.n_distinct < 100) THEN 'Consider Dropping'
        ELSE 'Keep'
    END as recommendation
FROM index_columns ic
LEFT JOIN column_stats cs ON 
    ic.schema_name = cs.schemaname 
    AND ic.table_name = cs.tablename 
    AND ic.column_name = cs.attname
WHERE cs.n_distinct IS NOT NULL
ORDER BY selectivity_percent ASC;

⚠️ 削除前の影響分析

依存関係の確認

制約との関連を確認

削除できないインデックスを特定:

-- 制約に使用されているインデックスの確認
SELECT 
    n.nspname as schema_name,
    t.relname as table_name,
    i.relname as index_name,
    con.conname as constraint_name,
    con.contype as constraint_type,
    CASE con.contype
        WHEN 'p' THEN 'PRIMARY KEY'
        WHEN 'u' THEN 'UNIQUE'
        WHEN 'f' THEN 'FOREIGN KEY'
        WHEN 'c' THEN 'CHECK'
        WHEN 'x' THEN 'EXCLUSION'
    END as constraint_type_desc,
    'Cannot drop - Used by constraint: ' || con.conname as warning
FROM pg_index ix
JOIN pg_class t ON t.oid = ix.indrelid
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_constraint con ON con.conindid = i.oid
WHERE n.nspname = 'public'
ORDER BY n.nspname, t.relname, i.relname;

クエリへの影響予測

実行計画の変化を確認

インデックス削除の影響をシミュレート:

-- インデックスを無効化してテスト
BEGIN;

-- インデックスを一時的に無効化
UPDATE pg_index 
SET indisvalid = false 
WHERE indexrelid = 'idx_users_email'::regclass;

-- 影響を受けるクエリの実行計画を確認
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users WHERE email = 'test@example.com';

-- ロールバック(インデックスは元に戻る)
ROLLBACK;

重要なクエリへの影響を事前確認:

-- 特定のインデックスを使用しているクエリを記録
CREATE TABLE index_usage_log (
    index_name TEXT,
    query_text TEXT,
    calls BIGINT,
    mean_time DOUBLE PRECISION,
    total_time DOUBLE PRECISION,
    recorded_at TIMESTAMP DEFAULT NOW()
);

-- pg_stat_statementsから使用状況を記録
INSERT INTO index_usage_log (index_name, query_text, calls, mean_time, total_time)
SELECT 
    'idx_users_email',
    query,
    calls,
    mean_exec_time,
    total_exec_time
FROM pg_stat_statements
WHERE query LIKE '%users%email%'
AND calls > 0;

🔧 安全な削除手順

基本的な削除コマンド

DROP INDEXの基本構文

シンプルな削除:

-- 基本的な削除
DROP INDEX index_name;

-- スキーマを指定して削除
DROP INDEX schema_name.index_name;

-- 存在確認付き削除(エラーを防ぐ)
DROP INDEX IF EXISTS index_name;

-- カスケード削除(依存オブジェクトも削除)
DROP INDEX index_name CASCADE;

-- 削除を制限(依存オブジェクトがある場合はエラー)
DROP INDEX index_name RESTRICT;

CONCURRENTLYオプションでの削除

ロックを最小化した削除

本番環境での推奨方法:

-- 通常の削除(テーブルをロック)
DROP INDEX idx_large_table;  -- 危険:長時間のロック

-- CONCURRENTLYで削除(ロックを最小化)
DROP INDEX CONCURRENTLY idx_large_table;  -- 推奨:ロック最小化

-- 注意点の確認
SELECT 
    pid,
    usename,
    application_name,
    state,
    query,
    wait_event_type,
    wait_event
FROM pg_stat_activity
WHERE query LIKE '%DROP INDEX CONCURRENTLY%';

CONCURRENTLYの制限事項:

-- トランザクション内では使用不可
BEGIN;
DROP INDEX CONCURRENTLY idx_test;  -- ERROR!
COMMIT;

-- 正しい使い方(トランザクション外で実行)
DROP INDEX CONCURRENTLY idx_test;

段階的な削除戦略

本番環境での慎重な削除手順

3段階削除プロセス:

-- ステップ1: インデックスを無効化(即座に元に戻せる)
UPDATE pg_index 
SET indisvalid = false 
WHERE indexrelid = 'idx_to_remove'::regclass;

-- 性能を1週間モニタリング
SELECT 
    schemaname,
    tablename,
    attname,
    avg_width,
    n_distinct
FROM pg_stats
WHERE tablename = 'target_table';

-- ステップ2: 問題なければインデックスの更新を停止
UPDATE pg_index 
SET indisready = false 
WHERE indexrelid = 'idx_to_remove'::regclass;

-- さらに1週間モニタリング

-- ステップ3: 最終的に削除
DROP INDEX IF EXISTS idx_to_remove;

🚀 大量インデックスの一括削除

バッチ削除スクリプト

条件に基づく一括削除

自動削除スクリプト:

-- 削除対象を確認してから実行する関数
CREATE OR REPLACE FUNCTION batch_drop_unused_indexes(
    p_threshold_days INTEGER DEFAULT 90,
    p_min_size_mb INTEGER DEFAULT 10,
    p_dry_run BOOLEAN DEFAULT TRUE
)
RETURNS TABLE(
    action TEXT,
    index_name TEXT,
    table_name TEXT,
    size_freed TEXT,
    command TEXT
) AS $$
DECLARE
    v_index RECORD;
    v_dropped_count INTEGER := 0;
    v_total_size BIGINT := 0;
BEGIN
    FOR v_index IN 
        SELECT 
            si.schemaname,
            si.tablename,
            si.indexrelname,
            pg_relation_size(si.indexrelid) as size_bytes,
            si.idx_scan,
            EXTRACT(EPOCH FROM (NOW() - pg_stat_get_last_scan_time(si.indexrelid)))/86400 as days_unused
        FROM pg_stat_user_indexes si
        JOIN pg_index ix ON si.indexrelid = ix.indexrelid
        WHERE NOT ix.indisprimary  -- プライマリキー除外
        AND NOT ix.indisunique      -- ユニークインデックス除外
        AND si.idx_scan = 0          -- 未使用
        AND pg_relation_size(si.indexrelid) > p_min_size_mb * 1024 * 1024
        ORDER BY pg_relation_size(si.indexrelid) DESC
    LOOP
        IF p_dry_run THEN
            -- ドライラン:実行せずに結果を返す
            RETURN QUERY SELECT 
                'DRY_RUN'::TEXT,
                v_index.indexrelname::TEXT,
                v_index.tablename::TEXT,
                pg_size_pretty(v_index.size_bytes)::TEXT,
                format('DROP INDEX %I.%I;', v_index.schemaname, v_index.indexrelname)::TEXT;
        ELSE
            -- 実際に削除
            BEGIN
                EXECUTE format('DROP INDEX %I.%I', v_index.schemaname, v_index.indexrelname);
                v_dropped_count := v_dropped_count + 1;
                v_total_size := v_total_size + v_index.size_bytes;
                
                RETURN QUERY SELECT 
                    'DROPPED'::TEXT,
                    v_index.indexrelname::TEXT,
                    v_index.tablename::TEXT,
                    pg_size_pretty(v_index.size_bytes)::TEXT,
                    format('DROP INDEX %I.%I;', v_index.schemaname, v_index.indexrelname)::TEXT;
                    
            EXCEPTION WHEN OTHERS THEN
                RETURN QUERY SELECT 
                    'ERROR'::TEXT,
                    v_index.indexrelname::TEXT,
                    v_index.tablename::TEXT,
                    pg_size_pretty(v_index.size_bytes)::TEXT,
                    SQLERRM::TEXT;
            END;
        END IF;
    END LOOP;
    
    IF NOT p_dry_run AND v_dropped_count > 0 THEN
        RETURN QUERY SELECT 
            'SUMMARY'::TEXT,
            format('%s indexes dropped', v_dropped_count)::TEXT,
            ''::TEXT,
            pg_size_pretty(v_total_size)::TEXT,
            format('Total space freed: %s', pg_size_pretty(v_total_size))::TEXT;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- ドライランで確認
SELECT * FROM batch_drop_unused_indexes(90, 10, TRUE);

-- 実際に削除(注意!)
SELECT * FROM batch_drop_unused_indexes(90, 10, FALSE);

スケジュール削除

メンテナンスウィンドウでの計画的削除

時間指定での削除実行:

-- メンテナンス時間に実行する削除ジョブ
CREATE OR REPLACE FUNCTION scheduled_index_cleanup()
RETURNS VOID AS $$
DECLARE
    v_start_time TIMESTAMP;
    v_end_time TIMESTAMP;
    v_index RECORD;
    v_dropped_count INTEGER := 0;
BEGIN
    v_start_time := NOW();
    
    -- 現在時刻がメンテナンスウィンドウか確認
    IF EXTRACT(HOUR FROM NOW()) NOT BETWEEN 2 AND 5 THEN
        RAISE NOTICE 'Not in maintenance window (2-5 AM). Exiting.';
        RETURN;
    END IF;
    
    -- ログテーブルに記録
    INSERT INTO maintenance_log (operation, started_at)
    VALUES ('Index Cleanup', v_start_time);
    
    -- 削除処理
    FOR v_index IN 
        SELECT indexrelname, schemaname
        FROM pg_stat_user_indexes
        WHERE idx_scan = 0
        AND pg_relation_size(indexrelid) > 50 * 1024 * 1024  -- 50MB以上
        LIMIT 5  -- 一度に5個まで
    LOOP
        BEGIN
            EXECUTE format('DROP INDEX CONCURRENTLY %I.%I', 
                          v_index.schemaname, v_index.indexrelname);
            v_dropped_count := v_dropped_count + 1;
            
            -- 各削除後に少し待機
            PERFORM pg_sleep(5);
            
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'Failed to drop %: %', v_index.indexrelname, SQLERRM;
        END;
    END LOOP;
    
    v_end_time := NOW();
    
    -- 完了をログに記録
    UPDATE maintenance_log 
    SET 
        completed_at = v_end_time,
        details = format('Dropped %s indexes', v_dropped_count)
    WHERE operation = 'Index Cleanup' 
    AND started_at = v_start_time;
END;
$$ LANGUAGE plpgsql;

📊 削除後の確認と最適化

削除成功の確認

システムカタログで確認

インデックスが削除されたことを確認:

-- 削除確認クエリ
SELECT 
    CASE 
        WHEN EXISTS (
            SELECT 1 FROM pg_indexes 
            WHERE indexname = 'idx_deleted_index'
        ) THEN 'ERROR: Index still exists!'
        ELSE 'SUCCESS: Index deleted successfully'
    END as deletion_status;

-- 削除後のテーブル状態確認
SELECT 
    t.tablename,
    COUNT(i.indexname) as remaining_indexes,
    pg_size_pretty(pg_relation_size(t.tablename::regclass)) as table_size,
    pg_size_pretty(pg_total_relation_size(t.tablename::regclass) - 
                   pg_relation_size(t.tablename::regclass)) as total_index_size
FROM pg_tables t
LEFT JOIN pg_indexes i ON t.tablename = i.tablename
WHERE t.schemaname = 'public'
AND t.tablename = 'target_table'
GROUP BY t.tablename;

パフォーマンス改善の測定

削除前後の比較

性能改善の確認:

-- 削除前後のINSERT性能測定
-- 削除前
\timing on
INSERT INTO test_table (column1, column2, column3)
SELECT 
    generate_series(1, 10000),
    md5(random()::text),
    NOW();
-- Time: 2543.234 ms

-- インデックス削除後
DROP INDEX idx_test_column2;

-- 削除後
INSERT INTO test_table (column1, column2, column3)
SELECT 
    generate_series(10001, 20000),
    md5(random()::text),
    NOW();
-- Time: 1832.456 ms (28%改善!)

空き領域の回収

VACUUMで領域を整理

削除後のメンテナンス:

-- インデックス削除後の領域回収
-- 通常のVACUUM(領域を再利用可能にする)
VACUUM ANALYZE target_table;

-- VACUUM FULL(領域を完全に回収)
VACUUM FULL target_table;

-- 削除による空き領域の確認
SELECT 
    schemaname,
    tablename,
    pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) as table_size,
    n_live_tup as live_tuples,
    n_dead_tup as dead_tuples,
    ROUND(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) as dead_tuple_ratio
FROM pg_stat_user_tables
WHERE tablename = 'target_table';

⚠️ トラブルシューティング

よくあるエラーと対処法

エラー1:依存関係によるエラー

問題と解決:

-- エラー: cannot drop index because other objects depend on it
DROP INDEX idx_referenced;
-- ERROR: cannot drop index idx_referenced because constraint fk_constraint on table other_table depends on it

-- 解決法1: 依存関係を確認
SELECT 
    dep.classid::regclass AS dependent_class,
    dep.objid::regclass AS dependent_object,
    dep.deptype,
    pg_describe_object(ref.classid, ref.objid, ref.objsubid) AS referenced_object
FROM pg_depend dep
JOIN pg_depend ref ON dep.objid = ref.objid
WHERE ref.objid = 'idx_referenced'::regclass;

-- 解決法2: CASCADEで強制削除(注意!)
DROP INDEX idx_referenced CASCADE;

エラー2:アクティブな接続によるブロック

ロック解除の方法:

-- ロックされている状況を確認
SELECT 
    pid,
    usename,
    application_name,
    query,
    state,
    wait_event_type,
    wait_event,
    pg_blocking_pids(pid) as blocked_by
FROM pg_stat_activity
WHERE query LIKE '%idx_%'
AND state != 'idle';

-- 強制的にセッションを終了(最終手段)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE pid IN (
    SELECT unnest(pg_blocking_pids(pid))
    FROM pg_stat_activity
    WHERE query LIKE '%DROP INDEX%'
);

エラー3:CONCURRENTLYの失敗

CONCURRENTLY特有の問題:

-- エラー: DROP INDEX CONCURRENTLY cannot be executed from a function
-- 関数やトランザクション内では使用不可

-- 解決:直接実行する
\set AUTOCOMMIT on
DROP INDEX CONCURRENTLY idx_large;

-- 失敗した場合のゴーストインデックス確認
SELECT 
    n.nspname,
    c.relname,
    c.relkind
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'i'
AND NOT EXISTS (
    SELECT 1 FROM pg_index i WHERE i.indexrelid = c.oid
);

💡 ベストプラクティス

削除前のチェックリスト

必須確認項目:

-- 包括的な削除前チェック関数
CREATE OR REPLACE FUNCTION pre_drop_index_check(p_index_name TEXT)
RETURNS TABLE(
    check_item TEXT,
    status TEXT,
    details TEXT
) AS $$
BEGIN
    -- 1. インデックスの存在確認
    RETURN QUERY
    SELECT 
        'Index Exists'::TEXT,
        CASE WHEN EXISTS(SELECT 1 FROM pg_indexes WHERE indexname = p_index_name)
             THEN 'OK' ELSE 'ERROR' END,
        CASE WHEN EXISTS(SELECT 1 FROM pg_indexes WHERE indexname = p_index_name)
             THEN 'Index found' ELSE 'Index not found' END;
    
    -- 2. 使用頻度確認
    RETURN QUERY
    SELECT 
        'Usage Statistics'::TEXT,
        CASE WHEN idx_scan > 100 THEN 'WARNING' ELSE 'OK' END,
        format('Used %s times', idx_scan)::TEXT
    FROM pg_stat_user_indexes
    WHERE indexrelname = p_index_name;
    
    -- 3. 制約確認
    RETURN QUERY
    SELECT 
        'Constraint Dependencies'::TEXT,
        CASE WHEN COUNT(*) > 0 THEN 'ERROR' ELSE 'OK' END,
        CASE WHEN COUNT(*) > 0 
             THEN format('%s constraints depend on this index', COUNT(*))
             ELSE 'No constraints' END
    FROM pg_constraint
    WHERE conindid = p_index_name::regclass;
    
    -- 4. サイズ確認
    RETURN QUERY
    SELECT 
        'Index Size'::TEXT,
        'INFO'::TEXT,
        pg_size_pretty(pg_relation_size(p_index_name::regclass))::TEXT;
    
    -- 5. 最終使用日時
    RETURN QUERY
    SELECT 
        'Last Used'::TEXT,
        CASE 
            WHEN pg_stat_get_last_scan_time(indexrelid) IS NULL THEN 'WARNING'
            WHEN pg_stat_get_last_scan_time(indexrelid) < NOW() - INTERVAL '30 days' THEN 'INFO'
            ELSE 'WARNING'
        END,
        COALESCE(pg_stat_get_last_scan_time(indexrelid)::TEXT, 'Never')::TEXT
    FROM pg_stat_user_indexes
    WHERE indexrelname = p_index_name;
END;
$$ LANGUAGE plpgsql;

-- 使用例
SELECT * FROM pre_drop_index_check('idx_users_email');

削除ポリシーの策定

組織的なインデックス管理:

-- インデックス管理ポリシーテーブル
CREATE TABLE index_management_policy (
    policy_id SERIAL PRIMARY KEY,
    policy_name TEXT NOT NULL,
    condition_type TEXT CHECK (condition_type IN ('UNUSED', 'LOW_USE', 'DUPLICATE', 'INEFFICIENT')),
    threshold_days INTEGER,
    threshold_scans INTEGER,
    min_size_mb INTEGER,
    action TEXT CHECK (action IN ('DROP', 'MONITOR', 'REVIEW')),
    auto_execute BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT NOW()
);

-- ポリシー例の登録
INSERT INTO index_management_policy (policy_name, condition_type, threshold_days, threshold_scans, min_size_mb, action, auto_execute)
VALUES 
    ('Drop Never Used Large Indexes', 'UNUSED', 90, 0, 100, 'DROP', FALSE),
    ('Review Rarely Used Indexes', 'LOW_USE', 30, 10, 50, 'REVIEW', FALSE),
    ('Drop Duplicate Indexes', 'DUPLICATE', NULL, NULL, NULL, 'DROP', FALSE),
    ('Monitor Inefficient Indexes', 'INEFFICIENT', NULL, 100, 10, 'MONITOR', FALSE);

ロールバック計画

削除を元に戻す準備:

-- インデックス定義のバックアップ
CREATE TABLE dropped_index_backup (
    backup_id SERIAL PRIMARY KEY,
    index_name TEXT,
    table_name TEXT,
    index_definition TEXT,
    dropped_at TIMESTAMP DEFAULT NOW(),
    dropped_by TEXT DEFAULT CURRENT_USER,
    index_size BIGINT,
    last_scan_count BIGINT
);

-- 削除前に定義を保存
INSERT INTO dropped_index_backup (index_name, table_name, index_definition, index_size, last_scan_count)
SELECT 
    i.indexname,
    i.tablename,
    i.indexdef,
    pg_relation_size(indexrelid),
    idx_scan
FROM pg_indexes i
LEFT JOIN pg_stat_user_indexes s ON i.indexname = s.indexrelname
WHERE i.indexname = 'idx_to_drop';

-- 必要に応じて復元
DO $$
DECLARE
    v_indexdef TEXT;
BEGIN
    SELECT index_definition INTO v_indexdef
    FROM dropped_index_backup
    WHERE index_name = 'idx_to_restore'
    ORDER BY dropped_at DESC
    LIMIT 1;
    
    IF v_indexdef IS NOT NULL THEN
        EXECUTE v_indexdef;
        RAISE NOTICE 'Index restored: %', v_indexdef;
    END IF;
END $$;

📈 自動化とモニタリング

定期的な削除ジョブ

cronで実行する削除スクリプト:

#!/bin/bash
# index_cleanup.sh

DB_NAME="production"
LOG_FILE="/var/log/postgresql/index_cleanup.log"

echo "Starting index cleanup: $(date)" >> $LOG_FILE

psql -d $DB_NAME -c "
    SELECT 
        indexrelname,
        pg_size_pretty(pg_relation_size(indexrelid)) as size,
        'Dropping' as action
    FROM pg_stat_user_indexes
    WHERE idx_scan = 0
    AND pg_relation_size(indexrelid) > 104857600  -- 100MB
    AND schemaname = 'public'
" >> $LOG_FILE

psql -d $DB_NAME -c "
    SELECT * FROM batch_drop_unused_indexes(90, 100, FALSE);
" >> $LOG_FILE

echo "Cleanup completed: $(date)" >> $LOG_FILE

アラート設定

問題のあるインデックスを通知:

-- 削除候補の監視ビュー
CREATE VIEW index_deletion_candidates AS
SELECT 
    schemaname,
    tablename,
    indexrelname,
    pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
    idx_scan as total_scans,
    EXTRACT(DAY FROM NOW() - pg_stat_get_last_scan_time(indexrelid)) as days_unused,
    CASE 
        WHEN idx_scan = 0 AND pg_relation_size(indexrelid) > 536870912 THEN 'CRITICAL'
        WHEN idx_scan = 0 AND pg_relation_size(indexrelid) > 104857600 THEN 'HIGH'
        WHEN idx_scan < 10 AND pg_relation_size(indexrelid) > 52428800 THEN 'MEDIUM'
        ELSE 'LOW'
    END as priority
FROM pg_stat_user_indexes
WHERE idx_scan < 100
   OR EXTRACT(DAY FROM NOW() - pg_stat_get_last_scan_time(indexrelid)) > 30
ORDER BY 
    CASE 
        WHEN idx_scan = 0 AND pg_relation_size(indexrelid) > 536870912 THEN 1
        WHEN idx_scan = 0 AND pg_relation_size(indexrelid) > 104857600 THEN 2
        WHEN idx_scan < 10 AND pg_relation_size(indexrelid) > 52428800 THEN 3
        ELSE 4
    END,
    pg_relation_size(indexrelid) DESC;

📚 まとめ:インデックス削除で性能を向上させる

PostgreSQLインデックス削除の重要ポイント:

未使用・重複・非効率なインデックスを定期的に特定
削除前に必ず依存関係と影響を確認
本番環境ではCONCURRENTLYで安全に削除
段階的削除で リスクを最小化
削除後は必ずVACUUMで領域を回収

不要なインデックスの削除は、データベースの健康維持に不可欠です。

今すぐ実践すべき3つのアクション:

  1. pg_stat_user_indexesで未使用インデックスを確認
  2. 100MB以上の未使用インデックスから削除を検討
  3. 削除前に必ずインデックス定義をバックアップ

適切なインデックス管理で、データベースのパフォーマンスを最大化しましょう!


この記事が役立ったら、データベース管理で悩んでいる仲間にもシェアしてください。一緒に最適なPostgreSQL環境を作りましょう!

コメント

タイトルとURLをコピーしました