【SQL Server】インデックスが使われない原因|暗黙変換・関数・LIKE・複合インデックスの落とし穴

【SQL Server】インデックスが使われない原因|暗黙変換・関数・LIKE・複合インデックスの落とし穴 SQL

SQL Serverでインデックスを作ったのに、実行計画を見ると Index Seek にならず、Index ScanTable Scan になっていることがあります。インデックスが存在していても、SQLの書き方、データ型、条件の形、統計情報、取得列によっては、SQL Serverがそのインデックスを効率よく使えません。

この記事では、SQL Serverでインデックスが使われない原因を、暗黙変換、列側関数、LIKE、複合インデックスの列順、OR条件、Key Lookup、統計情報、実行計画の確認方法まで実務向けに整理します。

先に結論
インデックスが使われないときは、まず実行計画で Seek PredicatePredicateCONVERT_IMPLICITIndex ScanKey Lookup、推定行数と実際の行数を確認します。よくある原因は、列側に関数をかけている、データ型が合わず暗黙変換が起きている、前方一致ではないLIKEを使っている、複合インデックスの先頭列を使っていない、取得列が多くLookupが大量発生している、統計情報が古い、計算列やフィルターインデックスの条件が合っていない、の7つです。
スポンサーリンク

まず見るチェックリスト

インデックスが使われない原因を探すときは、次の順番で確認すると迷いにくいです。

  1. 実行計画で Index SeekIndex ScanTable Scan のどれかを見る
  2. Seek PredicatePredicate のどちらに条件が入っているかを見る
  3. CONVERT_IMPLICIT や暗黙変換の警告がないかを見る
  4. WHERE句で列側に関数や計算をかけていないかを見る
  5. LIKEが前方一致か、先頭ワイルドカードかを見る
  6. 複合インデックスの先頭列を条件に使っているかを見る
  7. フィルターインデックスのWHERE条件とクエリ条件が一致しているかを見る
  8. 計算列インデックスを使うためのSETオプションが合っているかを見る
  9. Key Lookup が大量に繰り返されていないかを見る
  10. 統計情報が古く、推定行数が外れていないかを見る

実行計画そのものの読み方は、次の記事で詳しく整理しています。

SQL Serverの実行計画の見方

インデックスがあるのに使われない主な原因

原因 起きること 確認する場所
暗黙変換 列側が変換され、Seekしにくくなる CONVERT_IMPLICIT、実行計画の警告
列側関数 インデックスキーの値をそのまま検索できない WHERE句、Seek Predicate
LIKEの先頭ワイルドカード 先頭から範囲を絞れない LIKE '%abc'LIKE '%abc%'
複合インデックスの列順不一致 先頭列を使えず、広く読む インデックス定義、WHERE句、ORDER BY
取得列が多い Key Lookup が大量発生する Lookup回数、出力列、INCLUDE列
フィルターインデックスの条件不一致 小さいインデックスが使われない インデックス定義のWHERE句、クエリ条件
計算列インデックスのSETオプション不一致 作成済みインデックスがオプティマイザーに無視される SETオプション、計算列の決定性
統計情報が古い 行数見積もりが外れ、別の計画が選ばれる 推定行数と実際の行数、統計情報更新日時

原因1: 暗黙変換でインデックスが使われない

列とパラメータ、列とリテラルのデータ型が違うと、SQL Serverが暗黙変換を行うことがあります。Microsoft Learnでは、異なるデータ型の式を比較するとき、データ型の優先順位に従って低い優先順位の型が高い優先順位の型へ変換されると説明されています。この変換が列側に発生すると、インデックスを効率よく使えないことがあります。

implicit-conversion-bad.sql
-- CustomerCode列がvarchar(20)なのに、nvarcharパラメータで比較している例
DECLARE @CustomerCode nvarchar(20) = N'C001';

SELECT OrderID, CustomerCode, OrderDate
FROM dbo.Orders
WHERE CustomerCode = @CustomerCode;

