# 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)