Ajuste de desempenho para os Drivers da Microsoft para PHP para o SQL Server

Baixar o Driver PHP

Este artigo aborda como escrever código PHP rápido contra SQL Server, Banco de Dados SQL do Azure, Instância Gerenciada de SQL do Azure, Azure Synapse Analytics e banco de dados SQL no Microsoft Fabric. A orientação se aplica tanto ao SQLSRV quanto ao PDO_SQLSRV, que envolvem o mesmo Microsoft ODBC Driver subjacente para SQL Server.

Comece pelas mudanças de maior impacto

Se você só puder fazer três mudanças, faça estas alterações:

  • Ative o pool de conexões. Estabelecer uma nova conexão TLS com o SQL Server leva dezenas a centenas de milissegundos, dependendo do caminho da rede e da negociação do TLS. Reutilizar conexões agrupadas elimina esse custo por requisição. Veja Gerenciar conexões de forma eficiente.
  • Busque apenas as colunas e linhas que você precisa. SELECT * e consultas sem limites são as causas mais comuns de endpoints lentos. Veja Consultar apenas o que você precisa.
  • Use parâmetros com valor de tabela para inserções em lote. Para centenas de linhas ou mais, os parâmetros com valor de tabela (TVPs) geralmente são muito mais rápidos do que as instruções INSERT linha por linha e escalam linearmente com o número de linhas. Veja Inserir dados de forma eficiente.

Gerencie conexões de forma eficiente

O estabelecimento de conexão é a operação mais cara que o motorista realiza. Quase toda investigação de desempenho do PHP termina com uma correção de gerenciamento de conexão.

Habilitar o pool de conexões

O pooling reutiliza conexões ODBC entre requisições PHP em vez de desmontá-las no final da requisição. O objeto de conexão é descartado quando seu script termina, mas o handle ODBC subjacente permanece ativo no pool do gerenciador de drivers ODBC e é reutilizado pela próxima solicitação que pede a mesma cadeia de conexão.

Windows: O pooling de conexões está ativado por padrão. Para confirmar, deixe essa ConnectionPooling opção fora do seu DSN. Para desativar o pooling para depuração, defina ConnectionPooling=0.

Linux e macOS: O pooling de conexões não é uma opção DSN nessas plataformas. Ative-o no gerenciador de drivers definindo Pooling=Yes na seção [ODBC] de odbcinst.ini e defina um valor positivo para CPTimeout na seção do driver. Por exemplo:

[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

Encontre o caminho real da biblioteca com odbcinst -q -d -n "ODBC Driver 18 for SQL Server" ou ls /opt/microsoft/msodbcsql18/lib64/. O nome do arquivo incorpora a versão instalada do driver ODBC e muda a cada lançamento.

CPTimeout (em segundos) controla quanto tempo as conexões ociosas ficam na piscina antes de serem fechadas. Defina-o em um valor alto o suficiente para que a maioria das solicitações encontre uma conexão do pool, mas baixo o suficiente para que conexões obsoletas com um servidor após failover sejam descartadas em um prazo razoável. 60 a 300 segundos funcionam bem para a maioria das cargas de trabalho web.

Para mais detalhes, consulte pool de conexões.

Entenda o custo da primeira consulta

O MARS (conjunto de resultados ativos múltiplos) está habilitado por padrão. Quando MARS e o pool de conexões estão ativos, o driver redefine a conexão do pool na primeira consulta, e essa redefinição ignora qualquer tempo limite da consulta que você definiu para essa primeira consulta. Consultas posteriores na mesma conexão respeitam normalmente o timeout. Se você definir timeouts agressivos para a primeira consulta em uma carga de trabalho em pool, leve esse comportamento em conta ou desative o MARS com MultipleActiveResultSets=false se você não precisar dele. Veja a nota sobre MARS e pooling em pooling de conexões.

Conexões PDO persistentes não são suportadas

PDO_SQLSRV rejeita PDO::ATTR_PERSISTENT. Defini-lo no construtor gera uma exceção:

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

Use pooling de conexões ODBC para reutilização entre solicitações. É o mecanismo nativo do driver, funciona tanto com PDO_SQLSRV quanto com SQLSRV e encerra conexões ociosas em CPTimeout, o que também garante a atualização correta do token do Microsoft Entra.

Reutilize a conexão dentro de uma solicitação

Mesmo com pooling, abrir uma nova conexão PDO ou SQLSRV implica uma ida e volta ODBC para buscar e validar um handle em pool. Abra uma conexão uma vez por requisição e passe-a para todas as funções que precisem dela.

Dica

Um contêiner de injeção de dependência ou um acessor preguiçoso já é suficiente. O objetivo é evitar new PDO(...) no meio de um gerenciador de requisições.

Consulte apenas o que você precisa

As idas e voltas pela rede e a materialização de conjuntos de resultados dominam a latência das consultas na maioria das cargas de trabalho em PHP. As correções são as mesmas que se aplicam a todas as camadas de acesso a banco de dados.

Selecione apenas as colunas que você usa

SELECT * recupera todas as colunas, incluindo colunas varchar(max) e varbinary(max), que são muito maiores do que os dados que você realmente consome. Diga as colunas:

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

Busque apenas as fileiras que você precisa

Envie a filtragem para o SQL Server. Nunca carregue a tabela inteira no PHP só para filtrá-la em um loop 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);

