排查适用于 PHP 的 Microsoft SQL Server 驱动程序问题

下载 PHP 驱动程序

在使用 Microsoft Drivers for PHP for SQL Server 连接 SQL Server、Azure SQL 数据库、Azure SQL 托管实例 和 Microsoft Fabric 中的 SQL 数据库时,诊断并解决常见问题。

有关一般的错误和警告处理模式,请参见 处理错误与警告。 有关驱动程序端诊断信息捕获,请参见 日志记录活动

安装问题

扩展未加载

症状

  • phpinfo() 未列出 sqlsrvpdo_sqlsrv 部分。
  • PDOException: could not find driver 使用 sqlsrv: DSN 构建 PDO 时。
  • Fatal error: Uncaught Error: Call to undefined function sqlsrv_connect()

可能的原因和解决方案

  • php.ini中未启用扩展 。 确认 extension=sqlsrvextension=pdo_sqlsrv 均未被注释。 在 Windows 上,使用完整文件名 (extension=php_sqlsrv_84_ts_x64.dll)。 详情请参见 “加载驱动程序”。
  • 错误的螺纹安全配置。 驱动程序二进制文件必须与您的 PHP 构建的线程安全性匹配(ts 表示线程安全,nts 表示非线程安全)。 运行 php -i | grep "Thread Safety" 以检查。 从 下载页面下载匹配的二进制文件。
  • Microsoft ODBC 驱动缺失。 PHP 驱动程序封装了 Microsoft ODBC Driver for SQL Server。 在 Linux 和 macOS 上,加载扩展前先用包管理器安装 msodbcsql18 (或 msodbcsql17)。 在 Windows 上,从下载页面安装 ODBC 驱动。

确认安装成功:

php -m | grep -i sqlsrv

你应该在输出中同时看到 pdo_sqlsrvsqlsrv

在 Linux 或 macOS 上安装 PECL 或 PIE 失败

症状

error: ‘SQL_HANDLE_DBC’ undeclared (first use in this function)
fatal error: 'sql.h' file not found

修复:

安装驱动前先安装 ODBC 开发头:

  • Ubuntu 和 Debiansudo apt-get install unixodbc-dev
  • 红帽、Fedora和CentOSsudo dnf install unixODBC-devel
  • 阿尔卑斯apk add unixodbc-dev
  • macOSbrew install unixodbc

在 Apple 芯片上,光有头文件还不够。 Homebrew 会将 unixODBC 安装在 /opt/homebrew 下,而该路径不在编译器的默认搜索路径中,因此即使头文件已存在,构建仍会因同样的错误而失败。 在重试前设置编译器标志:

export CPPFLAGS="-I/opt/homebrew/opt/unixodbc/include/"
export LDFLAGS="-L/opt/homebrew/lib/"

然后用PIE重试:

pie install microsoft/sqlsrv
pie install microsoft/pdo_sqlsrv

或者使用 PECL:

sudo pecl install sqlsrv
sudo pecl install pdo_sqlsrv

如果安装完截头器后安装仍然失败,说明构建工具链可能不完整。 安装 phpizere2c以及一个C++编译器(build-essential 在Debian和Ubuntu上,在 gcc-c++ make Red Hat和Fedora上,在 build-base Alpine上)。 PIE 可在 Linux 和 macOS 上为你安装缺少的构建工具。

完整安装路径请参见 Linux和macOS安装教程

安装了多个PHP版本

症状

phpinfo() 在你的 Web 服务器中显示的是一个 PHP 版本,而 php -v 在命令行中显示的是另一个版本,并且该驱动似乎只在其中一个版本中加载了。

修复:

每个PHP版本都有自己的php.iniext目录。 在缺少驱动程序的环境中,使用 php --ini 定位正确的配置文件,并在其中添加 extension= 行。 任何 php.ini 更改后,重启网页服务器(Apache、Nginx + PHP-FPM 或 IIS)。

连接问题

无法连接到服务器

症状

SQLSTATE[08001]: [Microsoft][ODBC Driver 18 for SQL Server]TCP Provider: A connection attempt failed
SQLSTATE[HYT00]: [Microsoft][ODBC Driver 18 for SQL Server]Login timeout expired

