Leistungsoptimierung für die Microsoft-Treiber für PHP für SQL Server

PHP-Treiber herunterladen

Dieser Artikel behandelt, wie man schnellen PHP-Code gegen SQL Server, Azure SQL-Datenbank, Azure SQL Managed Instance, Azure Synapse Analytics und SQL Database in Microsoft Fabric schreibt. Die Leitlinien gelten sowohl für SQLSRV als auch für PDO_SQLSRV, die denselben zugrunde liegenden Microsoft ODBC-Treiber für SQL Server umwickeln.

Beginnen Sie mit den Veränderungen mit der größten Wirkung

Wenn Sie nur drei Änderungen vornehmen können, nehmen Sie diese vor:

  • Aktiviere Connection Pooling. Die Einrichtung einer neuen TLS-Verbindung zum SQL Server dauert je nach Netzwerkpfad und TLS-Verhandlung Dutzende bis Hunderte von Millisekunden. Die Wiederverwendung von Pool-Verbindungen eliminiert diese Kosten pro Anfrage. Siehe Verbindungen effizient verwalten.
  • Hol nur die Spalten und Zeilen, die du brauchst. SELECT * und unbeschränkte Abfragen sind die häufigsten Ursachen für langsame Endpunkte. Siehe "Abfrage nur das, was du brauchst".
  • Verwenden Sie tabellenwertige Parameter für Bulk-Einsätze. Für Hunderte von Zeilen oder mehr sind tabellenwertige Parameter (TVPs) typischerweise deutlich schneller als zeilen-für-Zeilen-Anweisungen INSERT und skalieren linear mit der Zeilenanzahl. Siehe Daten effizient einfügen.

Verwaltung von Verbindungen effizient

Die Verbindungseinrichtung ist die mit Abstand teuerste Operation, die der Fahrer ausführt. Fast jede PHP-Leistungsuntersuchung endet mit einer Lösung für die Verbindungsverwaltung.

Aktivieren von Verbindungspooling

Das Poolen von ODBC-Verbindungen verwendet die ODBC-Verbindungen über PHP-Anfragen wieder, anstatt sie am Request-Ende herunterzubauen. Das Connection-Objekt wird verworfen, wenn dein Skript endet, aber der zugrunde liegende ODBC-Handle bleibt im ODBC-Treibermanager-Pool aktiv und wird von der nächsten Anfrage wiederverwendet, die nach derselben Verbindungszeichenfolge fragt.

Windows: Connection Pooling ist standardmäßig aktiviert. Zur Bestätigung solltest du die ConnectionPooling Option in deinem DSN weglassen. Um das Pooling für Debugging zu deaktivieren, setze ConnectionPooling=0.

Linux und macOS: Connection Pooling ist auf diesen Plattformen keine DSN-Option. Aktivieren Sie es im Driver Manager, indem Sie in den [ODBC] Abschnitt von odbcinst.inisetzenPooling=Yes, und setzen Sie ein Plus CPTimeout unter der Strofe des Fahrers. Beispiel:

[ODBC]
Pooling=Yes

[ODBC Driver 18 for SQL Server]
Description=Microsoft ODBC Driver 18 for SQL Server
Driver=/opt/microsoft/msodbcsql18/lib64/libmsodbcsql-18.<version>.so.1.1
CPTimeout=120

Finde den tatsächlichen Bibliothekspfad mit odbcinst -q -d -n "ODBC Driver 18 for SQL Server" oder ls /opt/microsoft/msodbcsql18/lib64/. Der Dateiname bettet die installierte ODBC-Treiberversion ein und ändert sich mit jeder Version.

CPTimeout (in Sekunden) steuert, wie lange die Leerlaufverbindungen im Pool bleiben, bevor sie geschlossen werden. Stelle sie so hoch, dass die meisten Anfragen eine Pool-Verbindung finden, aber niedrig genug, dass veraltete Verbindungen zu einem failover-Server-Server relativ schnell eingestellt werden. 60 bis 300 Sekunden funktionieren für die meisten Web-Workloads gut.

Details finden Sie unter Connection Pooling.

Verstehen Sie die Kosten für die erste Abfrage

