Това е един от онези религиозни/политически дебати, които бушуват от години:трябва ли да използвам съхранени процедури или трябва да поставя ad hoc заявки в приложението си? Винаги съм бил привърженик на съхранените процедури по няколко причини:
- Не мога да внедря защита от инжектиране на SQL, ако заявката е конструирана в кода на приложението. Разработчиците може да са наясно с параметризираните заявки, но нищо не ги принуждава да ги използват правилно.
- Не мога да настроя заявка, която е вградена в изходния код на приложението, нито мога да наложа най-добри практики.
- Ако намеря възможност за настройка на заявка, за да я разгърна, трябва да компилирам и повторно разгръщам кода на приложението, вместо просто да променя съхранената процедура.
- Ако заявката се използва на множество места в приложението или в множество приложения и изисква промяна, трябва да я променя на няколко места, докато при съхранена процедура трябва да я променя само веднъж (проблеми с внедряването настрана).
Виждам също, че много хора се отказват от съхранените процедури в полза на ORM. За прости приложения това вероятно ще върви добре, но тъй като приложението ви става по-сложно, вероятно ще откриете, че избраният от вас ORM е просто неспособен да изпълнява определени модели на заявки, което ви принуждава да използвате съхранена процедура. Ако поддържа съхранени процедури, това е.
Въпреки че все още намирам всички тези аргументи за доста убедителни, те не са това, за което искам да говоря днес; Искам да говоря за представянето.
Много аргументи там просто ще кажат:"запазените процедури работят по-добре!" Това може да е било малко вярно в някакъв момент, но тъй като SQL Server добави възможността за компилиране на ниво израз, а не на ниво обект, и придоби мощна функционалност като optimize for ad hoc workloads
, това вече не е много силен аргумент. Настройката на индекса и разумните модели на заявки имат много по-голямо влияние върху производителността, отколкото изборът да се използва съхранена процедура някога; в съвременните версии се съмнявам, че ще откриете много случаи, в които точно същата заявка показва забележими разлики в производителността, освен ако не въвеждате и други променливи (като изпълнение на процедура локално срещу приложение в различен център за данни на различен континент).
Въпреки това има аспект на производителността, който често се пренебрегва при работа със ad hoc заявки:кешът на плана. Можем да използваме optimize for ad hoc workloads
за да предотвратим запълване на кеша ни от планове за еднократна употреба (Kimberly Tripp (@KimberlyLTripp) от SQLskills.com има страхотна информация за това тук) и това засяга плановете за еднократна употреба, независимо дали заявките се изпълняват от съхранена процедура или се изпълняват ad hoc. Различно въздействие, което може да не забележите, независимо от тази настройка, е когато еднак плановете заемат множество слотове в кеша поради разликите в SET
опции или незначителни делти в действителния текст на заявката. Целият феномен "бавно в приложението, бързо в SSMS" помогна на много хора да разрешат проблеми, включващи настройки като SET ARITHABORT
. Днес исках да говоря за разликите в текста на заявките и да демонстрирам нещо, което изненадва хората всеки път, когато го споменавам.
Кеш за запис
Да кажем, че имаме много проста система, работеща с AdventureWorks2012. И само за да докажем, че това не помага, активирахме optimize for ad hoc workloads
:
EXEC sp_configure 'показване на разширени опции', 1;GORECONFIGURE С OVERRIDE;GOEXEC sp_configure 'оптимизиране за ad hoc работни натоварвания', 1;GORECONFIGURE С OVERRIDE;
И след това освободете кеша на плана:
DBCC FREEPROCCACHE;
Сега генерираме няколко прости варианта на заявка, която иначе е идентична. Тези вариации потенциално могат да представляват стилове на кодиране за двама различни разработчици – леки разлики в бялото пространство, главни/малки букви и т.н.
SELECT TOP (1) SalesOrderID, OrderDate, SubTotalFROM Sales.SalesOrderHeaderWHERE SalesOrderID>=75120ORDER BY OrderDate DESC;GO -- промяна>=75120 на> 75119 (същата логика, тъй като това е INT)Sales TOP, Sales TOP SELECT OrderDate, SubTotalFROM Sales.SalesOrderHeaderWHERE SalesOrderID> 75119ORDER BY OrderDate DESC;GO -- променете заявката на всички малки букви GO изберете отгоре (1) salesorderid, orderdate, subtotalfrom sales.salesorderheader, където salesorderid> 75119 отстранете около поръчката от GO11 аргументът за topGO изберете топ 1 salesorderid, orderdate, subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GO -- добавете интервал след top 1GO изберете top 1 salesorderid, orderdate, subtotalfrom sales.salesorderheader7 salesorderid desc; -- премахнете интервалите между запетаитеGO изберете топ 1 salesorderid,orderdate,subtotalfrom sales.salesorderheaderwhere salesorderid> 75119order by orderdate desc;GOпредварително>Ако стартираме тази партида веднъж и след това проверим кеша на плана, виждаме, че имаме 6 копия на по същество същия план за изпълнение. Това е така, защото текстът на заявката е двоично хеширан, което означава, че регистърът на буквите и празното пространство правят разлика и могат да направят иначе идентичните заявки да изглеждат уникални за SQL Server.
ИЗБЕРЕТЕ [текст], size_in_bytes, usecounts, cacheobjtypeFROM sys.dm_exec_cached_plans КАТО pCROSS ПРИЛОЖИ sys.dm_exec_sql_text(p.plan_handle) КАТО tWHERE LOWER(t.[text]) КАТО '%ales';Резултати:
текст | размер_в_байтове | usecounts | cacheobjtype |
---|---|---|---|
изберете топ 1 за продажба, o… | 272 | 1 | Компилиран план за части |
изберете топ 1 за продажба, … | 272 | 1 | Компилиран план за части |
изберете топ 1 за продажба или поръчка, o… | 272 | 1 | Компилиран план за части |
изберете отгоре (1) salesorderid,… | 272 | 1 | Компилиран план за части |
ИЗБЕРЕТЕ ТОП (1) SalesOrderID,… | 272 | 1 | Компилиран план за части |
ИЗБЕРЕТЕ ТОП (1) SalesOrderID,… | 272 | 1 | Компилиран план за части |
Резултати след първото изпълнение на "идентични" заявки
Така че това не е напълно разточително, тъй като настройката ad hoc позволява на SQL Server да съхранява само малки мъничета при първо изпълнение. Ако стартираме пакета отново (без да освобождаваме кеша на процедурите), виждаме малко по-тревожен резултат:
текст | размер_в_байтове | usecounts | cacheobjtype |
---|---|---|---|
изберете топ 1 за продажба, o… | 49 152 | 1 | Компилиран план |
изберете топ 1 за продажба, … | 49 152 | 1 | Компилиран план |
изберете топ 1 за продажба или поръчка, o… | 49 152 | 1 | Компилиран план |
изберете отгоре (1) salesorderid,… | 49 152 | 1 | Компилиран план |
ИЗБЕРЕТЕ ТОП (1) SalesOrderID,… | 49 152 | 1 | Компилиран план |
ИЗБЕРЕТЕ ТОП (1) SalesOrderID,… | 49 152 | 1 | Компилиран план |
Резултати след второ изпълнение на "идентични" заявки
Същото се случва и за параметризираните заявки, независимо дали параметризацията е проста или принудителна. И същото се случва, когато настройката ad hoc не е активирана, освен че се случва по-рано.
Крайният резултат е, че това може да доведе до много раздуване на кеша на плана, дори за заявки, които изглеждат идентични – чак до две заявки, при които единият разработчик прави отстъп с табулатор, а другият отстъпва с 4 интервала. Не е нужно да ви казвам, че опитът за налагане на този тип последователност в екипа може да бъде навсякъде от досаден до невъзможен. Така че според мен това дава силен знак за модулиране, отстъпване на DRY и централизиране на този тип заявка в една съхранена процедура.
Предупреждение
Разбира се, ако поставите тази заявка в съхранена процедура, ще имате само едно копие от нея, така че напълно избягвате възможността да имате множество версии на заявката с малко по-различен текст на заявката. Сега можете също да твърдите, че различни потребители могат да създадат една и съща съхранена процедура с различни имена и във всяка съхранена процедура има лека вариация на текста на заявката. Макар че е възможно, мисля, че това представлява съвсем различен проблем. :-)