Dostrajanie wydajności sterowników firmy Microsoft dla PHP dla programu SQL Server

Pobieranie sterownika PHP

Ten artykuł opisuje, jak pisać szybki kod PHP na SQL Server, Azure SQL Database, Azure SQL Managed Instance, Azure Synapse Analytics oraz bazie danych SQL w Microsoft Fabric. Wytyczne te dotyczą zarówno SQLSRV, jak i PDO_SQLSRV, które obejmują ten sam podstawowy sterownik Microsoft ODBC dla SQL Server.

Zacznij od zmian o największym wpływie

Jeśli możesz wprowadzić tylko trzy zmiany, wprowadź te poprawki:

  • Włącz grupowanie połączeń. Nawiązanie nowego połączenia TLS z SQL Server zajmuje dziesiątki do setek milisekund, w zależności od ścieżki sieciowej i negocjacji TLS. Ponowne wykorzystanie łączeń w grupie eliminuje ten koszt za każde żądanie. Zobacz Zarządzaj połączeniami efektywnie.
  • Pobieraj tylko te kolumny i wiersze, których potrzebujesz. SELECT * oraz nieograniczone zapytania są najczęstszymi przyczynami wolno działających punktów końcowych. Zobacz Zapytaj tylko to, czego potrzebujesz.
  • Używaj parametrów o wartościach tabelarycznych do operacji wstawiania zbiorczego. Dla setek lub więcej wierszy parametry tabelowe (TVP) są zazwyczaj znacznie szybsze niż instrukcje wiersz po wierszu INSERT i skalują się liniowo wraz z liczbą wierszy. Zobacz Wstaw dane efektywnie.

Efektywnie zarządzaj połączeniami

Nawiązanie połączenia to najdroższa operacja wykonywana przez sterownik. Prawie każde badanie wydajności PHP kończy się poprawką zarządzania połączeniami.

Włącz buforowanie połączeń

Pooling ponownie wykorzystuje połączenia ODBC między żądaniami PHP zamiast ich usuwać na końcu żądania. Obiekt połączenia jest usuwany po zakończeniu skryptu, ale bazowy uchwyt ODBC pozostaje w puli menedżera sterowników ODBC i jest ponownie używany przez następne żądanie, które używa tego samego ciągu połączenia.

Windows: Pula połączeń jest domyślnie włączona. Aby to potwierdzić, pomiń opcję ConnectionPooling w swoim DSN. Aby wyłączyć mechanizm puli połączeń na potrzeby debugowania, ustaw ConnectionPooling=0.

Linux i macOS: Buforowanie połączeń nie jest dostępne jako opcja DSN na tych platformach. Włącz to w menedżerze sterowników, ustawiając Pooling=Yes w sekcji [ODBC] w odbcinst.ini, oraz ustaw dodatnią wartość CPTimeout w stanzie sterownika. Przykład:

