399 lines
11 KiB
Markdown
399 lines
11 KiB
Markdown
# 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) |