SqlConnection 必须显式调用 Dispose() 或用 using 包裹,否则连接池耗尽导致后续 Open() 无限等待;SqlCommand 必须参数化防注入并提升性能;SqlDataReader 需完整遍历且不可跨作用域传递;大批量操作应使用 SqlBulkCopy 或 TVP。

SqlConnection 必须显式调用 Dispose() 或用 using 包裹
不释放连接会导致连接池耗尽,后续请求卡在 Open() 上无限等待(超时前无报错)。ADO.NET 不会自动回收 SqlConnection,哪怕作用域结束、GC 触发也不行。
-
using是最安全写法,即使抛异常也能保证Dispose()执行 - 手动调用
Close()不等于释放资源,它只归还连接到池,仍需Dispose()清理内部句柄 - 避免把
SqlConnection声明为类字段长期持有——连接不是线程安全的,且池大小默认仅 100,易引发争抢
using (var conn = new SqlConnection(connStr))
{
conn.Open(); // 这里才真正从池中取连接
// ... 执行命令
} // 自动 Dispose(),连接归还池
SqlCommand 参数化查询必须用 SqlParameter,不能拼接字符串
拼接 SQL 字符串是 SQL 注入温床,而且会绕过查询计划缓存,每次执行都编译新计划,性能陡降。参数化不是“可选优化”,是强制要求。
- 参数名必须带
@前缀(如@id),且与AddWithValue()或Add()中名称严格一致 -
AddWithValue()有隐式类型推断风险:传null推成DBNull没问题,但传""可能被推成varchar(1),导致索引失效;建议显式用Add("@name", SqlDbType.NVarChar).Value = value - 参数顺序无关,但命名必须匹配 SQL 文本中的占位符
// ✅ 正确
cmd.CommandText = "SELECT * FROM Users WHERE Status = @status AND CreatedAt > @since";
cmd.Parameters.Add("@status", SqlDbType.VarChar).Value = "Active";
cmd.Parameters.Add("@since", SqlDbType.DateTime2).Value = DateTime.UtcNow.AddDays(-7);
<p>// ❌ 危险
cmd.CommandText = $"SELECT * FROM Users WHERE Name = '{userName}'";</p>
DataReader 读取时不能跳过字段或提前关闭连接
SqlDataReader 是前向只读流式读取器,依赖底层连接保持打开状态。一旦连接关闭或读取中断(如 break 出循环),未读完的数据会丢失,且可能锁住表(取决于隔离级别)。
- 必须用
while (reader.Read())完整遍历,或明确调用reader.NextResult()处理多结果集 - 不能在
Read()返回true后,用reader[0]访问字段——如果字段名变更或顺序调整,极易越界;应优先用reader["ColumnName"]或GetOrdinal()缓存索引 - 若需多次遍历或跨方法传递数据,立刻把结果转成
List或DataTable,别传SqlDataReader本身
批量操作别硬扛单条 INSERT/UPDATE,优先用 SqlBulkCopy 或 TVP
千条以上记录逐条 ExecuteNonQuery() 是典型性能陷阱:每条语句都走完整网络往返 + 解析 + 计划生成,耗时呈线性增长。1 万行可能要几秒甚至更久。
-
SqlBulkCopy是最快方案,适合从内存DataTable或IDataReader批量导入,但要求目标表结构严格匹配,且不能触发 INSERT 触发器(除非显式开启) - 表值参数(TVP)适合需要服务端逻辑的场景(如 INSERT 前校验),需先在 SQL Server 创建用户定义表类型,再用
DataTable绑定参数 - 避免用
UNION ALL拼大 SQL——SQL Server 对批处理长度有限制(max text repl size等),且解析压力大
连接字符串里记得开 Packet Size=8192(默认值),大数据量时可微调到 32767 提升吞吐,但别盲目调高——过大会增加内存碎片和网络延迟敏感度。