[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

Znajdź rzeczywistą ścieżkę biblioteki za pomocą odbcinst -q -d -n "ODBC Driver 18 for SQL Server" lub ls /opt/microsoft/msodbcsql18/lib64/. Nazwa pliku osadza zainstalowaną wersję sterownika ODBC i zmienia się z każdą wersją.

CPTimeout (w sekundach) określa, jak długo bezczynne połączenia pozostają w puli, zanim zostaną zamknięte. Ustaw ją na tyle wysoko, aby większość żądań korzystała z połączenia z puli, ale na tyle nisko, aby nieaktualne połączenia do serwera po przełączeniu awaryjnym były wycofywane w rozsądnym czasie. 60 do 300 sekund sprawdza się dobrze dla większości obciążeń webowych.

Szczegółowe informacje znajdują się w artykule Pula połączeń.

Zrozum koszt pierwszego zapytania

Domyślnie MARS (wiele aktywnych zestawów wyników) jest włączone. Gdy MARS i pula połączeń są aktywne, sterownik resetuje połączenie w grupie przy pierwszym zapytaniu, a ten reset ignoruje cały czasowy limit zapytania ustawiony dla tego pierwszego zapytania. Późniejsze zapytania dotyczące tego samego połączenia zwykle uznają timeout. Jeśli ustawiasz agresywne limity czasu dla pierwszego zapytania dla obciążenia korzystającego z puli połączeń, uwzględnij to zachowanie lub wyłącz MARS za pomocą MultipleActiveResultSets=false, jeśli go nie potrzebujesz. Zobacz uwagę dotyczącą MARS i puli połączeń w sekcji Pula połączeń.

Trwałe połączenia PDO nie są obsługiwane

PDO_SQLSRV odrzuca PDO::ATTR_PERSISTENT. Ustawienie tego w konstruktorze powoduje wyjątek:

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

Używaj puli połączeń ODBC do ponownego użycia między żądaniami. Jest to mechanizm natywny dla sterownika, działa zarówno z PDO_SQLSRV, jak i SQLSRV, i zamyka nieaktywne połączenia po CPTimeout (co również zapewnia prawidłowe odświeżanie tokenów Microsoft Entra).

Ponownie wykorzystaj połączenie w żądaniu

Nawet przy użyciu puli połączeń otwarcie nowego połączenia PDO lub SQLSRV wymaga komunikacji zwrotnej z ODBC w celu pobrania i zweryfikowania uchwytu z puli. Otwieraj połączenie raz na każde żądanie i przekazuj je każdej funkcji, która tego potrzebuje.

Tip

Wystarczy kontener wstrzykiwania zależności lub leniwie inicjowany akcesor. Chodzi o to, by uniknąć new PDO(...) w środku obsługi żądań.

Pytaj tylko to, czego potrzebujesz

Transfery sieciowe tam i z powrotem oraz materializacja zestawu wyników są główną przyczyną opóźnień zapytań w większości obciążeń PHP. Poprawki są takie same, jak na każdej warstwie dostępu do bazy danych.

Wybierz tylko kolumny, których używasz

SELECT * pobiera każdą kolumnę, w tym kolumny typu varchar(max) i varbinary(max), o rozmiarze znacznie przewyższającym dane, które rzeczywiście wykorzystujesz. Wymień kolumny:

<?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");

Przynieś tylko te rzędy, których potrzebujesz

Przenieś filtrowanie do programu SQL Server. Nigdy nie pobieraj całej tabeli do PHP tylko po to, żeby filtrować w pętli foreach .

<?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);

Stronicowanie dużych zestawów wyników

W przypadku widoku listy, który wyświetla kilkaset wierszy z milionów dostępnych, nie zwracaj wszystkich wierszy i nie pozostawiaj ich sortowania po stronie klienta. Użyj paginacji po stronie serwera z 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);
}

Wybierz odpowiednią metodę pobierania

  • Używaj fetch(PDO::FETCH_ASSOC) w pętli do iteracji strumieniowej, gdy nie potrzebujesz wszystkich wierszy naraz w pamięci.
  • Używaj fetchAll(PDO::FETCH_ASSOC), gdy wywołujący rzeczywiście potrzebuje całego zbioru (na przykład do wygenerowania pełnej odpowiedzi JSON).
  • Używaj fetchColumn(), gdy interesuje Cię tylko jedna wartość skalarna (COUNT, SUM lub MAX).
  • Użyj PDO::FETCH_KEY_PAIR lub PDO::FETCH_UNIQUE, aby tworzyć słowniki wyszukiwania bez drugiego przebiegu.

Tryby pobierania numerycznego (PDO::FETCH_NUM) są nieco szybsze niż tryby pobierania asocjacyjnego, ponieważ pomijają budowanie mapy nazw kolumn. Stawiaj na przejrzystość; przełączaj tylko wtedy, gdy profiler wykaże, że narzut związany z pobieraniem jest znaczący.

Preferuj SET NOCOUNT ON w procedurach i partiach przechowywanych

