Dapper 参数类型不匹配导致性能问题分析与修复
本文讲述了作者在排查生产环境性能问题时发现的一个由 Dapper 使用不当导致的 CPU 占用率过高的问题。
问题描述:
应用程序 CPU 占用率过高,平均 50% 以上,峰值可达 90%。经过分析,发现一个看似简单的 Dapper 查询是罪魁祸首。该查询对索引列使用 WHERE 子句进行过滤,理论上执行速度应该很快,但实际却耗费了大量的 CPU 时间。
根本原因:
问题源于 C# 代码中一个隐蔽的类型不匹配。当通过匿名对象传递 C# string 类型参数给 Dapper 时,Dapper 默认将其映射为 ADO.NET 的 nvarchar(4000) 类型。 如果数据库中的对应列是 varchar 类型,SQL Server 会强制将 varchar 列的所有值转换为 nvarchar 类型进行比较,这被称为 CONVERT_IMPLICIT。 由于类型转换,SQL Server 无法使用索引,导致全表扫描。
性能影响:
- 正确参数类型 (Index Seek): SQL Server 直接跳转到匹配的行,只需要少量逻辑读取,执行时间为微秒级。
- 隐式转换 (Index Scan): SQL Server 需要读取索引中的每一行,进行类型转换,然后进行比较。 逻辑读取量从少量变为数万次,执行时间大幅增加。
修复方案:
修复方法很简单,就是在 Dapper 中显式指定参数类型为 varchar,而不是默认的 nvarchar。
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);
var result = await connection.QueryFirstOrDefaultAsync<Product>(sql,
new { productCode = new DbString { Value = productCode, IsAnsi = true, Length = 100 } });
修复效果:
修复后,查询性能得到显著提升:
| 指标 |
修复前 (nvarchar) |
修复后 (varchar) |
| 扫描类型 |
Index SCAN |
Index SEEK |
| 逻辑读取 |
数万次 |
单个数字 |
| CPU 时间 |
毫秒 |
微秒 |
问题排查方法:
- 检查 Query Store: 查找包含 `@%nvarchar(4000)%' 的查询,这些查询可能存在隐式转换问题。
- 查看执行计划: 在执行计划中查找
CONVERT_IMPLICIT 警告。
- 代码搜索: 查找 Dapper 查询中,将字符串参数通过匿名对象传递给
varchar 列的代码。
最佳实践:
- 如果数据库列是
varchar 类型,则在 Dapper 中使用 DbType.AnsiString。
- 如果数据库列是
nvarchar 类型,则使用默认的 DbType.String。
- 确保参数类型和列类型匹配,并且参数大小与列大小一致。
- 在使用
DynamicParameters 时,添加注释说明使用 DbType.AnsiString 的原因,防止未来被误修改。
总结:
即使代码看起来正确,也可能存在隐蔽的性能问题。 务必检查 Dapper 参数类型使用,特别是当数据库列是 varchar 类型时,确保使用正确的参数类型,避免因类型不匹配导致的性能下降。