可能的原因和解决方案

  • 服务器无法访问。 检查服务器名称和端口是否正确。 从PHP主机测试原始TCP连接。

    # Linux and macOS
    nc -vz <server>.database.windows.net 1433
    
    # Windows PowerShell
    Test-NetConnection -ComputerName <server>.database.windows.net -Port 1433
    
  • 防火墙阻止了1433号的出站。 企业防火墙和云NSG经常会封锁出站端口1433。 添加例外规则,或者允许适用于你所在区域的 Azure SQL 数据库 IP 范围

  • Azure SQL server firewall. 在Azure门户的服务器级防火墙规则中添加客户端的公共IP。

  • 命名实例。 对于命名实例,请确认服务器上的 SQL Server Browser 服务正在运行,并且 UDP 1434 端口已开放。 或者,通过端口连接,而不是通过实例名连接。

登录失败

症状

SQLSTATE[28000]: [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Login failed for user '<user_id>'.

可能的原因和解决方案

  • SQL 认证模式已禁用。 本地 SQL Server 实例默认仅支持 Windows 认证。 在 SQL Server Management Studio 的服务器属性>中启用混合模式认证安全性,然后重启 SQL Server 服务。
  • Azure SQL credentials format. 从不会自动附加全限定用户名的工具连接时,Azure SQL 需要全限定用户名 (user@servername)。
  • 用户未映射到数据库。 确认登录在目标数据库中有用户映射,并且用户是否拥有所需的权限。
  • Prefer Microsoft Entra ID. 对于Azure SQL、Azure SQL 托管实例和Fabric中的SQL数据库,使用Microsoft Entra认证(Authentication=ActiveDirectoryMsiAuthentication=ActiveDirectoryServicePrincipal访问令牌)代替SQL登录。 请参阅 使用 Microsoft Entra 身份验证进行连接

为连接字符串属性“Authentication”指定的值无效

症状

SQLSTATE[08001]: [Microsoft][ODBC Driver 17 for SQL Server]Invalid value specified for connection string attribute 'Authentication'

原因:

ODBC 驱动程序报告了该错误,但真正的问题是 PDO_SQLSRV 绑定到了哪个驱动程序。 如果DSN不包含 Driver= 关键词,且主机同时安装了ODBC 17和ODBC 18,PDO_SQLSRV可以绑定到旧版本。 较旧的 ODBC 17.x 版本不识别较新的 Authentication 值,例如 ActiveDirectoryServicePrincipalActiveDirectoryDefault,即使是 ActiveDirectoryMsi 也需要 ODBC 17.3.1.1 或更高版本。

修复:

在 DSN 中固定驱动程序:

<?php
$dsn = "sqlsrv:Driver={ODBC Driver 18 for SQL Server};Server=$server;Database=$db;" .
       "Encrypt=true;Authentication=ActiveDirectoryMsi";
$conn = new PDO($dsn, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

方括号形式 ({ODBC Driver 18 for SQL Server}) 可转义驱动程序名称中的空格。 错误信息本身总是会注明报告该错误的驱动程序,因此,错误中的前缀 [Microsoft][ODBC Driver 17 for SQL Server] 是确认绑定了错误驱动程序的最快方法。

DSN字符串中指定了无效关键词“UID”

症状

SQLSTATE[IMSSP]: An invalid keyword 'UID' was specified in the DSN string.

原因:

PDO_SQLSRV 对 DSN 关键字实施允许列表限制,并且不接受在 DSN 中使用 UIDPWD。 PDO保留第二和第三个构造函数参数用于这些,PDO_SQLSRV在内部将其转换为ODBC UID/PWD

修复:

将用户名(以及用于SQL认证的密码)迁移到PDO构造函数中:

<?php
// SQL authentication.
$dsn = "sqlsrv:Driver={ODBC Driver 18 for SQL Server};Server=$server;Database=$db;Encrypt=true";
$conn = new PDO($dsn, $user, $password);

// User-assigned managed identity. Pass the identity's client ID as $username.
$dsn = "sqlsrv:Driver={ODBC Driver 18 for SQL Server};Server=$server;Database=$db;" .
       "Encrypt=true;Authentication=ActiveDirectoryMsi";
$conn = new PDO($dsn, $clientId, null);

相比之下,SQLSRV 过程驱动程序在传递给 sqlsrv_connect() 的连接选项数组中接受 UIDPWD

PDO_SQLSRV 静默忽略选项数组中的 AccessToken

症状

你有一个 Microsoft Entra 访问令牌(例如,来自 az account get-access-token --resource https://database.windows.net/ManagedIdentityCredentialClientSecretCredential),并将其作为第四个构造函数参数中的 ['AccessToken' => $token] 传递给 PDO_SQLSRV。 连接尝试失败时会出现令人困惑的错误,如 Windows logins are not supported in this version of SQL ServerLogin failed for user '',仿佛没有提供凭证。

原因:

PDO的第四构造子参数保留给驱动程序特定的属性常量(如 PDO::ATTR_ERRMODE整数键)。 PDO 会静默丢弃以字符串为键的条目(例如 AccessToken),因此 PDO_SQLSRV 根本接收不到该标记。 连接随后退回到 Windows 集成认证,服务器拒绝了该认证。

修复:

AccessToken 移至 DSN 字符串中。 把选项数组保留给 PDO::ATTR_* 常量。

<?php
$server = '<server>.database.windows.net';
$token  = getenv('SQL_ACCESS_TOKEN');   // raw JWT, no "Bearer " prefix

$dsn = "sqlsrv:Server=$server;Database=<database>;Encrypt=true;AccessToken=$token";
$conn = new PDO($dsn, null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

有关更多Microsoft Entra认证示例,包括PDO_SQLSRV的DSN表单,请参见使用Microsoft Entra认证连接

对于 SQLSRV 过程式用法,AccessToken 应放在传递给 sqlsrv_connect() 的连接信息数组中;sqlsrv_connect() 会为你将原始 JWT 封装为 SQL_COPT_SS_ACCESS_TOKEN

<?php
$server = '<server>.database.windows.net';
$token  = getenv('SQL_ACCESS_TOKEN');   // raw JWT, no "Bearer " prefix

$connectionInfo = [
    'Database'               => '<database>',
    'AccessToken'            => $token,
    'Encrypt'                => true,
    'TrustServerCertificate' => false,
    'Driver'                 => '{ODBC Driver 18 for SQL Server}',
];

$conn = sqlsrv_connect($server, $connectionInfo);
if ($conn === false) {
    print_r(sqlsrv_errors());
    exit(1);
}

TLS证书错误

症状

SQLSTATE[08001]: SSL Provider: The certificate chain was issued by an authority that is not trusted
SQLSTATE[08001]: SSL Provider: The target principal name is incorrect

解决方法

更倾向于使用可信证书。 仅应将 TrustServerCertificate=true 用于针对你所控制的服务器进行本地开发。

针对自签名证书的开发:

<?php
$server   = 'localhost';
$database = '<database>';
$user     = '<user_id>';
$password = '<password>';

$dsn = "sqlsrv:Server=$server;Database=$database;Encrypt=true;TrustServerCertificate=true";
$conn = new PDO($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

Caution

TrustServerCertificate=true 禁用服务器证书验证。 绝不要在生产、预发布或共享环境中使用该设置。

对于与证书通用名称不匹配的生产主机名(例如通过监听器连接时),请指定实际的证书主题:

<?php
$dsn = "sqlsrv:Server=<listener>;Database=<database>;Encrypt=true;HostNameInCertificate=*.database.windows.net;Authentication=ActiveDirectoryMsi";
$conn = new PDO($dsn, null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

连接超时

症状

SQLSTATE[HYT00]: Login timeout expired

可能的原因和解决方案

  • LoginTimeout 未设置或设置值过低,无法进行冷故障转移。 连接到 Azure SQL 时,在 DSN 中显式设置 LoginTimeout(以秒为单位)。 故障转移组的故障转移和冷启动数据库可能需要的时间会超过客户端短时超时限制。 有关选项的参考信息,请参见 连接选项
  • 空闲重连配额被截断。 如果你设置了 ConnectRetryCountConnectRetryInterval,请确保 LoginTimeout >= ConnectRetryCount * ConnectRetryInterval。 否则登录超时会提前结束重连循环。 参见空闲连接弹性。
<?php
$dsn = "sqlsrv:Driver={ODBC Driver 18 for SQL Server};Server=<server>.database.windows.net;Database=<database>;" .
       "Encrypt=true;LoginTimeout=90;ConnectRetryCount=5;ConnectRetryInterval=15;" .
       "Authentication=ActiveDirectoryMsi";
$conn = new PDO($dsn, null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

查询执行问题

PDO 的静默失败

症状

PDO::exec()PDOStatement::execute() 的调用会返回 false,但不会抛出异常。

修复:

在PHP 8.0及更高版本中,默认的PDO错误模式为 PDO::ERRMODE_EXCEPTION。 如果调用返回 false 但未抛出,应用程序将模式切换为 PDO::ERRMODE_SILENTPDO::ERRMODE_WARNING。 将其重新设为异常模式,以便在失败时抛出异常:

<?php
$conn = new PDO($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

如果无法全局更改模式,每次通话后检查 $conn->errorInfo() (或 $stmt->errorInfo())。 该数组包含 [SQLSTATE, driver code, driver message]

对象名称无效

症状

SQLSTATE[42S02]: [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Invalid object name 'Products'.

可能的原因和解决方案

  • 数据库上下文错误。 用一个简单的问题验证:

    <?php
    $stmt = $conn->query("SELECT DB_NAME()");
    echo $stmt->fetchColumn();
    
  • 缺少架构限定符。 使用完全限定的名称以避免依赖呼叫者的默认模式:

    SELECT * FROM dbo.Products;
    
  • 区分大小写。 使用区分大小写排序规则创建的数据库会将 productsProducts 视为不同的对象。 请与表定义中的大小写完全一致。

参数数量错误

症状

SQLSTATE[HY093]: Invalid parameter number
SQLSTATE[07002]: COUNT field incorrect or syntax error

修复:

对于PDO_SQLSRV,占位符的数量 ? 必须与你传递给 execute()的值数相匹配,每个 ? 只绑定一个标量(而非数组)。 对于命名参数,SQL中的每个 :name 参数都必须出现在数组中,反之亦然。

<?php
$stmt = $conn->prepare(
    "SELECT * FROM dbo.Products WHERE CategoryID = ? AND ListPrice > ?"
);
$stmt->execute([1, 50.0]);
foreach ($stmt as $row) {
    // ...
}

对于SQLSRV,将参数数组传递给 sqlsrv_query()sqlsrv_prepare()

<?php
$stmt = sqlsrv_query(
    $conn,
    "SELECT * FROM dbo.Products WHERE CategoryID = ? AND ListPrice > ?",
    [1, 50.0]
);
if ($stmt === false) {
    die(print_r(sqlsrv_errors(), true));
}

关于参数绑定的更广泛介绍,请参见 “执行参数化查询”。

PDO 模拟预处理会掩盖错误

症状

一个语句在一个连接上成功运行,但在另一个使用相同查询文本的连接上抛出语法错误。

原因:

PDO_SQLSRV支持模拟和原生预设语句。 模拟预处理 (PDO::ATTR_EMULATE_PREPARES = true) 会在客户端插值参数。 Native prepares(false)分别将查询和参数发送给服务器。 对于 TOP (?)、表值参数以及类型强制中的某些边缘情况,行为会有所不同。

修复:

生产环境中建议使用原生预处理。 在连接时设置 PDO::ATTR_EMULATE_PREPARES => false,以确保行为在不同环境中保持一致:

<?php
$conn = new PDO($dsn, null, null, [
    PDO::ATTR_ERRMODE          => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

有关何时使用每种模式的详细信息,请参见 PDO::prepare

数据类型问题

Unicode 字符显示为 ? 或乱码

症状

PHP 写入的行包含问号或替换字符,而非原始非 ASCII 字符。 读取结果返回乱码。

可能的原因和解决方案

  • 列类型是VARCHAR,不是NVARCHAR。 varchar 列使用代码页,而非 Unicode。 对国际化文本使用 nvarchar

  • 缺少关于PDO_SQLSRV的UTF-8编码提示。 当 SQL Server 列为 nvarchar 且 PHP 数据为 UTF-8 时,请告知驱动程序在 UTF-8(客户端)和 UTF-16(服务器)之间进行转换:

    <?php
    $conn = new PDO(
        "sqlsrv:Server=<server>;Database=<database>;Encrypt=true",
        $user,
        $password,
        [
            PDO::ATTR_ERRMODE                    => PDO::ERRMODE_EXCEPTION,
            PDO::SQLSRV_ATTR_ENCODING            => PDO::SQLSRV_ENCODING_UTF8,
        ]
    );
    
  • SQLSRV 驱动:明确请求 UTF-8SQLSRV_ENC_CHAR 是默认的8位系统代码页,而非UTF-8。 对于使用 SQLSRV 的 UTF-8,请在连接时设置 "CharacterSet" => "UTF-8",并在获取或绑定时将字面量 'UTF-8' 传递给 SQLSRV_PHPTYPE_STRING。 参见 发送和检索UTF-8数据

日期时间转换错误

症状

SQLSTATE[22007]: Invalid character value for cast specification

修复:

在 PDO_SQLSRV 上,不要绑定原始 DateTime 物体。 PDO 在绑定前会将绑定值转换为字符串,而 PHP 的 DateTime 没有 __toString() 方法,所以 execute([new DateTime(...)]) 会引发 Object of class DateTime could not be converted to string。 先格式化值,或者传递一个ISO 8601字符串(YYYY-MM-DD HH:MM:SS[.fff]),而不是本地格式的字符串。

<?php
$stmt = $conn->prepare("INSERT INTO dbo.Events (EventDate) VALUES (?)");
$stmt->execute([(new DateTime("2026-03-15 10:00:00"))->format("Y-m-d H:i:s.u")]);

要将 日期时间 列作为 DateTime 对象而非字符串取出,PDO_SQLSRV 上设置 statement 属性:

<?php
$stmt = $conn->prepare("SELECT EventDate FROM dbo.Events");
$stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true);
$stmt->execute();

详情请参见 “检索日期时间对象(PDO_SQLSRV)”。

十进制格式问题

症状

-1 到1之间的值缺少前置零,或者 moneysmallmoney 的值显示出意想不到的小数点位数。

修复:

PDO_SQLSRV 总是将小数数字值作为字符串提取,并保留其精确的精度和规模。 将 PDO::SQLSRV_ATTR_FORMAT_DECIMALS 设置为在 -1 和 1 之间的值添加前导零:

<?php
$conn->setAttribute(PDO::SQLSRV_ATTR_FORMAT_DECIMALS, true);

PDO::SQLSRV_ATTR_DECIMAL_PLACES 仅适用于 货币小额货币 的价值。 它会将显示比例设置为 0 到 4,并且可能会将显示的值四舍五入。 它不影响 小数数字

有关详细信息,请参阅 设置小数和货币格式 (PDO_SQLSRV)设置小数和货币格式 (SQLSRV)

交易问题

数据变更不会持续

症状

你在PHP中插入或更新的行,在你从另一个会话查询时不会出现。

原因:

PDO::beginTransaction() 会打开一个显式事务,该事务需要显式 commit()。 如果PHP脚本在未调用 commit()的情况下结束,PDO会在连接清理时回滚该事务。

修复:

始终将 beginTransaction()commit() 配对使用,并使用 try/catch 在发生错误时回滚:

<?php
try {
    $conn->beginTransaction();
    $conn->exec("INSERT INTO dbo.Orders (CustomerID, Total) VALUES (1, 100)");
    $conn->exec("UPDATE dbo.Inventory SET Stock = Stock - 1 WHERE ProductID = 5");
    $conn->commit();
} catch (PDOException $e) {
    $conn->rollBack();
    throw $e;
}

对于 SQLSRV,请使用 sqlsrv_begin_transactionsqlsrv_commitsqlsrv_rollback

死锁错误

症状

SQLSTATE[40001]: [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Transaction (Process ID 62) was deadlocked

修复:

用重试逻辑处理瞬态死锁错误。 将整个事务(而不仅仅是失败的语句)进行封装,以便较早的语句能在新的事务中重放。 关于面向生产环境的重试模式,请参见 PHP驱动着陆页上的示例。

反复出现的死锁表明存在设计问题。 捕获死锁图,并分析涉及哪些语句和锁类型。 常见的修复方法包括重新排序操作,使竞争事务以相同顺序获得锁,缩小事务范围,以及添加索引以缩短锁的持续时间。 完整攻略请参见 Deadlocks指南

连接弹性问题

不会发生重新连接

症状

即使你设置了 ConnectRetryCountConnectRetryInterval,空闲连接在 Azure SQL 数据库 故障转移后仍会保持断开状态。

可能的原因和解决方案

  • 激活的服务器端光标。 空闲连接复原功能仅会重新连接 空闲 连接。 打开的服务器端光标或待处理的交易会保持连接的活跃状态。 通过在故障切换窗口前使用 sqlsrv_free_stmt()$stmt = null; (PDO)释放服务器端光标,或切换到客户端缓冲光标。 参见空闲连接弹性。
  • 无法恢复的会话状态。 某些会话状态无法重新建立,包括临时表、全局和本地光标、事务上下文、应用锁、 EXECUTE AS/REVERTOLE自动化句柄、准备好的XML句柄和追踪标志。 这些会话状态中的任何一个都阻止了自动重新连接。
  • LoginTimeout 太小了。 如果 ConnectRetryCount * ConnectRetryInterval > LoginTimeout,驱动程序将在达到 LoginTimeout 时停止重试。 提高 LoginTimeout 以覆盖整个重试预算。

性能问题

关于慢查询、冷启动、大结果集和批量插入的诊断和修复,请参见 性能调优

启用驱动程序诊断

当应用层 error_log() 调用未提供足够的信息时,请启用驱动端日志记录。 它会报告司机打的每一个ODBC电话。

PDO_SQLSRV

php.ini 中设置 pdo_sqlsrv.log_severity,然后重新启动 Web 服务器。 该设置仅在初始化时可读取:

[pdo_sqlsrv]
pdo_sqlsrv.log_severity = 1

数值分别是 0 (关闭,默认值)、 -1 (错误、警告和通知)、 1 (错误)、 2 (警告)和 4 (通知)。

SQLSRV

在运行时启用日志,使用:sqlsrv_configure()

<?php
sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_CONN | SQLSRV_LOG_SYSTEM_STMT);
sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR | SQLSRV_LOG_SEVERITY_WARNING);

日志条目将写入 php.ini 中由 error_log 配置的文件。 有关子系统和严重度的完整列表,请参见 日志活动

容器与 CI 问题

Linux 上缺失的系统库

症状

error while loading shared libraries: libodbc.so.2: cannot open shared object file
error while loading shared libraries: libssl.so.1.1: cannot open shared object file

修复:

安装 PHP 驱动前先安装运行时依赖:

分销 安装命令
Ubuntu 和 Debian sudo apt-get install unixodbc libgssapi-krb5-2
红帽和 Fedora sudo dnf install unixODBC krb5-libs
Alpine apk add unixodbc gcompat

然后从Microsoft包仓库安装msodbcsql18。 关于发行版特定的软件包仓库和版本,请参见 ODBC 驱动安装指南

Docker 镜像构建成功,但连接在运行时失败

症状

镜像构建成功,PHP 也已启动,但 PDO::__construct() 抛出了一个 ODBC 驱动程序未找到的错误。

修复:

确认已在运行时镜像中安装 ODBC 驱动,而不是仅在构建阶段安装。 将 msodbcsql18unixodbc-dev 安装在部署到生产环境的同一阶段中。 在多阶段构建中,请在最终阶段安装它们。 基于Debian的单阶段安装如下:

# Pin to a specific PHP minor version in production, for example php:8.4.11-cli.
FROM php:8.4-cli
RUN apt-get update && apt-get install -y --no-install-recommends \
        curl gnupg2 apt-transport-https ca-certificates \
    && curl -sSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /usr/share/keyrings/microsoft.gpg \
    && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" > /etc/apt/sources.list.d/mssql-release.list \
    && apt-get update \
    && ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 unixodbc-dev \
    # $PHPIZE_DEPS ships in the official php image and includes gcc, make, autoconf, and re2c.
    && apt-get install -y --no-install-recommends $PHPIZE_DEPS \
    && pecl install sqlsrv pdo_sqlsrv \
    && docker-php-ext-enable sqlsrv pdo_sqlsrv \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*