Microsoft PHP(SQL Server용 PHP) 드라이버 성능 튜닝

PHP 드라이버 다운로드

이 글에서는 SQL Server, Azure SQL Database, Azure SQL Managed Instance, Azure Synapse Analytics, 그리고 Microsoft Fabric에서 SQL 데이터베이스를 대상으로 빠른 PHP 코드를 작성하는 방법을 다룹니다. 이 지침은 동일한 기본 Microsoft ODBC 드라이버 포 SQL Server 랩을 하는 SQLSRV와 PDO_SQLSRV 모두에 적용됩니다.

가장 영향력 있는 변화부터 시작하세요

세 가지 변경만 할 수 있다면 다음 사항들을 변경하세요:

  • 연결 풀링을 활성화하세요. SQL Server에 새로운 TLS 연결을 설정하는 데 네트워크 경로와 TLS 협상에 따라 수십 밀리초에서 수백 밀리초까지 걸립니다. 풀된 연결을 재사용하면 요청당 비용을 없앨 수 있습니다. ' 연결 관리 효율적'을 참조하세요.
  • 필요한 열과 행만 가져오세요. SELECT * 그리고 무한한 쿼리가 느린 엔드포인트의 가장 흔한 원인입니다. 필요한 것만 쿼리를 보세요.
  • 대량 인서트에는 테이블 값 매개변수를 사용하세요. 수백 행 이상에서는 테이블 값 매개변수(TVP)가 행별 INSERT 문장보다 훨씬 빠르며 행 수에 따라 선형적으로 확장됩니다. 참조 : 데이터를 효율적으로 삽입하기.

연결을 효율적으로 관리하세요

연결 구축은 드라이버가 수행하는 단일 작업 중 가장 비용이 많이 드는 작업입니다. 거의 모든 PHP 성능 조사는 연결 관리 수정으로 끝납니다.

연결 풀링 활성화

풀링은 요청 종료 시 오프닝 대신 PHP 요청 간에 ODBC 연결을 재사용합니다. 스크립트가 끝나면 연결 객체는 버려지지만, 기본 ODBC 핸들은 ODBC 드라이버 관리자의 풀에 남아 다음 요청에서 같은 연결 문자열을 요청할 때 재사용됩니다.

Windows: 연결 풀링은 기본적으로 켜져 있습니다. 확인하려면 DSN에서 해당 ConnectionPooling 옵션을 빼세요. 디버깅 ConnectionPooling=0을 위한 풀링을 비활성화하려면 .

리눅스와 macOS: 이 플랫폼들에서는 연결 풀링이 DSN 옵션이 아닙니다. 드라이버 관리자에서 의 섹션odbcinst.ini[ODBC] 설정 Pooling=Yes 하여 활성화하고, 드라이버의 연(stanza) 아래에 양수를 CPTimeout 설정하세요. 다음은 그 예입니다.

