На 27 юни PASS Performance Virtual Chapter проведе своето лятно изпълнение Palooza за 2013 г. – нещо като намален 24 часа PASS, но фокусиран единствено върху теми, свързани с представянето. Проведох сесия, озаглавена „10 лоши навика, които могат да убият производителността“, третирайки следните 10 концепции:
- ИЗБЕРЕТЕ *
- Слепи индекси
- Без префикс на схема
- Опции на курсора по подразбиране
- sp_ префикс
- Разрешаване на раздуване на кеша
- Широки типове данни
- По подразбиране на SQL сървъра
- Прекомерна употреба на функции
- „Работи на моята машина“
Може би си спомняте някои от тези теми от такива презентации като моята беседа „Лоши навици и най-добри практики“ или нашите седмични уебинари за настройка на заявки, които бях домакин с Кевин Клайн от началото на юни до тази седмица. (Тези 6 видеоклипа, между другото, ще бъдат достъпни в началото на август в YouTube.)
Моята сесия имаше 351 посетители и получих страхотни отзиви. Исках да се обърна към някои от това.
Първо, проблем с конфигурацията:използвах чисто нов микрофон и нямах представа, че всяко натискане на клавиш ще звучи като гръм. Реших този проблем с по-добро разположение на периферните си устройства, но искам да се извиня на всички засегнати от това.
След това изтеглянията; тестето и пробите се публикуват на сайта на събитието. Те са в долната част на страницата, но можете да ги изтеглите и от тук.
И накрая, това, което следва, е списък с въпроси, които бяха публикувани по време на сесията и исках да се уверя, че съм се обърнал към всички, на които не е било отговорено по време на въпроси и отговори на живо. Извинявам се, че притиснах това за малко по-малко от месец , но имаше много въпроси и не исках да ги публикувам на части.
В:Ако имате процес, който може да има изключително различни входни стойности за дадените параметри и резултатът е, че кешираният план не е оптимален за повечето случаи, най-добре ли е да създадете процедурата С ПРЕКОМПИЛИРАНЕ и да вземете малкия ефективността се удря при всяко изпълнение?
А: Ще трябва да подхождате към това за всеки отделен случай, тъй като наистина ще зависи от различни фактори (включително сложността на плана). Също така имайте предвид, че можете да направите прекомпилиране на ниво оператор, така че само засегнатите оператори да поемат удара, за разлика от целия модул. Пол Уайт ми напомни, че хората често "поправят" подслушването на параметри с RECOMPILE
, но твърде често това означава С ПРЕКОМПИЛИРАНЕ
в стил 2000 вместо много по-добрата ОПЦИЯ (ПРЕКОМПИЛИРАНЕ)
, което не само се ограничава до израза, но също така позволява вграждане на параметри, което WITH RECOMPILE
не. Така че, ако ще използвате RECOMPILE
за да предотвратите подслушването на параметри, добавете го към оператора, а не към модула.
А: Както по-горе, това ще зависи от цената и сложността на плановете и няма начин да се каже „Да, винаги ще има голям успех в производителността“. Трябва също така да сравните това с алтернативата.
В:Ако има клъстериран индекс на insertdate, по-късно, когато извличаме данни, използваме функция за преобразуване, ако използваме директно сравнение, датата на заявката не се чете, в реалния свят кой е по-добрият изборА: Не съм сигурен какво означава "четливо в реалния свят". Ако имате предвид, че искате изхода в определен формат, обикновено е по-добре да конвертирате в низ от страна на клиента. C# и повечето други езици, които вероятно използвате на ниво презентация, са повече от способни да форматират изхода дата/час от базата данни в какъвто регионален формат искате.
В:Как се определя колко пъти се използва кеширан план – има ли колона с тази стойност или някакви заявки в интернет, които ще дадат тази стойност? И накрая, ще бъдат ли такива отчитания уместни само след последното рестартиране?А: Повечето DMV са валидни само от последното стартиране на услугата и дори други могат да се промиват по-често (дори при поискване – както по невнимание, така и нарочно). Кешът на плана, разбира се, е в постоянен поток и AFAIK планове, които изпадат от кеша, не поддържат предишния си брой, ако се върнат обратно. Така че дори когато видите план в кеша, аз не съм 100% уверени, че можете да повярвате на броя на употребата, който откривате.
Въпреки това, това, което вероятно търсите, е sys.dm_exec_cached_plans.usecounts
и можете също да намерите sys.dm_exec_procedure_stats.execution_count
за да помогне за допълване на информация за процедури, при които отделни изявления в рамките на процедурите не са открити в кеша.
А: Основните опасения около това са възможността да се използва определен синтаксис, като OUTER APPLY
или променливи с функция с таблично значение. Не са ми известни случаи, при които използването на по-ниска съвместимост има пряко въздействие върху производителността, но няколко неща, които обикновено се препоръчват, са да изградите отново индекси и да актуализирате статистиката (и да накарате вашия доставчик да поддържа по-новото ниво на съвместимост възможно най-скоро). Виждал съм, че разрешава неочаквано влошаване на производителността в значителен брой случаи, но също така съм чувал някои мнения, че това не е необходимо и може би дори неразумно.
А: Не, поне по отношение на производителността, едно изключение, където SELECT *
няма значение, когато се използва вътре в EXISTS
клауза. Но защо бихте използвали *
тук? Предпочитам да използвам EXISTS (SELECT 1 ...
– оптимизаторът ще ги третира по същия начин, но по някакъв начин самодокументира кода и гарантира, че читателите разбират, че подзаявката не връща никакви данни (дори ако пропуснат големия EXISTS
навън). Някои хора използват NULL
, и нямам представа защо започнах да използвам 1, но намирам NULL
също леко неинтуитивен.
*Забележка* ще трябва да бъдете внимателни, ако се опитате да използвате EXISTS (ИЗБЕРЕТЕ *
вътре в модул, който е свързан със схема:
CREATE VIEW dbo.ThisWillNotWork WITH SCHEMABINDING AS SELECT BusinessEntityID FROM Person.Person AS p WHERE EXISTS (SELECT * FROM Sales.SalesOrderHeader AS h WHERE h.SalesPersonID = p.BusinessEntityID);
Получавате тази грешка:
Съобщение 1054, ниво 15, състояние 6, процедура ThisWillNotWork, ред 6Синтаксисът '*' не е разрешен в обекти, свързани със схема.
Въпреки това го промените на SELECT 1
работи добре. Така че може би това е още един аргумент за избягване на SELECT *
дори при този сценарий.
А: Вероятно има стотици на различни езици. Подобно на конвенциите за именуване, стандартите за кодиране са нещо много субективно. Всъщност няма значение каква конвенция решите, че работи най-добре за вас; ако харесвате tbl
префикси, полудя! Предпочитайте Pascal пред bigEndian, имайте го. Искате да поставите префикс на имената на колоните си с типа данни, като intCustomerID
, няма да те спирам. По-важното е да дефинирате конвенция и да я прилагате *последователно.*
Въпреки това, ако искате моите мнения, не ми липсват.
В:XACT_ABORT нещо ли може да се използва в SQL Server 2008 по-нататък?
А: Не знам за никакви планове за оттегляне на XACT_ABORT
така че трябва да продължи да работи добре. Честно казано, не виждам това да се използва много често сега, когато имаме TRY / CATCH
(и ХВЪРЛЯНЕ
от SQL Server 2012).
А: Не тествах това, но в много случаи замяната на скаларна функция с вградена функция с таблично значение може да има голямо влияние върху производителността. Проблемът, който намирам, е, че извършването на този превключвател може да бъде значително количество работа върху системата, която е написана преди APPLY
съществуваше или все още се управлява от хора, които не са възприели този по-добър подход.
А: Имам предвид две неща:(1) забавянето е свързано с времето за компилация или (2) забавянето е свързано с количеството данни, които се зареждат, за да удовлетворят заявката, и първия път, когато трябва да дойдат от диск а не памет. За (1) можете да изпълните заявката в SQL Sentry Plan Explorer и лентата на състоянието ще ви покаже времето за компилиране за първото и следващите извиквания (въпреки че една минута изглежда доста прекомерна за това и малко вероятно). Ако не откриете разлика, тогава това може да е просто естеството на системата:недостатъчно памет, за да поддържа количеството данни, което се опитвате да заредите с тази заявка в комбинация с други данни, които вече са били в буферния пул. Ако не вярвате, че нито едно от тях е проблемът, тогава вижте дали двете различни изпълнения всъщност дават различни планове – ако има някакви разлики, публикувайте плановете на answers.sqlperformance.com и ние ще се радваме да разгледаме . Всъщност заснемането на действителни планове и за двете изпълнения с помощта на Plan Explorer във всеки случай може също да ви уведоми за всякакви разлики в I/O и може да доведе до това, където SQL Server прекарва времето си при първото, по-бавно изпълнение.
В:Получавам подслушване на параметри с помощта на sp_executesql, ще реши ли това Оптимизиране за ad hoc работни натоварвания, тъй като само модулът на плана е в кеша?
А: Не, не мисля, че настройката Оптимизиране за ad hoc работни натоварвания ще помогне на този сценарий, тъй като подслушването на параметри предполага, че последващите изпълнения на същия план се използват за различни параметри и със значително различно поведение на производителността. Оптимизиране за ad hoc работни натоварвания се използва за минимизиране на драстичното въздействие върху кеша на плана, което може да се случи, когато имате голям брой различни SQL изрази. Така че освен ако не говорите за въздействие върху кеша на плана на много различни изрази, които изпращате до sp_executesql
– което не би се характеризирало като подслушване на параметри – мисля, че експериментирам с OPTION (RECOMPILE)
може да има по-добър резултат или, ако знаете стойностите на параметрите, които *правят* дават добри резултати при различни комбинации от параметри, използвайте OPTIMIZE FOR
. Този отговор от Пол Уайт може да даде много по-добра представа.
А: Разбира се, просто включете OPTION (RECOMPILE)
в динамичния SQL текст:
DBCC FREEPROCCACHE; USE AdventureWorks2012; GO SET NOCOUNT ON; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderHeader;'; GO EXEC sp_executesql N'SELECT TOP (1) * INTO #x FROM Sales.SalesOrderDetail OPTION (RECOMPILE);' GO SELECT t.[text], p.usecounts FROM sys.dm_exec_cached_plans AS p CROSS APPLY sys.dm_exec_sql_text(p.[plan_handle]) AS t WHERE t.[text] LIKE N'%Sales.' + 'SalesOrder%';
Резултати:1 ред, показващ Sales.SalesOrderHeader
запитване.
Сега, ако някое изявление в пакета НЕ включва OPTION (RECOMPILE)
, планът все още може да е кеширан, просто не може да се използва повторно.
А: Е, МЕЖДУ
не е семантично еквивалентен на >=И <код> , а по-скоро
>=И <=
, и оптимизира и работи по абсолютно същия начин. Във всеки случай нарочно не използвам BETWEEN
при заявки за период от време – винаги – защото няма начин да го направим отворен диапазон. С BETWEEN
, и двата края са включени и това може да бъде много проблематично в зависимост от основния тип данни (сега или поради някаква бъдеща промяна, за която може да не знаете). Заглавието може да изглежда малко грубо, но се впускам в големи подробности за това в следната публикация в блога:
Какво е общото между МЕЖДУ и дявола?
В:Какво всъщност прави "local fast_forward" в курсора?
А: FAST_FORWARD
всъщност е кратката форма на READ_ONLY
и САМО FORWARD_ONLY
. Ето какво правят те:
ЛОКАЛНО
прави така, че външните обхвати (по подразбиране курсорът еGLOBAL
освен ако не сте променили опцията на ниво екземпляр).САМО ЗА ЧЕТЕНЕ
прави така, че да не можете директно да актуализирате курсора, напр. с помощта наWHERE CURRENT OF
.САМО НАПРЕД
предотвратява възможността за превъртане, напр. с помощта наFETCH PRED
илиИЗВЛЕЧИ АБСОЛЮТНО
вместоFETCH NEXT
.
Задаването на тези опции, както демонстрирах (и писах в блога), може да окаже значително влияние върху производителността. Много рядко виждам курсори в производството, които всъщност трябва да се отклонят от този набор от функции, но обикновено те са написани така, че да приемат много по-скъпите настройки по подразбиране.
В:кое е по-ефективно, курсор или while цикъл?
А: A WHILE
цикълът вероятно ще бъде по-ефективен от еквивалентен курсор с опциите по подразбиране, но подозирам, че ще откриете малка или изобщо разлика, ако използвате LOCAL FAST_FORWARD
. Най-общо казано, WHILE
loop *е* курсор, без да се нарича курсор, и аз предизвиках някои много уважавани колеги да докажат, че греша миналата година. Техните WHILE
циклите не се представиха толкова добре.
А: A usp_
префикс (или всеки префикс, различен от sp_
, или без префикс по този въпрос) *не* има същото въздействие, което демонстрирах. Въпреки това намирам малка стойност в използването на префикс в съхранените процедури, защото много рядко има съмнение, че когато намеря код, който казва EXEC нещо
, че нещо е съхранена процедура – така че има малка стойност там (за разлика, да речем, префиксните изгледи, за да ги разграничат от таблици, тъй като те могат да се използват взаимозаменяемо). Давайки на всяка процедура един и същ префикс, също така е много по-трудно да намерите обекта, който търсите, в, да речем, Object Explorer. Представете си, ако всяко фамилно име в телефонния указател е с префикс с LastName_
– по какъв начин ви помага това?
А: Да! Е, ако сте на SQL Server 2008 или по-нова версия. След като идентифицирате два плана, които са идентични, те все още ще имат отделен plan_handle
стойности. И така, идентифицирайте този, който *не* искате да запазите, копирайте неговия plan_handle
, и го поставете в този DBCC
команда:
DBCC FREEPROCCACHE(0x06.....);В:Използването на if else и т.н. в процедура причинява ли лоши планове, оптимизиран ли е за първо изпълнение и оптимизира ли се само за този път? Така че разделите от кода във всеки IF трябва да бъдат превърнати в отделни процедури?
А: Тъй като SQL Server вече може да извършва оптимизация на ниво оператор, това има по-малко драстичен ефект днес, отколкото при по-старите версии, където цялата процедура трябваше да бъде прекомпилирана като една единица.
В:Понякога открих, че писането на динамичен sql може да бъде по-добро, защото елиминира проблема с подслушването на параметри за sp. Това истина ли е ? Има ли компромиси или други съображения за този сценарий?А: Да, динамичният SQL често може да попречи на подслушването на параметри, особено в случаите, когато масивна заявка за "кухненска мивка" има много допълнителни параметри. Разгледах някои други съображения във въпросите по-горе.
В:Ако имах изчислена колона в моята таблица като DATEPART(моя колона, година) и в индекс върху нея, щеше ли SQL сървър да използва това с SEEK?А: Би трябвало, но разбира се зависи от заявката. Индексът може да не е подходящ за покриване на изходните колони или за удовлетворяване на други филтри, а параметърът, който използвате, може да не е достатъчно селективен, за да оправдае търсене.
В:генерира ли се план за ВСЯКА заявка? Генерира ли се план дори за тривиални?А: Доколкото знам, план се генерира за всяка валидна заявка, дори за тривиални планове, освен ако няма грешка, която пречи на генерирането на план (това може да се случи в множество сценарии, като невалидни подсказки). Дали са кеширани или не (и колко дълго остават в кеша) зависи от редица други фактори, някои от които обсъдих по-горе.
В:Извикването на sp_executesql генерира ли (и използва ли повторно) кеширания план?
А: Да, ако изпратите точно същия текст на заявката, няма значение дали ще го изпратите директно или ще го изпратите чрез sp_executesql
, SQL Server ще кешира и използва повторно плана.
А: Не виждам защо не. Единственото притеснение, което бих имал, е, че при незабавна инициализация на файла разработчиците може да не забележат голям брой събития за автоматично нарастване, което може да отразява лоши настройки за автоматично разрастване, които могат да имат много различно въздействие върху производствената среда (особено ако някой от тези сървъри *не * имат активиран IFI).
В:С функцията в клаузата SELECT ще бъде ли правилно тогава да се каже, че е по-добре да се дублира код?
А: Лично аз бих казал да. Получих много производителност от подмяната на скаларни функции в SELECT
списък с вграден еквивалент, дори в случаите, когато трябва да повторя този код. Както бе споменато по-горе, обаче, в някои случаи може да откриете, че замяната на това с вградена функция с таблична стойност може да ви даде повторно използване на кода без гадно наказание за производителност.
А: Изкривяването на данните може да има фактор и подозирам, че зависи от това какъв вид данни генерирате/симулираш и колко далеч може да бъде изкривяването. Ако имате, да речем, колона varchar(100), която в производството обикновено е дълга 90 знака и генерирането на данни произвежда данни, които са средно 50 (което ще приеме SQL Server), ще откриете много различно влияние върху броя на страници и оптимизация и вероятно не много реалистични тестове.
Но ще бъда честен:този специфичен аспект не е нещо, в което съм инвестирал много време, защото обикновено мога да тормозя пътя си към получаване на реални данни. :-)
В:Всички създадени функции равни ли са при проверка на ефективността на заявката? Ако не, има ли списъци с известни функции, които трябва да избягвате, когато е възможно?А: Не, не всички функции са създадени равни по отношение на производителността. Има три различни типа функции, които можем да създадем (игнорирайки функциите на CLR за момента):
- Скаларни функции с множество изрази
- Функции със стойности на таблица с множество изрази
- Вградени функции със стойности на таблица
Вградените скаларни функции са споменати в документацията, но те са мит и от SQL Server Поне 2014 г. може да се спомене заедно със Sasquatch и чудовището от Лох Нес.
Като цяло, и бих го поставил в шрифт 80pt, ако можех, функциите с вградени стойности на таблица са добри, а другите трябва да се избягват, когато е възможно, тъй като са много по-трудни за оптимизиране.
Функциите могат също да имат различни свойства, които влияят на тяхната производителност, като например дали са детерминирани и дали са свързани със схема.
За много функционални модели определено има съображения за производителност, които трябва да вземете предвид и също така трябва да сте наясно с този елемент Connect, който има за цел да се справи с тях.
В:Можем ли да продължим да извеждаме суми без курсори?А: Да, можем; има няколко метода, различни от курсора (както е описано подробно в моя публикация в блога, Най-добри подходи за извеждане на суми – актуализирано за SQL Server 2012):
- Подзаявка в списъка SELECT
- Рекурсивен CTE
- Самоприсъединете се
- „Странна актуализация“
- Само за SQL Server 2012+:SUM() OVER() (използвайки по подразбиране / RANGE)
- Само SQL Server 2012+:SUM() OVER() (с помощта на ROWS)
Последната опция е най-добрият подход, ако използвате SQL Server 2012; ако не, има ограничения за другите опции без курсор, които често правят курсора по-привлекателен избор. Например, странният метод за актуализиране е недокументиран и не е гарантирано, че ще работи в реда, който очаквате; рекурсивният CTE изисква да няма пропуски в какъвто и последователен механизъм да използвате; и подходите за подзаявка и самостоятелно присъединяване просто не се мащабират.