Paginar grandes conjuntos de resultados

Para uma exibição em lista que mostra algumas centenas de linhas de milhões, não retorne todas as linhas e deixe o cliente ordená-las. Use paginação no servidor com 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);
}

Escolha o método certo de busca

  • Use fetch(PDO::FETCH_ASSOC) em loop para iteração em streaming quando não precisar de todas as linhas na memória ao mesmo tempo.
  • fetchAll(PDO::FETCH_ASSOC) Use quando o chamador realmente precisar de todo o conjunto (por exemplo, renderizando uma resposta JSON completa).
  • Use fetchColumn() quando você só se importa com um único escalar (um COUNT, SUM, ou MAX).
  • Use PDO::FETCH_KEY_PAIR ou PDO::FETCH_UNIQUE para criar dicionários de busca sem uma segunda passada.

Modos numéricos de busca (PDO::FETCH_NUM) são marginalmente mais rápidos que modos associativos porque pulam a construção do mapa de nomes de coluna. Prefira clareza; só mude quando um perfilador indicar que a sobrecarga da busca é significativa.

Preferência SET NOCOUNT ON em procedimentos armazenados e lotes

Cada instrução INSERT, UPDATE e DELETE retorna um token DONE_IN_PROC com o número de linhas afetadas, que o PHP normalmente descarta. O token não adiciona uma ida e volta pela rede, mas cada um ainda consome bytes transmitidos pela rede e um pouco de processamento do driver. Em um procedimento com várias instruções ou lote que executa centenas de instruções por chamada, essas economias se acumulam. Desligue:

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;

Inserir dados de forma eficiente

Escolha o método certo de inserção com base em quantas fileiras você está movendo. A escolha errada pode ser 100 vezes mais lenta.

Menos de cerca de 100 linhas: declaração preparada em um loop

Para pequenos lotes, execute uma única instrução preparada em um loop:

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

Envolva o loop em uma transação para que todos os inserts sejam comprometidos como uma unidade única e o log não precise ser esvaziado após cada linha:

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

Centenas a milhões de linhas: parâmetros com valores de tabela

Parâmetros de tabela (TVPs) enviam todo o lote para o SQL Server em uma única ida e volta e permitem que o SQL Server processe o conjunto como uma única instrução. Para lotes de centenas de linhas ou mais, os TVPs são tipicamente muito mais rápidos que um loop de declaração preparada e escalam linearmente com a contagem de linhas.

Primeiro, crie um tipo de tabela no servidor:

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

PDO_SQLSRV passa o TVP como um array associativo cuja chave é o nome do tipo e cujo valor é o conjunto de linhas. Vincule-o com 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();