[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

또는 로 실제 라이브러리 경로를 odbcinst -q -d -n "ODBC Driver 18 for SQL Server"ls /opt/microsoft/msodbcsql18/lib64/찾으세요. 파일 이름은 설치된 ODBC 드라이버 버전을 내장하며, 각 릴리스마다 변경됩니다.

CPTimeout (초 단위로) 유휴 연결이 풀에 머무르는 시간을 조절해 닫히기 전까지 제어합니다. 대부분의 요청이 풀링 연결을 찾을 수 있을 만큼 높게 설정하되, 실패한 서버와의 오래된 연결이 꽤 빨리 종료될 정도로 낮게 설정하세요. 60초에서 300초 정도면 대부분의 웹 작업에는 잘 작동합니다.

자세한 내용은 연결 풀링을 참조하세요.

첫 번째 쿼리 비용을 이해하세요

MARS(Multiple Active Result Sets)는 기본적으로 활성화되어 있습니다. MARS와 연결 풀링이 모두 활성화되어 있을 때, 드라이버는 쿼리에서 풀된 연결을 초기화하고, 그 초기화는 첫 쿼리에 설정된 쿼리 타임아웃을 무시합니다. 같은 연결에 대한 이후의 쿼리는 보통 타임아웃을 유지합니다. 풀된 워크로드에 공격적인 첫 쿼리 타임아웃을 설정한다면, 이 동작을 고려하거나, 필요하지 않다면 MARS를 비활성화 MultipleActiveResultSets=false 하세요. 연결 풀링의 MARS 및 풀링 노트를 참고하세요.

지속적인 PDO 연결은 지원되지 않습니다

PDO_SQLSRV 거부합니다 PDO::ATTR_PERSISTENT. 구성자 던지기로 설정하면:

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

교차 요청 재사용에는 ODBC 연결 풀링 을 사용하세요. 드라이버 네이티브 메커니즘으로, PDO_SQLSRV와 SQLSRV 모두에서 작동하고, 유휴 연결을 CPTimeout 종료시키며(토큰 새로고침도 정확하게 유지하는 Microsoft Entra 있습니다).

요청 내에서 연결을 재사용

풀링을 하더라도 새로운 PDO 또는 SQLSRV 연결을 열면 풀링 핸들을 가져오고 검증하기 위해 ODBC 왕복 처리가 발생합니다. 요청마다 한 번씩 연결을 열고 필요한 모든 함수에 전달하세요.

팁 (조언)

의존성 주입 컨테이너나 게으른 액세스 하나면 충분합니다. 요점은 요청 핸들러 중간에 끼는 것을 피 new PDO(...) 하는 것입니다.

필요한 것만 쿼리하세요

네트워크 왕복과 결과 집합 물질화가 대부분의 PHP 워크로드에서 쿼리 지연을 지배합니다. 수정 사항은 모든 데이터베이스 접근 계층에 적용되는 동일한 것들입니다.

사용하는 열만 선택하세요

SELECT * 실제로 소비하는 데이터보다 훨씬 큰 varchar(max)varbinary(max) 열을 포함한 모든 열을 끌어옵니다. 칼럼 이름을 말씀해 주세요:

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

필요한 행만 가져와요

필터링을 SQL Server로 푸시하세요. 루프 필터 foreach 를 위해 PHP에 전체 테이블을 가져오지 마세요.

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

큰 결과 집합 페이지 매김

수백만 행 중 몇 백 행만 보여주는 리스트 뷰라면, 모든 행을 반환하지 말고 클라이언트가 정리하도록 하세요. 서버 측 페이지네이션을 사용하여 다음을 사용하세요 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);
}

올바른 페치 방법을 선택하세요

  • 모든 행을 동시에 메모리에 넣을 필요가 없을 때 스트리밍 반복용으로 루프 내로 사용 fetch(PDO::FETCH_ASSOC) 하세요.
  • 발신자가 진짜로 전체 집합이 필요할 때(예: 전체 JSON 응답을 렌더링할 때) 사용합니다 fetchAll(PDO::FETCH_ASSOC) .
  • 단일 스칼라(a COUNT, , SUM) MAX에만 관심이 있을 때 사용하세요fetchColumn().
  • 두 번째 작업 없이 조회 사전을 사용 PDO::FETCH_KEY_PAIR 하거나 PDO::FETCH_UNIQUE 구축하세요.

수치 페치 모드(PDO::FETCH_NUM)는 연관 페치 모드보다 약간 빠르는데, 이는 컬럼-이름 맵을 생성하지 않기 때문입니다. 명확함을 선호하세요; 프로파일러가 fet 오버헤드를 중요하다고 표시할 때만 전환하세요.

저장 프로시저와 배치에서 선호합니다 SET NOCOUNT ON

