Jeg ser ofte folk kæmper med SQL Server, når de ser to forskellige eksekveringsplaner for, hvad de mener er den samme forespørgsel. Normalt opdages dette efter andre observationer, såsom vidt forskellige henrettelsestider. Jeg siger, at de tror, det er den samme forespørgsel, fordi nogle gange er det, og nogle gange er det ikke.
Et af de mest almindelige tilfælde er, når de tester en forespørgsel i SSMS og får en anden plan end den, de får fra deres applikation. Der er potentielt to faktorer i spil her (som også kan være relevante, når sammenligningen IKKE er mellem applikationen og SSMS):
- Applikationen har næsten altid forskellige
SET
indstillinger end SSMS (disse er ting somARITHABORT
,ANSI_NULLS
ogQUOTED_IDENTIFIER
). Dette tvinger SQL Server til at gemme de to planer separat; Erland Sommarskog har behandlet dette meget detaljeret i sin artikel, Langsom i applikationen, hurtig i SSMS?
- De parametre, som applikationen brugte, da dens kopi af planen først blev kompileret, kunne have været meget anderledes og ført til en anden plan end dem, der blev brugt første gang, forespørgslen blev kørt fra SSMS – dette er kendt som parametersniffing . Erland taler også i dybden om det, og jeg har ikke tænkt mig at gengive hans anbefalinger, men opsummere ved at minde dig om, at test af applikationens forespørgsel i SSMS ikke altid er nyttig, da det er ret usandsynligt, at det er en æble-til-æbler-test.
Der er et par andre scenarier, der er lidt mere uklare, som jeg tager op i min foredrag om dårlige vaner og bedste praksis. Dette er tilfælde, hvor planerne ikke er forskellige, men der er flere kopier af den samme plan, der blæser plancachen op. Jeg tænkte, at jeg skulle nævne dem her, fordi de altid overrasker så mange mennesker.
case og blanktegn er vigtige
SQL Server hashes forespørgselsteksten til et binært format, hvilket betyder, at hvert enkelt tegn i forespørgselsteksten er afgørende. Lad os tage følgende simple forespørgsler:
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO SELECT StoreID FROM Sales.Customer; GO -- original query GO SELECT StoreID FROM Sales.Customer; GO ----^---- extra space GO SELECT storeid FROM sales.customer; GO ---- lower case names GO select StoreID from Sales.Customer; GO ---- lower case keywords GO
Disse genererer naturligvis de nøjagtige samme resultater og genererer nøjagtig den samme plan. Men hvis vi ser på, hvad vi har i plan-cachen:
SELECT t.[text], p.size_in_bytes, p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t WHERE LOWER(t.[text]) LIKE N'%sales'+'.'+'customer%';
Resultaterne er uheldige:
Så i dette tilfælde er det klart, at case og whitespace er meget vigtige. Jeg talte om dette meget mere detaljeret i maj sidste år.
Skemahenvisninger er vigtige
Jeg har tidligere blogget om vigtigheden af at specificere skemapræfikset, når jeg refererer til ethvert objekt, men på det tidspunkt var jeg ikke helt klar over, at det også havde plan-cache-implikationer.
Lad os tage et kig på et meget simpelt tilfælde, hvor vi har to brugere med forskellige standardskemaer, og de kører nøjagtig den samme forespørgselstekst, uden at referere til objektet ved dets skema:
USE AdventureWorks2014; DBCC FREEPROCCACHE WITH NO_INFOMSGS; GO CREATE USER SQLPerf1 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Sales; CREATE USER SQLPerf2 WITHOUT LOGIN WITH DEFAULT_SCHEMA = Person; GO CREATE TABLE dbo.AnErrorLog(id INT); GRANT SELECT ON dbo.AnErrorLog TO SQLPerf1, SQLPerf2; GO EXECUTE AS USER = N'SQLPerf1'; GO SELECT id FROM AnErrorLog; GO REVERT; GO EXECUTE AS USER = N'SQLPerf2'; GO SELECT id FROM AnErrorLog; GO REVERT; GO
Hvis vi nu tager et kig på plancachen, kan vi trække sys.dm_exec_plan_attributes
ind. for at se præcis, hvorfor vi får to forskellige planer for identiske forespørgsler:
SELECT t.[text], p.size_in_bytes, p.usecounts, [schema_id] = pa.value, [schema] = s.name FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS pa INNER JOIN sys.schemas AS s ON s.[schema_id] = pa.value WHERE t.[text] LIKE N'%AnError'+'Log%' AND pa.attribute = N'user_id';
Resultater:
Og hvis du kører det hele igen, men tilføjer dbo.
præfiks til begge forespørgsler, vil du se, at der kun er én plan, der bliver brugt to gange. Dette bliver et meget overbevisende argument for altid fuldt ud at henvise til objekter.
SET indstillinger redux
Som en sidebemærkning kan du bruge en lignende tilgang til at bestemme om SET
indstillingerne er forskellige for to eller flere versioner af den samme forespørgsel. I dette tilfælde undersøger vi forespørgslerne involveret i flere planer genereret af forskellige opkald til den samme lagrede procedure, men du kan også identificere dem ved forespørgselsteksten eller forespørgselshash.
SELECT p.plan_handle, p.usecounts, p.size_in_bytes, set_options = MAX(a.value) FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS t CROSS APPLY sys.dm_exec_plan_attributes(p.plan_handle) AS a WHERE t.objectid = OBJECT_ID(N'dbo.procedure_name') AND a.attribute = N'set_options' GROUP BY p.plan_handle, p.usecounts, p.size_in_bytes;
Hvis du har flere resultater her, bør du se forskellige værdier for set_options
(som er en bitmaske). Det er kun begyndelsen; Jeg vil gå ud her og fortælle dig, at du kan bestemme, hvilket sæt muligheder der er aktiveret for hver plan ved at pakke værdien ud i henhold til afsnittet "Evaluering af sætindstillinger" her. Ja, jeg er så doven.
Konklusion
Der er flere grunde til, at du kan se forskellige planer for den samme forespørgsel (eller hvad du tror er den samme forespørgsel). I de fleste tilfælde kan du ret nemt isolere årsagen; udfordringen er ofte at vide at lede efter det i første omgang. I mit næste indlæg vil jeg tale om et lidt andet emne:hvorfor en database gendannet til en "identisk" server kan give forskellige planer for den samme forespørgsel.