Para um esquema não padrão, passe o esquema como o próximo elemento do array: ["ProductTableType" => $rows, "Sales"]. Para exemplos de sintaxe procedural SQLSRV e procedimentos armazenados, veja Usar parâmetros de valores de tabela.

Milhões de linhas: bcp ou BULK INSERT

Para operações realmente em massa (cargas de data warehouse, migrações iniciais), use BCP ou BULK INSERT em vez de PHP. Escreva seus dados em um arquivo delimitado ou de formato nativo, depois execute o bcp ou BULK INSERT a partir de um job agendado, etapa ETL ou script de administração.

Caution

Se você chamar o bcp a partir de PHP com shell_exec() ou proc_open(), nunca interpole dados não confiáveis na linha de comando. Use escapeshellarg() em todos os argumentos, e prefira executar a carga fora de banda em vez de em um caminho de requisição web.

Reduzir viagens de ida e volta

Cada ida e volta de rede entre PHP e SQL Server tem um custo fixo. Quando você envia cinco extratos em um lote, paga esse custo uma vez em vez de cinco vezes.

Para operações relacionadas que são executadas juntas, coloque as instruções em um único lote e consuma todos os conjuntos de resultados:

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

Para SQLSRV, use sqlsrv_next_result para avançar entre conjuntos de resultados.

Ative múltiplos conjuntos de resultados ativos quando precisar

Múltiplos Conjuntos de Resultados Ativos (MARS) permitem que uma única conexão tenha múltiplas instruções ativas. Sem o MARS, você não pode emitir uma nova consulta em uma conexão que ainda tem um conjunto de resultados aberto. Ambos os drivers ativam o MARS por padrão. Para desativá-lo, defina MultipleActiveResultSets=false na sua cadeia de conexão. Veja Desabilitar Múltiplos Conjuntos de Resultados Ativos (MARS).

O MARS é conveniente, mas não gratuito. Cada conjunto de resultados ativo consome recursos do lado do servidor. Prefira consumir um conjunto de resultados completo antes de começar outro. Use o MARS para desbloquear padrões de cursor genuinamente aninhados.

Ajuste declarações preparadas

Instruções preparadas evitam que o driver precise analisar novamente o SQL no servidor e permitem associar com segurança dados de entrada não confiáveis como parâmetros.

Prefira preparações nativas

PDO_SQLSRV pode preparar instruções em dois modos. As instruções preparadas nativas enviam o texto SQL para o servidor uma vez e reutilizam a instrução já analisada em cada execução, enviando apenas os valores dos parâmetros em cada execute(). Os prepares emulados mantêm o texto SQL no cliente e reconstruem uma string SQL completa com parâmetros interpolados em cada execução.

Configure PDO::ATTR_EMULATE_PREPARES => false para que o driver use instruções preparadas nativas. Preparações nativas permitem que o SQL Server armazene em cache e reutilize o plano de consulta, e evitam re-analisar o texto SQL em cada execução.

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

Reutilizar declarações preparadas

Prepare uma vez, execute muitas vezes. Cada chamada para prepare() envolve uma alocação de manipulador ODBC e um processamento sintático no servidor. Em um circuito quente, mantenha o $stmt objeto vivo e chame execute() dentro do loop:

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

Cuidado com TOP (?) e IN (?, ?, ...)

TOPrequer parênteses ao redor de um marcador de parâmetro, SELECT TOP (?) ..., para que o SQL Server possa analisar a contagem de linhas como um parâmetro. IN (?, ?, ?, ?) exige um número fixo de placeholders no momento da preparação. Para tamanhos dinâmicos IN de lista, construa a cadeia provisória a partir de uma contagem de inteiros validada, ou passe a lista como um parâmetro com valor em tabela.

Caution

Nunca interpole a entrada bruta do usuário no texto SQL (incluindo a contagem de placeholder). Faça a contagem com (int) antes de construir a string provisória e sempre passe os valores execute() reais como parâmetros.

Gerencie cursores e memória

O tipo padrão de cursor é PDO::CURSOR_FWDONLY, uma mangueira de incêndio apenas para frente. Ele transmite linhas para PHP uma de cada vez e não faz buffer, então um grande conjunto de resultados é limitado pela memória de buffer de linha em vez da contagem total de linhas. Geralmente é isso que você quer.