모든 INSERT, , 그리고 DELETE 문은 영향을 받은 행 수가 포함된 토큰을 DONE_IN_PROC 반환하며, PHP는 보통 이를 UPDATE폐기합니다. 토큰이 왕복 경로를 추가하지는 않지만, 각 토큰마다 와이어 바이트와 드라이버 작업이 소량 필요합니다. 수백 개의 호출을 실행하는 다중 문항 절차나 배치에서는 절감 효과가 누적됩니다. 꺼버리세요:

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;

효율적으로 데이터를 삽입하세요

이동하는 행 수에 따라 적절한 삽입 방식을 선택하세요. 잘못된 선택은 100배는 더 느려질 수 있습니다.

약 100행 미만: 루프 내 준비된 진술

소규모 배치의 경우, 루프 내에서 준비된 단일 문장을 실행합니다:

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

트랜잭션에서 루프를 랩해서 모든 인서트가 하나의 단위로 커밋되게 하고, 로그가 매 행마다 플러시되지 않아도 되도록 하세요:

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

수백만에서 수백만 행: 테이블 값 매개변수

테이블 값 매개변수(TVP)는 전체 배치를 한 번에 SQL Server로 전송하고, SQL Server가 이 집합을 하나의 문장으로 처리하도록 합니다. 수백 행 이상 배치의 경우, TVP는 일반적으로 준비된 진술 루프보다 훨씬 빠르며, 행 수에 따라 선형적으로 확장됩니다.

먼저, 서버에서 테이블 타입을 생성하세요:

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

PDO_SQLSRV TVP를 타입 이름이고 값이 행 집합인 연관 배열로 전달합니다. 다음과 같이 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();

기본 스키마가 아닌 경우, 배열의 다음 요소로 스키마를 전달합니다: ["ProductTableType" => $rows, "Sales"]. SQLSRV 절차적 구문 및 저장 프로시저 예시는 ' 테이블 값 매개변수 사용'을 참조하세요.

수백만 행: bcp 또는 BULK INSERT

진정한 대량 작업(데이터 웨어하우스 로드, 초기 마이그레이션)에는 BCPBULK INSERT PHP 대신 사용하세요. 구분된 또는 네이티브 포맷 파일에 데이터를 작성한 후 bcp 또는 BULK INSERT 예약된 작업, ETL 단계, 관리자 스크립트에서 실행하세요.

Caution

PHP shell_exec()proc_open()에서 BCP를 사용하거나 사용할 경우, 신뢰할 수 없는 입력을 명령줄에 보간하지 마세요. 모든 인수에 사용 escapeshellarg() 하며, 웹 요청 경로 대신 아웃 오브 밴드 방식으로 부하를 실행하는 것을 선호합니다.

왕복 횟수 감소

PHP와 SQL Server 간의 모든 네트워크 왕복에는 고정된 비용이 있습니다. 한 번에 다섯 개의 명세서를 보내면 그 비용을 다섯 번 대신 한 번 지불하면 됩니다.

관련 작업이 함께 실행되는 경우, 문장들을 하나의 배치로 모아 모든 결과 세트를 소모하세요:

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

SQLSRV의 경우, 결과 집합 간 이동에 사용 sqlsrv_next_result 하세요.

필요할 때 다중 활성 결과 세트를 활성화하세요

다중 활성 결과 집합(MARS)은 단일 연결에 여러 활성 문장을 가질 수 있게 합니다. MARS가 없으면 아직 결과 집합이 열린 연결에 대해 새 쿼리를 발행할 수 없습니다. 두 드라이버 모두 기본적으로 MARS를 활성화합니다. 끄려면 연결 문자열을 설정 MultipleActiveResultSets=false 하세요. 다중 활성 결과 세트 비활성화(MARS)를 참조하세요.

MARS는 편리하지만 무료는 아닙니다. 각 활성 결과 세트는 서버 측 자원을 소비합니다. 결과를 완전히 소비한 후에 다른 결과를 시작하는 것을 선호합니다. MARS를 사용해 진짜 중첩된 커서 패턴을 차단하지 마세요.

준비된 성명을 튠다