Każde polecenie INSERT, UPDATE i DELETE zwraca token DONE_IN_PROC z liczbą zmodyfikowanych wierszy, który język PHP zazwyczaj pomija. Ten token nie powoduje dodatkowego połączenia tam i z powrotem, ale każdy z nich nadal kosztuje bajty przesyłane przez sieć i wymaga niewielkiego nakładu pracy po stronie sterownika. W procedurze wielozadaniowej lub partii, która wykonuje setki instrukcji na jedno połączenie, oszczędności się sumują. Wyłącz to:

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;

Efektywnie wprowadzaj dane

Wybierz odpowiednią metodę wstawiania w zależności od liczby wierszy, które przesuwasz. Zły wybór może być 100 razy wolniejszy.

Mniej niż około 100 wierszy: przygotowane zdanie w pętli

Dla małych partii wykonaj pojedyncze przygotowane zdanie w pętli:

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

Umieść pętlę w transakcji, aby wszystkie operacje wstawiania zostały zatwierdzone jako jedna całość, a dziennik nie musiał być opróżniany po każdym wierszu:

<?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;
}

Od setek do milionów wierszy: parametry tabelowe

Parametry o wartościach tabelarycznych (TVP) wysyłają całą partię do programu SQL Server w jednym przebiegu i umożliwiają programowi SQL Server przetworzenie zbioru jako pojedynczej instrukcji. W przypadku partii obejmujących setki wierszy lub więcej TVP są zazwyczaj znacznie szybsze niż pętla z użyciem przygotowanych instrukcji i skalują się liniowo wraz z liczbą wierszy.

Najpierw stwórz typ tabeli na serwerze:

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

PDO_SQLSRV przekazuje TVP jako tablicę asocjacyjną, której kluczem jest nazwa typu, a wartością jest zbiór wierszy. Zwiąż ją z :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();

W przypadku schematu innego niż domyślny przekaż schemat jako następny element tablicy: ["ProductTableType" => $rows, "Sales"]. Przykłady składni proceduralnej SQLSRV i procedur przechowywanych można znaleźć w artykule Używaj parametrów tabelowych.

Miliony wierszy: bcp lub BULK INSERT

W przypadku naprawdę dużych operacji zbiorczych (ładowanie hurtowni danych, migracje początkowe) używaj bcp lub BULK INSERT zamiast PHP. Zapisz swoje dane do pliku w formacie ograniczonym lub natywnym, a następnie uruchom bcp lub BULK INSERT z zaplanowanego zadania, kroku ETL lub skryptu administratora.

Caution

Jeśli wywołujesz bcp z poziomu PHP za pomocą shell_exec() lub proc_open(), nigdy nie wstawiaj niezaufanych danych wejściowych do wiersza polecenia. Używam escapeshellarg() na każdym arteście i wolę uruchamiać ładowanie poza pasmem zamiast w ścieżce żądań webowych.

Zmniejsz liczbę podróży w obie strony

Każde połączenie sieciowe tam i z powrotem między PHP a SQL Server wiąże się ze stałym kosztem. Gdy wysyłasz pięć wyciągów w jednej partii, płacisz ten koszt raz zamiast pięciokrotnie.

W przypadku powiązanych operacji wykonywanych razem umieść instrukcje w jednej partii i przetwórz każdy zestaw wyników:

<?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);

W SQLSRV, aby przechodzić między zestawami wyników, użyj sqlsrv_next_result.

Włącz wiele aktywnych zestawów wyników, gdy potrzebujesz

Multiple Active Result Sets (MARS) pozwala pojedynczemu połączeniu mieć wiele aktywnych instrukcji. Bez MARS nie możesz wydać nowego zapytania na połączenie, które nadal ma otwarty zestaw wyników. Oba sterowniki domyślnie włączają MARS. Aby ją wyłączyć, wstaw MultipleActiveResultSets=false swój parametry połączenia. Zobacz Wyłącz wiele aktywnych zestawów wyników (MARS).

MARS jest wygodny, ale nie darmowy. Każdy aktywny zestaw wyników zużywa zasoby po stronie serwera. Najlepiej najpierw w pełni przetworzyć jeden zbiór wyników przed rozpoczęciem kolejnego. Użyj MARS, aby odblokować prawdziwie zagnieżdżone wzory kursora.