MARS (mehrere aktive Resultsets) ist standardmäßig aktiviert. Wenn sowohl MARS als auch Connection Pooling aktiv sind, setzt der Treiber die gepoolte Verbindung bei der ersten Abfrage zurück, und dieser Reset ignoriert alle von dir gesetzten Abfrage-Timeouts für diese erste Abfrage. Spätere Abfragen auf derselben Verbindung berücksichtigen die Auszeit normalerweise. Wenn du bei einer gepoolten Arbeitslast aggressive Erstanfrage-Timeouts einsetzt, berücksichtige dieses Verhalten oder deaktiviere MARS, MultipleActiveResultSets=false falls du es nicht brauchst. Siehe die MARS- und Pooling-Notiz unter Connection Pooling.

Persistente PDO-Verbindungen werden nicht unterstützt

PDO_SQLSRV lehnt PDO::ATTR_PERSISTENTab. Setzt man es auf die Konstruktor-Würfe:

SQLSTATE[IMSSP]: An unsupported attribute was designated on the PDO object.

Verwenden Sie ODBC Connection Pooling für Cross-Request-Wiederverwendung. Es ist der treiberbasierte Mechanismus, funktioniert sowohl für PDO_SQLSRV als auch für SQLSRV und schaltet Leerlaufverbindungen auf CPTimeout (was auch Microsoft Entra Token-Aktualisierung zuverlässig hält).

Verwenden Sie die Verbindung innerhalb einer Anfrage erneut

Selbst beim Pooling führt das Öffnen einer neuen PDO- oder SQLSRV-Verbindung zu einer ODBC-Rundreise, um einen gepoolten Handle abzurufen und zu validieren. Öffne eine Verbindung einmal pro Anfrage und übergebe sie an jede Funktion, die sie benötigt.

Tipp

Ein Abhängigkeitsinjektionsbehälter oder ein lazy accessor reicht aus. Der Punkt ist, mitten in einem Request-Handler zu vermeiden new PDO(...) .

Fragen Sie nur nach dem, was Sie brauchen

Netzwerk-Roundtrips und Materialisierung von Ergebnismengen dominieren die Abfragelatenz für die meisten PHP-Workloads. Die Korrekturen sind dieselben, die auf jede Datenbankzugriffsschicht angewendet werden.

Wählen Sie nur die Spalten aus, die Sie verwenden

SELECT * Zieht jede Spalte, einschließlich Varchar(max) und varbinary(max)- Spalten, die die Daten, die du tatsächlich konsumierst, bei weitem überragen. Benennen Sie die Spalten:

<?php
// Slow: fetches all columns, including a 2 MB LOB column
$stmt = $conn->query("SELECT * FROM dbo.Products");

// Fast: fetches only the two columns the caller uses
$stmt = $conn->query("SELECT ProductID, Name FROM dbo.Products");

Hol nur die Reihen, die du brauchst,

Schiebe Filterung auf SQL Server. Holen Sie niemals eine vollständige Tabelle in PHP, nur um in einer Schleife foreach zu filtern.

<?php
// Slow: transfers every row to PHP, then filters
$rows = $conn->query("SELECT * FROM dbo.Orders")->fetchAll(PDO::FETCH_ASSOC);
$recent = array_filter($rows, fn($r) => $r["OrderDate"] > "2026-01-01");

// Fast: filters on the server
$stmt = $conn->prepare("SELECT OrderID, CustomerID, Total FROM dbo.Orders WHERE OrderDate > ?");
$stmt->execute(["2026-01-01"]);
$recent = $stmt->fetchAll(PDO::FETCH_ASSOC);

Paginierung großer Ergebnismengen

Für eine Listenansicht, die ein paar hundert Zeilen aus Millionen anzeigt, gib nicht alle Zeilen zurück und lass den Client das regeln. Verwenden Sie serverseitige Paginierung mit OFFSET ... FETCH:

