【SQL Server】Missing Indexの見方|不足インデックス提案をそのまま作ってはいけない理由

【SQL Server】Missing Indexの見方|不足インデックス提案をそのまま作ってはいけない理由 SQL

SQL Serverの実行計画を見ると、緑色の Missing Index、または「不足インデックス」の提案が表示されることがあります。一見すると、表示された CREATE INDEX をそのまま実行すれば遅いSQLが直りそうに見えます。しかし、Missing Indexはあくまでクエリ最適化時に作られる推定ベースのヒントであり、そのまま本番に作ると重複インデックスや更新負荷の増加を招くことがあります。

この記事では、SQL ServerのMissing Indexの見方、sys.dm_db_missing_index_details などのDMVで候補を確認するSQL、提案をそのまま作ってはいけない理由、既存インデックスとの統合、作成前後の検証手順を実務向けに整理します。

先に結論
Missing Indexは「作るべきインデックスの答え」ではなく、「調査すべき候補」です。提案は単一クエリの最適化時の推定で作られ、列順・既存インデックスとの重複・更新負荷・インデックスサイズまでは十分に判断してくれません。まずDMVやQuery Storeで影響の大きいSQLを絞り、既存インデックスと統合できないか確認し、STATISTICS IO と実行計画で効果測定してから採用します。
スポンサーリンク

Missing Indexとは

Missing Indexは、SQL Serverのクエリオプティマイザが「このような非クラスタ化インデックスがあれば、このクエリのコストを下げられる可能性がある」と判断したときに出す提案です。実行計画上のMissing Index表示や、DMVの sys.dm_db_missing_index_detailssys.dm_db_missing_index_groupssys.dm_db_missing_index_group_stats から確認できます。

実行計画に出るため、遅いSQL調査の入口としてはとても便利です。ただし、実行されたあとに検証された結果ではなく、最適化時点の推定です。そのため、Missing Indexの提案は必ず既存インデックスや実測値と合わせて判断します。

SQL Serverの実行計画の見方

Missing Indexをそのまま作ってはいけない理由

Microsoft Learnでも、Missing Indexの提案には制限があり、そのまま作る処方箋ではなく、インデックス分析・設計・検証の情報源のひとつとして扱うべきだと説明されています。

制限 内容 実務での注意
単一クエリの推定 最適化時の推定であり、実行後に検証されるわけではない Query Storeや実測値で重要SQLか確認する
列順が不完全 等価条件列の順序までは最適に並べてくれない 選択性や既存インデックスを見て列順を決める
提案は非クラスタ化行ストア中心 一意インデックスやフィルターインデックスは提案されない 業務ルールに合う設計を別途検討する
INCLUDE列のサイズ評価が弱い 大量の付加列を提案することがある カバリング目的でも容量と更新負荷を見る
似た提案が増えやすい クエリごとに少し違う候補が出る 既存インデックスと統合して数を抑える
600グループ上限 Missing Indexグループは最大600件まで 放置すると新しい候補が収集されにくくなる

実行計画でMissing Indexを見る

SSMSの実行計画では、Missing Indexの提案が緑色のメッセージとして表示されることがあります。ただし、グラフィカルな実行計画では複数候補のうち一部しか見えないことがあります。より正確に見るには、実行計画XMLで MissingIndexesMissingIndex を検索します。

find-missing-index-in-plan-cache.sql
SELECT TOP (30)
    qs.execution_count,
    qs.total_logical_reads,
    qs.total_worker_time,
    SUBSTRING(
        st.text,
        (qs.statement_start_offset / 2) + 1,
        ((CASE qs.statement_end_offset
            WHEN -1 THEN DATALENGTH(st.text)
            ELSE qs.statement_end_offset
          END - qs.statement_start_offset) / 2) + 1
    ) AS statement_text,
    qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
WHERE CAST(qp.query_plan AS nvarchar(max)) LIKE '%<MissingIndexes>%'
ORDER BY qs.total_logical_reads DESC;

