How to Connect to SQL Server from PHP

7 min readSQL Server

Connecting to Microsoft SQL Server from PHP means installing an extension that does not ship with PHP itself. Microsoft maintains the Drivers for PHP for SQL Server: a pair of extensions, sqlsrv (a procedural API specific to SQL Server) and pdo_sqlsrv (a PDO driver). Both sit on top of the Microsoft ODBC Driver for SQL Server, which is a separate system package you must install first. The current release is 5.13.1 (as of June 2026), with day-one support for PHP 8.3, 8.4, and 8.5. For new code, pdo_sqlsrv is the right default because it gives you the same portable PDO API you would use for any other database. This guide covers the install chain, the TLS default that breaks local connections, prepared statements, transactions, reading generated keys, and the errors you will meet.

The install chain

Three layers have to line up, in order:

  1. The Microsoft ODBC Driver for SQL Server (Driver 18 is current). This is an OS package, not a PHP one. On Debian/Ubuntu you add Microsoft's apt repository and install msodbcsql18 plus unixodbc-dev. On Windows you download the MSI.

  2. The PHP extensions, installed through PECL:

    pecl install sqlsrv pdo_sqlsrv

    On Windows you download the prebuilt DLLs from Microsoft instead and drop them into the extension directory.

  3. The php.ini entries that load them:

    extension=sqlsrv
    extension=pdo_sqlsrv

Confirm both the ODBC driver and the PHP extension are present:

php -m | grep -i sqlsrv
# expect: pdo_sqlsrv  sqlsrv
odbcinst -q -d
# expect a section like: [ODBC Driver 18 for SQL Server]

The single most common failure here is could not find driver, which almost always means the ODBC system driver is missing even though the PHP extension loaded. The PHP extension is a thin layer; without the ODBC driver underneath it, there is nothing to connect with.

Connecting with PDO

pdo_sqlsrv uses a sqlsrv: DSN. The server goes in Server, the database in Database. Note that SQL Server uses a backslash for named instances, which must be escaped in a PHP string:

<?php
$dsn = 'sqlsrv:Server=127.0.0.1,1433;Database=shop';
 