준비된 문장은 드라이버가 서버에서 SQL을 다시 해석하지 않게 해주고, 신뢰할 수 없는 입력을 파라미터로 안전하게 바인딩할 수 있게 해줍니다.

토종 조리제를 선호하세요

PDO_SQLSRV 두 가지 모드로 진술문을 준비할 수 있습니다. 네이티브 프레퍼는 SQL 텍스트를 서버에 한 번 보내고, 파싱된 문은 각 실행마다 파라미터 값 execute()만 보내며 재사용합니다. 에뮬레이티드 준비 도구는 SQL 텍스트를 클라이언트에 유지하고, 각 실행마다 매개변수를 보간한 전체 SQL 문자열을 다시 만듭니다.

드라이버가 네이티브 프레어를 사용하도록 설정 PDO::ATTR_EMULATE_PREPARES => false 하세요. 네이티브 프레퍼는 SQL Server가 쿼리 계획을 캐시하고 재사용할 수 있게 하며, 매번 실행 시 SQL 텍스트를 재파싱하는 것을 피할 수 있습니다.

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

준비된 진술서 재사용

한 번 준비하고, 여러 번 처형하세요. 모든 prepare() 통화는 ODBC 핸들 할당과 서버 측 파싱 비용을 지불합니다. 핫 루프에서는 객체를 $stmt 살아 있게 유지하고 루프 내부에서 다음과 같은 호출을 합니다 execute() :

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

조심하세요 TOP (?)IN (?, ?, ...)

TOP매개변수 마커 SELECT TOP (?) ...를 괄호로 둘러야 하므로 SQL Server가 열 수를 매개변수로 파싱할 수 있습니다. IN (?, ?, ?, ?) 준비 시간에 고정된 자리 표시자 수를 요구합니다. 동적 IN 리스트 크기를 위해서는 검증된 정수 개수에서 자리 표시자 문자열을 만들거나, 테이블 값 매개변수로 리스트를 전달합니다.

Caution

사용자 입력을 원시 입력(자리 표시자 포함)에 보간하지 마세요. 자리 문자열을 만들기 전에 카 (int) 운트를 캐스트하고, 실제 값 execute() 들을 항상 매개변수로 전달하세요.

커서와 메모리 관리

기본 커서 유형은 , 전방 전용 소방호스입니다 PDO::CURSOR_FWDONLY. 이 방식은 한 번에 한 행씩 PHP로 스트리밍하고 버퍼링하지 않기 때문에, 큰 결과 세트는 전체 행 수가 아니라 행 버퍼 메모리로 제한됩니다. 보통 그게 원하는 방식이죠.

버퍼 커서는 뒤로 이동하거나 행을 세어야 할 때만 사용하세요

PDO::SQLSRV_CURSOR_BUFFERED (클라이언트 측 버퍼드 정적 커서)는 전체 결과 세트를 처음부터 PHP 메모리로 가져옵니다. 이 방법은 호출 rowCount(), 역방향 탐색, 그리고 명제를 재사용할 수 있게 해줍니다. 기본적으로 버퍼는 10,240 KB(10 MB) PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE로 제한되며, 결과 집합이 캡을 초과하는 쿼리는 PHP 메모리 오버플로우 대신 반환 false 됩니다. PHP 메모리 한도를 높일 수는 있지만, false 쿼리가 새 메모리 한도를 초과할 때 치 Allowed memory size exhausted 명적인 오류가 발생할 수 있습니다. 의도적으로 조율하세요. 커 서 유형 (PDO_SQLSRV)을 참조하세요.

서버 측 스크롤 가능한 커서(PDO::SQLSRV_CURSOR_STATIC, PDO::SQLSRV_CURSOR_DYNAMIC, ) PDO::SQLSRV_CURSOR_KEYSET버퍼는 클라이언트가 아닌 서버에 버퍼링되어 PHP 메모리를 소모하지 않습니다. 하지만 커서가 지속되는 동안 서버 측 자원을 보유하며, 전방 전방 전송보다 행당 속도가 느립니다.