Tune przygotowane oświadczenia

Przygotowane instrukcje oszczędzają sterownikowi ponownego parsowania SQL na serwerze i pozwalają bezpiecznie przypisać nieufne dane wejściowe jako parametry.

Preferuj rodzime preparaty

PDO_SQLSRV może przygotowywać oświadczenia w dwóch trybach. Natywne przygotowane instrukcje wysyłają tekst SQL do serwera tylko raz i ponownie używają przeanalizowanej instrukcji przy każdym wykonaniu, wysyłając tylko wartości parametrów przy każdym execute(). Emulowane przygotowania zachowują tekst SQL w kliencie i odbudowują pełny ciąg SQL z parametrami interpolowanymi przy każdym wykonaniu.

Ustaw PDO::ATTR_EMULATE_PREPARES => false, aby sterownik używał natywnych przygotowanych instrukcji. Natywne przygotowania pozwalają SQL Server buforować i ponownie używać planu zapytań, a także unikają ponownego analizowania tekstu SQL przy każdym wykonaniu.

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

Ponowne wykorzystanie przygotowanych instrukcji

Przygotuj raz, wykonaj wiele razy. Każde wywołanie prepare() wiąże się z alokacją uchwytu ODBC oraz parsowaniem po stronie serwera. W gorącej pętli utrzymuj $stmt obiekt przy życiu i wywołuj execute() wewnątrz pętli:

<?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"]]);
}

Uważaj na TOP (?) i IN (?, ?, ...)

TOP wymaga nawiasów wokół markera parametru SELECT TOP (?) ..., aby program SQL Server mógł zinterpretować liczbę wierszy jako parametr. IN (?, ?, ?, ?) wymaga stałej liczby symboli zastępczych na etapie przygotowania. W przypadku list o dynamicznym rozmiarze IN, albo zbuduj ciąg symboli zastępczych na podstawie zweryfikowanej liczby całkowitej, albo przekaż listę jako parametr o wartościach tabelarycznych.

Caution

Nigdy nie interpoluj surowych danych wejściowych użytkownika do tekstu SQL (w tym liczby zastępczych). Rzutuj wartość count za pomocą (int) przed zbudowaniem ciągu znaków symbolu zastępczego i zawsze przekazuj rzeczywiste wartości za pomocą execute() jako parametry.

Zarządzanie kursorami i pamięcią

Domyślny typ kursora to PDO::CURSOR_FWDONLY, węż strażacki działający tylko do przodu. Streamuje wiersze do PHP pojedynczo i nie buforuje, więc duży zbiór wyników jest ograniczony pamięcią wierszową, a nie całkowitą liczbą wierszy. Zazwyczaj tego właśnie chcesz.

Używaj kursorów buforowanych tylko wtedy, gdy musisz cofnąć się lub liczyć wiersze

PDO::SQLSRV_CURSOR_BUFFERED (statyczny kursor buforowany po stronie klienta) pobiera cały zestaw wyników do pamięci PHP od razu. To podejście umożliwia wywołanie rowCount(), cofanie się i ponowne wykorzystanie instrukcji. Domyślnie bufor jest ograniczony do 10 240 KB (10 MB) za pomocą PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, a zapytanie, którego zestaw wyników przekracza limit, zwraca false zamiast nadmiaru pamięci PHP. Możesz podnieść limit w kierunku limitu pamięci PHP, ale robiąc to, wymieniasz false zwrot na Allowed memory size exhausted prawdziwy błąd śmiertelny, gdy zapytanie przerasta nowy limit. Dostrajaj świadomie. Zobacz Typy kursorów (PDO_SQLSRV).

Kursory przewijalne po stronie serwera (PDO::SQLSRV_CURSOR_STATIC, PDO::SQLSRV_CURSOR_DYNAMIC, PDO::SQLSRV_CURSOR_KEYSET) są buforowane po stronie serwera zamiast po stronie klienta, więc nie zużywają pamięci PHP. Jednak zajmują zasoby po stronie serwera przez cały okres istnienia kursora i działają wolniej dla każdego wiersza niż kursory typu forward-only.

