两个驱动程序都可以执行一次语句,或预编译语句以便重复执行。 即使只运行一次查询,也要对用户提供的值使用参数。
将执行 API 的选择与获取结果的方式分开。 预编译查询并不会获取其行,而缓冲结果也不会使查询变为参数化查询。 以下示例会预编译一项只读查询,使用不同的值执行两次,并在下一次执行之前消耗每个结果。
| 执行风格 | SQLSRV | PDO_SQLSRV |
|---|---|---|
| 运行一次不带参数的查询 | sqlsrv_query | PDO::query 用于获取结果,PDO::exec 用于获取受影响行数 |
| 运行一次带有参数的查询 | sqlsrv_query($conn, $sql, $params) |
PDO::prepare() 后接 PDOStatement::execute() |
| 准备一次,使用不同参数反复执行 | sqlsrv_prepare + sqlsrv_execute | PDO::prepare + PDOStatement::execute |
sqlsrv_query() 在一次调用中准备并执行,并返回语句资源。
sqlsrv_prepare() 返回语句资源但不执行;每次执行时调用 sqlsrv_execute()。 PDO 的 query() 会执行查询并返回 PDOStatement,而 exec() 返回的是受影响行数,而不是结果对象。 成功 exec() 返回 0,因此将失败返回与 === false进行比较,而非 if (!$count)。
PDO::query() 不接受参数数组。 其可选参数用于配置获取。 对于参数化PDO_SQLSRV查询,使用 prepare() 和 execute()。
一次准备可以让你用不同值复用语句句柄。 这并不保证服务器永远不会重新编译执行计划。 PDO_SQLSRV 默认使用预编译执行,包括对 PDO::query() 也是如此;其直接查询属性控制这一独立选择。
准备一次,反复执行
这些都是独立且完整的命令行程序。 安装并加载所选扩展,并在 PHP 进程环境中设置 SQL_SERVER、SQL_DATABASE、SQL_USER 和 SQL_PASSWORD。 使用SQL Server测试数据库和客户端信任的服务器证书。 关于环境设置和无密码替代方案,请参见 连接服务器。
将实际的数据库名称、用户名和密码存储在环境变量中。 两个程序都会针对 PHP 驱动程序的 ODBC 风格连接处理要求,通过将右花括号写成两个,并用花括号括住每个值,对这些值转义一次。 转义连接值与绑定查询参数是分开的。
这些程序在两个内联行执行 Transact-SQL(T-SQL)查询,无需创建表或更改数据。 将每个程序分别保存到各自的 .php 文件中,并使用 php <file> 运行它。 不需要样本数据库。
SQLSRV
使用 &$productId 按引用传递 $productId。 然后每个 sqlsrv_execute() 都使用当前值。
SQLSRV_FETCH_ASSOC 返回以列名为键的数组。 结果 null 表示没有更多的行; false 表示错误。
<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$value = getenv($name);
if ($value === false || $value === '') {
throw new RuntimeException("Set $name before running this example.");
}
$config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$conn = sqlsrv_connect($config['SQL_SERVER'], [
'Database' => $config['SQL_DATABASE'],
'UID' => $config['SQL_USER'],
'PWD' => $config['SQL_PASSWORD'],
'Encrypt' => true,
'TrustServerCertificate' => false,
'LoginTimeout' => 30,
'CharacterSet' => 'UTF-8',
]);
if ($conn === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$sql = "SELECT Name FROM (VALUES (1, 'Bike'), (2, 'Helmet')) AS Product(ProductId, Name) WHERE ProductId = ?";
$productId = 1;
$stmt = null;
try {
$stmt = sqlsrv_prepare($conn, $sql, [&$productId], ['QueryTimeout' => 30]);
if ($stmt === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
foreach ([1 => 'Bike', 2 => 'Helmet'] as $productId => $expected) {
if (!sqlsrv_execute($stmt)) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$names = [];
while (($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) !== null) {
if ($row === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$names[] = $row['Name'];
}
if ($names !== [$expected]) {
throw new RuntimeException('Expected exactly one matching product.');
}
echo $names[0], PHP_EOL;
}
} finally {
if (is_resource($stmt)) {
sqlsrv_free_stmt($stmt);
}
sqlsrv_close($conn);
}
PDO_SQLSRV
使用 bindParam() 绑定 $productId 一次。 每个 execute() 都读取其当前值。
PDO::PARAM_INT 指定参数的PHP/PDO绑定类型;它不控制从结果中获取的列的PHP类型。 设置异常模式,使数据库错误中断执行,而不是误认为是空结果。
<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$value = getenv($name);
if ($value === false || $value === '') {
throw new RuntimeException("Set $name before running this example.");
}
$config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$server = $config['SQL_SERVER'];
$database = $config['SQL_DATABASE'];
$pdo = new PDO(
"sqlsrv:Server=$server;Database=$database;Encrypt=true;"
. "TrustServerCertificate=false;LoginTimeout=30",
$config['SQL_USER'],
$config['SQL_PASSWORD'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_UTF8,
]
);
$sql = "SELECT Name FROM (VALUES (1, 'Bike'), (2, 'Helmet')) AS Product(ProductId, Name) WHERE ProductId = ?";
$stmt = null;
try {
$stmt = $pdo->prepare($sql, [PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 30]);
$productId = 1;
$stmt->bindParam(1, $productId, PDO::PARAM_INT);
foreach ([1 => 'Bike', 2 => 'Helmet'] as $productId => $expected) {
$stmt->execute();
$names = $stmt->fetchAll(PDO::FETCH_COLUMN);
if ($names !== [$expected]) {
throw new RuntimeException('Expected exactly one matching product.');
}
echo $names[0], PHP_EOL;
$stmt->closeCursor();
}
} finally {
$stmt = null;
$pdo = null;
}
每个示例均输出:
Bike
Helmet
传递给PDO execute() 的数组中的值被处理为 PDO::PARAM_STR。 当 SQL 需要整数参数时(例如参数化的 TOP 计数),请使用 PDO::PARAM_INT 调用 bindValue(),然后在不带数组的情况下调用 execute()。
绑定值而不更改查询
参数标记表示一个值,而不是表名、列名、关键词或排序方向。 不要在 ? 周围加上引号。 对于动态标识符或 ASC/DESC,从应用程序控制的允许列表中选择,而不是串接不可信的文本。 对于 IN 列表,每个值都提供一个标记。
| 要求 | SQLSRV | PDO_SQLSRV |
|---|---|---|
| 为单次执行绑定值 | 将参数数组传递给 sqlsrv_query()。 |
在 prepare() 之后将数组传递给 execute(),或先绑定值。 |
| 重用变量当前值 | 将 &$variable 包含在提供给 sqlsrv_prepare() 的数组中。 |
使用该变量调用 bindParam(),然后在不带数组的情况下调用 execute()。 PHP 通过引用传递变量;不要在调用位置添加 &。 |
| 在绑定时绑定一个值 | 执行时使用参数数组中的一个值。 | 调用 bindValue();后续对原始变量的更改不会改变该绑定值。 |
| 指定一个整数输入 | 在需要显式类型时,使用带有 SQLSRV_PARAM_IN、SQLSRV_PHPTYPE_INT 和 SQLSRV_SQLTYPE_INT 的参数描述符。 |
通过 PDO::PARAM_INT 绑定。 位置 bindParam() 和 bindValue() 索引从 1 开始,而不是从 0 开始。 |
发送SQL NULL |
绑定 PHP null,并在操作要求时指定显式 SQL 类型。 |
通过 PDO::PARAM_NULL 绑定。 |
| 接收输出参数 | 使用带有 SQLSRV_PARAM_OUT 或 SQLSRV_PARAM_INOUT的引用变量。 |
使用 bindParam() 与 PDO::PARAM_INPUT_OUTPUT 和 所需的类型和长度。 |
在执行预编译语句期间,保持引用变量的存续。 SQLSRV程序是更改绑定变量,而不是替换参数数组。 PDO 程序在绑定后故意调用 execute(),且不传入数组;不要将这种模式与基于数组的执行混淆。
存储过程的输出值需要单独处理,包括完成该过程的结果处理。 不要从这些仅输入的例子中推断输出参数行为。 更多信息请参见 使用方向参数。
配置执行和获取行为
| 设置 | SQLSRV | PDO_SQLSRV |
|---|---|---|
| 执行超时 |
QueryTimeout 位于语句选项数组中。 |
PDO::SQLSRV_ATTR_QUERY_TIMEOUT 位于连接或语句上。 |
| 命名结果键 | 传递给 sqlsrv_fetch_array() 的 SQLSRV_FETCH_ASSOC。 |
PDO::FETCH_ASSOC 传递给获取方法,或设置为连接的默认获取模式。 |
| 仅前进结果 | 默认游标;按顺序获取。 | 默认游标;按顺序获取。 |
| 客户端缓冲结果 |
Scrollable => SQLSRV_CURSOR_CLIENT_BUFFERED。 |
使用 PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED 的 PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL。 |
| 直接执行与预编译执行 | 没有 PDO 直接查询属性。 选择合并 sqlsrv_query() 调用或显式准备/执行对。 |
PDO::SQLSRV_ATTR_DIRECT_QUERY 选择直接执行而非驱动程序默认的预备执行。 |
| 参数仿真 | 没有 PDO 模拟属性。 |
PDO::ATTR_EMULATE_PREPARES 与直接查询特性是分开的。 不要用它来代替绑定正确的类型。 |
这些示例返回小型结果集。
fetchAll() 会把每一行都加载到 PHP 内存中;对于大型结果集,请使用获取循环。 查询没有匹配不算执行错误。 SQLSRV 将结果结束报告为来自 sqlsrv_fetch_array() 的 null;PDO 将其报告为来自 fetch() 的 false。 所获取行中的 SQL NULL 仍为 PHP null。
不要只用 SELECT 行数作为唯一的成功检定。 仅前进光标不具备缓冲或可滚动光标的行计数功能。 获取并统计所需的行,或使用单独的 COUNT 查询。 关于光标特定的选项和限制,请参见 “指定光标类型并选择行”。
处理失败并完成结果
分别检查SQLSRV的准备、执行和获取操作。 预编译成功并不能证明执行一定会成功:服务器可能在执行时拒绝参数值、权限或语句本身。 当操作返回 false 时立即捕获 sqlsrv_errors()。 SQLSRV默认将许多警告视为错误,因此请检查完整的集合。
对于PDO,异常模式下的连接失败和数据库错误会抛出 PDOException。 在受保护的应用诊断中检查其SQLSTATE和驱动程序错误信息;不要在HTTP响应中返回异常文本或凭证。
prepare() 成功并不表示该语句已经运行。
| 症状 | Action |
|---|---|
| 每次执行都使用第一个值 | 对于 SQLSRV,通过引用绑定一个变量。 对于 PDO,请对会变化的变量使用 bindParam(),或为每个新值调用 bindValue()。 |
数值参数被拒绝,例如 TOP 计数 |
明确绑定所需的整数类型。 即使PHP值是整数,PDO execute([$value]) 仍然使用字符串参数。 |
| 第二个命令报告待处理结果 | 在重新使用连接前,先消费结果或释放它们。 对于PDO,当你不需要剩余结果时使用 closeCursor() 。 |
| 预期的输出参数没有发生变化 | 完成所有过程结果,并验证参数方向、PHP 类型和输出缓冲区长度。 |
| 内存使用随结果大小增长 | 将 fetchAll() 或不必要的客户端缓冲替换为顺序获取。 |
这些示例会在 finally 中释放各自的语句并释放各自的连接。 它们不会开始事务。 当多个写入必须同时提交或全部回滚时,使用显式 事务 。 重用预编译语句并不会把这些写入归入同一个事务。
目标与实施差异
预编译/执行 API 和 PHP 引用规则属于扩展以及 PHP/PDO。 SQL 目标决定支持哪些 T-SQL 语句和类型,ODBC 执行底层通信。 预编译一次可以减少重复工作,但并不能保证得到特定的服务器执行计划或实现加速。
输入绑定模式适用于 SQL Server、Azure SQL 数据库、Azure SQL 托管实例、Azure Synapse Analytics、Microsoft Fabric 中的 SQL 数据库以及 Microsoft Fabric 中的 Warehouse。 这些程序使用 SQL 认证来访问 SQL Server。 对于 Azure 加入,建议优先使用 Microsoft Entra 身份验证。 两个 Fabric 产品都需要这样做,仓库也需要禁用多个主动结果集(MARS)。 在重用 SQL Server 代码之前,请检查所选目标对模式变更、事务和存储过程的支持。 关于端点和认证的区别,请参见 Azure 和 Fabric 连接。