什么是 SQL 参数化查询?
SQL 参数化查询是一种将 SQL 语句的代码部分(结构)和数据部分(值)分开处理的技术。

- 先定义一个 SQL 查询的“模板”或“骨架”,其中不确定的数据值用占位符(如
@parameterName)代替。 - 通过参数对象(如
SqlParameter)将具体的值安全地传递给这个模板。
举个例子,对比一下非参数化和参数化查询:
非参数化查询 (不推荐 - 存在 SQL 注入风险)
// 危险!用户输入直接拼接到SQL字符串中 string userName = "admin'; DROP TABLE Users; --"; string sql = "SELECT * FROM Users WHERE Username = '" + userName + "' AND Password = 'somepass'";
userName 是恶意输入,admin'; DROP TABLE Users; --,最终执行的 SQL 语句就变成了:
SELECT * FROM Users WHERE Username = 'admin'; DROP TABLE Users; --' AND Password = 'somepass'
数据库会先执行查询,然后执行 DROP TABLE Users,导致灾难性后果,这就是 SQL 注入。
参数化查询 (推荐 - 安全且高效)

// 安全!SQL 语句和数据是分离的
string userName = "admin'; DROP TABLE Users; --";
string sql = "SELECT * FROM Users WHERE Username = @username AND Password = @password";
// 使用 SqlParameter 对象来传递值
SqlParameter[] parameters = new SqlParameter[]
{
new SqlParameter("@username", userName),
new SqlParameter("@password", "somepass")
};
在这种情况下,数据库引擎会先解析 SELECT * FROM Users WHERE Username = @username AND Password = @password 这个语句的结构,它会把 @username 这个占位符的值当作一个纯粹的字符串来处理,而不会去执行其中的分号或SQL命令,SQL 注入被完全阻止了。
为什么必须使用参数化查询?(核心优势)
防止 SQL 注入攻击 (最重要的原因)
这是参数化查询最核心、最不可替代的价值,如上所述,它将数据和代码分离,确保用户输入永远不会被解释为 SQL 命令的一部分,数据库引擎只会将参数值作为数据处理,从而从根本上杜绝了 SQL 注入的可能性。
提高性能
对于频繁执行的 SQL 语句,数据库可以缓存其执行计划。
- 非参数化查询:
"WHERE Name = 'Alice'"和"WHERE Name = 'Bob'"被数据库视为两个完全不同的 SQL 语句,因此需要分别解析和生成执行计划。 - 参数化查询:
"WHERE Name = @name"无论@name的值是什么,都只被视为一个 SQL 语句,数据库可以缓存这个通用语句的执行计划,当再次执行时,只需将新的参数值填入即可,大大减少了解析和编译的开销。
数据类型安全和代码可读性
- 类型安全:
SqlParameter允许你明确指定参数的数据类型(如SqlDbType.Int,SqlDbType.NVarChar),这可以防止因数据类型不匹配导致的错误,并让代码意图更清晰。 - 可读性:使用
@username这样的命名参数,比用问号 (在某些数据库中)或者手动拼接字符串要清晰得多,代码更易于维护。
SqlParameter 详解
SqlParameter 是 ADO.NET 中用于表示 SQL 查询参数的类,它位于 System.Data.SqlClient 命名空间下。

常用属性
| 属性名 | 类型 | 描述 |
|---|---|---|
ParameterName |
string |
参数的名称,必须与 SQL 语句中的占位符匹配(如 @username)。 |
Value |
object |
参数的值,可以是基本类型(int, string, bool)或 DBNull。 |
SqlDbType |
SqlDbType |
指定参数的 SQL Server 数据类型(如 Int, NVarChar, DateTime),虽然通常可以不设,但显式设置是最佳实践。 |
Size |
int |
对于字符串类型(NVarChar, VarChar),指定其最大长度,对于 Xml 或 Udt 类型,指定大小。 |
Direction |
ParameterDirection |
指定参数是输入、输出、输入输出还是返回值,默认为 Input。 |
IsNullable |
bool |
指示参数值是否可以为 null。 |
Precision |
byte |
对于数值类型,指定总位数。 |
Scale |
byte |
对于数值类型,指定小数位数。 |
常用方法
主要使用 SqlParameter 的构造函数来创建实例。
SqlParameter 的使用示例
下面我们通过 ADO.NET 的经典步骤(连接、命令、执行、读取)来看 SqlParameter 的具体应用。
示例 1:查询数据
using System;
using System.Data;
using System.Data.SqlClient;
public class UserRepository
{
private readonly string _connectionString = "Your_Connection_String_Here";
public User GetUserById(int userId)
{
User user = null;
string sql = "SELECT Id, Username, Email FROM Users WHERE Id = @userId";
// 使用 using 语句确保资源被正确释放
using (SqlConnection connection = new SqlConnection(_connectionString))
{
// 创建 SqlCommand 对象
using (SqlCommand command = new SqlCommand(sql, connection))
{
// 添加参数
command.Parameters.AddWithValue("@userId", userId);
try
{
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
if (reader.Read())
{
user = new User
{
Id = reader.GetInt32(0),
Username = reader.GetString(1),
Email = reader.GetString(2)
};
}
}
}
catch (Exception ex)
{
// 处理异常
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
}
return user;
}
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
代码解析:
SqlCommand command = new SqlCommand(sql, connection);:创建命令对象,SQL 模板中使用了@userId占位符。command.Parameters.AddWithValue("@userId", userId);:这是最简单添加参数的方式,它会自动根据userId变量的类型推断SqlParameter的SqlDbType。- 当
ExecuteReader执行时,@userId会被安全地替换为传入的userId值。
示例 2:插入数据
public int AddUser(string username, string email)
{
string sql = "INSERT INTO Users (Username, Email) VALUES (@username, @email); SELECT SCOPE_IDENTITY();";
int newId = -1;
using (SqlConnection connection = new SqlConnection(_connectionString))
{
using (SqlCommand command = new SqlCommand(sql, connection))
{
// 添加参数
command.Parameters.Add(new SqlParameter("@username", SqlDbType.NVarChar, 50));
command.Parameters["@username"].Value = username;
command.Parameters.AddWithValue("@email", email);
try
{
connection.Open();
// ExecuteScalar 用于执行查询并返回结果的第一行第一列
// SCOPE_IDENTITY() 会返回新插入行的 ID
object result = command.ExecuteScalar();
if (result != null && result != DBNull.Value)
{
newId = Convert.ToInt32(result);
}
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
}
return newId;
}
代码解析:
- 这里展示了两种添加参数的方式:
AddWithValue和new SqlParameter。 SCOPE_IDENTITY()是一个常用的 SQL 函数,用于获取当前会话中最后插入的标识值(自增ID)。ExecuteScalar()适合执行这种只返回单个值的查询。
示例 3:使用输出参数
有时我们需要从存储过程或SQL语句中获取返回值。
public void GetUserNameAndCount(int userId, out string userName, out int totalUsers)
{
userName = null;
totalUsers = -1;
string sql = "SELECT @userNameOut = Username FROM Users WHERE Id = @userId; SELECT @totalUsersOut = COUNT(*) FROM Users;";
using (SqlConnection connection = new SqlConnection(_connectionString))
{
using (SqlCommand command = new SqlCommand(sql, connection))
{
// 输入参数
command.Parameters.AddWithValue("@userId", userId);