プランキャッシュは再起動、メモリ圧迫、再コンパイルなどで消えます。継続的に見るならQuery Storeの実行計画も確認します。

SQL Server Query Storeの使い方

DMVでMissing Index候補を一覧する

データベース内のMissing Index候補を一覧するには、Missing Index系DMVを結合します。よく使うのは sys.dm_db_missing_index_detailssys.dm_db_missing_index_groupssys.dm_db_missing_index_group_stats です。

missing-index-candidates.sql
SELECT TOP (50)
    DB_NAME(mid.database_id) AS database_name,
    OBJECT_SCHEMA_NAME(mid.object_id, mid.database_id) AS schema_name,
    OBJECT_NAME(mid.object_id, mid.database_id) AS table_name,
    migs.user_seeks,
    migs.user_scans,
    migs.avg_total_user_cost,
    migs.avg_user_impact,
    migs.last_user_seek,
    migs.last_user_scan,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns,
    (migs.avg_total_user_cost * migs.avg_user_impact *
        (migs.user_seeks + migs.user_scans)) AS estimated_improvement
FROM sys.dm_db_missing_index_group_stats AS migs
JOIN sys.dm_db_missing_index_groups AS mig
    ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details AS mid
    ON mid.index_handle = mig.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY estimated_improvement DESC;

estimated_improvement は便利な並び替え指標ですが、絶対的な改善量ではありません。推定コスト、推定影響度、seek/scan回数を掛け合わせた目安として扱います。

優先度の低い候補を除外する

Missing Index候補が多い場合は、まず優先度の低いものを落とします。たとえば、実行回数が少ない、最後に使われた日時が古い、推定改善値が小さい、INCLUDE列が多すぎる候補は、いきなり作成せず後回しにします。

filter-low-value-missing-indexes.sql
SELECT TOP (100)
    OBJECT_SCHEMA_NAME(mid.object_id, mid.database_id) AS schema_name,
    OBJECT_NAME(mid.object_id, mid.database_id) AS table_name,
    migs.user_seeks,
    migs.user_scans,
    migs.last_user_seek,
    migs.last_user_scan,
    migs.avg_total_user_cost,
    migs.avg_user_impact,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns,
    LEN(ISNULL(mid.included_columns, '')) AS included_columns_text_length,
    (migs.avg_total_user_cost * migs.avg_user_impact *
        (migs.user_seeks + migs.user_scans)) AS estimated_improvement,
    CASE
        WHEN (migs.user_seeks + migs.user_scans) < 10 THEN 'LOW_EXECUTIONS'
        WHEN migs.avg_user_impact < 20 THEN 'LOW_IMPACT'
        WHEN LEN(ISNULL(mid.included_columns, '')) > 1000 THEN 'TOO_MANY_INCLUDED_COLUMNS'
        ELSE 'REVIEW'
    END AS review_status
FROM sys.dm_db_missing_index_group_stats AS migs
JOIN sys.dm_db_missing_index_groups AS mig
    ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details AS mid
    ON mid.index_handle = mig.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY estimated_improvement DESC;

上の閾値は例です。重要なのは、Missing Index候補を一覧したあとに、すべてを同じ重みで扱わないことです。業務上重要な画面・API・バッチで繰り返し発生している候補から優先します。

DMVの列の読み方

意味 インデックス設計での扱い
equality_columns = のような等価条件に使われた列 キー列の前半候補。列順は選択性や既存インデックスを見て決める
inequality_columns ><BETWEEN などの範囲条件列 等価条件列の後ろに置くことが多い
included_columns SELECTや出力に必要な付加列 INCLUDE 候補。ただし増やしすぎに注意する
avg_user_impact インデックス追加でコストが下がる割合の推定 高くても実測で確認する
user_seeks / user_scans 候補が役立つと推定された回数 累積値なので起動時刻やリセットタイミングを見る

SQL Serverのカバリングインデックスとは

CREATE INDEX文を生成する例

