Det här är en av de religiösa/politiska debatterna som har rasat i flera år:ska jag använda lagrade procedurer, eller ska jag lägga in ad hoc-frågor i min ansökan? Jag har alltid varit en förespråkare för lagrade procedurer, av några anledningar:
- Jag kan inte implementera SQL-injektionsskydd om frågan är konstruerad i applikationskoden. Utvecklarna kan vara medvetna om parametriserade frågor men ingenting tvingar dem att använda dem på rätt sätt.
- Jag kan inte ställa in en fråga som är inbäddad i applikationens källkod, och jag kan inte heller tillämpa några bästa metoder.
- Om jag hittar en möjlighet till justering av frågor måste jag kompilera om och distribuera om applikationskoden för att kunna distribuera den, i motsats till att bara ändra den lagrade proceduren.
- Om frågan används på flera ställen i applikationen, eller i flera applikationer, och den kräver en ändring, måste jag ändra den på flera ställen, medan jag med en lagrad procedur bara behöver ändra den en gång (distributionsproblem åt sidan).
Jag ser också att många människor avstår från lagrade procedurer till förmån för ORM. För enkla applikationer kommer detta förmodligen att gå okej, men när din applikation blir mer komplex kommer du sannolikt att upptäcka att din valda ORM helt enkelt inte är kapabel att utföra vissa frågemönster, vilket *tvingar* dig att använda en lagrad procedur. Om det stöder lagrade procedurer, det vill säga.
Även om jag fortfarande tycker att alla dessa argument är ganska övertygande, är de inte vad jag vill prata om idag; Jag vill prata om prestanda.
Många argument där ute kommer helt enkelt att säga, "lagrade procedurer fungerar bättre!" Det kan ha varit marginellt sant någon gång, men eftersom SQL Server lade till möjligheten att kompilera på satsnivå snarare än objektnivå, och har fått kraftfull funktionalitet som optimize for ad hoc workloads
, detta är inte längre ett särskilt starkt argument. Indexjustering och förnuftiga frågemönster har en mycket större inverkan på prestanda än att välja att använda en lagrad procedur någonsin kommer att göra; på moderna versioner tvivlar jag på att du kommer att hitta många fall där exakt samma fråga uppvisar märkbara prestandaskillnader, såvida du inte också introducerar andra variabler (som att köra en procedur lokalt kontra en applikation i ett annat datacenter på en annan kontinent).
Som sagt, det finns en prestandaaspekt som ofta förbises när man hanterar ad hoc-frågor:plancachen. Vi kan använda optimize for ad hoc workloads
för att förhindra engångsplaner från att fylla upp vår cache (Kimberly Tripp (@KimberlyLTripp) från SQLskills.com har bra information om detta här), och det påverkar engångsplaner oavsett om frågorna körs från en lagrad procedur eller körs ad hoc. En annan effekt som du kanske inte märker, oavsett denna inställning, är när den är identisk planer tar upp flera platser i cachen på grund av skillnader i SET
alternativ eller mindre delta i själva frågetexten. Hela fenomenet "långsamt i applikationen, snabbt i SSMS" har hjälpt många människor att lösa problem som involverar inställningar som SET ARITHABORT
. Idag ville jag prata om skillnader i frågetext och visa något som överraskar människor varje gång jag tar upp det.
Cache att bränna
Låt oss säga att vi har ett väldigt enkelt system som kör AdventureWorks2012. Och bara för att bevisa att det inte hjälper har vi aktiverat optimize for ad hoc workloads
:
EXEC sp_configure 'visa avancerade alternativ', 1;GORECONFIGURE WITH OVERRIDE;GOEXEC sp_configure 'optimera för ad hoc-arbetsbelastningar', 1;GORECONFIGURE WITH OVERRIDE;
Och frigör sedan plancachen:
DBCC FREEPROCCACHE;
Nu genererar vi några enkla varianter av en fråga som annars är identisk. Dessa varianter kan potentiellt representera kodningsstilar för två olika utvecklare – små skillnader i vitt utrymme, versaler/gemener, etc.
SELECT TOP (1) SalesOrderID, OrderDate, SubTotalFROM Sales.SalesOrderHeaderWHERE SalesOrderID>=75120ORDER BY OrderDate DESC;GO -- ändra>=75120 till> 75119 (samma logik eftersom det är ett INT)GO SELECT TOPID ( OrderDate, SubTotalFROM Sales.SalesOrderHeader WHERE SalesOrderID> 75119ORDER BY OrderDate DESC;GO -- ändra frågan till alla gemenerGO välj översta (1) försäljningsorderid, orderdatum, delsumma från försäljning.försäljningsorderrubrik där försäljningsorderid> 75119 beställningen tas bort efter orderdatumet -- GO tar bort beställningen; argumentet för topGO välj topp 1 försäljningsorderid, orderdatum, delsumma från försäljning.försäljningsorderrubrik där försäljningsorderid> 75119order efter orderdatum desc;GO -- lägg till ett mellanslag efter top 1GO välj topp 1 försäljningsorderid, orderdatum, delsumma från försäljning.försäljningsorderrubrik där försäljningsorderid> 7511 de9 orderc; -- ta bort mellanslagen mellan kommateckenGO välj topp 1 försäljningsorderid, orderdatum, delsumma från försäljning.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GOOm vi kör den batchen en gång och sedan kontrollerar planens cache, ser vi att vi har 6 kopior av, i princip, exakt samma exekveringsplan. Detta beror på att frågetexten är binär hashad, vilket betyder att skiftläge och blanksteg gör skillnad och kan få i övrigt identiska frågor att se unika ut för SQL Server.
VÄLJ [text], size_in_bytes, usecounts, cacheobjtypeFROM sys.dm_exec_cached_plans AS pCROSS APPLY sys.dm_exec_sql_text(p.plan_handle) AS TWHERE LOWER(t.[text]) LIKE '%ales''.saleser'%+;Resultat:
text | storlek_i_byte | användningsantal | cacheobjtype |
---|---|---|---|
välj topp 1 försäljningsorderid, o... | 272 | 1 | Kompilerad planstub |
välj topp 1 försäljningsorderid, … | 272 | 1 | Kompilerad planstub |
välj topp 1 försäljningsorderid, o... | 272 | 1 | Kompilerad planstub |
välj topp (1) försäljningsorderid,... | 272 | 1 | Kompilerad planstub |
VÄLJ TOP (1) Försäljningsorder-ID,... | 272 | 1 | Kompilerad planstub |
VÄLJ TOP (1) Försäljningsorder-ID,... | 272 | 1 | Kompilerad planstub |
Resultat efter första körning av "identiska" frågor
Så detta är inte helt slösaktigt, eftersom ad hoc-inställningen har tillåtit SQL Server att endast lagra små stubbar vid första körning. Om vi kör batchen igen (utan att frigöra procedurcachen) ser vi ett något mer alarmerande resultat:
text | storlek_i_byte | användningsantal | cacheobjtype |
---|---|---|---|
välj topp 1 försäljningsorderid, o... | 49 152 | 1 | Kompilerad plan |
välj topp 1 försäljningsorderid, … | 49 152 | 1 | Kompilerad plan |
välj topp 1 försäljningsorderid, o... | 49 152 | 1 | Kompilerad plan |
välj topp (1) försäljningsorderid,... | 49 152 | 1 | Kompilerad plan |
VÄLJ TOP (1) Försäljningsorder-ID,... | 49 152 | 1 | Kompilerad plan |
VÄLJ TOP (1) Försäljningsorder-ID,... | 49 152 | 1 | Kompilerad plan |
Resultat efter andra körning av "identiska" frågor
Samma sak händer för parametrerade frågor, oavsett om parametreringen är enkel eller påtvingad. Och samma sak händer när ad hoc-inställningen inte är aktiverad, förutom att det händer tidigare.
Nettoresultatet är att detta kan producera mycket plancache-uppblåsning, även för frågor som ser identiska ut – ända ner till två frågor där en utvecklare gör indrag med en tabb och den andra indrag med 4 mellanslag. Jag behöver inte berätta att det kan vara allt från tråkigt till omöjligt att försöka genomdriva den här typen av konsistens i ett team. Så i mitt sinne ger detta en stark nick till att modularisera, ge efter för DRY och centralisera den här typen av frågor till en enda lagrad procedur.
En varning
Naturligtvis, om du placerar den här frågan i en lagrad procedur kommer du bara att ha en kopia av den, så att du helt undviker möjligheten att ha flera versioner av frågan med lite olika frågetext. Nu kan du också hävda att olika användare kan skapa samma lagrade procedur med olika namn, och i varje lagrad procedur finns det en liten variation av frågetexten. Även om det är möjligt, tror jag att det representerar ett helt annat problem. :-)