18.5.26

Неудобная правда о скалярных функциях


Автор: Luca Biondi, The Dangerous Truth About Scalar Functions in 45 Seconds

Скалярные пользовательские функции (Scalar UDF) — это тихие убийцы производительности SQL Server. В этой статье я расскажу, почему они скрывают свою истинную стоимость и как превращают быстрый запрос в покадровую катастрофу.

В двух словах

  • Скалярные UDF принудительно вызывают покадровое выполнение (RBAR — Row-By-Agonizing-Row): они обходят мощные алгоритмы оптимизатора, основанные на работе с наборами данных. 
  • Подавление параллелизма (Parallelism): Традиционные скалярные функции заставляют весь ваш запрос выполняться в одном потоке. 
  • Скрытые затраты: Планы выполнения часто показывают стоимость UDF как 0%, маскируя огромные накладные расходы на ЦП. 
  • Исправление: Используйте встроенные табличные функции (Inline Table-Valued Functions, iTVF) или функцию встраивания скалярных функций (Scalar Inlining), появившуюся в SQL Server 2019+. 

Мы все любим чистый код, и обёртывание логики в переиспользуемую скалярную пользовательскую функцию кажется хорошей практикой в разработке программного обеспечения. Однако в мире баз данных этот «чистый код» часто оказывается кошмаром для производительности. Каждый раз, когда вы используете скалярную функцию в предложении SELECT или WHERE, вы, по сути, говорите SQL Server отказаться от своего высокоскоростного движка и работать как электронная таблица 1990-х годов. Давайте разберём механику.

🔍 Диагностика: проблема «чёрного ящика»

SQL Server относится к скалярным UDF как к «чёрному ящику» (Black Box). Когда оптимизатор запросов генерирует план, он не «заглядывает внутрь» функции. Это приводит к двум критическим последствиям:

  • Итеративное выполнение (Iterative Execution): Если в вашей таблице 1 миллион строк, функция будет вызвана 1 миллион раз. Это переключение контекста между движком и контекстом UDF уничтожает пропускную способность (throughput).
  • Последовательные планы (Serial Plans): До выхода SQL Server 2019 запрос, содержащий скалярную UDF, был вынужден выполняться последовательно (serial execution plan). Никакого параллелизма, сколько бы ядер у вас ни было.

Обратитесь к Официальной документации Microsoft о встраивании UDF для более глубокого понимания внутренних механизмов.

🧪 Запрос: скалярная функция против встроенной

Сравните влияние на производительность самостоятельно. Обратите внимание на разницу во «Времени ЦП» в статистике выполнения.

-- Опасный подход со скалярной функцией
CREATE FUNCTION dbo.fn_GetDiscount (@Price MONEY)
RETURNS MONEY
AS
BEGIN
    RETURN (@Price * 0.10);
END;
GO

-- Этот запрос будет выполняться построчно!
SELECT OrderID, dbo.fn_GetDiscount(TotalDue) 
FROM Sales.SalesOrderHeader;
-- Производительный подход со встроенной табличной функцией
CREATE FUNCTION dbo.itvf_GetDiscount (@Price MONEY)
RETURNS TABLE
AS
RETURN (
    SELECT @Price * 0.10 AS Discount
);
GO

-- Позволяет оптимизатору встроить логику в основной запрос
SELECT S.OrderID, D.Discount
FROM Sales.SalesOrderHeader S
CROSS APPLY dbo.itvf_GetDiscount(S.TotalDue) D;

Исправление: современные стратегии

Если вы работаете с SQL Server 2019 (15.x) или новее, многие скалярные функции теперь «встраиваются» автоматически. Однако существует множество требований (отсутствие циклов WHILE, отсутствие GETDATE() и т.д.), которые могут отключить эту функцию.

Правило эксперта: Если ваша функция может быть записана как один оператор RETURN SELECT, всегда используйте встроенную табличную функцию (iTVF). Это заставляет оптимизатор рассматривать логику как часть плана выполнения, включая параллелизм и точную оценку стоимости.

🚀 Моя рабочая стратегия

За 25 лет настройки производительности я видел, как UDF превращали запросы, выполняющиеся за доли секунды, в 10-минутные ожидания. Моя стратегия: Проверьте sys.dm_exec_function_stats. Это динамическое административное представление отслеживает время выполнения ваших функций. Если вы видите функцию с миллионами выполнений и высоким значением total_worker_time — это ваше узкое место. Не доверяйте процентному соотношению стоимости в плане выполнения — когда дело касается UDF, оно лжёт.

Вывод

  • Переключение контекста (Context Switching): Накладные расходы скалярных UDF часто превышают логику, которую они содержат.
  • Убийца параллелизма: Одна скалярная UDF может удерживать выполнение вашего запроса на одном ядре ЦП.
  • Рефакторинг: Преобразуйте скалярные UDF во встроенные табличные функции (iTVF) для немедленного повышения производительности.



Комментариев нет:

Отправить комментарий