DMVの情報からCREATE INDEX文のたたき台を作ることはできます。ただし、このSQLが出力した文をそのまま本番実行するのではなく、レビュー用の候補として使います。

generate-create-index-candidates.sql
SELECT TOP (30)
    'CREATE INDEX IX_' +
        REPLACE(REPLACE(OBJECT_NAME(mid.object_id, mid.database_id), '[', ''), ']', '') +
        '_missing_' + CONVERT(varchar(20), mid.index_handle) +
        ' ON ' + mid.statement + ' (' +
        ISNULL(mid.equality_columns, '') +
        CASE
            WHEN mid.equality_columns IS NOT NULL
             AND mid.inequality_columns IS NOT NULL THEN ', '
            ELSE ''
        END +
        ISNULL(mid.inequality_columns, '') + ')' +
        ISNULL(' INCLUDE (' + mid.included_columns + ')', '') AS create_index_candidate,
    migs.user_seeks,
    migs.user_scans,
    migs.avg_total_user_cost,
    migs.avg_user_impact
FROM sys.dm_db_missing_index_group_stats AS migs
JOIN sys.dm_db_missing_index_groups AS mig
    ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details AS mid
    ON mid.index_handle = mig.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY migs.avg_total_user_cost * migs.avg_user_impact *
         (migs.user_seeks + migs.user_scans) DESC;

生成された文を見たら、次の観点で必ず修正します。インデックス名を命名規則に合わせる、列順を見直す、INCLUDE列を削る、既存インデックスと統合する、一意性やフィルター条件を考慮する、更新負荷と容量を確認する、という流れです。

既存インデックスと統合する

Missing Index候補を見る前に、対象テーブルの既存インデックスを確認します。同じ先頭列を持つ似たインデックスがすでにあるなら、新規作成ではなく統合や拡張で済むことがあります。

check-existing-indexes.sql
SELECT
    i.name AS index_name,
    i.type_desc,
    i.is_unique,
    ic.key_ordinal,
    ic.is_included_column,
    c.name AS column_name
FROM sys.indexes AS i
JOIN sys.index_columns AS ic
    ON ic.object_id = i.object_id
   AND ic.index_id = i.index_id
JOIN sys.columns AS c
    ON c.object_id = ic.object_id
   AND c.column_id = ic.column_id
WHERE i.object_id = OBJECT_ID(N'dbo.Orders')
ORDER BY i.name, ic.key_ordinal, ic.index_column_id;

似たインデックスが増えると、SELECTは速くなるかもしれませんが、INSERT、UPDATE、DELETE、バックアップ、メンテナンス、メモリ使用量に悪影響が出ます。既存インデックスの使用状況も合わせて見ます。

check-index-usage-before-create.sql
SELECT
    OBJECT_SCHEMA_NAME(i.object_id) AS schema_name,
    OBJECT_NAME(i.object_id) AS table_name,
    i.name AS index_name,
    us.user_seeks,
    us.user_scans,
    us.user_lookups,
    us.user_updates,
    us.last_user_seek,
    us.last_user_scan,
    us.last_user_update
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS us
    ON us.object_id = i.object_id
   AND us.index_id = i.index_id
   AND us.database_id = DB_ID()
WHERE i.object_id = OBJECT_ID(N'dbo.Orders')
  AND i.name IS NOT NULL
ORDER BY us.user_updates DESC, us.user_seeks DESC;

列順を決める

Missing Indexの提案では、等価条件列と範囲条件列は分かりますが、等価条件列の最適な順番まで完全には決めてくれません。一般的には、等価条件列を先に置き、その中では選択性の高い列を左側に置くことを検討します。その後ろに範囲条件列を置き、必要な返却列をINCLUDEに回します。

条件 置き場所の目安
等価条件 キー列の前半 CustomerID = @CustomerID
範囲条件 等価条件列の後ろ OrderDate >= @FromDate
並び替え キー列で吸収できるか確認 ORDER BY OrderDate DESC
返すだけの列 INCLUDE TotalAmountStatus

