Start Debugging

Dapper のデフォルト nvarchar パラメータが SQL Server のインデックスを静かに殺すしくみ

Dapper を通じて送られる C# 文字列はデフォルトで nvarchar(4000) になり、SQL Server に暗黙変換と完全なインデックススキャンを強制します。DbType.AnsiString でこれを修正する方法を紹介します。

ミリ秒で終わるはずのクエリが突然這うように遅くなります。実行プランは seek ではなく index scan を示し、CPU はすべての行の変換で残業しています。犯人は? varchar カラムに対して Dapper を通じて渡された C# string パラメータです。

この問題は .NET コミュニティでまた周回しており、それには理由があります: 微妙で、よくあり、クエリを 最大 268 倍遅く できます。

なぜ実行プランに nvarchar(4000) が現れるのか

C# の文字列を anonymous object 経由で Dapper に渡すと、Dapper はデフォルトで nvarchar(4000) にマップします:

const string sql = "SELECT * FROM Products WHERE ProductCode = @productCode";
var result = await connection.QueryFirstOrDefaultAsync<Product>(
    sql, new { productCode });

ProductCodevarchar(50) カラムなら、SQL Server は型不一致を見ます。Unicode の nvarcharvarchar より優先順位が高いので、engine は比較前にカラム値を nvarchar に昇格させるために、インデックスのすべての行で CONVERT_IMPLICIT を適用します。

つまり index seek なし、です。SQL Server はインデックス全体を行ごとにスキャンし、行くうちに変換していきます。

問題の発見

決定的な兆候は実行プランにあります。CONVERT_IMPLICIT を言及する index scan オペレーターの警告を探してください。こうも確認できます:

SELECT * FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE st.text LIKE '%ProductCode%'
ORDER BY qs.total_worker_time DESC;

シンプルなルックアップクエリで total_worker_time が高いのは赤旗です。

DbType.AnsiString で修正

修正はストレートです: デフォルトの DbType.String ではなく DbType.AnsiString を使うよう Dapper に指示します:

var parameters = new DynamicParameters();
parameters.Add("productCode", productCode, DbType.AnsiString, size: 50);

var result = await connection.QueryFirstOrDefaultAsync<Product>(
    sql, parameters);

正しいカラムサイズと一緒に DbType.AnsiString を指定することで、生成されるパラメータがカラム型と正確に一致します。SQL Server は今やそのために設計された index seek を使えます。

いつ最も重要になるか

小さなテーブルでは問題が完全に隠れることがあります。データが増えるにつれてパフォーマンスの崖が現れます: 100,000 行のテーブルは 176 倍の遅延を示す可能性があり、100 万行のテーブルではさらに悪化します。varchar カラム (legacy データベースや Unicode を必要としないシステムでよくあります) で Dapper を使っているなら、パラメータ型を監査してください。

Dapper の QueryExecute メソッドに渡される anonymous object を project-wide に grep するのが良い出発点です。varchar カラムをターゲットにするすべての string パラメータは DbType.AnsiString の候補です。

< 戻る