Filtrerede indekser er forbløffende kraftfulde, men jeg ser stadig en del forvirring derude omkring dem – især om de kolonner, der bruges i filtrene, og hvad der sker, når du vil stramme filtrene.
Et nyligt spørgsmål på dba.stackexchange bad om hjælp til, hvorfor kolonner brugt i filteret til et filtreret indeks skulle inkluderes i indeksets 'inkluderede' kolonner. Udmærket spørgsmål – bortset fra at jeg følte, at det startede på en dårlig præmis, fordi de kolonner ikke skulle være med i indekset . Ja, de hjælper, men ikke på den måde, som spørgsmålet syntes at antyde.
For at spare dig for at se på selve spørgsmålet, er her en hurtig oversigt:
For at tilfredsstille denne forespørgsel...
SELECT Id, DisplayName FROM Users WHERE Reputation > 400000;
…følgende filtrerede indeks er ret godt:
CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club ON dbo.Users ( DisplayName, Id ) INCLUDE ( Reputation ) WHERE Reputation > 400000;
Men på trods af at have dette indeks på plads, anbefaler Query Optimizer følgende indeks, hvis den filtrerede værdi er strammet til f.eks. 450000.
CREATE NONCLUSTERED INDEX IndexThatWasMissing ON dbo.Users ( Reputation ) INCLUDE ( DisplayName, Id );
Jeg parafraserer spørgsmålet lidt her, som starter med at henvise til denne situation og derefter bygger et andet eksempel, men ideen er den samme. Jeg ville bare ikke gøre tingene mere komplicerede ved at involvere en separat tabel.
Point er - indekset foreslået af QO er det originale indeks, men vendt på hovedet. Det originale indeks havde Reputation i INCLUDE-listen og DisplayName og Id som nøglekolonner, mens det nye anbefalede indeks er den modsatte vej rundt med Reputation som nøglekolonnen og DisplayName &ID i INCLUDE. Lad os se på hvorfor.
Spørgsmålet refererer til et indlæg af Erik Darling, hvor han forklarer, at han tunede '450.000'-forespørgslen ovenfor ved at sætte Reputation i INCLUDE-kolonnen. Erik viser, at uden Reputation i INCLUDE-listen, skal en forespørgsel, der filtrerer til en højere værdi af Reputation, lave opslag (dårligt!), eller måske endda give helt op på det filtrerede indeks (potentielt endnu værre). Han konkluderer, at det at have omdømme-kolonnen i INCLUDE-listen lader SQL have statistik, så det kan træffe bedre valg, og viser, at med Reputation i INCLUDE en række forskellige forespørgsler, som alle filtrerer på højere omdømmeværdier, alle scanner hans filtrerede indeks.
I et svar på dba.stackexchange-spørgsmålet påpeger Brent Ozar, at Eriks forbedringer ikke er særlig store, fordi de forårsager scanninger. Jeg vender tilbage til det, fordi det er et interessant punkt i sig selv og noget forkert.
Lad os først tænke lidt over indekser generelt.
Et indeks giver en ordnet struktur til et sæt data. (Jeg kunne være pedantisk og påpege, at læsning af data i et indeks fra start til slut kan springe dig fra side til side på en tilsyneladende tilfældig måde, men stadig mens du læser gennem siderne, følger du pointerne fra en side til den næste kan du være sikker på, at dataene er ordnet. Inden for hver side kan du endda hoppe rundt for at læse dataene i rækkefølge, men der er en liste, der viser dig, hvilke dele (slots) af siden, der skal læses i hvilken rækkefølge. er ingen mening i mit pedanteri undtagen at svare dem, der er lige så pedantiske, som vil kommentere, hvis jeg ikke gør det.)
Og denne rækkefølge er i henhold til nøglekolonnerne – det er den nemme del, som alle får. Det er nyttigt, ikke kun for at kunne undgå at omarrangere dataene senere, men også for hurtigt at kunne finde en bestemt række eller række rækker ved disse kolonner.
Indeksets bladniveauer indeholder værdierne i alle kolonner i INCLUDE-listen, eller i tilfælde af et Clustered Index, værdierne på tværs af alle kolonnerne i tabellen (undtagen ikke-vedvarende beregnede kolonner). De andre niveauer i indekset indeholder kun nøglekolonnerne og (hvis indekset ikke er unikt) rækkens unikke adresse – som enten er nøglerne til det klyngede indeks (med rækkens uniquiifier, hvis det klyngede indeks heller ikke er unikt ) eller RowID-værdien for en heap, nok til at give nem adgang til alle de andre kolonneværdier for rækken. Bladniveauerne inkluderer også alle 'adresse'-oplysninger.
Men det er ikke det interessante for dette indlæg. Det interessante med dette indlæg er, hvad jeg mener med "til et sæt data". Husk, at jeg sagde "Et indeks giver en ordnet struktur til et sæt data ".
I et klynget indeks er det datasæt hele tabellen, men det kan være noget andet. Du kan sikkert allerede forestille dig, hvordan de fleste ikke-klyngede indekser ikke involverer alle kolonnerne i tabellen. Dette er en af de ting, der gør ikke-klyngede indekser så nyttige, fordi de typisk er meget mindre end den underliggende tabel.
I tilfælde af en indekseret visning, kan vores datasæt være resultaterne af en hel forespørgsel, inklusive sammenføjninger på tværs af mange tabeller! Det er til et andet indlæg.
Men i et filtreret indeks er det ikke kun en kopi af en undergruppe af kolonner, men også en undergruppe af rækker. Så i eksemplet her er indekset kun på tværs af brugere med mere end 400.000 omdømme.
CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club_NoInclude ON dbo.Users ( DisplayName, Id ) WHERE Reputation > 400000;
Dette indeks tager de brugere, der har mere end 400.000 omdømme, og bestiller dem efter DisplayName og Id. Det kan være unikt, fordi (antagelig) Id-kolonnen allerede er unik. Hvis du prøver noget lignende på dit eget bord, skal du muligvis være forsigtig med det.
Men på dette tidspunkt er indekset ligeglad med, hvad omdømmet er for hver bruger – det er bare ligeglad med, om omdømmet er højt nok til at være i indekset eller ej. Hvis en brugers omdømme bliver opdateret, og det tipper over tærsklen, vil brugerens DisplayName og Id blive indsat i indekset. Hvis det falder under, slettes det fra indekset. Det er ligesom at have et separat bord til high rollers, bortset fra at vi får folk ind på det bord ved at øge deres omdømmeværdi over 400k-tærsklen i den underliggende tabel. Det kan gøre dette uden at skulle gemme selve omdømmeværdien.
Så hvis vi nu vil finde folk, der har en tærskel over 450.000, så mangler det indeks nogle oplysninger.
Ja, vi kunne trygt sige, at alle, vi finder, er i det indeks – men indekset indeholder ikke nok information i sig selv til at filtrere yderligere på Reputation. Hvis jeg fortalte dig, at jeg havde en alfabetisk liste over Oscar-vindende film for bedste film fra 1990'erne (American Beauty, Braveheart, Dances With Wolves, English Patient, Forrest Gump, Schindler's List, Shakespeare in Love, Silence of the Lambs, Titanic, Unforgiven) , så kan jeg forsikre dig om, at vinderne for 1994-1996 ville være en delmængde af dem, men jeg kan ikke svare på spørgsmålet uden først at få noget mere information.
Det er klart, at mit filtrerede indeks ville være mere nyttigt, hvis jeg havde inkluderet året, og potentielt endnu mere, hvis året var en nøglekolonne, da min nye forespørgsel ønsker at finde dem for 1994-1996. Men jeg har formentlig designet dette indeks omkring en forespørgsel til at liste alle filmene fra 1990'erne i alfabetisk rækkefølge. Den forespørgsel er ligeglad med, hvad det faktiske år er, kun om det er i 1990'erne eller ej, og jeg behøver ikke engang at returnere året – kun titlen – så jeg kan scanne mit filtrerede indeks for at få resultaterne. Til den forespørgsel behøver jeg ikke engang at omarrangere resultaterne eller finde udgangspunktet – mit indeks er virkelig perfekt.
Et mere praktisk eksempel på at være ligeglad med værdien af kolonnen i filteret er på status, såsom:
WHERE IsActive = 1
Jeg ser ofte kode, der flytter data fra en tabel til en anden, når rækker holder op med at være 'aktive'. Folk vil ikke have, at gamle rækker roder op i deres tabel, og de erkender, at deres 'varme' data kun er en lille delmængde af alle deres data. Så de flytter deres køledata over i en arkivtabel og holder deres aktive tabel lille.
Et filtreret indeks kan gøre dette for dig. Bag scenen. Så snart du opdaterer rækken og ændrer den IsActive-kolonne til noget andet end 1. Hvis du kun interesserer dig for at have aktive data i de fleste af dine indekser, så er filtrerede indekser ideelle. Det vil endda bringe rækker tilbage i indekserne, hvis IsActive-værdien ændres tilbage til 1.
Men du behøver ikke at sætte IsActive på INCLUDE-listen for at opnå dette. Hvorfor vil du gemme værdien – du ved allerede hvad værdien er – den er 1! Medmindre du beder om at returnere værdien, burde du ikke have brug for den. Og hvorfor skulle du returnere værdien, når du allerede ved, at svaret er 1, ikke?! Bortset fra at frustrerende nok vil den statistik, som Erik refererer til i sit indlæg, drage fordel af at være på INCLUDE-listen. Du behøver det ikke til forespørgslen, men du bør inkludere det til statistikken.
Lad os tænke over, hvad Query Optimizer skal gøre for at finde ud af nytten af et indeks.
Før den overhovedet kan gøre meget, skal den overveje, om indekset er en kandidat. Det giver ingen mening at bruge et indeks, hvis det ikke har alle de rækker, der kan være nødvendige – ikke medmindre vi har en effektiv måde at få resten på. Hvis jeg vil have film fra 1985-1995, så er mit indeks over 1990'er film ret meningsløst. Men for 1994-1996 er det måske ikke dårligt.
På dette tidspunkt, ligesom enhver indeksovervejelse, er jeg nødt til at tænke over, om det vil hjælpe nok til at finde dataene og få dem i en rækkefølge, der vil hjælpe med at udføre resten af forespørgslen (muligvis for en Merge Join, Stream Aggregate, tilfredsstillende en ORDER BY eller forskellige andre årsager). Hvis mit forespørgselsfilter matcher indeksfilteret nøjagtigt, behøver jeg ikke at filtrere yderligere – det er nok at bruge indekset. Det lyder godt, men hvis det ikke matcher nøjagtigt, hvis mit forespørgselsfilter er strammere end indeksfilteret (som mit eksempel fra 1994-1996 eller Eriks 450.000), bliver jeg nødt til at have disse årværdier eller omdømmeværdier at tjekke – forhåbentlig at få dem enten fra INCLUDED på bladniveau eller et sted i mine nøglekolonner. Hvis de ikke er i indekset, bliver jeg nødt til at lave et opslag for hver række i mit filtrerede indeks (og ideelt set have en idé om, hvor mange gange mit opslag vil blive kaldt, hvilket er den statistik, som Erik vil have kolonnen inkluderet for).
Ideelt set er ethvert indeks, jeg planlægger at bruge, ordnet korrekt (via tasterne), INKLUDERER alle de kolonner, jeg skal returnere, og er forfiltreret til kun de rækker, jeg har brug for. Det ville være det perfekte indeks, og min udførelsesplan vil være en scanning.
Det er rigtigt, en SCAN. Ikke en søgning, men en scanning. Det starter på den første side af mit indeks og bliver ved med at give mig rækker, indtil jeg har så mange, som jeg har brug for, eller indtil der ikke er flere rækker at returnere. Ikke at springe nogen over, ikke sortere dem – bare give mig rækkerne i rækkefølge.
En søgning tyder på, at jeg ikke har brug for hele indekset, hvilket betyder, at jeg spilder ressourcer på at vedligeholde den del af indekset, og for at forespørge på det, skal jeg finde udgangspunktet og blive ved med at tjekke rækkerne for at se, om jeg har ramte enden eller ej. Hvis min scanning har et prædikat, så er jeg nødt til at gennemse (og teste) flere data, end jeg har brug for, men hvis mine indeksfiltre er perfekte, så skal Query Optimizer genkende det og ikke skulle udføre disse kontroller .
Sidste tanker
INCLUDEs er ikke kritiske for filtrerede indekser. De er nyttige til at give nem adgang til kolonner, som kan være nyttige for din forespørgsel, og hvis du tilfældigvis stramme det, der er i dit filtrerede indeks med en kolonne, uanset om det er nævnt i filteret eller ej, bør du overveje at have den kolonne i blandingen. Men på det tidspunkt burde du spørge, om dit indekss filter er det rigtige, hvad du ellers skal have på din INCLUDE-liste, og endda hvad nøglekolonnen(r) skal være. Eriks forespørgsler spillede ikke godt, fordi han havde brug for information, der ikke var i indekset, selvom han havde nævnt kolonnen i filteret. Han fandt også god brug for statistikken, og jeg vil stadig opfordre dig til at inkludere filterkolonnerne af den grund. Men at sætte dem i en INCLUDE giver dem ikke mulighed for pludselig at begynde at lave en søgning, for det er ikke sådan noget indeks fungerer, uanset om det er filtreret eller ej.
Jeg vil have dig, læser, til at forstå filtrerede indekser rigtig godt. De er utrolig nyttige, og når du begynder at forestille dig dem som tabeller i deres egne rettigheder, kan de blive en del af dit overordnede databasedesign. De er også en grund til altid at bruge indstillingerne ANSI_NULLs og QUOTED_IDENTIFIER, fordi du får fejl fra filtreret indeks, medmindre disse indstillinger er slået TIL, men forhåbentlig sikrer du dig allerede, at de altid er tændt alligevel.
Åh, og de film var Forrest Gump, Braveheart og The English Patient.
@rob_farley