複合インデックスの列順は、今後単独記事でさらに深掘りできます。この記事では、Missing Index候補をそのまま採用せず、列の役割を分けて設計し直すことを重視します。

作成前にサイズと更新負荷を見る

候補インデックスの列が多いほど、インデックスは大きくなります。INCLUDE列が多い候補は、特にサイズと更新負荷を確認します。

check-table-index-size.sql
SELECT
    OBJECT_SCHEMA_NAME(i.object_id) AS schema_name,
    OBJECT_NAME(i.object_id) AS table_name,
    i.name AS index_name,
    SUM(ps.used_page_count) * 8.0 / 1024 AS used_mb,
    SUM(ps.row_count) AS row_count
FROM sys.indexes AS i
JOIN sys.dm_db_partition_stats AS ps
    ON ps.object_id = i.object_id
   AND ps.index_id = i.index_id
WHERE i.object_id = OBJECT_ID(N'dbo.Orders')
GROUP BY i.object_id, i.name
ORDER BY used_mb DESC;

SQL Serverのインデックス断片化を確認・再構築する方法

作成前後で効果を検証する

インデックスを作る前後で、同じSQL、同じパラメータ、同じ条件で比較します。実行時間だけでなく、logical reads、実行計画、Key Lookupの有無、Sortの有無、更新負荷も見ます。

measure-before-after.sql
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

-- 作成前後で同じ条件で比較する
SELECT
    OrderID,
    CustomerID,
    OrderDate,
    TotalAmount,
    Status
FROM dbo.Orders
WHERE CustomerID = 100
  AND OrderDate >= '2026-05-01'
  AND OrderDate <  '2026-06-01';

SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;

Missing Indexの提案を採用しても、暗黙変換、列への関数適用、LIKEの前方ワイルドカード、統計情報の古さが残っていると、期待どおりに使われないことがあります。

SQL Serverでインデックスが使われない原因

SQL Serverで統計情報を更新する方法

採用判断チェックリスト

最終的にインデックスを作る前に、次の項目を確認します。ひとつでも不安が残る場合は、検証環境での比較や既存インデックスとの統合案を先に作ります。

確認項目 採用しやすい状態 見送り・再設計しやすい状態
対象SQL Query Storeで実行回数や累積読み取りが大きい 一度だけ実行されたSQL、業務影響が小さいSQL
既存インデックス 同じ役割のインデックスがなく、統合案も明確 似たインデックスが複数あり、重複が増える
列順 等価条件、範囲条件、ORDER BYの役割を説明できる DMVの出力順をそのまま信じている
INCLUDE列 返却に必要な列だけに絞れている SELECT列を全部入れてインデックスが大きくなる
更新負荷 INSERT/UPDATE/DELETEへの影響を許容できる 更新が多い表で保守時間やログ容量が読めない
効果測定 作成前後の実行計画とlogical readsを比較できる 速くなったか確認する手段がない

Query Storeで継続的に見る

Missing IndexのDMVやプランキャッシュは、再起動やフェイルオーバー、DBのオフライン化などで消えることがあります。継続的に見たい場合はQuery Storeを使い、実行回数やlogical readsの大きいSQLから優先順位を決めます。

query-store-plans-with-missing-index.sql
SELECT TOP (20)
    qsq.query_id,
    SUM(qrs.count_executions) AS executions,
    AVG(qrs.avg_logical_io_reads) AS avg_logical_reads,
    SUM(qrs.count_executions) * AVG(qrs.avg_logical_io_reads) AS estimated_total_reads,
    MAX(qsp.plan_id) AS sample_plan_id,
    MIN(qsrsi.start_time) AS first_seen,
    MAX(qsrsi.end_time) AS last_seen
FROM sys.query_store_query AS qsq
JOIN sys.query_store_plan AS qsp
    ON qsp.query_id = qsq.query_id