列が varchar、パラメータが nvarchar のように型が合っていない場合、照合順序や型の優先順位によって、列側に CONVERT_IMPLICIT が出ることがあります。実行計画で暗黙変換の警告や CONVERT_IMPLICIT を確認します。

implicit-conversion-good.sql
-- 列定義に合わせてパラメータ型をそろえる
DECLARE @CustomerCode varchar(20) = 'C001';

SELECT OrderID, CustomerCode, OrderDate
FROM dbo.Orders
WHERE CustomerCode = @CustomerCode;

アプリケーション側のパラメータ型も重要です。.NETやORMからSQL Serverへ渡す型が、テーブル定義と合っているか確認します。文字列、数値、日付で型が微妙に違うだけでも、実行計画に影響することがあります。

暗黙変換を実行計画から探す

キャッシュされた実行計画XMLから CONVERT_IMPLICIT を含むSQLを探すこともできます。調査の入口として使い、見つかったSQLを個別に確認します。

find-convert-implicit-in-plan-cache.sql
SELECT TOP (50)
    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 '%CONVERT_IMPLICIT%'
ORDER BY qs.total_logical_reads DESC;

継続的に見るならQuery Storeも有効です。Query Storeで重いSQLを見つけ、その実行計画に暗黙変換がないか確認します。

SQL Server Query Storeの使い方

原因2: 列側に関数をかけている

WHERE句で列側に関数や計算をかけると、インデックスキーの値をそのまま使って範囲を絞りにくくなります。典型例は、日付列に CONVERTYEAR をかける書き方です。

function-on-column-bad.sql
-- 悪い例: OrderDate列に関数をかけている
SELECT OrderID, OrderDate, CustomerID
FROM dbo.Orders
WHERE CONVERT(date, OrderDate) = '2026-05-01';
function-on-column-good.sql
-- 改善例: 列側を加工せず、範囲条件にする
SELECT OrderID, OrderDate, CustomerID
FROM dbo.Orders
WHERE OrderDate >= '2026-05-01'
  AND OrderDate <  '2026-05-02';

この書き方なら、OrderDate のインデックスで範囲検索しやすくなります。日付のみ比較する方法は次の記事でも整理しています。

SQLで時間を無視して日付のみで比較する方法

原因3: LIKEの先頭ワイルドカード

LIKE 'abc%' のような前方一致は、インデックスで範囲を絞れることがあります。一方、LIKE '%abc'LIKE '%abc%' のように先頭がワイルドカードの場合、先頭から値を探せないため、広く読む必要が出ます。

条件 インデックス利用のしやすさ 理由
LIKE 'abc%' 使いやすい 先頭から範囲を絞れる
LIKE '%abc' 使いにくい 先頭が不明で範囲を絞れない
LIKE '%abc%' 使いにくい 任意位置検索になる
LIKE @keyword + '%' 条件次第 パラメータ値と統計情報の影響を受ける
like-prefix-search.sql
-- 前方一致。Name列のインデックスを使いやすい
SELECT CustomerID, CustomerName
FROM dbo.Customers
WHERE CustomerName LIKE 'tanaka%';

-- 部分一致。通常のB-treeインデックスでは絞りにくい
SELECT CustomerID, CustomerName
FROM dbo.Customers
WHERE CustomerName LIKE '%tanaka%';

部分一致検索が重要な要件なら、通常のB-treeインデックスだけで解決しようとせず、全文検索、検索用の別列、N-gramのような検索設計を検討します。

原因4: 複合インデックスの列順が合っていない

複合インデックスは、列順が重要です。たとえば (CustomerID, OrderDate) のインデックスは、CustomerID を条件に使う検索に向いています。しかし OrderDate だけで検索する場合は、先頭列を使えないため、効きにくいことがあります。

composite-index-order.sql
CREATE INDEX IX_Orders_CustomerID_OrderDate
ON dbo.Orders (CustomerID, OrderDate);