<?php
function fetchPage(PDO $conn, int $page, int $pageSize): array {
    $stmt = $conn->prepare(
        "SELECT OrderID, CustomerID, Total
         FROM dbo.Orders
         ORDER BY OrderID
         OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"
    );
    // With native prepares, execute([...]) binds values as strings.
    // OFFSET and FETCH NEXT require integer bindings; bind explicitly.
    $stmt->bindValue(1, ($page - 1) * $pageSize, PDO::PARAM_INT);
    $stmt->bindValue(2, $pageSize, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

Wählen Sie die richtige Fetch-Methode

  • Nutze fetch(PDO::FETCH_ASSOC) in einer Schleife für Streaming-Iterationen, wenn du nicht alle Zeilen gleichzeitig im Speicher brauchst.
  • Verwenden fetchAll(PDO::FETCH_ASSOC) Sie, wenn der Anrufer tatsächlich das gesamte Set benötigt (zum Beispiel beim Rendern einer vollständigen JSON-Antwort).
  • Benutze fetchColumn() , wenn dir nur ein einzelner Skalar wichtig ist (ein COUNT, SUM, oder MAX).
  • Nutze PDO::FETCH_KEY_PAIR oder PDO::FETCH_UNIQUE baue Suchwörterbücher ohne einen zweiten Durchgang.

Numerische Abrufmodi (PDO::FETCH_NUM) sind geringfügig schneller als assoziative Abrufmodi, da sie das Erstellen der Spaltennamensabbildung überspringen. Bevorzuge Klarheit; Wechsle nur, wenn ein Profiler den Fetch-Overhead als signifikant markiert.

Bevorzuge SET NOCOUNT ON in gespeicherten Prozeduren und Chargen

Jede INSERT, , and-Anweisung UPDATEDELETE liefert ein DONE_IN_PROC Token mit der betroffenen Zeilenanzahl, die PHP typischerweise verworfen hat. Der Token fügt keine Rundreise hinzu, aber jeder Token kostet trotzdem Bytes auf der Leitung und eine kleine Menge Treiberarbeit. In einem Multi-Statement-Verfahren oder Batch, das Hunderte von Statements pro Anruf ausführt, summieren sich die Einsparungen. Schalten Sie es aus:

CREATE OR ALTER PROCEDURE dbo.ProcessOrder
    @OrderID INT
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE dbo.Inventory SET Stock = Stock - 1 WHERE ProductID IN (SELECT ProductID FROM dbo.OrderLines WHERE OrderID = @OrderID);
    UPDATE dbo.Orders SET Status = 'Processed' WHERE OrderID = @OrderID;
END;

Daten effizient einfügen

Wählen Sie die richtige Einführungsmethode basierend darauf, wie viele Reihen Sie bewegen. Die falsche Wahl kann hundertmal langsamer sein.

Weniger als etwa 100 Zeilen: vorbereitete Aussage in einer Schleife

Für kleine Chargen führe eine einzelne vorbereitete Anweisung in einer Schleife aus:

<?php
$stmt = $conn->prepare("INSERT INTO dbo.Products (Name, Price) VALUES (?, ?)");
foreach ($products as $p) {
    $stmt->execute([$p["name"], $p["price"]]);
}

Wickle die Schleife in eine Transaktion, sodass alle Inserts als eine Einheit committen und das Log nicht nach jeder Zeile ausgefüllt werden muss:

<?php
$conn->beginTransaction();
try {
    $stmt = $conn->prepare("INSERT INTO dbo.Products (Name, Price) VALUES (?, ?)");
    foreach ($products as $p) {
        $stmt->execute([$p["name"], $p["price"]]);
    }
    $conn->commit();
} catch (PDOException $e) {
    $conn->rollBack();
    throw $e;
}

Hunderte bis Millionen von Zeilen: tabellenwertige Parameter

Tabellenwerte Parameter (TVPs) senden den gesamten Batch in einer Rundenrunde-Runde an SQL Server und lassen SQL Server die Menge als eine einzige Anweisung verarbeiten. Bei Chargen von Hunderten von Zeilen oder mehr sind TVPs typischerweise viel schneller als eine vorbereitete Statement-Schleife und skalieren linear mit der Zeilenanzahl.

Zuerst erstellen Sie einen Tabellentyp auf dem Server:

CREATE TYPE dbo.ProductTableType AS TABLE (
    Name  NVARCHAR(100),
    Price DECIMAL(10, 2)
);

PDO_SQLSRV übergibt das TVP als assoziatives Array, dessen Schlüssel der Typname ist und dessen Wert die Zeilenmenge ist. Binde es mit PDO::PARAM_LOB:

<?php
$rows = [];
foreach ($products as $p) {
    $rows[] = [$p["name"], $p["price"]];
}
$tvpInput = ["ProductTableType" => $rows];

$stmt = $conn->prepare(
    "INSERT INTO dbo.Products (Name, Price) SELECT Name, Price FROM ?"
);
$stmt->bindParam(1, $tvpInput, PDO::PARAM_LOB);
$stmt->execute();

Für ein nicht-standardmäßiges Schema gib das Schema als nächstes Element des Arrays durch: ["ProductTableType" => $rows, "Sales"]. Für die SQLSRV-Prozedursyntax und Beispiele für gespeicherte Prozeduren siehe Tabellenwertige Parameter verwenden.

Millionen von Reihen: bcp oder BULK INSERT

Für wirklich Massenabläufe (Data Warehouse-Loads, erste Migrationen) verwende BCP oder BULK INSERT stattdessen PHP. Schreibe deine Daten in eine deseparate oder native-formatige Datei und führe dann BCP oder BULK INSERT aus einem geplanten Job, einem ETL-Schritt oder einem Admin-Skript aus.

Caution

Wenn du von PHP mit shell_exec() oder proc_open()zu BCP ausgibst, interpoliere niemals unzuverlässige Eingaben in die Kommandozeile. Verwenden escapeshellarg() Sie bei jedem Argument und bevorzugen es, den Load Out-of-Band auszuführen, statt in einem Web-Request-Pfad.

Reduziere Hin- und Rückflüge

Jede Netzwerk-Rundreise zwischen PHP und SQL Server hat eine feste Kosten. Wenn Sie fünf Kontoauszüge als eine Charge senden, zahlen Sie diese Kosten einmal statt fünfmal.

Für verwandte Arbeiten, die zusammen ausgeführt werden, packen Sie die Anweisungen in eine Charge und verbrauchen Sie jede Ergebnismenge:

<?php
$sql = "
    SELECT * FROM dbo.Customers WHERE CustomerID = ?;
    SELECT * FROM dbo.Orders WHERE CustomerID = ?;
    SELECT * FROM dbo.Addresses WHERE CustomerID = ?;
";
$stmt = $conn->prepare($sql);
$stmt->execute([$id, $id, $id]);

$customer = $stmt->fetch(PDO::FETCH_ASSOC);

$stmt->nextRowset();
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);

$stmt->nextRowset();
$addresses = $stmt->fetchAll(PDO::FETCH_ASSOC);

Für SQLSRV verwenden Sie die Option sqlsrv_next_result , um zwischen Ergebnissätzen vorzugehen.

Aktiviere mehrere aktive Ergebnissätze, wenn du sie brauchst

Multiple Active Result Sets (MARS) ermöglicht es einer einzelnen Verbindung, mehrere aktive Anweisungen zu haben. Ohne MARS kann man keine neue Abfrage auf eine Verbindung stellen, die noch ein offenes Ergebnisset hat. Beide Treiber aktivieren MARS standardmäßig. Um es auszuschalten, setze MultipleActiveResultSets=false deinen Verbindungszeichenfolge ein. Siehe Deaktivieren mehrerer aktiver Ergebnissätze (MARS).

MARS ist bequem, aber nicht kostenlos. Jede aktive Ergebnismenge verbraucht serverseitige Ressourcen. Bevorzugen Sie es, ein Ergebnisset vollständig zu konsumieren, bevor Sie mit einem neuen beginnen. Nutze MARS, um wirklich verschachtelte Cursormuster zu entblocken.

Tun Sie vorbereitete Aussagen

Prepared Statements speichern den Treiber vor dem erneuten Parsen von SQL auf dem Server und ermöglichen es Ihnen, nicht vertrauenswürdige Eingaben sicher als Parameter zu binden.

Bevorzuge einheimische Zubereitungen

PDO_SQLSRV können Anweisungen in zwei Modi vorbereiten. Native Prepares senden den SQL-Text einmal an den Server und verwenden die geparste Anweisung für jede Ausführung wieder, wobei nur die Parameterwerte auf jedem execute()gesendet werden. Emulierte Vorbereitungen speichern den SQL-Text im Client und erstellen eine vollständige SQL-Zeichenkette mit bei jeder Ausführung interpolierten Parametern.

Stelle es so ein, PDO::ATTR_EMULATE_PREPARES => false dass der Fahrer native Vorbereitungen verwendet. Native Prepares ermöglichen es SQL Server, den Abfrageplan zu cachen und wiederzuverwenden, und sie vermeiden es, SQL-Text bei jeder Ausführung neu zu parsen.

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

Wiederverwenden vorbereiteter Aussagen

Bereite dich einmal vor, führe viele aus. Jeder Aufruf prepare() kostet eine ODBC-Handle-Zuweisung und eine serverseitige Parse. In einer Hot Loop halten Sie das $stmt Objekt lebendig und rufen execute() Sie innerhalb der Schleife auf:

<?php
// Fast: one prepare, many executes.
$stmt = $conn->prepare("UPDATE dbo.Inventory SET Stock = Stock - ? WHERE ProductID = ?");
foreach ($orderLines as $line) {
    $stmt->execute([$line["qty"], $line["productId"]]);
}

// Slow: re-prepares the same SQL on every iteration.
foreach ($orderLines as $line) {
    $stmt = $conn->prepare("UPDATE dbo.Inventory SET Stock = Stock - ? WHERE ProductID = ?");
    $stmt->execute([$line["qty"], $line["productId"]]);
}

Achte auf TOP (?) und IN (?, ?, ...)

TOPbenötigt Klammern um einen Parametermarker, SELECT TOP (?) ..., sodass SQL Server die Zeilenanzahl als Parameter analysieren kann. IN (?, ?, ?, ?) erfordert eine feste Platzhalterzahl zur Vorbereitungszeit. Für dynamische IN Listengrößen baut man entweder die Platzhalter-Zeichenkette aus einer validierten Ganzzahlzahl oder übergibt die Liste als tabellenwertigen Parameter.

Caution

Interpolieren Sie niemals die rohe Benutzereingabe in den SQL-Text (einschließlich der Platzhalterzahl). Wirke den Count mit aus (int) , bevor du die Platzhalter-String baust, und gib die tatsächlichen Werte immer als Parameter durch execute() .

Cursor und Speicher verwalten

Der Standardcursortyp ist PDO::CURSOR_FWDONLY, ein nur vorwärtsgerichteter Feuerschlauch. Es streamt Zeilen einzeln nach PHP und puffert nicht, sodass ein großer Ergebnissatz durch den Zeilenpufferspeicher statt durch die gesamte Zeilenanzahl begrenzt ist. Das ist normalerweise das, was du willst.

Verwende gepufferte Cursor nur, wenn du rückwärts gehen oder Zeilen zählen musst.

PDO::SQLSRV_CURSOR_BUFFERED (ein clientseitiger gepufferter statischer Cursor) das gesamte Ergebnisset sofort in den PHP-Speicher abruft. Dieser Ansatz erlaubt es, die Aussage aufzurufen rowCount(), rückwärts zu suchen und wiederzuverwenden. Standardmäßig ist der Puffer auf 10.240 KB (10 MB) PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZEbegrenzt, und eine Abfrage, deren Ergebnismenge die Obergrenze überschreitet, gibt anstelle von überlaufendem PHP-Speicher zurück.false Man kann die Obergrenze auf das PHP-Speicherlimit erhöhen, aber das tauscht eine false Rendite gegen einen wirklich Allowed memory size exhausted fatalen Fehler ein, wenn eine Abfrage das neue Limit überschreitet. Stimmt absichtlich. Siehe Cursortypen (PDO_SQLSRV).

Serverseitige scrollbare Cursor (PDO::SQLSRV_CURSOR_STATIC, PDO::SQLSRV_CURSOR_DYNAMIC, ) PDO::SQLSRV_CURSOR_KEYSETpuffern auf dem Server statt auf dem Client, sodass sie keinen PHP-Speicher verbrauchen. Sie speichern jedoch serverseitige Ressourcen für die Dauer des Cursors und sind pro Reihe langsamer als nur vorwärtsgesteuerte Ressourcen.

Verwende die Standard-Forward-Only für das Streamen von Reads. Nutze gepufferte clientseitige Systeme für kleine Ergebnismengen, wenn du sie brauchst rowCount() , oder rückwärts scrollen. Vermeide serverseitige scrollbare Cursors, es sei denn, du machst etwas Bestimmtes.

<?php
// Fast, low memory: default forward-only, one row at a time
$stmt = $conn->prepare("SELECT OrderID, Total FROM dbo.Orders");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // ...
}

