11 KiB
SQL Server 连接与查询采坑文档
问题概述
本项目在 PHP + ThinkPHP 框架下连接 SQL Server 数据库时,遇到了一系列连接和查询问题,现整理如下。
问题一:PHP 缺少 SQL Server 驱动
现象
SQLSTATE[IMSSP]: This extension requires the Microsoft ODBC Driver for SQL Server to communicate with SQL Server
原因
PHP 未安装 sqlsrv 和 pdo_sqlsrv 扩展。
解决方案
-
下载对应 PHP 版本的驱动:
- 下载地址:Microsoft Drivers for PHP for SQL Server
- 版本选择:需匹配 PHP 版本、NTS/TS、x86/x64
-
将驱动文件复制到 PHP 扩展目录:
php_sqlsrv.dllphp_pdo_sqlsrv.dll
-
在
php.ini中启用扩展:extension=php_sqlsrv.dll extension=php_pdo_sqlsrv.dll -
重启 PHP 服务。
注意事项
- Windows 7 兼容性:SQLSRV 5.13.1 支持 Windows 7,但需要安装 Visual C++ Redistributable 2017。
- 避免重复加载:检查
php.ini中是否有重复的扩展配置。
问题二:缺少 ODBC Driver 依赖
现象
This extension requires the Microsoft ODBC Driver for SQL Server
原因
SQLSRV 扩展依赖 Microsoft ODBC Driver for SQL Server。
解决方案
-
安装 ODBC Driver 17 for SQL Server:
-
安装 Visual C++ Redistributable 2017:
问题三:SQL Server 端口配置错误
现象
用户 'xxx' 登录失败
原因
SQL Server 未使用默认端口 1433,而是使用了动态端口。
解决方案
-
在 SQL Server 上查询实际端口:
SELECT local_tcp_port FROM sys.dm_exec_connections WHERE session_id = @@SPID; -
修改
.env配置文件:[MSSQL] HOSTNAME = 192.168.0.156 HOSTPORT = 50109 ; 实际端口 -
使用 IP+端口方式连接,避免使用实例名。
问题四:参数绑定错误(HY093)
现象
SQLSTATE[HY093]: Invalid parameter number: parameter was not defined
原因分析
子问题 4.1:SQL Server PDO 不支持表达式中的参数
错误代码:
// SQL 模板
WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit)
// 参数绑定
$params = [':offset' => 0, ':limit' => 20];
问题:SQL Server 的 PDO 驱动不支持在算术表达式中使用命名参数。
解决方案:预先计算分页范围,传递单个参数:
$startRow = ($page - 1) * $limit + 1;
$endRow = $page * $limit;
$params = [':start_row' => $startRow, ':end_row' => $endRow];
SQL 模板改为:
WHERE RowID BETWEEN :start_row AND :end_row
子问题 4.2:统计查询与数据查询参数不匹配
错误代码:
$allParams = array_merge($dateParams, $isolationParams, [':start_row', ':end_row']);
Db::query($countSql, $allParams); // countSql 中没有 :start_row 和 :end_row
问题:countSql(统计查询)和 dataSql(数据查询)使用了相同的参数数组,但 countSql 不需要分页参数。
解决方案:将参数数组分开:
$countParams = array_merge($dateParams, $isolationParams);
$dataParams = array_merge($countParams, [':start_row', ':end_row']);
Db::query($countSql, $countParams);
Db::query($dataSql, $dataParams);
子问题 4.3:日志 SQL 与执行 SQL 混淆
错误代码:
$logSql = self::buildLogSql($sqlTemplate, $dateCondition, $isolationSql, $allParams);
$dataSql = $logSql; // $logSql 已替换参数值
Db::query($dataSql, $allParams); // 参数绑定失败
问题:buildLogSql 函数返回的 SQL 已经把参数值替换进去(用于日志),但代码用这个 SQL 去做参数绑定,导致 PDO 找不到占位符。
解决方案:让 buildLogSql 返回两个版本的 SQL:
return ['dataSql' => $dataSql, 'logSql' => $logSql];
问题五:参数前缀错误(IMSSP)
现象
SQLSTATE[IMSSP]: Tried to bind parameter number 65536. SQL Server supports a maximum of 2100 parameters.
原因分析
子问题 5.1:手动使用 @ 前缀导致参数编号溢出
错误代码:
// SQL 模板
WHERE [登记时间] >= @start_date
// 参数绑定
$params = ['@start_date' => $startDate];
问题:SQL Server 的 PDO 驱动在手动使用 bindValue 时,会把 @start_date 解析为参数编号 65536(0xFFFF),导致参数编号溢出。
尝试方案 1(失败):改用 : 前缀
// SQL 模板
WHERE [登记时间] >= :start_date
// 参数绑定
$params = [':start_date' => $startDate];
结果:仍然报错 HY093: Invalid parameter number,因为 SQL Server 的 PDO 驱动在 CTE(WITH 子句)中对 : 前缀的参数支持不佳。
子问题 5.2:ThinkPHP 自动添加 : 前缀
根本原因:ThinkPHP 的 PDOConnection 类在 bindValue 方法中会自动添加 : 前缀:
// vendor/topthink/think-orm/src/db/PDOConnection.php:1358
protected function bindValue(array $bind = []): void
{
foreach ($bind as $key => $val) {
// 占位符
$param = is_numeric($key) ? $key + 1 : ':' . $key; // 自动添加 : 前缀
// ...
}
}
这意味着:
- 参数
@start_date→ 变成:@start_date❌ - 参数
:start_date→ 变成::start_date❌ - 参数
start_date→ 变成:start_date✅
解决方案
正确的参数绑定方式:
// 1. SQL 模板中使用 : 前缀
$dateCondition = " AND [登记时间] >= :start_date";
// 2. 参数数组中使用无前缀键名
$dateParams['start_date'] = $startDate;
// 3. ThinkPHP 自动处理为 :start_date → 绑定成功!
完整示例:
// SQL 模板
WHERE [登记时间] >= :start_date AND [登记时间] <= :end_date
AND [销方全称] = :iso_0
// 参数数组
$params = [
'start_date' => '2024-02-02',
'end_date' => '2026-06-22',
'iso_0' => '喀喇沁旗中医蒙医医院'
];
// 执行查询
Db::connect('mssql')->query($sql, $params);
buildLogSql 函数修改:
private static function buildLogSql($sqlTemplate, $dateCondition, $isolationSql, $allParams)
{
$dataSql = str_replace(['{date_condition}', '{isolation_condition}'], [$dateCondition, $isolationSql], $sqlTemplate);
$logSql = $dataSql;
foreach ($allParams as $key => $value) {
$placeholder = ':' . $key; // 替换时自动添加 : 前缀
if (is_string($value)) {
$logSql = str_replace($placeholder, "'" . $value . "'", $logSql);
} else {
$logSql = str_replace($placeholder, (string)$value, $logSql);
}
}
return ['dataSql' => $dataSql, 'logSql' => $logSql];
}
修改的文件
-
MssqlQuery.php:
queryChuku方法:参数键名从:start_date改为start_datequeryChukuReal方法:参数键名从@start_row改为start_rowqueryRuku方法:参数键名从:start_date改为start_datequeryRukuReal方法:参数键名从@start_row改为start_row- SQL 模板:保持
:前缀占位符 buildLogSql方法:替换时自动添加:前缀
-
DataIsolation.php:
buildCustomFilter方法:参数键名从@iso_0改为iso_0buildAutoFilter方法:参数键名从@iso_0改为iso_0
总结
| 方案 | SQL 占位符 | 参数键名 | ThinkPHP 处理后 | 结果 |
|---|---|---|---|---|
| 错误 1 | @start_date |
@start_date |
:@start_date |
❌ IMSSP 错误 |
| 错误 2 | :start_date |
:start_date |
::start_date |
❌ HY093 错误 |
| 正确 | :start_date |
start_date |
:start_date |
✅ 成功 |
问题六:日志记录缺失
现象
无法看到生成的 SQL 语句,难以排查问题。
解决方案
在查询执行前后添加详细日志:
Log::debug("出库查询 - 统计SQL: " . $countSql);
Log::debug("出库查询 - 统计参数: " . json_encode($countParams));
Log::debug("出库查询 - 数据SQL: " . $dataSql);
Log::debug("出库查询 - 数据参数: " . json_encode($dataParams));
排查流程总结
连接失败
↓
检查 PHP 扩展是否安装
↓
检查 ODBC Driver 是否安装
↓
检查 SQL Server 端口配置
↓
连接成功但查询失败
↓
检查 SQL 语法是否正确
↓
检查参数绑定是否匹配
↓
添加日志记录调试
↓
定位具体问题并修复
关键代码修改
1. SQL 模板修改
文件:app/service/MssqlQuery.php
// 修改前
WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit)
// 修改后
WHERE RowID BETWEEN :start_row AND :end_row
2. 参数绑定修改
// 修改前
$offset = ($page - 1) * $limit;
$allParams = array_merge($dateParams, $isolationParams, [
':offset' => $offset,
':limit' => $limit
]);
// 修改后
$startRow = ($page - 1) * $limit + 1;
$endRow = $page * $limit;
$countParams = array_merge($dateParams, $isolationParams);
$dataParams = array_merge($countParams, [
':start_row' => $startRow,
':end_row' => $endRow
]);
3. buildLogSql 函数修改
// 修改前
private static function buildLogSql(...)
{
// ...
return $logSql;
}
// 修改后
private static function buildLogSql(...)
{
// ...
return ['dataSql' => $dataSql, 'logSql' => $logSql];
}
总结
| 问题 | 根本原因 | 解决方案 |
|---|---|---|
| 驱动缺失 | PHP 未安装 sqlsrv 扩展 | 安装对应版本驱动 |
| ODBC 依赖 | 缺少 ODBC Driver | 安装 ODBC Driver 17 |
| 端口错误 | SQL Server 使用动态端口 | 查询实际端口并配置 |
| 参数表达式 | SQL Server PDO 不支持 | 预先计算参数值 |
| 参数不匹配 | 统计/数据查询共用参数 | 分开参数数组 |
| 日志混淆 | 执行 SQL 使用了日志版本 | 返回分离的 SQL |
| 参数前缀错误 | ThinkPHP 自动添加 : 前缀 |
参数键名用无前缀,SQL 用 : 前缀 |