Use cursores com buffer apenas quando precisar recuar ou contar linhas

PDO::SQLSRV_CURSOR_BUFFERED (um cursor estático armazenado em buffer no lado do cliente) carrega todo o conjunto de resultados na memória do PHP antecipadamente. Essa abordagem permite que você chame rowCount(), retroceda e reutilize a instrução. Por padrão, o buffer é limitado a 10.240 KB (10 MB) por meio de PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, e uma consulta cujo conjunto de resultados excede esse limite retorna false em vez de causar estouro da memória do PHP. Você pode elevar o limite até o limite de memória do PHP, mas, ao fazer isso, você troca um retorno false por um erro fatal Allowed memory size exhausted real quando uma consulta excede o novo limite. Ajuste com cuidado. Veja Tipos de cursor (PDO_SQLSRV).

Cursores roláveis do lado do servidor (PDO::SQLSRV_CURSOR_STATIC, PDO::SQLSRV_CURSOR_DYNAMIC, PDO::SQLSRV_CURSOR_KEYSET) mantêm um buffer no servidor em vez de no cliente, portanto não consomem memória do PHP. No entanto, eles mantêm recursos no lado do servidor durante toda a vida útil do cursor e são mais lentos por linha do que os cursores somente de avanço.

Use o padrão "somente para avanço" para leituras por streaming. Use o lado do cliente com buffer para conjuntos de resultados pequenos quando precisar rowCount() ou rolagem para trás. Evite cursores roláveis do lado do servidor, a menos que você esteja fazendo algo específico.

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

Para uma análise completa, veja Tipos de cursor (PDO_SQLSRV) e Tipos de cursor (SQLSRV).

Fluxar valores binários grandes e de caracteres

Para varbinary(max), varchar(max), nvarchar(max), xml e outros tipos grandes, use fluxos PHP em vez de materializar o valor inteiro na memória:

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

Para inserir ou atualizar com valores grandes, use SendStreamParamsAtExec=false no SQLSRV para enviar dados de fluxo em blocos após sqlsrv_execute(). Para detalhes, veja Enviar dados como um fluxo.

Definir tempos limite apropriados

Timeouts são tanto parâmetros de desempenho quanto de confiabilidade. Consultas de longa duração mantêm conexões do pool ocupadas e prejudicam outras solicitações.

Tempo limite da instrução

Defina um limite de tempo por instrução para que uma consulta fora de controle não ocupe uma conexão do pool indefinidamente. Para PDO_SQLSRV:

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

Para SQLSRV, passe "QueryTimeout" => 30 no array de opções para sqlsrv_query ou sqlsrv_prepare.

Defina um valor que combine com sua carga de trabalho. Para uma requisição web síncrona, de 15 a 30 segundos é o normal. Para um trabalho em lote de fundo, alguns minutos podem ser razoáveis. Nunca defina o timeout para zero (ilimitado) em uma requisição web.

Tempo limite do logon

LoginTimeout na string de conexão controla por quanto tempo o driver espera para estabelecer uma conexão. Defina um valor explícito ao conectar ao Banco de Dados SQL do Azure ou Instância Gerenciada de SQL do Azure para que cold starts e failovers em grupos de failover não travem o cliente indefinidamente. Valores de 30 a 90 segundos funcionam bem para a maioria das cargas de trabalho em nuvem. Para detalhes sobre dimensionamento LoginTimeout contra ConnectRetryCount * ConnectRetryInterval e os modos de falha resultantes, veja Tempo de espera da conexão. Para a referência de opções, veja Opções de conexão.

Encaminhar cargas de trabalho de somente leitura para uma réplica

Para consultas somente de leitura para um banco de dados em um grupo de disponibilidade Always On, Instância Gerenciada de SQL do Azure ou Banco de Dados SQL do Azure com expansão de leitura ou geo-réplica, adicione ApplicationIntent=ReadOnly à sua cadeia de conexão:

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