// Buffered: only when you need rowCount() or seeking
$stmt = $conn->prepare("SELECT * FROM dbo.SmallLookup", [
    PDO::ATTR_CURSOR                    => PDO::CURSOR_SCROLL,
    PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
]);
$stmt->execute();
$rowCount = $stmt->rowCount();

Für eine vollständige Aufschlüsselung siehe Cursor-Typen (PDO_SQLSRV) und Cursor-Typen (SQLSRV).

Ströme große Binär- und Zeichenwerte

Für varbinary(max),varchar(max),nvarchar(max),xml und andere große Typen verwenden Sie PHP-Streams, anstatt den gesamten Wert im Speicher zu materialisieren:

<?php
$stmt = $conn->prepare("SELECT Name, PhotoBlob FROM dbo.Products WHERE ProductID = ?");
$stmt->execute([$id]);
$stmt->bindColumn("PhotoBlob", $photo, PDO::PARAM_LOB);
$stmt->fetch(PDO::FETCH_BOUND);

// $photo is a stream resource; write it directly to disk without loading it all
$outFile = fopen("/tmp/photo.bin", "wb");
stream_copy_to_stream($photo, $outFile);
fclose($outFile);

Für das Einfügen oder Aktualisieren großer Werte verwenden Sie in SQLSRV, SendStreamParamsAtExec=false um Stromdaten in Chunks nach sqlsrv_execute()zu senden. Für Details siehe Daten als Strom senden.

