SQL Server dispone di un ottimizzatore basato sui costi che utilizza la conoscenza delle varie tabelle coinvolte in una query per produrre quello che ritiene essere il piano più ottimale nel tempo a disposizione durante la compilazione. Questa conoscenza include tutti gli indici esistenti, le loro dimensioni e qualsiasi statistica di colonna esistente. Parte di ciò che serve per trovare il piano di query ottimale è cercare di ridurre al minimo il numero di letture fisiche necessarie durante l'esecuzione del piano.
Una cosa che mi è stata chiesta alcune volte è perché l'ottimizzatore non considera cosa c'è nel pool di buffer di SQL Server durante la compilazione di un piano di query, poiché sicuramente ciò potrebbe velocizzare l'esecuzione di una query. In questo post ti spiego perché.
Scoprire i contenuti del pool di buffer
Il primo motivo per cui l'ottimizzatore ignora il pool di buffer è che è un problema non banale capire cosa c'è nel pool di buffer a causa del modo in cui è organizzato il pool di buffer. Le pagine dei file di dati sono controllate nel pool di buffer da piccole strutture di dati chiamate buffer, che tengono traccia di cose come (elenco non esaustivo):
- L'ID della pagina (numero file:page-number-in-file)
- L'ultima volta che è stato fatto riferimento alla pagina (usata dallo scrittore pigro per aiutare a implementare l'algoritmo utilizzato meno di recente che crea spazio libero quando necessario)
- La posizione di memoria della pagina da 8 KB nel pool di buffer
- Se la pagina è sporca o meno (una pagina sporca contiene modifiche che non sono state ancora riscritte nella memoria durevole)
- L'unità di allocazione a cui appartiene la pagina (spiegata qui) e l'ID dell'unità di allocazione possono essere utilizzati per capire a quale tabella e indice fa parte la pagina
Per ogni database che ha pagine nel pool di buffer, c'è un elenco hash di pagine, in ordine di ID pagina, che è rapidamente ricercabile per determinare se una pagina è già in memoria o se deve essere eseguita una lettura fisica. Tuttavia, nulla consente facilmente a SQL Server di determinare quale percentuale del livello foglia per ogni indice di una tabella è già in memoria. Il codice dovrebbe scansionare l'intero elenco di buffer per il database, alla ricerca di buffer che mappano le pagine per l'unità di allocazione in questione. E più pagine in memoria per un database, più tempo impiegherebbe la scansione. Sarebbe proibitivo farlo come parte della compilazione di query.
Se sei interessato, ho scritto un post qualche tempo fa con del codice T-SQL che scansiona il pool di buffer e fornisce alcune metriche, usando il DMV sys.dm_os_buffer_descriptors .
Perché l'utilizzo dei contenuti del pool di buffer sarebbe pericoloso
Facciamo finta che *esiste* un meccanismo altamente efficiente per determinare i contenuti del pool di buffer che l'ottimizzatore può utilizzare per aiutarlo a scegliere quale indice utilizzare in un piano di query. L'ipotesi che esplorerò è che se l'ottimizzatore sa abbastanza di un indice meno efficiente (più grande) è già in memoria, rispetto all'indice più efficiente (più piccolo) da usare, dovrebbe scegliere l'indice in memoria perché lo farà riduci il numero di letture fisiche richieste e la query verrà eseguita più velocemente.
Lo scenario che userò è il seguente:una tabella BigTable ha due indici non cluster, Index_A e Index_B, che coprono entrambi completamente una query particolare. La query richiede un'analisi completa del livello foglia dell'indice per recuperare i risultati della query. La tabella ha 1 milione di righe. Index_A ha 200.000 pagine a livello foglia e Index_B ha 1 milione di pagine a livello foglia, quindi una scansione completa di Index_B richiede l'elaborazione di cinque volte più pagine.
Ho creato questo esempio inventato su un laptop che esegue SQL Server 2019 con 8 core di processore, 32 GB di memoria e dischi a stato solido. Il codice è il seguente:
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 records per page = 200,000 pages GO CREATE NONCLUSTERED INDEX Index_B ON BigTable (c2) INCLUDE (c4); -- 1 record per page = 1 million pages GO CHECKPOINT; GO
E poi ho cronometrato le query forzate:
DBCC DROPCLEANBUFFERS; GO -- Index_A not in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A)); GO -- CPU time = 796 ms, elapsed time = 764 ms -- Index_A in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A)); GO -- CPU time = 312 ms, elapsed time = 52 ms DBCC DROPCLEANBUFFERS; GO -- Index_B not in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B)); GO -- CPU time = 2952 ms, elapsed time = 2761 ms -- Index_B in memory SELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B)); GO -- CPU time = 1219 ms, elapsed time = 149 ms
Puoi vedere quando nessuno dei due indici è in memoria, Index_A è facilmente l'indice più efficiente da usare, con un tempo di query trascorso di 764 ms contro 2.761 ms usando Index_B, e lo stesso vale quando entrambi gli indici sono in memoria. Tuttavia, se Index_B è in memoria e Index_A non lo è, se la query utilizza Index_B (149 ms) verrà eseguita più velocemente rispetto a se utilizza Index_A (764 ms).
Ora consentiamo all'ottimizzatore di basare la scelta del piano su ciò che è nel pool di buffer...
Se Index_A non è per lo più in memoria e Index_B è principalmente in memoria, sarebbe più efficiente compilare il piano di query per utilizzare Index_B, per una query in esecuzione in quell'istante. Anche se Index_B è più grande e richiederebbe più cicli della CPU per la scansione, le letture fisiche sono molto più lente rispetto ai cicli aggiuntivi della CPU, quindi un piano di query più efficiente riduce al minimo il numero di letture fisiche.
Questo argomento vale solo e un piano di query "usa Index_B" è solo più efficiente di un piano di query "usa Index_A", se Index_B rimane principalmente in memoria e Index_A rimane per lo più non in memoria. Non appena la maggior parte di Index_A è in memoria, il piano di query "usa Index_A" sarebbe più efficiente e il piano di query "usa Index_B" è la scelta sbagliata.
Le situazioni in cui il piano "usa l'indice_B" compilato è meno efficiente del piano "usa l'indice_A" basato sui costi sono (generalizzando):
- L'indice_A e l'indice_B sono entrambi in memoria:il piano compilato impiegherà quasi tre volte più tempo
- Nessuno dei due indici è residente in memoria:il piano compilato richiede più di 3,5 volte
- Index_A è residente in memoria e Index_B no:tutte le letture fisiche eseguite dal piano sono estranee E ci vorrà ben 53 volte più tempo
Riepilogo
Sebbene nel nostro esercizio di riflessione, l'ottimizzatore possa utilizzare la conoscenza del pool di buffer per compilare la query più efficiente in un solo istante, sarebbe un modo pericoloso per guidare la compilazione del piano a causa della potenziale volatilità dei contenuti del pool di buffer, rendendo l'efficienza futura di il piano memorizzato nella cache è altamente inaffidabile.
Ricorda, il compito dell'ottimizzatore è trovare rapidamente un buon piano, non necessariamente il miglior piano per il 100% di tutte le situazioni. A mio parere, l'ottimizzatore di SQL Server fa la cosa giusta ignorando il contenuto effettivo del pool di buffer di SQL Server e si basa invece sulle varie regole di determinazione dei costi per produrre un piano di query che probabilmente sarà il più efficiente per la maggior parte del tempo .