JOIN sys.query_store_runtime_stats AS qrs
    ON qrs.plan_id = qsp.plan_id
JOIN sys.query_store_runtime_stats_interval AS qsrsi
    ON qsrsi.runtime_stats_interval_id = qrs.runtime_stats_interval_id
WHERE qsp.query_plan LIKE N'%<MissingIndexes>%'
  AND qsrsi.start_time >= DATEADD(HOUR, -48, SYSDATETIME())
GROUP BY qsq.query_id
ORDER BY estimated_total_reads DESC;

Query Storeで候補を絞ると、単発の重いSQLではなく、累積で効いているSQLを見つけやすくなります。パラメータ値によって実行計画が変わる場合は、パラメータスニッフィングも合わせて確認します。

SQL Serverのパラメータスニッフィングとは

権限とリセットタイミング

Missing Index系DMVの参照には、SQL Serverでは VIEW SERVER STATE、SQL Server 2022以降では VIEW SERVER PERFORMANCE STATE が必要になる場合があります。Azure SQL Databaseではサービスレベルやロールによって必要権限が変わります。

また、Missing Index情報は永続化されません。SQL Serverの再起動、フェイルオーバー、DBのオフライン化、対象テーブルのメタデータ変更、そのテーブルへの ALTER INDEX などで消えることがあります。重要な候補は、定期的に保存するかQuery Storeから追えるようにしておきます。

check-sql-server-start-time.sql
SELECT sqlserver_start_time
FROM sys.dm_os_sys_info;

やってはいけないパターン

NG なぜ危険か 代わりにやること
実行計画のMissing Indexをそのまま作る 既存インデックスとの重複や列順の問題を見落とす 既存インデックスと統合して候補を作り直す
提案が出たSQLだけを見る 他のSQLの更新負荷や実行計画に悪影響が出る Query Storeでワークロード全体を見る
INCLUDE列を全部採用する インデックスが大きくなりすぎる 返却に本当に必要な列だけに絞る
似たインデックスを何本も作る 更新負荷、容量、保守時間が増える 同じ先頭キーのインデックスを統合する
効果測定なしで本番投入する 速くなったか判断できない STATISTICS IOと実行計画を保存して比較する

よくある質問

Missing Indexが出たら必ずインデックス不足ですか?

必ずではありません。暗黙変換、統計情報の古さ、パラメータ値の偏り、SQLの書き方が原因で、別のインデックス候補が出ているだけの場合もあります。実行計画と実測値を確認してから判断します。

Missing Indexの推奨インデックス名をそのまま使ってよいですか?

避けたほうがよいです。生成SQLは調査用の候補です。命名規則に合わせ、対象テーブル、主要キー列、用途が分かる名前にします。

Missing IndexのDMVに出ないなら問題はありませんか?

そうとは限りません。提案はtrivial planでは出ないことがあり、再起動などで消えます。また、一意インデックスやフィルターインデックスのような候補は提案されません。実行計画、Query Store、業務要件を合わせて見ます。

作ったインデックスが使われているかはどう確認しますか?

実行計画で対象インデックスが使われているか、STATISTICS IO のlogical readsが減ったか、sys.dm_db_index_usage_stats のseek/scanが増えているかを確認します。ただし、usage statsも再起動などでリセットされます。

参考

Tune nonclustered indexes with missing index suggestions – Microsoft Learn

sys.dm_db_missing_index_details – Microsoft Learn

まとめ

SQL ServerのMissing Indexは、遅いSQL調査の入口として便利です。ただし、提案は単一クエリの最適化時の推定であり、列順、既存インデックスとの重複、INCLUDE列のサイズ、更新負荷、ワークロード全体への影響までは十分に判断してくれません。

Missing Indexを見つけたら、まずDMVやQuery Storeで影響の大きい候補を絞り、既存インデックスと統合できないか確認します。そのうえで STATISTICS IO、実行計画、Query Storeで作成前後を比較し、本当に効果のあるインデックスだけを採用しましょう。