Einstellen geeigneter Timeouts

Timeouts sind genauso sehr Leistungseinstellungen wie Zuverlässigkeitseinstellungen. Lange hängende Anfragen halten Pool-Verbindungen und verhungern andere Anfragen.

Anweisungs-Timeout

Setze ein Zeitlimit pro Anweisung, damit eine Runaway-Abfrage eine Pool-Verbindung nicht unbegrenzt hält. Für PDO_SQLSRV:

<?php
$stmt = $conn->prepare("SELECT ... FROM dbo.HugeTable ...");
$stmt->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, 30); // seconds
$stmt->execute();

Für SQLSRV gib das Options-Array an sqlsrv_query oder sqlsrv_prepareein."QueryTimeout" => 30

Setze einen Wert, der zu deiner Arbeitsbelastung passt. Für eine synchrone Webanfrage sind typischerweise 15 bis 30 Sekunden. Für einen Hintergrund-Batch-Job könnten mehrere Minuten sinnvoll sein. Setze den Timeout bei einer Webanfrage niemals auf null (unbegrenzt).

Anmeldungstimeout

LoginTimeoutIn der Verbindungszeichenfolge steuert er, wie lange der Treiber wartet, um eine Verbindung herzustellen. Setze einen expliziten Wert beim Verbinden mit Azure SQL-Datenbank oder Azure SQL Managed Instance, damit Cold Starts und Failover-Group-Failovers den Client nicht unbegrenzt blockieren. Werte von 30 bis 90 Sekunden funktionieren für die meisten Cloud-Workloads gut. Details zur Größenbestimmung LoginTimeout gegen ConnectRetryCount * ConnectRetryInterval und den daraus resultierenden Fehlermodi finden Sie unter Verbindungszeit. Für die Optionsreferenz siehe Verbindungsoptionen.

