![**C# の文字列が Dapper で SQL Server インデックスを静かに破壊する理由**
Dapper を使って SQL Server データベースへクエリを投げる際、文字列結合や文字列補間(string interpolation)でクエリを作成することはよくあります。
しかし、この一見無害な手法がインデックスの性能を黙って破壊してしまうケースがあります。
---
## なぜ起きるのか
1. **暗黙の型変換**
`string` と `int`・`bool` など非文字列型を結合すると、SQL Server は列値を `nvarchar` に変換せざるを得ません。
2. **インデックス回避**
この暗黙変換により最適化器は既存の数値や日付インデックスを利用できず、フルテーブルスキャンが発生します。
---
## 問題を引き起こす典型的なパターン
| パターン | 何をしているか | インデックスへの影響 |
|---------|-----------------|----------------------|
| `WHERE Id = " + id`(文字列結合) | `Id` 列を `nvarchar` に変換 | フルスキャン |
| `$"SELECT * FROM Users WHERE IsActive = {isActive}"`(補間) | ブール値も同様に `nvarchar` へ変換 | フルスキャン |
| `WHERE CreatedDate >= @date.ToString()` | 日付を文字列へ変換 | インデックスが失われる |
---
## 修正方法
1. **インライン値ではなくパラメータを使用する**
```csharp
var sql = "SELECT * FROM Users WHERE Id = @Id";
connection.Query<User>(sql, new { Id = id });
```
2. **型の一貫性を保つ**
列が期待する正確な型(`int`、`DateTime` など)で渡す。
3. **C# で暗黙変換を避ける**
必要なら明示的にキャストまたは変換し、安全かつ意図した変換のみ行う。
---
## 簡易チェックリスト
- [ ] Dapper に渡す値は文字列化せず、型付きである。
- [ ] 変数データと SQL フラグメントのインライン結合を行わない。
- [ ] すべてのクエリに `@ParameterName` プレースホルダーを使用する。
これらのガイドラインに従えば、インデックスの整合性を保ちつつクエリを高速かつ効率的に維持できます。](/_next/image?url=%2Fscreenshots%2F2026-03-07%2F1772840340164.webp&w=3840&q=75)
2026/03/07 7:55
**C# の文字列が Dapper で SQL Server インデックスを静かに破壊する理由** Dapper を使って SQL Server データベースへクエリを投げる際、文字列結合や文字列補間(string interpolation)でクエリを作成することはよくあります。 しかし、この一見無害な手法がインデックスの性能を黙って破壊してしまうケースがあります。 --- ## なぜ起きるのか 1. **暗黙の型変換** `string` と `int`・`bool` など非文字列型を結合すると、SQL Server は列値を `nvarchar` に変換せざるを得ません。 2. **インデックス回避** この暗黙変換により最適化器は既存の数値や日付インデックスを利用できず、フルテーブルスキャンが発生します。 --- ## 問題を引き起こす典型的なパターン | パターン | 何をしているか | インデックスへの影響 | |---------|-----------------|----------------------| | `WHERE Id = " + id`(文字列結合) | `Id` 列を `nvarchar` に変換 | フルスキャン | | `$"SELECT * FROM Users WHERE IsActive = {isActive}"`(補間) | ブール値も同様に `nvarchar` へ変換 | フルスキャン | | `WHERE CreatedDate >= @date.ToString()` | 日付を文字列へ変換 | インデックスが失われる | --- ## 修正方法 1. **インライン値ではなくパラメータを使用する** ```csharp var sql = "SELECT * FROM Users WHERE Id = @Id"; connection.Query<User>(sql, new { Id = id }); ``` 2. **型の一貫性を保つ** 列が期待する正確な型(`int`、`DateTime` など)で渡す。 3. **C# で暗黙変換を避ける** 必要なら明示的にキャストまたは変換し、安全かつ意図した変換のみ行う。 --- ## 簡易チェックリスト - [ ] Dapper に渡す値は文字列化せず、型付きである。 - [ ] 変数データと SQL フラグメントのインライン結合を行わない。 - [ ] すべてのクエリに `@ParameterName` プレースホルダーを使用する。 これらのガイドラインに従えば、インデックスの整合性を保ちつつクエリを高速かつ効率的に維持できます。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
.NET/Dapper アプリケーションでは、C# の文字列を
nvarchar(4000) として渡すと、SQL Server が varchar 列に対して暗黙の型変換(implicit conversions)を実行します。これにより、インデックス検索がスキャンに置き換わり、論理読み取り数が単桁から数万に膨らみ、CPU/I/O の使用率が急増します(例:CONVERT_IMPLICIT(nvarchar(255), [Sales].[ProductCode], 0))。正確性には影響しませんが、実行計画や Query Store の警告で明らかになります。特に
SQL_Latin1_General_CP1_CI_AS などの照合順序では顕著です。
修正: パラメータを ANSI として明示的に宣言し、列サイズと一致させます。
var p = new DynamicParameters(); p.Add("productCode", productCode, DbType.AnsiString, size: 100); await conn.QueryFirstOrDefaultAsync<Product>(sql, p);
または匿名オブジェクトを使用する場合:
new { productCode = new DbString { Value = productCode, IsAnsi = true, Length = 100 } }
スキーマ変更、インデックス更新、クエリ書き換えは不要です。パフォーマンスの改善は即座に実感できます。
監査ヒント: Query Store で
@nvarchar(4000) を検索し、varchar 列へ文字列を渡す匿名オブジェクトをコード内でスキャンしてください。
ベストプラクティス: 将来のリグレッション防止のために、
DbType.AnsiString(または IsAnsi = true)を使用した理由をコメントしておくことが推奨されます。
この簡単な調整でサーバー負荷を低減し、コスト削減とスケールアップが実現します。結果として開発者・ユーザー・広範な .NET/SQL Server コミュニティ全体に恩恵をもたらします。
本文
最近直面した本番環境のパフォーマンス問題
アプリケーションが「熱い」状態で動作していました。CPU は平均 50 % を超え、ピーク時には 90 % に達します。診断スナップショットを取得し、CPU 時間の高いクエリから順に解析を開始しました。
一番問題になっていたのは?
インデックス付きカラムに対して単純な WHERE を使った Dapper クエリです。高速であるべきなのに、1 回実行あたり数千ミリ秒という CPU 時間が発生し、日々数十万回の実行で「2 文字型の不一致」が C# コード上では全く見えませんでした。
実際に何が起きているのか?
ほぼすべての .NET プロジェクトで Dapper を使うと、次のようなパターンが現れます。
const string sql = "SELECT * FROM Products WHERE ProductCode = @productCode"; var result = await connection.QueryFirstOrDefaultAsync<Product>(sql, new { productCode });
シンプルでクリーン。
ProductCode がデータベース上の varchar カラムなら、静かにパフォーマンスを破壊します。
C# の文字列を匿名オブジェクト経由で渡すと、Dapper は
nvarchar(4000) にマッピングします。これは ADO.NET で System.String がデフォルトで使用する型です。安全なデフォルトとしては妥当ですが、カラムが varchar の場合、SQL Server は比較前に列のすべての値を nvarchar に変換しなくてはいけません。この変換は CONVERT_IMPLICIT と呼ばれ、インデックスを使えないため毎回フルスキャンになります。
実行計画で次のように現れます:
CONVERT_IMPLICIT(nvarchar(255), [Sales].[ProductCode], 0)
SQL Server が「完璧なインデックスがあったはずなのに、Unicode パラメータと比較するためにすべての行を変換せざるを得ない」と告げています。
実際にどれほど悪影響があるか?
計算で見ると凄まじいです。1 億行程度のテーブルで
ProductCode に非クラスタ化インデックスがあると仮定します。正しいパラメータ型なら SQL Server は インデックスシーク を実行し、数十ミリオロジカルリード(マイクロ秒単位)で済みます。
一方、暗黙変換が発生すると インデックススキャン が走り、インデックス内のすべての行を読み取り、各値を変換して比較します。数十万の論理リードに拡大し、クエリ実行回数を掛け合わせれば CPU の重大な問題となります。
当社では 1 クエリがデータベースサーバ全体の CPU 消費量のかなりの部分を占めていました。複雑さやインデックス不良ではなく、パラメータ型のミスマッチだけで発生していたのです。
注:照合順序
影響はデータベースの照合順序によって異なります。最も一般的なではフルインデックススキャンが発生し、最悪ケースです。一部 Windows 照合順序(例:SQL_Latin1_General_CP1_CI_AS)でもインデックスシークが可能ですが、暗黙変換のオーバーヘッドは残ります。いずれにせよ、パラメータ型を列型と一致させることが正解です。Latin1_General_CI_AS
修正方法
修正はほぼ極めて簡単です:Dapper に varchar であることを明示します。
DynamicParameters と DbType.AnsiString を使います。
const string sql = "SELECT * FROM Products WHERE ProductCode = @productCode"; var parameters = new DynamicParameters(); parameters.Add("productCode", productCode, DbType.AnsiString, size: 100); var result = await connection.QueryFirstOrDefaultAsync<Product>(sql, parameters);
DbType.AnsiString は ADO.NET に varchar パラメータを送るよう指示します。デフォルトの DbType.String(C# の文字列)では nvarchar が送られます。
size はカラム定義に合わせてください。例えば varchar(255) なら size: 255 とします。これで SQL Server はパラメータ型を列型と正確に一致させ、クエリプランの再利用も効率的になります。
匿名オブジェクトを好む場合は、より簡潔な
DbString を使用できます:
var result = await connection.QueryFirstOrDefaultAsync<Product>(sql, new { productCode = new DbString { Value = productCode, IsAnsi = true, Length = 100 } });
どちらの方法も同じ結果、すなわち
varchar パラメータを送ります。
前後比較
| 指標 | 修正前(nvarchar) | 修正後(varchar) |
|---|---|---|
| スキャンタイプ | INDEX SCAN | INDEX SEEK |
| 論理リード数 | 数万 | 数十 |
| 実行あたり CPU | ミリ秒 | マイクロ秒 |
スキーマ変更・インデックス追加・クエリ書き換えは不要。Dapper に正しいパラメータ型を渡すだけです。
アプリケーションでの検出方法
この問題があるか疑う場合、次の手順で確認できます。
-
Query Store で暗黙変換を検索
SELECT TOP 20 qsqt.query_sql_text, qsrs.avg_cpu_time, qsrs.count_executions FROM sys.query_store_runtime_stats qsrs JOIN sys.query_store_plan qsp ON qsrs.plan_id = qsp.plan_id JOIN sys.query_store_query qsq ON qsp.query_id = qsq.query_id JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id WHERE qsqt.query_sql_text LIKE '%@%nvarchar(4000)%' ORDER BY qsrs.avg_cpu_time * qsrs.count_executions DESC; -
実行計画で
を探すCONVERT_IMPLICIT
カラムに対してフィルタリングされるクエリに現れたら検出です。varchar -
C# コードを検索
匿名オブジェクト経由で文字列パラメータを渡し、対象が
カラムの場合:varchar// これが問題のパターン await connection.QueryAsync<T>(sql, new { someVarcharColumn });
原則
- 列が varchar の場合は
を使用。DbType.AnsiString - 列が nvarchar の場合はデフォルトの
で問題なし。DbType.String - パラメータ型を列型と合わせ、サイズも一致させる。
ヒント:
を使う理由をコメントで説明しておくと、将来リファクタリング時に「簡素化」されて再びDynamicParametersになり、問題が戻ってきません。new { productCode }
var parameters = new DynamicParameters(); // DbType.AnsiString 必須: Products.ProductCode は varchar(100)。 // これを指定しないと Dapper は nvarchar(4000) を送信し、毎行 CONVERT_IMPLICIT が発生 // インデックスシークが失われます。 parameters.Add("productCode", productCode, DbType.AnsiString, size: 100);
クエリの監査を実施
このバグはほぼ見えないものです。コードは正しく、クエリは正しい結果を返し、ログにもエラーが出ません。ただ遅く動くだけで、原因が分からない状態に陥ります。実行計画や Query Store データを調べるまで問題の所在がわかりません。
Dapper と SQL Server を使用していて、列が
varchar の場合は今日中にパラメータ使用方法を監査しましょう。匿名オブジェクトで文字列を渡すすべてのケースが、表面上は正しく見えるフルテーブルスキャンを隠しています。
さらに読む
- DbType 列挙体 –
とAnsiString
の違いなど、ADO.NET データ型に関する Microsoft ドキュメント。String - クエリ処理アーキテクチャ ガイド – SQL Server がクエリをどのように処理し、暗黙変換やプランキャッシュがどう働くかを深掘り。
- データ型変換(Database Engine) – 暗黙・明示変換と、型優先順位ルールについての Microsoft 参照。