-- 使いやすい: 先頭列CustomerIDを条件に使っている
SELECT OrderID, CustomerID, OrderDate
FROM dbo.Orders
WHERE CustomerID = 100
  AND OrderDate >= '2026-05-01';

-- 使いにくい: 先頭列CustomerIDを使っていない
SELECT OrderID, CustomerID, OrderDate
FROM dbo.Orders
WHERE OrderDate >= '2026-05-01';

複合インデックスの列順は、WHERE句の等価条件、範囲条件、JOIN条件、ORDER BY、実行回数を見て決めます。単に条件に出てくる列を全部並べればよいわけではありません。

原因5: フィルターインデックスの条件が合っていない

フィルターインデックスは、テーブル全体ではなく一部の行だけを対象にする非クラスタ化インデックスです。Microsoft Learnでも、よく定義された部分集合を選択するクエリに適しており、フルテーブルの非クラスタ化インデックスより小さく、保守コストやストレージを減らせると説明されています。

filtered-index-example.sql
-- 未処理の注文だけを対象にするフィルターインデックス
CREATE INDEX IX_Orders_Unprocessed
ON dbo.Orders (OrderDate)
INCLUDE (CustomerID, TotalAmount)
WHERE ProcessedFlag = 0;

-- 使いやすい: フィルター条件と一致している
SELECT OrderID, CustomerID, OrderDate, TotalAmount
FROM dbo.Orders
WHERE ProcessedFlag = 0
  AND OrderDate >= '2026-05-01';

-- 使いにくい: フィルター条件がクエリから判断しにくい
DECLARE @ProcessedFlag bit = 0;

SELECT OrderID, CustomerID, OrderDate, TotalAmount
FROM dbo.Orders
WHERE ProcessedFlag = @ProcessedFlag
  AND OrderDate >= '2026-05-01';

フィルターインデックスを使わせたい場合は、クエリ条件がフィルター条件の部分集合だとSQL Serverが判断できる必要があります。また、フィルターインデックスのフィルター条件では LIKE はサポートされません。部分一致検索のためにフィルターインデックスを作る設計は避けます。

原因6: 計算列インデックスの条件を満たしていない

列側関数を避けられない場合、計算列を作ってその計算列にインデックスを作る方法があります。ただし、計算列インデックスには決定性、精度、データ型、SETオプションなどの条件があります。Microsoft Learnでは、QUOTED_IDENTIFIER をONにする必要があり、さらにSELECT実行側の接続でも必要なSETオプションが一致しないと、オプティマイザーがそのインデックスを無視すると説明されています。

computed-column-index-example.sql
-- 日付だけで検索するための計算列例
ALTER TABLE dbo.Orders
ADD OrderDateOnly AS CONVERT(date, OrderDate) PERSISTED;

CREATE INDEX IX_Orders_OrderDateOnly
ON dbo.Orders (OrderDateOnly);

SELECT OrderID, CustomerID, OrderDate
FROM dbo.Orders
WHERE OrderDateOnly = '2026-05-01';
computed-column-required-set-options.sql
-- 計算列インデックスやインデックス付きビューで重要なSETオプション
SET ANSI_NULLS ON;
SET ANSI_PADDING ON;
SET ANSI_WARNINGS ON;
SET ARITHABORT ON;
SET CONCAT_NULL_YIELDS_NULL ON;
SET QUOTED_IDENTIFIER ON;
SET NUMERIC_ROUNDABORT OFF;

計算列インデックスは便利ですが、関数を使った検索をすべて計算列で解決するものではありません。まずは列側を加工しない範囲条件にできないかを検討し、それでも難しい場合に候補にします。

原因7: OR条件で広く読まれる

OR 条件では、複数の条件をまとめて評価するため、インデックスを効率よく使いにくいことがあります。条件ごとに使いたいインデックスが違う場合は、UNION ALL で分けると改善することがあります。

or-condition-vs-union-all.sql
-- OR条件で広く読まれることがある
SELECT OrderID, CustomerID, Status, OrderDate
FROM dbo.Orders
WHERE CustomerID = 100
   OR Status = 'Cancelled';