Nur-lese-Workloads an eine Replik routen

Für schreibgeschützte Abfragen gegen eine Datenbank in einer Always On-Verfügbarkeitsgruppe, Azure SQL Managed Instance oder Azure SQL-Datenbank mit Read-Scale-out oder Geo-Replica fügen Sie Ihrer Verbindungszeichenfolge hinzuApplicationIntent=ReadOnly:

<?php
$dsn = "sqlsrv:Server=<listener>;Database=<database>;" .
       "Encrypt=true;ApplicationIntent=ReadOnly";

Nur-Lese-Routing sendet die Verbindung an eine synchronisierte sekundäre Replik und entlastet die Arbeit von der primären. Kombinieren Sie dies mit MultiSubnetFailover=true der schnellsten Verbindung zu Multi-Subnet-Verfügbarkeitsgruppen-Listenern.

Beobachte die Leistung vom Server aus

Das clientseitige Timing sagt nur, wie lange eine Anfrage von Anfang zu Ende gedauert hat. Um herauszufinden, warum es langsam war, nutzen Sie die integrierten Diagnosen von SQL Server.

Abfragespeicher

Abfragespeicher erfasst Ausführungspläne, Laufzeitstatistiken und Wartestatistiken für jede Abfrage in der Datenbank. Sie ist standardmäßig in Azure SQL-Datenbank, Azure SQL Managed Instance und SQL Database in Fabric aktiviert. Auf SQL Server aktivieren Sie es pro Datenbank:

