Sqlserver
 sql >> база данни >  >> RDS >> Sqlserver

Скаларно вграждане на UDF в SQL Server 2019

Скаларните UDF винаги са били нож с две остриета – те са чудесни за разработчиците, които успяват да абстрахират досадната логика, вместо да я повтарят през всичките си заявки, но са ужасни за производителността по време на изпълнение в производството, защото оптимизаторът не не се справя добре с тях. По същество това, което се случва, е изпълненията на UDF да се държат отделно от останалата част от плана за изпълнение и така те се извикват веднъж за всеки ред и не могат да бъдат оптимизирани въз основа на прогнозен или действителен брой редове или сгънати в останалата част от плана.

Тъй като, въпреки най-добрите ни усилия след SQL Server 2000, не можем ефективно да спрем използването на скаларни UDF, не би ли било чудесно да накараме SQL Server просто да се справя с тях по-добре?

SQL Server 2019 въвежда нова функция, наречена Scalar UDF Inlining. Вместо да държи функцията отделна, тя е включена в цялостния план. Това води до много по-добър план за изпълнение и от своя страна по-добра производителност по време на изпълнение.

Но първо, за да илюстрираме по-добре източника на проблема, нека започнем с двойка прости таблици само с няколко реда, в база данни, работеща на SQL Server 2017 (или на 2019 г., но с по-ниско ниво на съвместимост):

CREATE DATABASE Whatever;
GO
ALTER DATABASE Whatever SET COMPATIBILITY_LEVEL = 140;
GO
USE Whatever;
GO
 
CREATE TABLE dbo.Languages
(
  LanguageID int PRIMARY KEY,
  Name sysname
);
 
CREATE TABLE dbo.Employees
(
  EmployeeID int PRIMARY KEY,
  LanguageID int NOT NULL FOREIGN KEY REFERENCES dbo.Languages(LanguageID)
);
 
INSERT dbo.Languages(LanguageID, Name) VALUES(1033, N'English'), (45555, N'Klingon');
 
INSERT dbo.Employees(EmployeeID, LanguageID)
  SELECT [object_id], CASE ABS([object_id]%2) WHEN 1 THEN 1033 ELSE 45555 END 
  FROM sys.all_objects;

Сега имаме проста заявка, където искаме да покажем на всеки служител и името на основния им език. Да кажем, че тази заявка се използва на много места и/или по различни начини, така че вместо да вграждаме присъединяване в заявката, ние пишем скаларен UDF, за да абстрахираме това присъединяване:

CREATE FUNCTION dbo.GetLanguage(@id int)
RETURNS sysname
AS
BEGIN
  RETURN (SELECT Name FROM dbo.Languages WHERE LanguageID = @id);
END

Тогава нашата действителна заявка изглежда така:

SELECT TOP (6) EmployeeID, Language = dbo.GetLanguage(LanguageID)
  FROM dbo.Employees;

Ако погледнем плана за изпълнение на заявката, нещо странно липсва:

План за изпълнение, показващ достъп до служители, но не и до езици

Как се осъществява достъп до таблицата Езици? Този план изглежда много ефективен, защото – подобно на самата функция – абстрахира част от включената сложност. Всъщност този графичен план е идентичен със заявка, която просто присвоява константа или променлива на Language колона:

SELECT TOP (6) EmployeeID, Language = N'Sanskrit'
  FROM dbo.Employees;

Но ако изпълните проследяване спрямо оригиналната заявка, ще видите, че всъщност има шест извиквания към функцията (по едно за всеки ред) в допълнение към основната заявка, но тези планове не се връщат от SQL Server.

Можете също да потвърдите това, като проверите sys.dm_exec_function_stats , но това не е гаранция :

SELECT [function] = OBJECT_NAME([object_id]), execution_count 
  FROM sys.dm_exec_function_stats
  WHERE object_name(object_id) IS NOT NULL;
function         execution_count
-----------      ---------------
GetLanguage                    6

SentryOne Plan Explorer ще покаже изявленията, ако генерирате действителен план от продукта, но ние можем да ги получим само от trace и все още няма събрани или показани планове за отделните извиквания на функции:

Проследяване на оператори за отделни скаларни извиквания на UDF

Всичко това ги прави много трудни за отстраняване на проблеми, защото трябва да ги преследвате, дори когато вече знаете, че са там. Освен това може да създаде истинска бъркотия в анализа на ефективността, ако сравнявате два плана въз основа на неща като прогнозни разходи, тъй като не само съответните оператори се крият от физическата диаграма, но и разходите не са включени никъде в плана.