-- 条件ごとに分ける案。重複が出る条件なら除外条件やUNIONを検討する
SELECT OrderID, CustomerID, Status, OrderDate
FROM dbo.Orders
WHERE CustomerID = 100
UNION ALL
SELECT OrderID, CustomerID, Status, OrderDate
FROM dbo.Orders
WHERE Status = 'Cancelled'
  AND CustomerID <> 100;

ただし、UNION ALL へ書き換えれば必ず速くなるわけではありません。重複条件、結果件数、ソート有無、実行計画を比較して判断します。

原因8: Key Lookupが大量に発生している

インデックス自体は使われていても、取得列がインデックスに含まれていないと、Key Lookup で不足列を取りに行くことがあります。少数行なら問題になりにくいですが、大量行でLookupが繰り返されると遅くなります。

key-lookup-example.sql
CREATE INDEX IX_Orders_CustomerID
ON dbo.Orders (CustomerID);

-- CustomerIDではSeekできても、取得列が多いとLookupが発生することがある
SELECT
    OrderID,
    CustomerID,
    OrderDate,
    TotalAmount,
    ShippingAddress
FROM dbo.Orders
WHERE CustomerID = 100;

よく使う取得列を INCLUDE すると、Lookupを減らせることがあります。Microsoft Learnでも、非キー列を含めることで、クエリが必要とする列をインデックス内で満たし、テーブルやクラスタ化インデックスへのアクセスを避けられると説明されています。

include-column-example.sql
CREATE INDEX IX_Orders_CustomerID_Covering
ON dbo.Orders (CustomerID)
INCLUDE (OrderDate, TotalAmount, ShippingAddress);

ただし、INCLUDE列を増やしすぎるとインデックスが大きくなり、更新負荷も増えます。実行回数が多いSQL、業務上重要なSQL、logical readsが大きいSQLから優先して検討します。

原因9: 統計情報が古い

インデックスがあっても、統計情報が古いと行数見積もりが外れ、期待したインデックスが選ばれないことがあります。実行計画で推定行数と実際の行数が大きくズレる場合は、統計情報も確認します。

check-stats-for-index.sql
SELECT
    s.name AS stats_name,
    sp.last_updated,
    sp.rows,
    sp.rows_sampled,
    sp.modification_counter
FROM sys.stats AS s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
WHERE s.object_id = OBJECT_ID(N'dbo.Orders')
ORDER BY sp.modification_counter DESC;

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

原因10: パラメータ値によって最適な計画が違う

あるパラメータ値では少数行、別の値では大量行を返す場合、同じインデックスでも最適な実行計画が変わります。少数行向けの計画が大量行に再利用されると、Lookupが大量発生して遅くなることがあります。

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

Query Storeを使うと、同じSQLに複数の実行計画があるか、計画変更で遅くなったかを追いやすくなります。

SQL Server Query Storeの使い方

インデックス利用状況を確認する

既存インデックスが実際に使われているかは、sys.dm_db_index_usage_stats で確認できます。ただし、この情報はSQL Server再起動やDBのデタッチなどでリセットされます。一時点の参考情報として扱います。

index-usage-stats.sql
SELECT
    OBJECT_SCHEMA_NAME(i.object_id) AS schema_name,
    OBJECT_NAME(i.object_id) AS table_name,
    i.name AS index_name,
    i.index_id,
    i.type_desc,
    us.user_seeks,
    us.user_scans,
    us.user_lookups,
    us.user_updates,
    us.last_user_seek,
    us.last_user_scan,
    us.last_user_lookup,
    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 OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
ORDER BY us.user_seeks DESC, us.user_scans DESC;

使われていないように見えるインデックスでも、月次処理や障害対応で使われることがあります。削除判断は、業務処理、実行頻度、メンテナンス履歴を確認してから行います。

Missing Indexはそのまま作らない