스트리밍 리딩에는 기본 전달 전용을 사용하세요. 필요할 rowCount() 때는 클라이언트 측에서 버퍼링된 결과 세트를 사용하거나 역스크롤을 하세요. 서버 쪽에서 스크롤 가능한 커서는 특별히 할 일이 아니면 피하세요.

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

자세한 내용은 커 서 타입(PDO_SQLSRV)커서 타입(SQLSRV)을 참조하세요.

대형 이진 및 문자 값 스트리밍

varbinary(max),varchar(max),nvarchar(max), xml 및 기타 큰 타입의 경우, 전체 값을 메모리에 물질화하는 대신 PHP 스트림을 사용합니다:

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

큰 값으로 삽입하거나 업데이트할 때는 SendStreamParamsAtExec=false SQLSRV에서 스트림 데이터를 청크 sqlsrv_execute()로 전송하는 데 사용하세요. 자세한 내용은 ' 데이터를 스트림으로 전송'을 참조하세요.

적절한 시간 제한 설정

타임아웃은 신뢰성 설정만큼이나 성능 설정이기도 합니다. 롱행 쿼리는 풀 연결을 유지하고 다른 요청을 고축시킵니다.

명령문 제한 시간

문장당 시간 제한을 설정해 런어웨이 쿼리가 풀 연결을 무기한 유지하지 않도록 하세요. PDO_SQLSRV 말씀드리자면:

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

SQLSRV의 경우, 옵션 배열 sqlsrv_query 을 또는 sqlsrv_prepare에 전달 "QueryTimeout" => 30 합니다.

업무량에 맞는 값을 설정하세요. 동기식 웹 요청의 경우 보통 15초에서 30초 정도가 걸립니다. 백그라운드 배치 작업의 경우 몇 분이 적절할 수 있습니다. 웹 요청에서 타임아웃을 0(무제한)으로 설정하지 마세요.

로그인 제한 시간

LoginTimeout연결 문자열에서는 드라이버가 연결을 구축하는 데 얼마나 오래 기다리는지 제어합니다. Azure SQL Database나 Azure SQL Managed Instance에 연결할 때 명시적인 값을 설정해 콜드 스타트와 페일오버 그룹 페일오버가 클라이언트를 무한히 정지시키지 않도록 하세요. 30초에서 90초 사이의 값이 대부분의 클라우드 워크로드에 적합합니다. 크기 LoginTimeout 대비 ConnectRetryCount * ConnectRetryInterval 및 그에 따른 실패 모드에 대한 자세한 내용은 연결 타임아웃을 참조하세요. 옵션 참고 사항은 연결 옵션을 참조하세요.

읽기 전용 워크로드를 복제본으로 라우팅합니다

Always On 가용성 그룹, Azure SQL Managed Instance 또는 읽기 스케일아웃 또는 지오리플리카가 있는 Azure SQL Database에 대한 읽기 전용 쿼리를 할 때, 연결 문자열에 다음을 추가 ApplicationIntent=ReadOnly 하세요:

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

읽기 전용 라우팅은 연결을 동기화된 보조 복제본으로 보내 주 복제본에서 작업을 분리합니다. 멀티서브넷 가용성 그룹 리스너와 가장 빠르게 연결하기 위해 결합 MultiSubnetFailover=true 하세요.

서버의 성능을 관찰하세요

클라이언트 측 타이밍은 쿼리가 끝에서 끝까지 걸린 시간을 알려줍니다. 느렸는지 알아보려면 SQL Server에 내장된 진단 기능을 활용하세요.

쿼리 스토어 (쿼리 저장소)

