SQL Server har en omkostningsbaseret optimering, der bruger viden om de forskellige tabeller, der er involveret i en forespørgsel, til at producere, hvad den beslutter er den mest optimale plan i den tid, den har til rådighed under kompileringen. Denne viden omfatter uanset hvilke indekser der findes og deres størrelser og hvilken kolonnestatistik der findes. En del af det, der skal bruges til at finde den optimale forespørgselsplan, er at forsøge at minimere antallet af fysiske læsninger, der er nødvendige under planens udførelse.
En ting, jeg er blevet spurgt et par gange om, er, hvorfor optimeringsværktøjet ikke overvejer, hvad der er i SQL Server-bufferpuljen, når der kompileres en forespørgselsplan, da det helt sikkert kunne få en forespørgsel til at køre hurtigere. I dette indlæg vil jeg forklare hvorfor.
Udregning af bufferpuljens indhold
Den første grund til, at optimeringsværktøjet ignorerer bufferpuljen, er, at det er et ikke-trivielt problem at finde ud af, hvad der er i bufferpuljen på grund af den måde, bufferpuljen er organiseret på. Datafilsider styres i bufferpuljen af små datastrukturer kaldet buffere, som sporer ting som (ikke-udtømmende liste):
- Sidens ID (filnummer:sidenummer-i-fil)
- Sidste gang siden blev refereret (brugt af den dovne skribent til at hjælpe med at implementere den mindst nyligt brugte algoritme, der skaber ledig plads, når det er nødvendigt)
- Hukommelsesplaceringen af 8KB-siden i bufferpuljen
- Om siden er snavset eller ej (en snavset side har ændringer, som endnu ikke er blevet skrevet tilbage til holdbar lagring)
- Den allokeringsenhed siden tilhører (forklaret her) og allokeringsenhedens ID kan bruges til at finde ud af, hvilken tabel og hvilket indeks siden er en del af
For hver database, der har sider i bufferpuljen, er der en hash-liste over sider, i side-id-rækkefølge, som hurtigt er søgbar for at afgøre, om en side allerede er i hukommelsen, eller om en fysisk læsning skal udføres. Men intet tillader nemt SQL Server at bestemme, hvor stor en procentdel af bladniveauet for hvert indeks i en tabel, der allerede er i hukommelsen. Kode ville skulle scanne hele listen af buffere til databasen og lede efter buffere, der kortlægger sider for den pågældende allokeringsenhed. Og jo flere sider i hukommelsen for en database, jo længere tid vil scanningen tage. Det ville være uoverkommeligt dyrt at gøre som en del af forespørgselskompileringen.
Hvis du er interesseret, skrev jeg et indlæg for et stykke tid tilbage med noget T-SQL-kode, der scanner bufferpuljen og giver nogle metrics ved hjælp af DMV sys.dm_os_buffer_descriptors .
Hvorfor det ville være farligt at bruge bufferpoolindhold
Lad os foregive, at der *er* en meget effektiv mekanisme til at bestemme bufferpuljeindholdet, som optimizeren kan bruge til at hjælpe den med at vælge hvilket indeks, der skal bruges i en forespørgselsplan. Den hypotese, jeg vil udforske, er, at hvis optimeringsværktøjet ved nok af et mindre effektivt (større) indeks allerede er i hukommelsen, sammenlignet med det mest effektive (mindre) indeks at bruge, bør den vælge indekset i hukommelsen, fordi det vil reducere antallet af fysiske læsninger, og forespørgslen vil køre hurtigere.
Scenariet, jeg vil bruge, er som følger:en tabel BigTable har to ikke-klyngede indekser, Index_A og Index_B, der begge fuldstændig dækker en bestemt forespørgsel. Forespørgslen kræver en komplet scanning af indeksets bladniveau for at hente forespørgselsresultaterne. Tabellen har 1 million rækker. Index_A har 200.000 sider på bladniveau, og Index_B har 1 million sider på bladniveau, så en komplet scanning af Index_B kræver behandling af fem gange flere sider.
Jeg skabte dette konstruerede eksempel på en bærbar computer, der kører SQL Server 2019 med 8 processorkerner, 32 GB hukommelse og solid state-diske. Koden er som følger:
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
Og så timede jeg de konstruerede forespørgsler:
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
Du kan se, når ingen af indekserne er i hukommelsen, Index_A er let det mest effektive indeks at bruge, med en forløbet forespørgselstid på 764ms mod 2.761ms ved hjælp af Index_B, og det samme gælder, når begge indekser er i hukommelsen. Men hvis Index_B er i hukommelsen, og Index_A ikke er, hvis forespørgslen bruger Index_B (149ms), vil den køre hurtigere, end hvis den bruger Index_A (764ms).
Lad os nu tillade optimeringsværktøjet at basere planvalget på, hvad der er i bufferpuljen...
Hvis Index_A for det meste ikke er i hukommelsen, og Index_B for det meste er i hukommelsen, ville det være mere effektivt at kompilere forespørgselsplanen for at bruge Index_B til en forespørgsel, der kører på det tidspunkt. Selvom Index_B er større og ville have brug for flere CPU-cyklusser at scanne igennem, er fysiske læsninger meget langsommere end de ekstra CPU-cyklusser, så en mere effektiv forespørgselsplan minimerer antallet af fysiske læsninger.
Dette argument gælder kun, og en "brug Index_B" forespørgselsplan er kun mere effektiv end en "brug Index_A" forespørgselsplan, hvis Index_B forbliver det meste i hukommelsen, og Index_A forbliver for det meste ikke i hukommelsen. Så snart det meste af Index_A er i hukommelsen, ville "brug Index_A"-forespørgselsplanen være mere effektiv, og "brug Index_B"-forespørgselsplanen er det forkerte valg.
De situationer, hvor den kompilerede "brug Index_B"-plan er mindre effektiv end den omkostningsbaserede "brug Index_A"-plan, er (generalisering):
- Index_A og Index_B er begge i hukommelsen:den kompilerede plan vil tage næsten tre gange længere tid
- Ingen af indekserne er i hukommelsen:Den kompilerede plan tager over 3,5 gange længere
- Index_A er hukommelsesresident, og Index_B er det ikke:alle fysiske læsninger udført af planen er uvedkommende, OG det vil tage hele 53 gange længere
Oversigt
Selvom optimeringsværktøjet i vores tankeøvelse kan bruge bufferpuljeviden til at kompilere den mest effektive forespørgsel på et enkelt øjeblik, ville det være en farlig måde at drive plankompilering på på grund af den potentielle volatilitet af bufferpuljens indhold, hvilket gør den fremtidige effektivitet af den cachelagrede plan meget upålidelig.
Husk, at optimizerens opgave er at finde en god plan hurtigt, ikke nødvendigvis den bedste plan for 100 % af alle situationer. Efter min mening gør SQL Server-optimeringsværktøjet det rigtige ved at ignorere det faktiske indhold af SQL Server-bufferpuljen og stoler i stedet på de forskellige omkostningsregler i stedet for at producere en forespørgselsplan, der sandsynligvis vil være den mest effektive det meste af tiden .