O SQL Server tem um otimizador baseado em custo que usa o conhecimento sobre as várias tabelas envolvidas em uma consulta para produzir o que ele decide ser o plano mais ideal no tempo disponível durante a compilação. Esse conhecimento inclui quaisquer índices existentes e seus tamanhos e quaisquer estatísticas de coluna existentes. Parte do que é necessário para encontrar o plano de consulta ideal é tentar minimizar o número de leituras físicas necessárias durante a execução do plano.
Uma coisa que me perguntaram algumas vezes é por que o otimizador não considera o que está no pool de buffers do SQL Server ao compilar um plano de consulta, pois certamente isso poderia fazer uma consulta ser executada mais rapidamente. Neste post, explico o porquê.
Descobrindo o conteúdo do pool de buffers
A primeira razão pela qual o otimizador ignora o buffer pool é que é um problema não trivial descobrir o que está no buffer pool devido à forma como o buffer pool está organizado. As páginas do arquivo de dados são controladas no buffer pool por pequenas estruturas de dados chamadas buffers, que rastreiam coisas como (lista não exaustiva):
- O ID da página (número do arquivo:page-number-in-file)
- A última vez que a página foi referenciada (usada pelo escritor preguiçoso para ajudar a implementar o algoritmo usado menos recentemente que cria espaço livre quando necessário)
- O local de memória da página de 8 KB no buffer pool
- Se a página está suja ou não (uma página suja tem alterações que ainda não foram gravadas no armazenamento durável)
- A unidade de alocação à qual a página pertence (explicada aqui) e o ID da unidade de alocação podem ser usados para descobrir de qual tabela e índice a página faz parte
Para cada banco de dados que possui páginas no buffer pool, há uma lista de hash de páginas, na ordem do ID da página, que pode ser pesquisada rapidamente para determinar se uma página já está na memória ou se uma leitura física deve ser executada. No entanto, nada permite que o SQL Server determine facilmente qual porcentagem do nível folha para cada índice de uma tabela já está na memória. O código teria que varrer toda a lista de buffers do banco de dados, procurando por buffers que mapeiam páginas para a unidade de alocação em questão. E quanto mais páginas na memória para um banco de dados, mais demoraria a verificação. Seria proibitivamente caro fazer parte da compilação de consultas.
Se você estiver interessado, escrevi um post um tempo atrás com um código T-SQL que verifica o pool de buffers e fornece algumas métricas, usando o DMV sys.dm_os_buffer_descriptors .
Por que usar o conteúdo do buffer pool seria perigoso
Vamos fingir que *existe* um mecanismo altamente eficiente para determinar o conteúdo do buffer pool que o otimizador pode usar para ajudá-lo a escolher qual índice usar em um plano de consulta. A hipótese que vou explorar é se o otimizador sabe que um índice menos eficiente (maior) já está na memória, comparado ao índice mais eficiente (menor) a ser usado, ele deve escolher o índice na memória porque ele reduza o número de leituras físicas necessárias e a consulta será executada mais rapidamente.
O cenário que vou usar é o seguinte:uma tabela BigTable possui dois índices não clusterizados, Index_A e Index_B, ambos cobrindo completamente uma determinada consulta. A consulta requer uma verificação completa do nível folha do índice para recuperar os resultados da consulta. A tabela tem 1 milhão de linhas. Index_A tem 200.000 páginas em seu nível de folha e Index_B tem 1 milhão de páginas em seu nível de folha, portanto, uma varredura completa de Index_B requer o processamento de cinco vezes mais páginas.
Criei este exemplo artificial em um laptop executando o SQL Server 2019 com 8 núcleos de processador, 32 GB de memória e discos de estado sólido. O código é o seguinte:
CREATE TABLE BigTable ( c1 BIGINT IDENTITY, c2 AS (c1 * 2), c3 CHAR (1500) DEFAULT 'a', c4 CHAR (5000) DEFAULT 'b'); GO INSERT INTO BigTable DEFAULT VALUES; GO 1000000 CREATE NONCLUSTERED INDEX Index_A ON BigTable (c2) INCLUDE (c3);-- 5 registros por página =200.000 páginasGO CREATE NONCLUSTERED INDEX Index_B ON BigTable (c2) INCLUDE (c4);-- 1 registro por página =1 milhão de páginasGO CHECKPOINT;GO
E então eu cronometrei as consultas artificiais:
DBCC DROPCLEANBUFFERS;GO -- Index_A não está na memóriaSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- tempo de CPU =796 ms, tempo decorrido =764 ms -- Index_A na memóriaSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- tempo de CPU =312 ms, tempo decorrido =52 ms DBCC DROPCLEANBUFFERS;GO -- Index_B não está na memóriaSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO- - Tempo de CPU =2952 ms, tempo decorrido =2761 ms -- Index_B in memorySELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO-- Tempo de CPU =1219 ms, tempo decorrido =149 ms
Você pode ver quando nenhum índice está na memória, Index_A é facilmente o índice mais eficiente a ser usado, com um tempo de consulta decorrido de 764ms contra 2.761ms usando Index_B, e o mesmo acontece quando ambos os índices estão na memória. No entanto, se Index_B estiver na memória e Index_A não estiver, se a consulta usar Index_B (149ms), ela será executada mais rapidamente do que se usar Index_A (764ms).
Agora vamos permitir que o otimizador baseie a escolha do plano no que está no buffer pool…
Se Index_A não estiver na memória e Index_B estiver principalmente na memória, seria mais eficiente compilar o plano de consulta para usar Index_B, para uma consulta em execução naquele instante. Embora Index_B seja maior e precise de mais ciclos de CPU para varrer, as leituras físicas são muito mais lentas do que os ciclos extras de CPU, portanto, um plano de consulta mais eficiente minimiza o número de leituras físicas.
Esse argumento só é válido e um plano de consulta “use Index_B” é apenas mais eficiente do que um plano de consulta “use Index_A”, se Index_B permanecer principalmente na memória e Index_A permanecer principalmente fora da memória. Assim que a maior parte do Index_A estiver na memória, o plano de consulta “use Index_A” será mais eficiente e o plano de consulta “use Index_B” será a escolha errada.
As situações em que o plano compilado “use Index_B” é menos eficiente do que o plano “use Index_A” baseado em custo são (generalizando):
- Index_A e Index_B estão ambos na memória:o plano compilado levará quase três vezes mais
- Nenhum índice é residente na memória:o plano compilado demora 3,5 vezes mais
- Index_A é residente na memória e Index_B não:todas as leituras físicas realizadas pelo plano são irrelevantes, E levará 53 vezes mais
Resumo
Embora em nosso exercício de pensamento, o otimizador possa usar o conhecimento do buffer pool para compilar a consulta mais eficiente em um único instante, seria uma maneira perigosa de conduzir a compilação do plano devido à potencial volatilidade do conteúdo do buffer pool, tornando a eficiência futura do o plano em cache altamente não confiável.
Lembre-se, o trabalho do otimizador é encontrar um bom plano rapidamente, não necessariamente o melhor plano para 100% de todas as situações. Na minha opinião, o otimizador do SQL Server faz a coisa certa ao ignorar o conteúdo real do pool de buffers do SQL Server e, em vez disso, confia nas várias regras de custo para produzir um plano de consulta que provavelmente será o mais eficiente na maioria das vezes .