Używaj domyślnego "tylko do przodu" do czytania strumieniowego. Używaj buforowania po stronie klienta dla małych zestawów wyników, gdy potrzebujesz rowCount() lub przewijania wstecz. Unikaj przewijanych kursorów po stronie serwera, chyba że robisz coś konkretnego.

<?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();

Pełne podziały można znaleźć w artykule Typy kursorów (PDO_SQLSRV) i Typy kursorów (SQLSRV).

Przesyłanie strumieniowe dużych wartości binarnych i znakowych

Dla varbinary(max), varchar(max), nvarchar(max), xml i innych dużych typów używaj strumieni PHP zamiast materializowania całej wartości w pamięci:

<?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);

W przypadku wstawiania lub aktualizowania dużych wartości użyj w SQLSRV elementu SendStreamParamsAtExec=false, aby po sqlsrv_execute() przesyłać dane strumieniowe partiami. Szczegóły można znaleźć w artykule Wyślij dane jako strumień.

Ustaw odpowiednie limity czasu

Limity czasu są ustawieniami dotyczącymi wydajności w równym stopniu jak ustawieniami dotyczącymi niezawodności. Zapytania wykonywane zbyt długo zajmują połączenia w puli i blokują obsługę innych żądań.

Limit czasu instrukcji

Ustaw limit czasu dla każdej instrukcji, aby niekontrolowane zapytanie nie zajmowało połączenia z puli bezterminowo. Dla PDO_SQLSRV:

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

W przypadku SQLSRV przekaż element "QueryTimeout" => 30 w tablicy opcji do sqlsrv_query lub sqlsrv_prepare.

Ustaw wartość odpowiadającą twojemu obciążeniu. Dla synchronicznego żądania webowego zwykle trwa 15 do 30 sekund. W przypadku zadania wsadowego działającego w tle kilka minut może być akceptowalne. Nigdy nie ustawiaj limitu czasu na zero (nieograniczony) w żądaniu internetowym.

Limit czasu logowania

LoginTimeout w parametrze połączenia określa, jak długo sterownik czeka na nawiązanie połączenia. Ustaw jawnie określoną wartość podczas łączenia z usługą Azure SQL Database lub Azure SQL Managed Instance, aby zimne uruchomienia i przełączenia awaryjne grupy trybu failover nie powodowały bezterminowego zawieszenia klienta. Wartości od 30 do 90 sekund dobrze sprawdzają się w większości obciążeń chmurowych. Szczegółowe informacje na temat doboru rozmiarów LoginTimeout i ConnectRetryCount * ConnectRetryInterval oraz wynikających z tego trybów awarii można znaleźć w sekcji Limit czasu połączenia. Odniesienie do opcji można znaleźć w Opcjach połączenia.

Kieruj obciążenia tylko do odczytu do repliki

W przypadku zapytań tylko do odczytu względem bazy danych w grupie dostępności Always On, Azure SQL Managed Instance lub Azure SQL Database z funkcją skalowania odczytu lub geo-repliką dodaj ApplicationIntent=ReadOnly do parametrów połączenia:

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

Routing tylko do odczytu kieruje połączenie do zsynchronizowanej repliki wtórnej, odciążając replikę podstawową. Połącz z MultiSubnetFailover=true, aby uzyskać najszybsze połączenie z listenerami grup dostępności obejmujących wiele podsieci.

Obserwuj wydajność serwera

Timing po stronie klienta pokazuje tylko, ile czasu zajęło zapytanie od początku do końca. Aby dowiedzieć się, dlaczego było wolniej, skorzystaj z wbudowanej diagnostyki SQL Server.

Magazyn zapytań

Query Store rejestruje plany wykonania, statystyki w czasie działania oraz statystyki oczekiwania dla każdego zapytania w bazie danych. Domyślnie jest włączony w Azure SQL Database, Azure SQL Managed Instance oraz bazie danych SQL w Fabric. Na SQL Server włącz to dla każdej bazy danych:

