SQL Serverで「インデックスがあるのに遅い」原因の多くは、WHERE句やJOIN条件での暗黙的な型変換です。特に「文字列列 = 数値」のようにデータ型が噛み合わない比較は、列側にCONVERTが掛かってIndex Seekが崩れ、スキャンに落ちやすくなります。この記事では、インデックスが効かなくなる代表的なデータ型の組み合わせと、現場で再発を防ぐ書き方・設計のコツを整理します。
なぜデータ型の不一致でインデックスが効かなくなるのか
SQL Serverは比較や代入の場面で、両辺の型が一致していないときに「どちらか一方をもう一方へ変換」して評価します。このとき基準になるのがデータ型の優先順位です。優先順位が高い型へ、低い型が合わせに行く(暗黙変換される)ため、式の形が内部的に変わります。
ポイントは次の1点です。
- インデックスが張られている列側に変換(CONVERT/CAST)や式が掛かると、B-treeの並び順をそのまま使えず、Index Seek(シーク)になりにくい
つまり「型の不一致」そのものよりも、暗黙変換の結果として列側が変換されることが問題になります。実行計画では、PredicateにCONVERT_IMPLICITが見えたり、PlanAffectingConvertの警告が出たりします。
データ型の優先順位(実務で頻出する範囲の抜粋)
SQL Serverの優先順位は非常に長いですが、インデックスの効きに影響しやすい部分だけを抜粋すると次のイメージです。ここで「左ほど強い」と覚えておくと、どちらが変換されるかを推測しやすくなります。
| カテゴリ | 優先順位が高い → 低い(抜粋) | よく起きる事故 |
|---|---|---|
| 文字列 | nvarchar/nchar → varchar/char | varchar列にN’…’(nvarchar)が来て列側変換 |
| 日付時刻 | datetime2 → datetime → smalldatetime → date | 列にCONVERT(date, col)を掛けて日付検索が遅い |
| 数値 | float/real → decimal/numeric → bigint → int → smallint → tinyint | int列にdecimal/floatパラメータが来て列側変換 |
まず押さえる:SARGable(サーガブル)とSeek/Scanの違い
インデックスを効かせられる条件(SARGable)とは、ざっくり言うと「列をそのまま使って範囲や一致を指定できる形」です。逆に列に関数や演算が掛かると、キー順に沿って絞り込めず、インデックス全体を舐める処理になりがちです。
| 書き方 | 列側に式が掛かる? | 期待できる動き | コメント |
|---|---|---|---|
col = @val | いいえ | Index Seekになりやすい | 基本形。統計情報・選択度次第でScanになることもある |
CONVERT(int, col) = 99 | はい | Scanになりやすい | 列側変換でB-treeを活かしづらい |
col + 1 = 100 | はい | Scanになりやすい | 可能ならcol = 99へ変形する |
col LIKE 'abc%' | いいえ | Seekになりやすい | 前方一致はキー順で範囲を絞れる |
col LIKE '%abc' | (関数ではないが) | Scanになりやすい | 先頭ワイルドカードはキー順で絞れない |
代表例:文字列列に数値を渡すとSeekが崩れる
質問で多いのがこのパターンです。列が文字列で、比較値が数値リテラル(あるいはint型パラメータ)になっているケースです。
ケースA:文字列型の列(インデックスあり)に数値を渡す
SELECT *
FROM dbo.tbl
WHERE index_string_col = 99; -- 列: nvarchar / 右辺: 数値
データ型の優先順位では、一般に数値型の方が文字列型より優先順位が高いため、内部的には概ね次のような評価になります。
CONVERT(int, index_string_col) = 99
この瞬間、インデックスのキー(文字列の並び)と「数値としての大小関係」が一致しなくなります。例えば文字列の並びでは'100'は'99'より前に来ることがありますが、数値としては逆です。結果としてB-treeの順序を使った絞り込みができず、Index ScanやTable Scanに落ちやすい、という挙動になります。
さらに厄介なのは、列に数値以外(空文字、記号、全角数字など)が混ざっていると、列側変換で実行時エラーになり得る点です。性能だけでなく、運用上の安全性も落ちます。
ケースB:数値型の列(インデックスあり)に文字列を渡す
SELECT *
FROM dbo.tbl
WHERE index_int_col = '999'; -- 列: int / 右辺: 文字列
この場合、優先順位の高いint側に合わせて、右辺がintへ変換されます。
index_int_col = CONVERT(int, '999')
列側はintのままなので、インデックスの並び順をそのまま活かせます。結果としてIndex Seekが成立しやすい、という違いが出ます。
リテラルの書き方だけで「型」が変わることに注意
型不一致は「列の型」と「値の型」の組み合わせで起きますが、値側は“書き方”で型が変わるため、意図せず事故ることがあります。
| 見た目 | SQL Serverが解釈しやすい型のイメージ | 危険になりやすい例 | 安全な書き方 |
|---|---|---|---|
1 | int(範囲内なら) | なし(int列の比較なら自然) | そのままでOK |
1.0 | decimal/numeric 系になりやすい | int_col = 1.0 で列側がdecimalへ変換されやすい | int_col = 1、またはCONVERT(int, 1.0)を値側で明確化 |
1e0 | float になりやすい | decimal_col = 1e0 で列側がfloatへ変換されやすい | 値側をdecimalへ:decimal_col = CONVERT(decimal(10,2), 1e0) |
'abc' | varchar(文脈次第) | 照合順序や列型で変換が入る | 列型に合わせて明示:CONVERT(varchar(50), 'abc') |
N'abc' | nvarchar | varchar列の比較で列側がnvarcharへ変換されやすい | varchar列なら'abc'、nvarchar列ならN'abc' |
インデックスが効かなくなりやすい「データ型の組み合わせ」一覧
実務で遭遇頻度が高い組み合わせを、暗黙変換が「どちら側」に掛かりやすいかという観点で整理します。ここに挙げるのは「発生したら要注意」のパターンで、実際のSeek/Scanは統計情報や行数、条件の選択度にも左右されます。
| インデックス列の型 | 比較値の型(リテラル/変数) | 起きがちな暗黙変換 | 影響 | 基本対策 |
|---|---|---|---|---|
| varchar/char | nvarchar/nchar(例:N'abc') | CONVERT(nvarchar, varchar_col)(列側)になりやすい | Seek崩れ・Scan化 | 値側をvarcharに揃える、または列をnvarcharへ統一 |
| nvarchar/nchar | int/bigint など数値 | CONVERT(int, nvarchar_col)(列側)になりやすい | Seek崩れ・Scan化 | 値側をnvarcharへ明示変換、可能なら設計を数値型へ |
| int/bigint | decimal/numeric(小数あり、または1.0のような定数) | CONVERT(decimal, int_col)(列側)になりやすい | Seek崩れ・Scan化 | パラメータ型をintに直す、アプリ側の型推論を見直す |
| decimal/numeric | float/real(または1e0のような定数) | CONVERT(float, decimal_col)(列側)になりやすい | Seek崩れ・誤差問題も | 比較値をdecimalで扱う(floatを避ける) |
| datetime/datetime2 | date(または列にCONVERT(date, col)) | 列側に関数が掛かると非SARGable | 日付検索が遅い | 範囲条件(>= と <)へ分解 |
| uniqueidentifier | nvarchar(GUID文字列) | 通常は値側がuniqueidentifierへ変換されるが、列が文字列の場合は逆転 | 列が文字列だとSeek崩れ | GUIDはuniqueidentifierで保持し、値側をその型で渡す |
| 異なる照合順序(collation)の文字列列 | 別collationの文字列/リテラル | 暗黙のCOLLATE付与や明示COLLATEで列側が式化 | Join/検索が遅い | DB/列の照合順序を統一、必要なら値側へCOLLATE |
varchar列にN’…’を渡す罠(Unicode/非Unicodeの不一致)
地味に多いのが、varchar列(非Unicode)に対して、アプリやSQLでN'文字列'(Unicode)を渡してしまうケースです。
-- 列: varchar(50) にインデックスあり
SELECT *
FROM dbo.tbl
WHERE varchar_col = N'abc';
このとき、比較はUnicode側に寄るため、内部的に列側がnvarcharへ変換される形になりやすく、Seekが崩れます。特にアプリ側でパラメータが常にnvarcharで送られていると、クエリは一見同じでも「常に列側変換が走る」状態になり、テーブルが育った途端に遅くなる、という事故に繋がります。
対策
- 非Unicodeで十分なら、値側を明示的にvarcharへ:
WHERE varchar_col = CONVERT(varchar(50), @val) - 将来的に多言語対応が必要なら、列側をnvarcharへ統一(設計レベルで整合を取る)
- アプリから渡すパラメータは「型」と「長さ」も指定する(型推論任せにしない)
int列にdecimalを渡す罠(ORM/パラメータ推論で起きやすい)
「数値なら何でも同じ」と思いがちですが、SQL Serverでは数値型にも優先順位があります。たとえばインデックス付きのint列に対し、アプリ側からdecimalとしてパラメータを渡すと、優先順位により列側がdecimalへ寄せられてしまい、Seekが崩れることがあります。
-- 列: int(インデックスあり)
-- パラメータ: decimal(18,2) などで渡ってくる想定
SELECT *
FROM dbo.tbl
WHERE int_col = @p_decimal;
テストデータが少ない環境では気づきにくく、本番で件数が増えた段階で初めて「同じSQLなのに急に遅い」現象として顕在化します。
対策
- アプリ側のパラメータ型を列に合わせてintで渡す
- どうしてもdecimalで受け取るなら、値側をintへ明示変換(ただし小数が来る可能性があるなら設計を見直す)
decimal列にfloatを渡す罠(Seekだけでなく結果の正確性も壊れる)
floatやrealは近似値型です。decimal列(正確な固定小数)と混ぜると、暗黙変換の影響でSeekが崩れるだけでなく、等価比較が思った通りに一致しないケースも出ます。
-- 列: decimal(10,2)(インデックスあり)
SELECT *
FROM dbo.tbl
WHERE price = @p_float; -- @p_float が float の場合
金額・レート・数量のように「正確さ」が必要な列は、アプリ側も含めてdecimal系に寄せるのが安全です。
日付検索でCONVERT(date, datetime_col)を使うと遅くなる理由
日付列にインデックスがあるのに遅い、という相談でよく出てくるのが、datetime列を日付に丸めて比較しているケースです。
-- 列: datetime(インデックスあり)
SELECT *
FROM dbo.tbl
WHERE CONVERT(date, datetime_col) = '2025-03-01';
この形は列側に関数が掛かっているため、SARGableではありません。対策は「日付の等価比較」を「日時の範囲」に落とすことです。
SELECT *
FROM dbo.tbl
WHERE datetime_col >= '2025-03-01'
AND datetime_col < '2025-03-02';
この書き方なら列はそのまま、インデックスのキー順で範囲をSeekできるため、大幅に改善することがあります。日付型・datetime2型でも考え方は同じで、範囲条件の上限は「翌日未満」として書くと安全です。
JOINで起きる「型不一致」もScanの温床
WHERE句だけでなくJOIN条件も同様です。結合キーの型が異なると、暗黙変換が列側に乗ったテーブルから、結合がスキャン寄りになります。特に片側が文字列、片側が数値、あるいはvarcharとnvarcharが混ざっているケースは要注意です。
SELECT *
FROM dbo.A a
JOIN dbo.B b
ON a.Code = b.Code; -- a.Code: varchar, b.Code: nvarchar など
結合キーは「同じ意味の値」を表すので、設計段階で型を統一するのが最も効果的です。どうしても統一できないなら、変換はインデックスが無い側、または値側に寄せる設計を検討します(ただし根本解決ではありません)。
実行計画で暗黙変換を見抜く具体的な見方
「本当に暗黙変換でSeekが崩れているのか」は、実行計画を見ると短時間で判断できます。SSMSで実際の実行プランを表示し、次のポイントを確認します。
- 演算子がIndex SeekではなくIndex Scan/Table Scanになっていないか
- Seekがある場合、プロパティのSeek Predicatesに列が素直に出ているか(Predicate側に押し込まれていないか)
- PredicateやCompute Scalarで
CONVERT_IMPLICITが出ていないか - 警告として
PlanAffectingConvertが表示されていないか
「変換がある=必ず遅い」ではありませんが、列側のCONVERT_IMPLICITが見えたら、まず書き方やパラメータ型を疑うのが近道です。
実務での最優先ルール:変換するなら「値側」
対策の基本はシンプルです。
- 列に合わせて値(リテラル/変数/パラメータ)の型を揃える
- 列側にCAST/CONVERT、文字列関数、演算を掛けない
例えば、どうしても「文字列列に数値で検索したい」なら、値側を文字列にします。
DECLARE @id int = 123;
SELECT *
FROM dbo.tbl
WHERE nvarchar_col = CONVERT(nvarchar(10), @id);
逆に列側の変換(CONVERT(int, nvarchar_col))に逃げると、Seekが崩れるだけでなく、変換不能な値が混ざった瞬間にエラーになるなど運用リスクも増えます。
アプリ側で起きやすい原因:AddWithValueとパラメータ長の不一致
SQLは正しく見えるのに実行計画だけが悪い場合、アプリ側のパラメータ送信が原因になっていることがあります。典型は「型推論」に任せてしまい、SQL Serverが期待する型・長さとズレるケースです。
| よくある送信 | 起きること | 推奨 |
|---|---|---|
| 文字列を常にnvarcharで送る | varchar列がnvarcharへ暗黙変換されやすい | 列に合わせてvarchar/nvarcharを使い分け、サイズも指定 |
| 数値をdecimalで送る | int列がdecimalへ暗黙変換されやすい | キー検索はint/bigintなど列型に合わせる |
| 文字列パラメータの長さが極端に大きい | 推定行数が崩れ、プラン選択が悪化することがある | 最大長ではなく、列定義に近い長さを指定する |
ここを直すだけで「同じSQLなのに突然遅い」問題が改善することは珍しくありません。
どうしても型が揃えられないときの現実的な逃げ道
レガシーな設計や外部システム連携の都合で、どうしても「本来数値のものが文字列で入っている」「GUIDが文字列で保存されている」など、型の統一が難しい場合があります。その場合は、クエリの書き方だけでなく、インデックス設計で回避する手もあります。
計算列(computed column)+インデックス
例として、文字列に入っている数値を検索したい場合、数値に変換した計算列を作り、それにインデックスを張る方法があります。
ALTER TABLE dbo.tbl
ADD index_string_col_as_int AS TRY_CONVERT(int, index_string_col) PERSISTED;
CREATE INDEX IX_tbl_index_string_col_as_int
ON dbo.tbl(index_string_col_as_int)
WHERE index_string_col_as_int IS NOT NULL;
こうすると、検索側は数値で書けて、かつインデックスも使いやすくなります。重要なのは「変換を列に掛ける」のではなく、「変換結果を列として保持し、そこにインデックスを張る」という発想です。
二重持ち(正規化しつつ検索用の型も持つ)
データ投入時に正しい型へ変換して別列に保持し、検索はその列で行う設計です。ストレージは増えますが、検索性能と安定運用を優先したいシステムでは現実解になります。
チェックリスト:遅いSQLを見つけたら最初に確認すること
- 比較している両辺のデータ型は一致しているか(varchar vs nvarchar、int vs decimal など)
- WHERE/JOIN条件で列側にCAST/CONVERT/関数/演算が掛かっていないか
- 実行計画に
CONVERT_IMPLICITが出ていないか - アプリ側のパラメータ型・長さが列定義と合っているか
- 日付検索は範囲条件(>= と <)で書けないか
- 結合キーの型はテーブル間で統一されているか
実務でのまとめ:再発を防ぐ設計・実装の指針
SQL Serverでインデックスが効かなくなる「データ型の組み合わせ」問題は、根本的には列側を式にしてしまう暗黙変換の問題です。対策は次の順で効きます。
- 設計で型を統一する(同じ意味の列は同じ型・同じ照合順序へ)
- クエリは列を素のまま比較し、値側を列型へ揃える
- アプリのパラメータ送信を「型・長さ」まで含めて厳密化する
- やむを得ない場合は計算列+インデックスで検索用の道を作る
インデックスの有無だけでは性能は決まりません。インデックスが使える形(SARGable)で書けているか、そして型が揃っているか。この2点を習慣としてチェックするだけで、性能トラブルの多くは未然に防げます。

コメント