sql >> Databáze >  >> RDS >> Sqlserver

Skalární vkládání UDF v SQL Server 2019

Skalární UDF byly vždy dvousečný meč – jsou skvělé pro vývojáře, kteří se dostanou k abstrahování zdlouhavé logiky místo toho, aby ji opakovali ve všech svých dotazech, ale jsou hrozné z hlediska běhového výkonu v produkci, protože optimalizátor to nedokáže. nezacházet s nimi pěkně. V zásadě se stane, že provádění UDF jsou držena odděleně od zbytku plánu provádění, takže jsou volána jednou pro každý řádek a nelze je optimalizovat na základě odhadovaného nebo skutečného počtu řádků nebo je skládat do zbytku plánu.

Vzhledem k tomu, že navzdory našemu nejlepšímu úsilí od SQL Serveru 2000 nejsme schopni účinně zastavit používání skalárních UDF, nebylo by skvělé, aby SQL Server jednoduše zpracovával je lépe?

SQL Server 2019 představuje novou funkci nazvanou Scalar UDF Inlining. Místo toho, aby byla funkce oddělena, je začleněna do celkového plánu. To vede k mnohem lepšímu plánu provádění a následně k lepšímu výkonu za běhu.

Ale nejprve, abychom lépe ilustrovali zdroj problému, začněme s párem jednoduchých tabulek s několika řádky v databázi běžící na SQL Server 2017 (nebo na 2019, ale s nižší úrovní kompatibility):

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;

Nyní máme jednoduchý dotaz, kde chceme ukázat každému zaměstnanci a jméno jeho primárního jazyka. Řekněme, že tento dotaz se používá na mnoha místech a/nebo různými způsoby, takže místo vytvoření spojení do dotazu napíšeme skalární UDF, abychom spojení odstranili:

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

Náš skutečný dotaz pak vypadá asi takto:

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

Pokud se podíváme na plán provádění dotazu, něco tam kupodivu chybí:

Prováděcí plán zobrazující přístup k zaměstnancům, ale ne k jazykům

Jak se přistupuje k tabulce Jazyky? Tento plán vypadá velmi efektivně, protože – stejně jako funkce samotná – abstrahuje část složitosti, kterou s tím souvisí. Ve skutečnosti je tento grafický plán identický s dotazem, který pouze přiřazuje konstantu nebo proměnnou k Language sloupec:

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

Ale pokud spustíte trasování proti původnímu dotazu, uvidíte, že ve skutečnosti existuje šest volání funkce (jedno pro každý řádek) kromě hlavního dotazu, ale tyto plány nejsou vráceny serverem SQL.

Můžete to také ověřit kontrolou sys.dm_exec_function_stats , ale toto není záruka :

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 zobrazí příkazy, pokud vygenerujete skutečný plán z produktu, ale můžeme je získat pouze ze sledování a stále nejsou shromážděny ani zobrazeny žádné plány pro jednotlivá volání funkcí:

Trasování příkazů pro jednotlivá skalární volání UDF

To vše velmi ztěžuje jejich odstraňování, protože je musíte jít lovit, i když už víte, že tam jsou. Pokud porovnáváte dva plány na základě věcí, jako jsou odhadované náklady, může to také způsobit skutečný chaos v analýze výkonu, protože nejen že se relevantní operátoři skrývají před fyzickým diagramem, ani náklady nejsou nikde v plánu zahrnuty.

Rychle vpřed na SQL Server 2019

Po všech těch letech problematického chování a nejasných hlavních příčin se jim to podařilo tak, že některé funkce lze optimalizovat do celkového plánu provádění. Skalární UDF Inlining zviditelní objekty, ke kterým přistupují, pro odstraňování problémů *a* umožňuje jejich začlenění do strategie prováděcího plánu. Nyní odhady mohutnosti (na základě statistik) umožňují strategie spojení, které jednoduše nebyly možné, když byla funkce volána jednou pro každý řádek.

Můžeme použít stejný příklad jako výše, buď vytvořit stejnou sadu objektů v databázi SQL Server 2019, nebo vyčistit mezipaměť plánu a zvýšit úroveň kompatibility na 150:

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

Nyní, když znovu spustíme náš šestiřádkový dotaz:

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

Získáme plán, který zahrnuje tabulku Jazyky a náklady spojené s přístupem k ní:

Plán, který zahrnuje přístup k objektům odkazovaným uvnitř skalárního UDF

Zde optimalizátor zvolil spojení vnořených smyček, ale za jiných okolností mohl zvolit jinou strategii spojení, zvážit paralelismus a v podstatě mohl zcela změnit tvar plánu. Pravděpodobně to neuvidíte v dotazu, který vrací 6 řádků a v žádném případě nepředstavuje problém s výkonem, ale ve větším měřítku ano.

Plán odráží, že funkce není volána na řádek – zatímco hledání je ve skutečnosti provedeno šestkrát, můžete vidět, že samotná funkce se již nezobrazuje v sys.dm_exec_function_stats . Jednou nevýhodou, kterou můžete odstranit, je to, že pokud použijete tento DMV k určení, zda je funkce aktivně používána (jak to často děláme u procedur a indexů), nebude to již spolehlivé.

Upozornění

Ne každá skalární funkce je inlineable, a i když je funkce *inline inline, nemusí být nutně inline v každém scénáři. To často souvisí buď se složitostí funkce, se složitostí dotazu, nebo s kombinací obojího. Můžete zkontrolovat, zda je funkce inlineable v sys.sql_modules zobrazení katalogu:

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

A pokud z jakéhokoli důvodu nechcete, aby byla určitá funkce (nebo jakákoli funkce v databázi) vložena, nemusíte se při řízení tohoto chování spoléhat na úroveň kompatibility databáze. Nikdy se mi nelíbilo to volné spojení, které je podobné přepínání místností a sledování jiného televizního pořadu místo pouhé změny kanálu. Můžete to ovládat na úrovni modulu pomocí možnosti 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

A můžete to ovládat na úrovni databáze, ale odděleně od úrovně kompatibility:

ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = OFF;

I když byste museli mít docela dobrý případ použití, abyste se s tím kladivem rozhoupali, IMHO.

Závěr

Nyní nenaznačuji, že můžete jít a abstrahovat každý kousek logiky pryč do skalárního UDF a předpokládat, že nyní se SQL Server postará o všechny případy. Pokud máte databázi s velkým množstvím skalárního využití UDF, měli byste si stáhnout nejnovější SQL Server 2019 CTP, obnovit tam zálohu databáze a zkontrolovat DMV, abyste viděli, kolik z těchto funkcí bude možné vložit, až přijde čas. Mohlo by to být hlavní odrážka, až se budete příště dohadovat o upgradu, protože v podstatě získáte zpět všechen ten výkon a promarněný čas při odstraňování problémů.

Pokud mezitím trpíte skalárním výkonem UDF a nebudete v dohledné době upgradovat na SQL Server 2019, mohou existovat jiné způsoby, jak tyto problémy zmírnit.

Poznámka:Tento článek jsem napsal a zařadil do fronty, než jsem si uvědomil, že jsem již zveřejnil jiný článek.


  1. 2 způsoby, jak vypsat všechny funkce v MySQL

  2. Jak funguje TIME_TO_SEC() v MariaDB

  3. Příklady GETDATE() v SQL Server (T-SQL)

  4. Rozšíření mysql je zastaralé a bude v budoucnu odstraněno:použijte místo něj mysqli nebo PDO