ALTER DATABASE <database_name> SET QUERY_STORE = ON;

Dann nutzen Sie die Abfragespeicher-Berichte von SQL Server Management Studio, um Ihre langsamsten und am häufigsten ausgeführten Abfragen zu finden. Siehe Überwachung der Leistung mit dem Abfragespeicher.

Azure SQL Query Performance Insight

Für die Azure SQL-Datenbank zeigt das Query Performance Insight des Azure-Portals automatisch die ressourcenintensivsten Anfragen ohne Konfiguration an. Weitere Informationen finden Sie unter Query Performance Insight für Azure SQL-Datenbank.

SET STATISTICS für einmalige Untersuchung

Für eine einzelne Abfrage, die Sie profilieren möchten, führen Sie sie in SQL Server Management Studio mit aktivierten Statistiken aus:

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

-- your query here

Hohe logische Lesungen bedeuten fast immer einen fehlenden oder unbrauchbaren Index. Hohe CPU-Zeit bei niedrigen logischen Lesevorgängen bedeutet meist einen schlechten Plan (Parameter-Sniffing, eine implizite Konvertierung, die die Verwendung von Indexen verhindert, oder eine skalare Funktion, die Parallelität verhindert).

Erweiterte Events für Fahrerverfolgung auf Fahrerebene

Um genau zu sehen, was der Treiber an SQL Server sendet (einschließlich der tatsächlichen Parameterwerte, die er interpoliert), erfassen Sie eine Extended Events-Sitzung mit den und sql_batch_completed Eventsrpc_completed.

Prüfliste für die Leistung

Verwenden Sie diese Checkliste als Pre-Deployment-Review jeder PHP-Anwendung, die sich mit dem SQL Server verbindet:

Area Prüfen Reference
Connection Connection Pooling ist für die Plattform aktiviert und konfiguriert Verwaltung von Verbindungen effizient
Connection Die Anwendung verwendet Verbindungen innerhalb einer Anfrage erneut und öffnet keine Verbindungen pro Abfrage Verwenden Sie die Verbindung innerhalb einer Anfrage erneut
Connection LoginTimeoutbehandelt Cold Starts und Failover für Azure SQL Login-Timeout
Query Abfragen wählen nur die benötigten Spalten aus, nein SELECT * Wählen Sie nur die Spalten aus, die Sie verwenden
Query Das Filtern erfolgt in SQL, nicht in PHP mit array_filter Hol nur die Reihen, die du brauchst,
Query Große Ergebnismengen sind paginiert mit OFFSET ... FETCH Paginate-große Ergebnismengen
Query Gespeicherte Prozeduren-Menge SET NOCOUNT ON Bevorzugen SET NOCOUNT ON
Einfügungen Bulk-Inserts verwenden tabellenwertige Parameter, keine pro-Zeilen-Schleifen Daten effizient einfügen
Erklärungen PDO::ATTR_EMULATE_PREPARES ist auf false festgelegt. Bevorzuge einheimische Zubereitungen
Erklärungen Die Anwendung verwendet vorbereitete Anweisungen über Ausführungen hinweg erneut Wiederverwenden vorbereiteter Aussagen
Cursors Die Anwendung verwendet den standardmäßig nur vorwärtsgerichteten Cursor, es sei denn, es ist ein Puffern erforderlich. Cursor und Speicher verwalten
Gedächtnis Große Binär- und Zeichenwerte werden gestreamt, nicht materialisiert Ströme große Binär- und Zeichenwerte
Timeouts Für alle benutzerorientierten Abfragen ist ein Statement-Timeout gesetzt Aussage-Timeout
Routing Schreibgeschützte Workloads werden dort gesetzt ApplicationIntent=ReadOnly , wo eine Replik existiert Routen nur lesebezogene Workloads
Beobachtbarkeit Abfragespeicher ist aktiviert und wird regelmäßig überprüft Abfragespeicher