Бързо пренасочване към SQL Server 2019

След всички тези години на проблемно поведение и неясни първопричини, те са направили така, че някои функции да могат да бъдат оптимизирани в цялостния план за изпълнение. Скаларното UDF Inlining прави обектите, до които имат достъп, видими за отстраняване на неизправности *и* им позволява да бъдат сгънати в стратегията на плана за изпълнение. Сега оценките на мощността (въз основа на статистически данни) позволяват стратегии за присъединяване, които просто не са били възможни, когато функцията е била извикана веднъж за всеки ред.

Можем да използваме същия пример като по-горе, или да създадем същия набор от обекти в база данни на SQL Server 2019, или да изчистим кеша на плана и да повишим нивото на съвместимост до 150:

ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
GO
ALTER DATABASE Whatever SET COMPATIBILITY_LEVEL = 150;
GO

Сега, когато стартираме отново нашата шестредова заявка:

SELECT TOP (6) EmployeeID, Language = dbo.GetLanguage(LanguageID)
  FROM dbo.Employees;

Получаваме план, който включва таблицата за езици и разходите, свързани с достъпа до нея:

План, който включва достъп до обекти, посочени в скаларния UDF

Тук оптимизаторът е избрал присъединяване на вложени цикли, но при различни обстоятелства може да избере различна стратегия за присъединяване, да обмисли паралелизъм и да е по същество свободен да промени напълно формата на плана. Малко вероятно е да видите това в заявка, която връща 6 реда и по никакъв начин не представлява проблем с производителността, но в по-голям мащаб би могло.

Планът отразява, че функцията не се извиква на ред – докато търсенето всъщност се изпълнява шест пъти, можете да видите, че самата функция вече не се показва в sys.dm_exec_function_stats . Един недостатък, който можете да премахнете, е, че ако използвате този DMV, за да определите дали дадена функция се използва активно (както често правим за процедури и индекси), това вече няма да е надеждно.

Предупреждения

Не всяка скаларна функция е вградена и дори когато функция * е* нелинейна, тя не е задължително да бъде вградена във всеки сценарий. Това често е свързано или със сложността на функцията, сложността на включената заявка или комбинацията от двете. Можете да проверите дали дадена функция е вградена в sys.sql_modules изглед на каталог:

SELECT OBJECT_NAME([object_id]), definition, is_inlineable
  FROM sys.sql_modules;

И ако по някаква причина не искате определена функция (или която и да е функция в база данни) да бъде вградена, не е нужно да разчитате на нивото на съвместимост на базата данни, за да контролирате това поведение. Никога не съм харесвал това хлабаво свързване, което е подобно на смяна на стаи, за да гледате различно телевизионно предаване, вместо просто да сменяте канала. Можете да контролирате това на ниво модул, като използвате опцията INLINE:

ALTER FUNCTION dbo.GetLanguage(@id int)
RETURNS sysname
WITH INLINE = OFF
AS
BEGIN
  RETURN (SELECT Name FROM dbo.Languages WHERE LanguageID = @id);
END
GO

И можете да контролирате това на ниво база данни, но отделно от нивото на съвместимост:

ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = OFF;

Въпреки че ще трябва да имате доста добър случай на използване, за да замахнете този чук, IMHO.

Заключение

Сега, аз не предлагам да отидете и да абстрахирате всяка част от логиката в скаларен UDF и да приемете, че сега SQL Server просто ще се погрижи за всички случаи. Ако имате база данни с много скаларно използване на UDF, трябва да изтеглите най-новия SQL Server 2019 CTP, да възстановите резервно копие на вашата база данни там и да проверите DMV, за да видите колко от тези функции ще бъдат интегрирани, когато дойде времето. Това може да бъде основен момент следващия път, когато спорите за надстройка, тъй като по същество ще получите обратно цялата тази производителност и загубата на време за отстраняване на неизправности.

Междувременно, ако страдате от скаларна производителност на UDF и няма да надграждате до SQL Server 2019 скоро, може да има други начини да помогнете за смекчаване на проблема(ите).

Забележка:Написах и поставих тази статия на опашка, преди да разбера, че вече съм публикувал друга статия на друго място.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Връщане на списък с всички сървърни тригери в SQL Server

  2. Ред на изпълнение на SQL заявката

  3. Разлика между sys.views, sys.system_views и sys.all_views в SQL Server

  4. Типове курсори на SQL Server - Какво представляват статичните курсори в SQL Server | Урок за SQL Server / Урок за TSQL

  5. Извличане на данни от съхранена процедура, която има множество набори от резултати