ALTER DATABASE <database_name> SET QUERY_STORE = ON;

Następnie użyj raportów Query Store SQL Server Management Studio, aby znaleźć najwolniejsze i najczęściej wykonywane zapytania. Zobacz Monitorowanie wydajności za pomocą Query Store.

Azure SQL — Wgląd w wydajność zapytań

Dla Azure SQL Database, Query Performance Insight portalu Azure automatycznie pokazuje największe zapytania zużywające zasoby bez żadnej konfiguracji. Aby uzyskać więcej informacji, zobacz Szczegółowe informacje o wydajności zapytań dla usługi Azure SQL Database.

SET STATISTICS do jednorazowej analizy

Dla pojedynczego zapytania, które chcesz profilować, uruchom je w SQL Server Management Studio z włączonymi statystykami:

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

-- your query here

Wysokie odczyty logiczne prawie zawsze oznaczają brak lub nieużyteczny indeks. Wysoki czas CPU przy niewielkiej liczbie odczytów logicznych zwykle oznacza nieoptymalny plan wykonania (parametr sniffing, niejawna konwersja uniemożliwiająca użycie indeksu lub funkcja skalarna uniemożliwiająca wykonywanie równoległe).

Rozszerzone zdarzenia dla śledzenia na poziomie kierowcy

Aby dokładnie zobaczyć, co sterownik wysyła do SQL Server (w tym rzeczywiste wartości parametrów, które interpoluje), przechwyć sesję Extended Events, używając zdarzeń rpc_completed i sql_batch_completed

Lista kontrolna wydajności

Użyj tej listy kontrolnej jako przeglądu przed wdrożeniem dowolnej aplikacji PHP łączącej się z SQL Server:

Area Sprawdź Reference
Connection Buforowanie połączeń jest włączone i skonfigurowane dla platformy Efektywnie zarządzaj połączeniami
Connection Aplikacja ponownie wykorzystuje połączenia w ramach żądania i nie otwiera połączeń na każde zapytanie Ponownie wykorzystaj połączenie w żądaniu
Connection LoginTimeout obejmuje zimne uruchomienia i tryb failover dla usługi Azure SQL Czas logowania
Query Zapytania wybierają tylko potrzebne kolumny, nie SELECT * Wybierz tylko kolumny, których używasz
Query Filtrowanie odbywa się w SQL, a nie w PHP z array_filter Pobierz tylko te wiersze, których potrzebujesz
Query Duże zbiory wyników są dzielone na strony za pomocą OFFSET ... FETCH Paginuj duże zbiory wyników
Query Zestaw procedur przechowywanych SET NOCOUNT ON Preferuj SET NOCOUNT ON
Wstawki Operacje zbiorczego wstawiania używają parametrów typu tabelarycznego, a nie pętli przetwarzających każdy wiersz osobno Efektywnie wprowadzaj dane
Statements PDO::ATTR_EMULATE_PREPARES jest ustawiona na wartość false Preferuj lokalne produkty
Statements Aplikacja wykorzystuje przygotowane instrukcje wielokrotnie podczas wykonywania Ponowne wykorzystanie przygotowanych instrukcji
Cursors Aplikacja używa domyślnego kursora tylko do przodu, chyba że jest potrzebne buforowanie Zarządzanie kursorami i pamięcią
Memory Duże wartości binarne i znakowe są przesyłane strumieniowo, a nie materializowane Przesyłaj strumieniowo duże wartości binarne i znakowe
Timeouts Limit czasu wykonania instrukcji jest ustawiony dla wszystkich zapytań widocznych dla użytkownika Czas na oświadczenie
Trasowanie Obciążenia tylko do odczytu ustawione ApplicationIntent=ReadOnly tam, gdzie istnieje replika Kieruj obciążenia tylko do odczytu
Observability Query Store jest włączany i regularnie przeglądany Magazyn zapytań