$pdo = new PDO($dsn, 'appuser', 'secret', [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

ERRMODE_EXCEPTION makes failures throw PDOException instead of returning false. FETCH_ASSOC returns rows as associative arrays.

Prepared statements

pdo_sqlsrv supports both named and positional placeholders. Positional ? is the most portable:

$stmt = $pdo->prepare('SELECT id, name, price FROM products WHERE price > ?');
$stmt->execute([100]);
 
foreach ($stmt as $row) {
    echo "{$row['id']}: {$row['name']}\n";
}

Placeholders stand in for values only, never for table or column names.

The Encrypt-defaults-on TLS trap

This is the trap that catches almost everyone the first time, and it changed with the driver generation. ODBC Driver 18 defaults Encrypt to yes. Earlier drivers defaulted to no. So code that connected fine against an older driver, or a tutorial written before Driver 18, suddenly fails against a local SQL Server with a self-signed certificate:

SQLSTATE[08001]: ... SSL Provider: The certificate chain was issued by an
authority that is not trusted.

The connection is now encrypted by default and the client is correctly refusing to trust an untrusted certificate. The wrong fix is to disable encryption. The right fixes, in order of preference:

  • Production: install a certificate the client trusts and leave Encrypt=yes. Verify the server name matches the certificate.

  • Local development only: keep encryption on but skip certificate validation with TrustServerCertificate=yes:

    $dsn = 'sqlsrv:Server=127.0.0.1,1433;Database=shop;Encrypt=yes;TrustServerCertificate=yes';

Resist the reflex of Encrypt=no. That sends credentials and data in clear text, and it is the opposite of what the new default is trying to protect you from.

Transactions

Wrap multi-statement work so a failure rolls everything back:

$pdo->beginTransaction();
try {
    $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
        ->execute([100, 1]);
    $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
        ->execute([100, 2]);
    $pdo->commit();
} catch (\Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

Reading generated keys

SQL Server has no RETURNING clause and no reliable lastInsertId() story across all cases. The idiomatic approach is the OUTPUT clause, which returns inserted values as a result set directly from the INSERT:

$stmt = $pdo->prepare(
    'INSERT INTO products (name, price) OUTPUT INSERTED.id VALUES (?, ?)'
);
$stmt->execute(['Wrench', 19.90]);
$id = $stmt->fetchColumn();

OUTPUT INSERTED.id is more robust than the older SCOPE_IDENTITY() pattern because it returns directly from the statement and works correctly when multiple rows are inserted at once. Avoid @@IDENTITY, which can return the wrong value if a trigger inserts into another table with its own identity column.

Connecting with the sqlsrv procedural API

If you prefer or inherit the SQL Server-specific API, sqlsrv is procedural. Connection options are passed as an array, and parameters are bound positionally as a separate array:

$conn = sqlsrv_connect('127.0.0.1,1433', [
    'Database'             => 'shop',
    'Uid'                  => 'appuser',
    'PWD'                  => 'secret',
    'Encrypt'              => true,
    'TrustServerCertificate' => true, // dev only
]);
 
if ($conn === false) {
    die(print_r(sqlsrv_errors(), true));
}
 
$sql = 'SELECT id, name FROM products WHERE price > ?';
$stmt = sqlsrv_query($conn, $sql, [100]);
 
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
    echo "{$row['id']}: {$row['name']}\n";
}

sqlsrv_connect returns false on failure rather than throwing, so check the return value and read sqlsrv_errors(). This is the same trap PDO's exception mode removes, which is one reason pdo_sqlsrv is the friendlier default.

Named instances and Azure SQL

For a named instance, give the instance after a backslash and let SQL Server Browser (UDP 1434) resolve the port, or specify the port directly:

// Named instance, resolved by SQL Server Browser
$dsn = 'sqlsrv:Server=dbhost\\SQLEXPRESS;Database=shop';
 
// Explicit host and port (no Browser dependency)
$dsn = 'sqlsrv:Server=dbhost,1433;Database=shop';

The explicit host,port form is more reliable in containerized and firewalled environments because it does not depend on the Browser service being reachable. Azure SQL Database always uses TLS, so Encrypt=yes is correct there by default; just make sure the server's firewall allows your client IP.

Connection pooling

PHP's request-per-process model closes connections at the end of each request, so there is no application-level pool the way a long-running service has. On Linux, the unixODBC driver manager can pool ODBC connections at the system level, configured in odbcinst.ini. PDO also offers ATTR_PERSISTENT, which reuses the underlying connection across requests, but persistent connections can be handed to the next request mid-transaction, so use them with care and never while holding locks.

Errors you actually hit

SymptomCauseFix
could not find driverThe Microsoft ODBC Driver is missing, even if the PHP extension loadedInstall msodbcsql18; confirm with odbcinst -q -d
SSL Provider: The certificate chain ... is not trustedDriver 18 defaults Encrypt=yes and the cert isn't trustedInstall a trusted cert (prod) or add TrustServerCertificate=yes (dev only)
SQLSTATE[08001] ... Login timeout expiredWrong host/port, server not listening, or firewallUse explicit Server=host,1433; check the port and firewall
Login failed for user 'appuser'Wrong credentials, or SQL Server is in Windows-auth-only modeVerify the password; enable Mixed Mode authentication on the server
The specified network name is no longer availableNamed instance can't be resolved (SQL Server Browser unreachable)Use explicit host,port instead of host\instance
sqlsrv_query returns false silentlyThe procedural API reports errors via return values, not exceptionsCheck the return value and read sqlsrv_errors(); or use pdo_sqlsrv

For exploring SQL Server data and drafting queries before they go into PHP, Mako connects to SQL Server with AI-powered autocomplete. Try it free at mako.ai.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.