SQL Server heeft een op kosten gebaseerd optimalisatieprogramma dat kennis gebruikt over de verschillende tabellen die bij een query betrokken zijn om te produceren wat volgens hem het meest optimale plan is in de beschikbare tijd tijdens het compileren. Deze kennis omvat alle indexen en hun grootte en welke kolomstatistieken er ook zijn. Een deel van wat nodig is om het optimale queryplan te vinden, is proberen het aantal fysieke leesbewerkingen dat nodig is tijdens de uitvoering van het plan te minimaliseren.
Een ding dat mij een paar keer is gevraagd, is waarom de optimizer geen rekening houdt met wat zich in de SQL Server-bufferpool bevindt bij het samenstellen van een queryplan, want dat zou een query zeker sneller kunnen laten uitvoeren. In dit bericht leg ik uit waarom.
De inhoud van de bufferpool achterhalen
De eerste reden waarom de optimizer de bufferpool negeert, is dat het een niet-triviaal probleem is om erachter te komen wat zich in de bufferpool bevindt vanwege de manier waarop de bufferpool is georganiseerd. Pagina's met gegevensbestanden worden in de bufferpool beheerd door kleine gegevensstructuren, buffers genaamd, die zaken volgen als (niet-uitputtende lijst):
- De ID van de pagina (bestandsnummer:paginanummer-in-bestand)
- De laatste keer dat naar de pagina werd verwezen (gebruikt door de luie schrijver om het minst recent gebruikte algoritme te helpen implementeren dat indien nodig vrije ruimte creëert)
- De geheugenlocatie van de 8KB-pagina in de bufferpool
- Of de pagina nu vuil is of niet (een vuile pagina bevat wijzigingen die nog niet zijn teruggeschreven naar duurzame opslag)
- De toewijzingseenheid waartoe de pagina behoort (hier uitgelegd) en de toewijzingseenheid-ID kan worden gebruikt om erachter te komen van welke tabel en index de pagina deel uitmaakt
Voor elke database die pagina's in de bufferpool heeft, is er een hashlijst van pagina's, in pagina-ID-volgorde, die snel kan worden doorzocht om te bepalen of een pagina al in het geheugen staat of dat er een fysieke lezing moet worden uitgevoerd. Niets stelt SQL Server echter gemakkelijk in staat om te bepalen welk percentage van het bladniveau voor elke index van een tabel zich al in het geheugen bevindt. Code zou de hele lijst met buffers voor de database moeten scannen, op zoek naar buffers die pagina's toewijzen voor de betreffende toewijzingseenheid. En hoe meer pagina's in het geheugen voor een database, hoe langer de scan zou duren. Het zou onbetaalbaar zijn om te doen als onderdeel van het compileren van zoekopdrachten.
Als je geïnteresseerd bent, heb ik een tijdje geleden een bericht geschreven met wat T-SQL-code die de bufferpool scant en enkele statistieken geeft, met behulp van de DMV sys.dm_os_buffer_descriptors .
Waarom het gebruik van de inhoud van een bufferpool gevaarlijk zou zijn
Laten we doen alsof er *een zeer efficiënt mechanisme is om de inhoud van de bufferpool te bepalen die de optimizer kan gebruiken om te helpen kiezen welke index moet worden gebruikt in een queryplan. De hypothese die ik ga onderzoeken is dat als de optimizer genoeg weet van een minder efficiënte (grotere) index die zich al in het geheugen bevindt, vergeleken met de meest efficiënte (kleinere) index die moet worden gebruikt, hij de index in het geheugen moet kiezen omdat hij verminder het aantal fysieke leesbewerkingen dat nodig is en de query wordt sneller uitgevoerd.
Het scenario dat ik ga gebruiken is als volgt:een tabel BigTable heeft twee niet-geclusterde indexen, Index_A en Index_B, die beide een bepaalde query volledig dekken. De query vereist een volledige scan van het bladniveau van de index om de queryresultaten op te halen. De tabel heeft 1 miljoen rijen. Index_A heeft 200.000 pagina's op leaf-niveau en Index_B heeft 1 miljoen pagina's op leaf-niveau, dus een volledige scan van Index_B vereist vijf keer meer pagina's verwerken.
Ik heb dit gekunstelde voorbeeld gemaakt op een laptop met SQL Server 2019 met 8 processorkernen, 32 GB geheugen en solid-state schijven. De code is als volgt:
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
En toen timede ik de gekunstelde vragen:
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
U kunt zien wanneer geen van beide indexen in het geheugen is, Index_A is gemakkelijk de meest efficiënte index om te gebruiken, met een verstreken querytijd van 764 ms tegen 2761 ms bij gebruik van Index_B, en hetzelfde geldt wanneer beide indexen in het geheugen staan. Als Index_B zich echter in het geheugen bevindt en Index_A niet, wordt de query sneller uitgevoerd als Index_B (149 ms) wordt gebruikt dan wanneer Index_A (764 ms) wordt gebruikt.
Laten we de optimizer nu toestaan om de keuze van het plan te baseren op wat zich in de bufferpool bevindt...
Als Index_A zich meestal niet in het geheugen bevindt en Index_B meestal in het geheugen, zou het efficiënter zijn om het queryplan te compileren om Index_B te gebruiken voor een query die op dat moment wordt uitgevoerd. Hoewel Index_B groter is en meer CPU-cycli nodig zou hebben om doorheen te scannen, zijn fysieke leesbewerkingen veel langzamer dan de extra CPU-cycli, dus een efficiënter queryplan minimaliseert het aantal fysieke leesbewerkingen.
Dit argument gaat alleen op, en een "use Index_B"-queryplan is alleen efficiënter dan een "use Index_A"-queryplan, als Index_B grotendeels in het geheugen blijft en Index_A meestal niet in het geheugen. Zodra het grootste deel van Index_A in het geheugen zit, zou het "use Index_A"-queryplan efficiënter zijn en is het "use Index_B"-queryplan de verkeerde keuze.
De situaties waarin het samengestelde plan "Gebruik Index_B" minder efficiënt is dan het op kosten gebaseerde plan "Gebruik Index_A" zijn (algemeen):
- Index_A en Index_B zijn beide in het geheugen:het gecompileerde plan duurt bijna drie keer langer
- Geen van beide indexen is geheugenresident:het gecompileerde plan duurt 3,5 keer langer
- Index_A is geheugenresident en Index_B niet:alle fysieke leesbewerkingen die door het plan worden uitgevoerd, zijn vreemd, EN het duurt maar liefst 53 keer langer
Samenvatting
Hoewel in onze denkoefening de optimizer bufferpoolkennis kan gebruiken om de meest efficiënte query op een enkel moment te compileren, zou het een gevaarlijke manier zijn om de compilatie van plannen te stimuleren vanwege de potentiële volatiliteit van de inhoud van de bufferpool, waardoor de toekomstige efficiëntie van het in de cache opgeslagen plan is zeer onbetrouwbaar.
Onthoud dat het de taak van de optimizer is om snel een goed plan te vinden, niet noodzakelijk het beste plan voor 100% van alle situaties. Naar mijn mening doet de SQL Server-optimizer het juiste door de feitelijke inhoud van de SQL Server-bufferpool te negeren en vertrouwt hij in plaats daarvan op de verschillende kostenregels om een queryplan te produceren dat waarschijnlijk het meest efficiënt is. .