Gefilterde indexen zijn verbazingwekkend krachtig, maar ik zie er nog steeds wat verwarring over, vooral over de kolommen die in de filters worden gebruikt en wat er gebeurt als je de filters wilt aanscherpen.
Een recente vraag op dba.stackexchange vroeg om hulp over waarom kolommen die in het filter van een gefilterde index worden gebruikt, moeten worden opgenomen in de 'inbegrepen' kolommen van de index. Uitstekende vraag - behalve dat ik het gevoel had dat het begon met een slecht uitgangspunt, omdat die kolommen niet in de index zouden moeten worden opgenomen . Ja, ze helpen, maar niet op de manier waarop de vraag leek te suggereren.
Om te voorkomen dat u naar de vraag zelf hoeft te kijken, volgt hier een korte samenvatting:
Om aan deze vraag te voldoen...
SELECT Id, DisplayName FROM Users WHERE Reputation > 400000;
...de volgende gefilterde index is redelijk goed:
CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club ON dbo.Users ( DisplayName, Id ) INCLUDE ( Reputation ) WHERE Reputation > 400000;
Maar ondanks het feit dat deze index aanwezig is, beveelt de Query Optimizer de volgende index aan als de gefilterde waarde wordt aangescherpt tot bijvoorbeeld 450000.
CREATE NONCLUSTERED INDEX IndexThatWasMissing ON dbo.Users ( Reputation ) INCLUDE ( DisplayName, Id );
Ik parafraseer de vraag hier een beetje, die begint met te verwijzen naar deze situatie en dan een ander voorbeeld opbouwt, maar het idee is hetzelfde. Ik wilde het alleen niet ingewikkelder maken door er een aparte tabel bij te betrekken.
Punt is - de index die door de QO wordt gesuggereerd, is de originele index, maar op zijn kop gezet. De oorspronkelijke index had Reputatie in de INCLUDE-lijst en DisplayName en Id als sleutelkolommen, terwijl de nieuwe aanbevolen index het tegenovergestelde is met Reputatie als de sleutelkolom en DisplayName &ID in de INCLUDE. Laten we eens kijken waarom.
De vraag verwijst naar een bericht van Erik Darling, waarin hij uitlegt dat hij de bovenstaande '450.000'-query heeft afgestemd door Reputatie in de kolom INCLUDE te plaatsen. Erik laat zien dat zonder Reputatie in de INCLUDE-lijst, een zoekopdracht die naar een hogere waarde van Reputatie filtert, Lookups moet doen (slecht!), of misschien zelfs de gefilterde index helemaal moet opgeven (mogelijk nog erger). Hij concludeert dat met de kolom Reputatie in de INCLUDE-lijst SQL statistieken heeft, zodat het betere keuzes kan maken, en laat zien dat met Reputatie in de INCLUDE een verscheidenheid aan query's die allemaal filteren op hogere reputatiewaarden, allemaal zijn gefilterde index scannen.
In een antwoord op de dba.stackexchange-vraag wijst Brent Ozar erop dat de verbeteringen van Erik niet bijzonder groot zijn omdat ze scans veroorzaken. Ik kom daar nog op terug, want het is op zich een interessant punt en enigszins onjuist.
Laten we eerst eens nadenken over indexen in het algemeen.
Een index geeft een geordende structuur aan een set gegevens. (Ik zou pedant kunnen zijn en erop wijzen dat het lezen van de gegevens in een index van begin tot eind u op een schijnbaar lukrake manier van pagina naar pagina zou kunnen springen, maar toch terwijl u door pagina's leest, de verwijzingen van de ene pagina naar de volgende keer kunt u erop vertrouwen dat de gegevens zijn geordend. Binnen elke pagina kunt u zelfs rondspringen om de gegevens in volgorde te lezen, maar er is een lijst die u laat zien welke delen (slots) van de pagina in welke volgorde moeten worden gelezen. heeft geen zin in mijn pedanterie, behalve om antwoord te geven aan degenen die al even pedant zijn die commentaar zullen geven als ik het niet doe.)
En deze volgorde is volgens de belangrijkste kolommen - dat is het makkelijke dat iedereen krijgt. Het is niet alleen handig om te voorkomen dat de gegevens later opnieuw worden gerangschikt, maar ook om snel een bepaalde rij of reeks rijen bij die kolommen te kunnen vinden.
De bladniveaus van de index bevatten de waarden in alle kolommen in de INCLUDE-lijst, of, in het geval van een geclusterde index, de waarden in alle kolommen in de tabel (behalve niet-persistente berekende kolommen). De andere niveaus in de index bevatten alleen de sleutelkolommen en (als de index niet uniek is) het unieke adres van de rij - dit zijn ofwel de sleutels van de geclusterde index (met de uniquifier van de rij als de geclusterde index ook niet uniek is ) of de RowID-waarde voor een heap, genoeg om gemakkelijk toegang te krijgen tot alle andere kolomwaarden voor de rij. De bladniveaus bevatten ook alle 'adres'-informatie.
Maar dat is niet het interessante aan deze post. Het interessante deel van dit bericht is wat ik bedoel met "naar een set gegevens". Onthoud dat ik zei:"Een index geeft een geordende structuur aan een set gegevens ".
In een geclusterde index is die gegevensset de hele tabel, maar het kan ook iets anders zijn. U kunt zich waarschijnlijk al voorstellen hoe de meeste niet-geclusterde indexen niet alle kolommen van de tabel omvatten. Dit is een van de dingen die niet-geclusterde indexen zo handig maken, omdat ze doorgaans een stuk kleiner zijn dan de onderliggende tabel.
In het geval van een geïndexeerde weergave, onze set gegevens, kunnen dit de resultaten zijn van een hele query, inclusief joins over veel tabellen! Dat is voor een andere post.
Maar in een gefilterde index is het niet alleen een kopie van een subset van kolommen, maar ook een subset van rijen. Dus in het voorbeeld hier is de index alleen van toepassing op de gebruikers met een reputatie van meer dan 400.000.
CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club_NoInclude ON dbo.Users ( DisplayName, Id ) WHERE Reputation > 400000;
Deze index neemt de gebruikers met een reputatie van meer dan 400.000 en rangschikt ze op DisplayName en Id. Het kan uniek zijn omdat (vermoedelijk) de Id-kolom al uniek is. Als je iets soortgelijks aan je eigen tafel probeert, moet je daar misschien voorzichtig mee zijn.
Maar op dit moment maakt het de index niet uit wat de reputatie is voor elke gebruiker - het maakt alleen uit of de reputatie hoog genoeg is om in de index te staan of niet. Als de reputatie van een gebruiker wordt bijgewerkt en de drempel overschrijdt, worden de DisplayName en ID van de gebruiker in de index ingevoegd. Als het eronder komt, wordt het uit de index verwijderd. Het is net alsof je een aparte tafel hebt voor de high rollers, behalve dat we mensen aan die tafel krijgen door hun reputatiewaarde te verhogen tot boven de 400k-drempel in de onderliggende tabel. Het kan dit doen zonder de Reputatiewaarde zelf op te slaan.
Dus als we nu mensen willen vinden met een drempel van meer dan 450k, mist die index wat informatie.
Natuurlijk kunnen we vol vertrouwen zeggen dat iedereen die we zullen vinden in die index staat, maar de index bevat op zichzelf niet genoeg informatie om verder te filteren op reputatie. Als ik je vertelde dat ik een alfabetische lijst had met Oscar-winnende films uit de jaren 90 (American Beauty, Braveheart, Dances With Wolves, English Patient, Forrest Gump, Schindler's List, Shakespeare in Love, Silence of the Lambs, Titanic, Unforgiven) , dan kan ik je verzekeren dat de winnaars voor 1994-1996 een subset daarvan zouden zijn, maar ik kan de vraag niet beantwoorden zonder eerst wat meer informatie te krijgen.
Het is duidelijk dat mijn gefilterde index nuttiger zou zijn als ik het jaar had opgenomen, en mogelijk nog meer als het jaar een belangrijke kolom was, aangezien mijn nieuwe zoekopdracht die voor 1994-1996 wil vinden. Maar ik heb deze index waarschijnlijk ontworpen rond een zoekopdracht om alle films uit de jaren negentig in alfabetische volgorde te vermelden. Die zoekopdracht maakt niet uit wat het eigenlijke jaar is, alleen of het in de jaren negentig is of niet, en ik hoef niet eens het jaartal terug te geven - alleen de titel - dus ik kan mijn gefilterde index scannen om de resultaten te krijgen. Voor die zoekopdracht hoef ik de resultaten niet eens opnieuw te ordenen of het startpunt te vinden - mijn index is echt perfect.
Een meer praktisch voorbeeld van het niet geven van de waarde van de kolom in het filter is de status, zoals:
WHERE IsActive = 1
Ik zie vaak code die gegevens van de ene tabel naar de andere verplaatst wanneer rijen niet meer 'actief' zijn. Mensen willen niet dat oude rijen hun tabel overvol maken, en ze erkennen dat hun 'hot' data slechts een kleine subset is van al hun data. Dus verplaatsen ze hun koelgegevens naar een archieftabel, waardoor hun actieve tafel klein blijft.
Een gefilterde index kan dit voor u doen. Achter de schermen. Zodra u de rij bijwerkt en die IsActive-kolom wijzigt in iets anders dan 1. Als u alleen actieve gegevens in de meeste van uw indexen wilt hebben, zijn gefilterde indexen ideaal. Het brengt zelfs rijen terug in de indexen als de IsActive-waarde terug verandert in 1.
Maar u hoeft IsActive niet in de INCLUDE-lijst te zetten om dit te bereiken. Waarom zou je de waarde willen opslaan - je weet al wat de waarde is - het is 1! Tenzij je vraagt om de waarde terug te geven, zou je het niet nodig moeten hebben. En waarom zou je de waarde teruggeven als je al weet dat het antwoord 1 is, toch?! Behalve dat frustrerend, zullen de statistieken waarnaar Erik in zijn post verwijst, profiteren van het feit dat ze in de INCLUDE-lijst staan. Je hebt het niet nodig voor de zoekopdracht, maar je moet het wel opnemen voor de statistieken.
Laten we eens nadenken over wat de Query Optimizer moet doen om het nut van een index te achterhalen.
Voordat het überhaupt veel kan doen, moet het overwegen of de index een kandidaat is. Het heeft geen zin om een index te gebruiken als deze niet alle rijen heeft die nodig kunnen zijn - niet tenzij we een effectieve manier hebben om de rest te krijgen. Als ik films van 1985-1995 wil, dan is mijn index van films uit de jaren 90 behoorlijk zinloos. Maar voor 1994-1996 is het misschien niet slecht.
Op dit punt moet ik, net als bij elke indexoverweging, nadenken of het genoeg zal helpen om de gegevens te vinden en in een volgorde te krijgen die zal helpen bij het uitvoeren van de rest van de query (mogelijk voor een Merge Join, Stream Aggregate, bevredigend een ORDER BY of verschillende andere redenen). Als mijn zoekfilter exact overeenkomt met het indexfilter, hoef ik niet verder te filteren - alleen de index gebruiken is voldoende. Dit klinkt geweldig, maar als het niet precies overeenkomt, als mijn queryfilter strakker is dan het indexfilter (zoals mijn voorbeeld uit 1994-1996, of Erik's 450.000), dan heb ik die jaarwaarden of reputatiewaarden nodig om te controleren - hopelijk krijgen ze ze ofwel van de INCLUDEd op het bladniveau of ergens in mijn belangrijkste kolommen. Als ze niet in de index staan, moet ik een opzoeking doen voor elke rij in mijn gefilterde index (en idealiter een idee hebben over hoe vaak mijn opzoeking zal worden aangeroepen, wat de statistieken zijn die Erik wil de kolom opgenomen voor).
Idealiter is elke index die ik van plan ben te gebruiken correct geordend (via de toetsen), OMVAT alle kolommen die ik moet retourneren en is vooraf gefilterd tot alleen de rijen die ik nodig heb. Dat zou de perfecte index zijn, en mijn uitvoeringsplan zal een Scan zijn.
Juist, een SCAN. Geen zoeken, maar een scan. Het begint op de eerste pagina van mijn index en blijft me rijen geven totdat ik er zoveel heb als ik nodig heb, of totdat er geen rijen meer zijn om terug te keren. Ik sla er geen over, sorteer ze niet - geef me gewoon de rijen op volgorde.
A Seek zou suggereren dat ik niet de hele index nodig heb, wat betekent dat ik middelen verspil aan het onderhouden van dat deel van de index, en om het te doorzoeken moet ik het startpunt vinden en rijen blijven controleren om te zien of ik het einde raken of niet. Als mijn scan een predikaat heeft, moet ik natuurlijk meer gegevens doorzoeken (en testen) dan nodig is, maar als mijn indexfilters perfect zijn, zou de Query Optimizer dat moeten herkennen en die controles niet hoeven uit te voeren .
Laatste gedachten
INCLUDE's zijn niet essentieel voor gefilterde indexen. Ze zijn handig om gemakkelijke toegang te bieden tot kolommen die nuttig kunnen zijn voor uw zoekopdracht, en als u de inhoud van uw gefilterde index per kolom aanscherpt, of deze nu in het filter wordt vermeld of niet, kunt u overwegen om die kolom in de mix. Maar op dat moment zou u zich moeten afvragen of het filter van uw index de juiste is, wat u nog meer in uw INCLUDE-lijst zou moeten hebben, en zelfs wat de belangrijkste kolom(men) zouden moeten zijn. Eriks zoekopdrachten speelden niet goed omdat hij informatie nodig had die niet in de index stond, ook al had hij de kolom in het filter genoemd. Hij vond ook een goed gebruik voor de statistieken, en ik zou u toch willen aanmoedigen om om die reden de filterkolommen op te nemen. Maar als ze in een INCLUDE worden geplaatst, kunnen ze niet plotseling een zoekactie beginnen, want zo werkt geen enkele index, of deze nu gefilterd is of niet.
Ik wil dat u, lezer, gefilterde indexen heel goed begrijpt. Ze zijn ongelooflijk handig en als je ze begint te zien als tabellen met hun eigen rechten, kunnen ze onderdeel worden van je algehele databaseontwerp. Ze zijn ook een reden om altijd de instellingen ANSI_NULLs en QUOTED_IDENTIFIER te gebruiken, omdat je fouten krijgt van de gefilterde index, tenzij die instellingen AAN staan, maar hopelijk weet je al zeker dat ze altijd aan staan.
Oh, en die films waren Forrest Gump, Braveheart en The English Patient.
@rob_farley