쿼리 저장소는 데이터베이스의 모든 쿼리에 대해 실행 계획, 런타임 통계, 대기 통계를 캡처합니다. 기본적으로 Azure SQL Database, Azure SQL Managed Instance, 그리고 Fabric의 SQL 데이터베이스에서 활성화되어 있습니다. SQL Server에서 데이터베이스별로 활성화하세요:

ALTER DATABASE <database_name> SET QUERY_STORE = ON;

그 다음 SQL Server Management Studio의 쿼리 저장소 보고서를 사용해 가장 느리고 자주 실행되는 쿼리를 찾아보세요. 쿼리 저장소를 이용한 성능 모니터링을 참조하세요.

Azure SQL Query Performance Insight

Azure SQL Database의 경우, Azure 포털의 쿼리 성능 인사이트가 설정 없이도 자원을 가장 많이 사용하는 쿼리를 자동으로 보여줍니다. 자세한 내용은 Azure SQL Database에 대한 Query Performance Insight를 참조하세요.

SET STATISTICS 일회성 조사

프로파일링하고 싶은 단일 쿼리는 통계 기능을 켜고 SQL Server Management Studio에서 실행하세요:

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

-- your query here

높은 논리 읽기는 거의 항상 인덱스가 누락되었거나 사용할 수 없음을 의미합니다. 높은 CPU 시간과 낮은 논리 읽기는 보통 나쁜 계획(매개변수 스니핑, 인덱스 사용을 방해하는 암묵적 변환, 병렬성을 방해하는 스칼라 함수)을 의미합니다.

운전자 수준 추적을 위한 확장 이벤트

드라이버가 SQL Server에 보내는 내용(보간하는 실제 매개변수 값 포함)을 정확히 확인하려면, and sql_batch_completed events를 사용하여 rpc_completed확장 이벤트 세션을 캡처하세요.

성능 체크리스트

이 체크리스트를 SQL Server에 연결하는 모든 PHP 애플리케이션의 사전 배포 검토로 활용하세요:

Area 확인 Reference
Connection 플랫폼에 대해 연결 풀링이 활성화되고 설정되어 있습니다 연결을 효율적으로 관리하세요
Connection 애플리케이션은 요청 내에서 연결을 재사용하며 쿼리마다 연결을 열지 않습니다 요청 내에서 연결을 재사용
Connection LoginTimeoutAzure SQL의 콜드 스타트와 페일오버를 다룹니다 로그인 타임아웃
쿼리 쿼리는 필요한 열만 선택합니다. SELECT * 사용하는 열만 선택하세요
쿼리 필터링은 PHP가 아니라 SQL에서 이루어집니다 array_filter 필요한 행만 가져와요
쿼리 큰 결과 집합은 다음과 같이 페이지 처리됩니다 OFFSET ... FETCH 큰 결과 집합을 페이지 설정하려 합니다
쿼리 저장된 프로시저 집합 SET NOCOUNT ON 선호 SET NOCOUNT ON
삽입 벌크 인서트는 각 행 루프가 아닌 테이블 값의 매개변수를 사용합니다 효율적으로 데이터를 삽입하세요
Statements PDO::ATTR_EMULATE_PREPARES이(가) false로 설정됩니다. 토종 조리제를 선호하세요
Statements 애플리케이션은 실행 전반에 걸쳐 준비된 문장을 재사용합니다 준비된 진술서 재사용
Cursors 애플리케이션은 버퍼링이 필요하지 않은 한 기본 순방향 전용 커서를 사용합니다 커서와 메모리 관리
Memory 큰 이진 및 문자 값은 구체화되지 않고 스트리밍됩니다 대형 이진 및 문자 값 스트리밍
타임아웃 모든 사용자 대상 쿼리에 대해 문장 타임아웃이 설정됩니다 명장 타임아웃
Routing 복제본이 존재하는 곳에 읽기 전용 워크로드가 설정 ApplicationIntent=ReadOnly 됩니다 읽기 전용 작업 경로
Observability 쿼리 저장소는 활성화되어 있으며 정기적으로 검토됩니다 쿼리 저장소