実行計画にMissing Indexが出ることがあります。これは有用なヒントですが、そのまま本番で作るものではありません。既存インデックスとの重複、列順、INCLUDE列、更新負荷、他SQLへの影響を確認します。

missing-index-candidates.sql
SELECT TOP (30)
    DB_NAME(mid.database_id) AS database_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,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns
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;

Missing Indexの提案は、実行されたSQL単体に寄った候補です。似たインデックスを増やしすぎると、INSERT/UPDATE/DELETEが遅くなり、容量も増えます。

改善前後を必ず測る

インデックスやSQLを直したら、実行計画だけでなく、STATISTICS IOSTATISTICS TIME、実行回数、logical readsを比較します。

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

-- 改善前後で同じ条件・同じパラメータで比較する
SELECT OrderID, CustomerID, OrderDate, TotalAmount
FROM dbo.Orders
WHERE CustomerID = 100
  AND OrderDate >= '2026-05-01'
  AND OrderDate <  '2026-06-01';

SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;

改善後に tempdb の使用量が減ることもあります。SortやHash Matchが減ったか、Spillが消えたかも確認します。

SQL Serverでtempdbが肥大化する原因と確認方法

やってはいけない対応

NG対応 なぜ危ないか 代わりにやること
インデックスを闇雲に増やす 更新負荷と容量が増え、他SQLに悪影響が出る 重要SQLから実測して追加する
Missing Indexを全部作る 重複インデックスが増えやすい 既存インデックスと統合する
Scanをすべて悪者にする 大量取得ではScanが妥当なこともある 取得割合とlogical readsで判断する
列側関数を放置する SeekしにくいSQLが残る 範囲条件や計算列を検討する
型不一致をアプリ側で放置する 暗黙変換が再発する パラメータ型と列定義をそろえる
効果測定なしで本番反映する 速くなったか判断できない 実行計画とIOを保存して比較する

よくある質問

Index Scanは必ず悪いですか?

必ず悪いわけではありません。テーブルやインデックスの大部分を読むSQLでは、Scanのほうが妥当なことがあります。少数行を取りたいのにScanになっている場合に原因を調べます。

暗黙変換はどこで確認できますか?

実行計画の警告、Predicate、XML内の CONVERT_IMPLICIT で確認できます。列側に変換が出ている場合は、パラメータ型やリテラル型を列定義に合わせます。

LIKE検索でインデックスを使うにはどうしますか?

前方一致の LIKE 'abc%' は使いやすいです。先頭ワイルドカードの LIKE '%abc%' は通常のB-treeインデックスでは効きにくいため、全文検索や検索用設計を検討します。

複合インデックスは列を全部入れればよいですか?

いいえ。列順が重要です。等価条件、範囲条件、JOIN、ORDER BY、実行頻度、選択性を見て設計します。取得列はキー列ではなくINCLUDE列にするほうがよい場合もあります。

統計情報を更新すればインデックスが使われますか?

統計情報が古いことが原因なら改善する可能性があります。ただし、SQLの書き方や型不一致が原因なら、統計情報更新だけでは解決しません。

インデックスが使われているかどうかは何で判断しますか?

実行計画、STATISTICS IO、Query Store、sys.dm_db_index_usage_stats を組み合わせて判断します。単にIndex Seekが出ているかだけでなく、logical readsと実行時間も見ます。

まとめ

SQL Serverでインデックスが使われない原因は、インデックスが存在しないことだけではありません。暗黙変換、列側関数、LIKEの先頭ワイルドカード、複合インデックスの列順、フィルターインデックスの条件不一致、計算列インデックスのSETオプション不一致、OR条件、Key Lookup、統計情報の古さ、パラメータ値の偏りなどで、期待した実行計画にならないことがあります。

まず実行計画で Seek PredicatePredicateCONVERT_IMPLICITKey Lookup、推定行数と実際の行数を確認します。そのうえで、SQLの条件式、データ型、複合インデックスの列順、INCLUDE列、統計情報を見直し、STATISTICS IO と実行時間で改善前後を比較しましょう。