I mit sidste indlæg begyndte jeg at skitsere den proces, jeg gennemgår, når jeg tuner forespørgsler - specifikt når jeg opdager, at jeg skal tilføje et nyt indeks eller ændre et eksisterende. Indtil dette tidspunkt har vi identificeret den problematiske forespørgsel, det indeks, jeg har brug for, hvilke indekser der i øjeblikket findes på bordet, og om disse indekser bliver brugt eller ej. Når vi har disse data, kan vi gå videre til de næste trin i processen.
Trin 5:Hvad bruger et indeks
Ud over at se, hvor ofte et indeks bruges (eller ej), er det en fordel at vide hvilke forespørgsler bruge et indeks, især hvis jeg ønsker at flette det med et andet indeks. Heldigvis har Jonathan Kehayias allerede skrevet en forespørgsel for at hjælpe med at identificere, hvilke planer der bruger et specifikt indeks. Hans version kan bruges til plancachen - den eneste udfordring er, at informationen er forbigående, så du kan ikke fange hver forespørgsel, der bruger et bestemt indeks. Query Store kan hjælpe med det – jeg har ændret hans forespørgsel for at få de samme oplysninger fra planerne i Query Store:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED DECLARE @IndexName AS NVARCHAR(128) = N'[IX_Sales_OrderLines_AllocatedStockItems]', @lb AS nchar(1) = N'[', @rb AS nchar(1) = N']'; -- Make sure the name passed is appropriately quoted IF (LEFT(@IndexName, 1) <> @lb AND RIGHT(@IndexName, 1) <> @rb) SET @IndexName = QUOTENAME(@IndexName); --Handle the case where the left or right was quoted manually but not the opposite side IF LEFT(@IndexName, 1) <> @lb SET @IndexName = @rb + @IndexName; IF RIGHT(@IndexName, 1) <> @rb SET @IndexName = @IndexName + @rb; ;WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') SELECT stmt.value('(@StatementText)[1]', 'varchar(max)') AS SQL_Text, obj.value('(@Database)[1]', 'varchar(128)') AS DatabaseName, obj.value('(@Schema)[1]', 'varchar(128)') AS SchemaName, obj.value('(@Table)[1]', 'varchar(128)') AS TableName, obj.value('(@Index)[1]', 'varchar(128)') AS IndexName, obj.value('(@IndexKind)[1]', 'varchar(128)') AS IndexKind, query_plan FROM ( SELECT query_plan FROM ( SELECT TRY_CONVERT(XML, [qsp].[query_plan]) AS [query_plan] FROM sys.query_store_plan [qsp] ) tp ) AS tab (query_plan) CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS batch(stmt) CROSS APPLY stmt.nodes('.//IndexScan/Object[@Index=sql:variable("@IndexName")]') AS idx(obj) OPTION(MAXDOP 1, RECOMPILE);
Det er værd at bemærke, at dette er endnu et punkt, hvor jeg kan befinde mig meget dybt i et kaninhul, afhængigt af antallet af indekser, jeg gennemgår, og antallet af forespørgsler, der bruger dem. Hvis det er muligt, vil jeg også overveje eksekveringstællinger (fra Query Store eller planens cache) for ikke bare at forstå hvad forespørgsel bruger et indeks, men hvor ofte den forespørgsel udføres. Det er her indekstuning bliver en kunst. Jeg kan indsamle en latterlig mængde data... men jeg har ikke uendelig tid til analyser, så jeg er nødt til at foretage en vurdering af, hvor mange forespørgsler jeg vil gennemgå.
Trin 6:Test
På det enkleste betyder at teste et indeks at tage den problematiske forespørgsel og fange plan- og ydeevnedata (varighed, IO, CPU osv.), og derefter oprette indekset, køre forespørgslen igen og fange den samme information. Hvis ydeevnen forbedres, er du godt i gang!
Det er sjældent så enkelt.
Til at begynde med har jeg ofte mindst to varianter af et indeks, som jeg vil teste, nogle gange flere. Jeg starter med min baseline, derefter opretter jeg alle indeksvariationerne, rydder plancachen og ser, hvad SQL Server vælger. Derefter ruller jeg igennem og tvinger hvert indeks med et hint, og fanger planen og præstationsmålingerne for hver udførelse. Bemærk:dette forudsætter, at jeg har nok diskplads til alle indekserne ... hvis ikke, så opretter jeg dem én ad gangen og tester. Til sidst sammenligner jeg tallene. Hvis jeg bare tilføjer et nyt indeks, er jeg næsten færdig. Men hvis jeg ændrer et indeks eller slår et par sammen, kan det blive kompliceret.
I en ideel verden, hvis jeg ændrer et eksisterende indeks, finder jeg de hyppigste/vigtigeste forespørgsler, der bruger det aktuelle indeks og får deres planer og ydeevnemålinger (dette er nemt med Query Store). Så ændrer jeg indekset, kører alle disse forespørgsler igen og ser, om jeg får væsentlige ændringer i planens form og/eller ydeevne.
Hvis jeg fletter to indekser, gør jeg det samme, men med alle forespørgsler, der bruger begge indekser, og tester derefter igen med det flettede indeks.
Hvis jeg tilføjer/ændrer/fletter flere indekser til en tabel, så skal jeg hente alle relevante forespørgsler og deres planer og målinger, ændre indekserne og derefter hente alle oplysningerne igen og sammenligne. Dette kan være ekstremt tidskrævende, afhængigt af hvor mange forskellige forespørgsler der er. Det er her, det er en kunstform, og du skal bestemme, hvor mange forespørgsler du virkelig skal teste. Det er en funktion af udførelseshyppighed, forespørgsels betydning/relevans og den tid, jeg har til rådighed/tildelt.
Endelig, hvis jeg tilføjer et indeks til en tabel, og jeg ikke fjerner nogen eksisterende, så har jeg tilføjet overhead for INSERTs, DELETEs og potentielt OPDATERINGER. Ydeevnetestning af denne ændring er mulig, men du har brug for et testmiljø og evnen til at køre en belastningstest og fange metrics før og efter ændring relateret til varighed, IO og CPU.
Det er mange venner, og derfor er det ironisk, at jeg først tænkte på at sige, at indeksjustering var let. Det er måske ikke altid nemt, men det er muligt. Det er et spørgsmål om flid og at holde styr på det hele.
Trin 7:Implementering
Efter at jeg har gennemgået de nye indeks så meget som muligt, er vi klar til produktion. Jeg indrømmer, at jeg ser indeksændringer som lav risiko, især nye. Hvis det er et problem, kan du droppe det med det samme og vende tilbage til den oprindelige tilstand. Med et modificer/flet/slip scenarie vil du have alt scriptet, så du kan ændre og genskabe indekser efter behov for at nulstille indekserne. Jeg anbefaler altid, at du først deaktiverer indekser i stedet for at droppe dem, da du så ikke behøver at bekymre dig om definitionen - hvis du skal tilføje indekset tilbage, genopbygger du det simpelthen.
Oversigt
Din metode til at tilføje og/eller konsolidere indekser kan være anderledes! Ligesom forespørgselsindstilling er der ingen perfekt proces. For enhver, der er ny til indekstuning, giver dette forhåbentlig en start på elementer til gennemgang og vigtige overvejelser. Det er umuligt at tilføje indekser uden at tilføje en vis mængde overhead - og igen er det her, kunsten kommer ind:du skal afgøre, om fordelen ved indekset opvejer omkostningerne ved ændringer.
Indeksjustering er en evig, iterativ proces - jeg tror aldrig, du er færdig, fordi kodeændringer, nye tabeller eller funktionalitet tilføjes, og data i tabellerne ændres. Kimberly har to indlæg (https://www.sqlskills.com/blogs/kimberly/spring-cleaning-your-indexes-part-i/ og https://www.sqlskills.com/blogs/kimberly/spring-cleaning- your-indexes-part-ii/), der taler om at rydde op i dine indekser - nu er det tid til at komme i gang! Og endelig, når nogen spørger, "hvor mange indekser skal der være for et bord?" Jeg svarer med noget i stil med, "det færreste antal du behøver for at tilfredsstille så mange forespørgsler som muligt." Der er ikke noget magisk tal — jeg har set tabeller med nul indekser, og jeg har set tabeller med over 100 (jeg er sikker på, at nogle af jer har set højere tællinger). Hverken nul eller 100 er godt, men det "rigtige" tal er et, du skal finde ud af ved hjælp af de tilgængelige data og din erfaring.