# 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` 扩展。 ### 解决方案 1. 下载对应 PHP 版本的驱动: - 下载地址:[Microsoft Drivers for PHP for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/php/download-drivers-php-sql-server?view=sql-server-ver16) - 版本选择:需匹配 PHP 版本、NTS/TS、x86/x64 2. 将驱动文件复制到 PHP 扩展目录: - `php_sqlsrv.dll` - `php_pdo_sqlsrv.dll` 3. 在 `php.ini` 中启用扩展: ```ini extension=php_sqlsrv.dll extension=php_pdo_sqlsrv.dll ``` 4. 重启 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。 ### 解决方案 1. 安装 ODBC Driver 17 for SQL Server: - 下载地址:[ODBC Driver for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/odbc/download-odbc-driver-for-sql-server) 2. 安装 Visual C++ Redistributable 2017: - 下载地址:[Visual Studio 2017 Redistributable](https://learn.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redist?view=msvc-170) --- ## 问题三:SQL Server 端口配置错误 ### 现象 ``` 用户 'xxx' 登录失败 ``` ### 原因 SQL Server 未使用默认端口 1433,而是使用了动态端口。 ### 解决方案 1. 在 SQL Server 上查询实际端口: ```sql SELECT local_tcp_port FROM sys.dm_exec_connections WHERE session_id = @@SPID; ``` 2. 修改 `.env` 配置文件: ```ini [MSSQL] HOSTNAME = 192.168.0.156 HOSTPORT = 50109 ; 实际端口 ``` 3. 使用 IP+端口方式连接,避免使用实例名。 --- ## 问题四:参数绑定错误(HY093) ### 现象 ``` SQLSTATE[HY093]: Invalid parameter number: parameter was not defined ``` ### 原因分析 #### 子问题 4.1:SQL Server PDO 不支持表达式中的参数 **错误代码**: ```php // SQL 模板 WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit) // 参数绑定 $params = [':offset' => 0, ':limit' => 20]; ``` **问题**:SQL Server 的 PDO 驱动不支持在算术表达式中使用命名参数。 **解决方案**:预先计算分页范围,传递单个参数: ```php $startRow = ($page - 1) * $limit + 1; $endRow = $page * $limit; $params = [':start_row' => $startRow, ':end_row' => $endRow]; ``` SQL 模板改为: ```sql WHERE RowID BETWEEN :start_row AND :end_row ``` #### 子问题 4.2:统计查询与数据查询参数不匹配 **错误代码**: ```php $allParams = array_merge($dateParams, $isolationParams, [':start_row', ':end_row']); Db::query($countSql, $allParams); // countSql 中没有 :start_row 和 :end_row ``` **问题**:countSql(统计查询)和 dataSql(数据查询)使用了相同的参数数组,但 countSql 不需要分页参数。 **解决方案**:将参数数组分开: ```php $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 混淆 **错误代码**: ```php $logSql = self::buildLogSql($sqlTemplate, $dateCondition, $isolationSql, $allParams); $dataSql = $logSql; // $logSql 已替换参数值 Db::query($dataSql, $allParams); // 参数绑定失败 ``` **问题**:`buildLogSql` 函数返回的 SQL 已经把参数值替换进去(用于日志),但代码用这个 SQL 去做参数绑定,导致 PDO 找不到占位符。 **解决方案**:让 `buildLogSql` 返回两个版本的 SQL: ```php return ['dataSql' => $dataSql, 'logSql' => $logSql]; ``` --- ## 问题五:参数前缀错误(IMSSP) ### 现象 ``` SQLSTATE[IMSSP]: Tried to bind parameter number 65536. SQL Server supports a maximum of 2100 parameters. ``` ### 原因分析 #### 子问题 5.1:手动使用 `@` 前缀导致参数编号溢出 **错误代码**: ```php // SQL 模板 WHERE [登记时间] >= @start_date // 参数绑定 $params = ['@start_date' => $startDate]; ``` **问题**:SQL Server 的 PDO 驱动在手动使用 `bindValue` 时,会把 `@start_date` 解析为参数编号 65536(0xFFFF),导致参数编号溢出。 **尝试方案 1(失败)**:改用 `:` 前缀 ```php // SQL 模板 WHERE [登记时间] >= :start_date // 参数绑定 $params = [':start_date' => $startDate]; ``` **结果**:仍然报错 `HY093: Invalid parameter number`,因为 SQL Server 的 PDO 驱动在 CTE(WITH 子句)中对 `:` 前缀的参数支持不佳。 #### 子问题 5.2:ThinkPHP 自动添加 `:` 前缀 **根本原因**:ThinkPHP 的 `PDOConnection` 类在 `bindValue` 方法中会自动添加 `:` 前缀: ```php // 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` ✅ ### 解决方案 **正确的参数绑定方式**: ```php // 1. SQL 模板中使用 : 前缀 $dateCondition = " AND [登记时间] >= :start_date"; // 2. 参数数组中使用无前缀键名 $dateParams['start_date'] = $startDate; // 3. ThinkPHP 自动处理为 :start_date → 绑定成功! ``` **完整示例**: ```php // 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 函数修改**: ```php 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]; } ``` ### 修改的文件 1. **MssqlQuery.php**: - `queryChuku` 方法:参数键名从 `:start_date` 改为 `start_date` - `queryChukuReal` 方法:参数键名从 `@start_row` 改为 `start_row` - `queryRuku` 方法:参数键名从 `:start_date` 改为 `start_date` - `queryRukuReal` 方法:参数键名从 `@start_row` 改为 `start_row` - SQL 模板:保持 `:` 前缀占位符 - `buildLogSql` 方法:替换时自动添加 `:` 前缀 2. **DataIsolation.php**: - `buildCustomFilter` 方法:参数键名从 `@iso_0` 改为 `iso_0` - `buildAutoFilter` 方法:参数键名从 `@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 语句,难以排查问题。 ### 解决方案 在查询执行前后添加详细日志: ```php 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` ```php // 修改前 WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit) // 修改后 WHERE RowID BETWEEN :start_row AND :end_row ``` ### 2. 参数绑定修改 ```php // 修改前 $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 函数修改 ```php // 修改前 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 用 `:` 前缀 | --- ## 参考文献 - [Microsoft Drivers for PHP for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/php/download-drivers-php-sql-server) - [ODBC Driver for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/odbc/download-odbc-driver-for-sql-server) - [ThinkPHP 数据库操作](https://www.kancloud.cn/manual/thinkphp6_0/1037537)