O roteamento somente-leitura envia a conexão para uma réplica secundária sincronizada, reduzindo a carga da réplica primária. Combine com o MultiSubnetFailover=true para a conexão mais rápida aos ouvintes do grupo de disponibilidade em várias sub-redes.

Observe o desempenho do servidor

A temporização no lado do cliente apenas informa quanto tempo uma consulta levou de ponta a ponta. Para descobrir por que estava lento, use os diagnósticos integrados do SQL Server.

Repositório de Consultas

A Repositório de Consultas captura planos de execução, estatísticas de execução e estatísticas de espera para cada consulta no banco de dados. Ele está ativado por padrão no Banco de Dados SQL do Azure, Instância Gerenciada de SQL do Azure e SQL Database no Fabric. No SQL Server, ative-o por banco de dados:

ALTER DATABASE <database_name> SET QUERY_STORE = ON;

Depois, use os relatórios Repositório de Consultas do SQL Server Management Studio para encontrar suas consultas mais lentas e frequentemente executadas. Veja Monitorando o desempenho com a Repositório de Consultas.

Insights de desempenho de consultas do SQL do Azure

Para o Banco de Dados SQL do Azure, o Query Performance Insight do portal Azure mostra automaticamente as consultas que mais consomem recursos sem qualquer configuração. Para obter mais informações, confira Análise de Desempenho de Consultas para o Banco de Dados SQL do Azure.

SET STATISTICS para investigação única

Para uma única consulta que você deseja perfilar, execute-a no SQL Server Management Studio com estatísticas ativadas:

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

-- your query here

Leituras lógicas altas quase sempre significam um índice ausente ou inutilizável. Alto tempo de CPU com baixo número de leituras lógicas geralmente significa um plano de execução ruim (sniffing de parâmetros, uma conversão implícita que impede o uso de índices ou uma função escalar que impede o paralelismo).

Eventos Estendidos para rastreamento em nível de motorista

Para ver exatamente o que o driver envia para o SQL Server (incluindo os valores reais dos parâmetros que ele interpola), capture uma sessão de Eventos Estendidos usando os eventos rpc_completed e sql_batch_completed.

Lista de verificação de desempenho

Use esta lista de verificação como uma revisão pré-implantação de qualquer aplicativo PHP que se conecte ao SQL Server:

Area Verificação Reference
Connection O pool de conexões está habilitado e configurado para a plataforma Gerencie conexões de forma eficiente
Connection O aplicativo reutiliza conexões dentro de uma requisição e não abre conexões por consulta Reutilize a conexão dentro de uma solicitação
Connection LoginTimeout abrange inicializações a frio e failover para o SQL do Azure Tempo de encerramento do login
Consulta As consultas selecionam apenas as colunas necessárias, não SELECT * Selecione apenas as colunas que você usa
Consulta Filtragem acontece em SQL, não em PHP com array_filter Busque apenas as fileiras que você precisa
Consulta Grandes conjuntos de resultados são paginados com OFFSET ... FETCH Paginar grandes conjuntos de resultados
Consulta Conjunto de procedimentos armazenados SET NOCOUNT ON Preferir SET NOCOUNT ON
Inserções Inserções em lote usam parâmetros com valor de tabela, não loops linha por linha Inserir dados de forma eficiente
Statements PDO::ATTR_EMULATE_PREPARES é definido como false. Prefira preparações nativas
Statements A aplicação reutiliza instruções preparadas entre execuções Reutilizar declarações preparadas
Cursors O aplicativo usa o cursor padrão somente de avanço, a menos que seja necessário armazenamento em buffer. Gerencie cursores e memória
Memory Grandes valores binários e de caracteres são transmitidos por streaming, não materializados Transmitir valores binários grandes e valores de caractere
Tempo de espera O tempo limite da instrução está configurado para todas as consultas voltadas ao usuário Tempo limite da instrução
Routing Cargas de trabalho somente de leitura definidas para ApplicationIntent=ReadOnly, onde existe uma réplica Encaminhar cargas de trabalho somente leitura
Observability A Repositório de Consultas é ativada e revisada regularmente Repositório de Consultas