Вопрос

Почему скалярнозначные функции, по-видимому, заставляют запросы выполняться кумулятивно медленнее, чем больше раз подряд они используются?

У меня есть эта таблица, которая была создана на основе данных, приобретенных у третьей стороны.

Я кое-что урезал, чтобы сделать этот пост короче...но просто для того, чтобы вы получили представление о том, как все устроено.

CREATE TABLE [dbo].[GIS_Location](
        [ID] [int] IDENTITY(1,1) NOT NULL, --PK
        [Lat] [int] NOT NULL,
        [Lon] [int] NOT NULL,
        [Postal_Code] [varchar](7) NOT NULL,
        [State] [char](2) NOT NULL,
        [City] [varchar](30) NOT NULL,
        [Country] [char](3) NOT NULL,

CREATE TABLE [dbo].[Address_Location](
    [ID] [int] IDENTITY(1,1) NOT NULL, --PK
    [Address_Type_ID] [int] NULL,
    [Location] [varchar](100) NOT NULL,
    [State] [char](2) NOT NULL,
    [City] [varchar](30) NOT NULL,
    [Postal_Code] [varchar](10) NOT NULL,
    [Postal_Extension] [varchar](10) NULL,
    [Country_Code] [varchar](10) NULL,

Тогда у меня есть две функции, которые ищут LAT и LON.

CREATE FUNCTION [dbo].[usf_GIS_GET_LAT]
(
    @City VARCHAR(30),
    @State CHAR(2)
)
RETURNS INT 
WITH EXECUTE AS CALLER
AS
BEGIN
    DECLARE @LAT INT

    SET @LAT = (SELECT TOP 1 LAT FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City)

RETURN @LAT
END


CREATE FUNCTION [dbo].[usf_GIS_GET_LON]
(
    @City VARCHAR(30),
    @State CHAR(2)
)
RETURNS INT 
WITH EXECUTE AS CALLER
AS
BEGIN
    DECLARE @LON INT

    SET @LON = (SELECT TOP 1 LON FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City)

RETURN @LON
END

Когда я запускаю следующее...

SET STATISTICS TIME ON

SELECT
    dbo.usf_GIS_GET_LAT(City,[State]) AS Lat,
    dbo.usf_GIS_GET_LON(City,[State]) AS Lon
FROM
    Address_Location WITH(NOLOCK)
WHERE
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)

SET STATISTICS TIME OFF

100 ~= 8 мс, 200 ~= 32 мс, 400 ~= 876 мс

--Редактировать Извините, я должен был выразиться более ясно.Я не собираюсь настраивать запрос, указанный выше.Это всего лишь пример, показывающий, что время выполнения становится медленнее, чем больше записей оно просматривает.В приложении реального мира функции используются как часть предложения where для построения радиуса вокруг города и штата, включающего все записи в этом регионе.

Это было полезно?

Решение

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

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

Как побочный эффект встроенности, они не могут содержать никакого процедурного кода (нет объявления @variable;установите @variable = ..;возврат).Однако они могут возвращать несколько строк и столбцов.

Вы могли бы переписать свои функции примерно так:

create function usf_GIS_GET_LAT(
    @City varchar (30),
    @State char (2)
)
returns table
as return (
  select top 1 lat
  from GIS_Location with (nolock) 
  where [State] = @State
    and [City] = @City
);

GO

create function usf_GIS_GET_LON (
    @City varchar (30),
    @State char (2)
)
returns table
as return (
  select top 1 LON
  from GIS_Location with (nolock)
  where [State] = @State
    and [City] = @City
);

Синтаксис их использования также немного отличается:

select
    Lat.Lat,
    Lon.Lon
from
    Address_Location with (nolock)
    cross apply dbo.usf_GIS_GET_LAT(City,[State]) AS Lat
    cross apply dbo.usf_GIS_GET_LON(City,[State]) AS Lon
WHERE
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)

Другие советы

Они этого не делают.

В скалярных функциях нет ошибки, которая приводила бы к экспоненциальному снижению производительности в зависимости от количества строк в скалярной функции, с которой выполняется.Попробуйте свои тесты еще раз и взгляните на SQL profiler, просмотрев столбцы CPU, READS и DURATION.Увеличьте размер теста, чтобы включить тесты, которые занимают больше секунды, двух секунд, пяти секунд.

CREATE FUNCTION dbo.slow
(
    @ignore int
)
RETURNS INT 
AS
BEGIN
    DECLARE @slow INT
    SET @slow = (select count(*) from sysobjects a 
        cross join sysobjects b 
        cross join sysobjects c 
        cross join sysobjects d 
        cross join sysobjects e 
        cross join sysobjects f
    where a.id = @ignore) 

    RETURN @slow
END
go
SET STATISTICS TIME ON

select top 1 dbo.slow(id)
from sysobjects
go
select top 5 dbo.slow(id)
from sysobjects
go
select top 10 dbo.slow(id)
from sysobjects
go
select top 20 dbo.slow(id)
from sysobjects
go
select top 40 dbo.slow(id)
from sysobjects

SET STATISTICS TIME OFF

Выходной сигнал

SQL Server Execution Times:
   CPU time = 203 ms,  elapsed time = 202 ms.


SQL Server Execution Times:
   CPU time = 889 ms,  elapsed time = 939 ms.

SQL Server Execution Times:
   CPU time = 1748 ms,  elapsed time = 1855 ms.

SQL Server Execution Times:
   CPU time = 3541 ms,  elapsed time = 3696 ms.


SQL Server Execution Times:
   CPU time = 7207 ms,  elapsed time = 7392 ms.

Имейте в виду, что если вы запускаете скалярную функцию для строк в результирующем наборе, скалярная функция будет выполняться для каждой строки без глобальной оптимизации.

Вы можете обернуть свою функциональность во встроенный TVF, это будет намного быстрее:

http://sqlblog.com/blogs/alexander_kuznetsov/archive/2008/05/23/reuse-your-code-with-cross-apply.aspx

вы вызываете функцию два раза (два обращения select к базе данных) для каждой строки в результирующем наборе.

чтобы ускорить выполнение запроса, перейдите прямо к GIS_Location и пропустите функции:

SELECT
    g.Lat,
    g.Lon
FROM
    Address_Location        l WITH(NOLOCK)
    INNER JOIN GIS_Location g WITH(NOLOCK) WHERE l.State = g.State AND l.City = g.City
WHERE
    ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)

Я не уверен, почему предложение NOLOCK или crazy where я просто скопировал из вопроса...

Проще говоря, потому что SQL-выражения с пользовательскими функциями менее эффективны, чем SQL-выражения без них.Логика выполнения не может быть оптимизирована;и служебные данные функции (включая протоколы вызова) должны быть понесены для каждой строки.

Совет КМике очень хорош.ГДЕ ..IN (ВЫБРАТЬ что-либо) вряд ли будет эффективным шаблоном, и в этом случае его можно легко заменить СОЕДИНЕНИЕМ.

Посмотрим, сработает ли это лучше...Или, может быть, отдельное внутреннее соединение?

select a.*,
(select top 1 g.Lat from GIS_Location g where g.City = a.City and g.State = a.State) as Lat,
(select top 1 g.Lon from GIS_Location g where g.City = a.City and g.State = a.State) as Lon
from Address_Location a
where a.ID in (select top 100 ID from Address_Location order by ID desc)

Что касается производительности скалярной функции, я не уверен.

Обычно скалярные функции работают намного медленнее, чем встроенные аналоги TVF.К счастью, для многих сценариев это изменится.

SQL Server 2019 представит Скалярная вставка UDF:

Функция из набора функций интеллектуальной обработки запросов. Эта функция повышает производительность запросов, которые вызывают скалярные UDF-файлы в SQL Server (начиная с предварительного просмотра SQL Server 2019).

Скалярные пользовательские функции T-SQL

Определяемые пользователем функции, которые реализованы в Transact-SQL и возвращают единственное значение данных, называются скалярными пользовательскими функциями T-SQL. T-SQL UDFS - это элегантный способ добиться повторного использования кода и модульности во всех SQL-запросах.Некоторые вычисления (например, сложные бизнес-правила) проще выразить в императивной форме UDF.UDFS помогают в создании сложной логики, не требуя опыта в написании сложных SQL-запросов.

Скалярные UDF-файлы обычно в конечном итоге работают плохо по следующим причинам.

  • Итеративный вызов
  • Отсутствие затрат
  • Интерпретируемое исполнение
  • Последовательное выполнение

Автоматическое встраивание скалярных UDF-файлов

Целью функции встраивания Scalar UDF является повышение производительности запросов, которые вызывают скалярные UDF-файлы T-SQL, где выполнение UDF является основным узким местом.

Благодаря этой новой функции скалярные UDF автоматически преобразуются в скалярные выражения или скалярные подзапросы, которые подставляются в вызывающем запросе вместо оператора UDF.Затем эти выражения и подзапросы оптимизируются. В результате план запроса больше не будет иметь определяемой пользователем функции operator, но его эффекты будут наблюдаться в плане, как представления или встроенные TVFS.


Встроенные скалярные требования к UDFS

Скаляр Т-компонент SQL UDF могут быть рядными, если выполнены все следующие условия являются правдой:

  • UDF записывается с использованием следующих конструкций:

    1. ОБЪЯВЛЯТЬ, УСТАНАВЛИВАТЬ:Объявление переменных и их назначения.
    2. ВЫБЕРИТЕ:SQL-запрос с присвоением одной /нескольких переменных1.
    3. ЕСЛИ/ELSE:Ветвление с произвольными уровнями вложенности.
    4. Возврат:Один или несколько операторов возврата.
    5. UDF:Вызовы вложенных/рекурсивных функций2.
    6. Прочее:Реляционные операции, такие как EXISTS, ISNULL.
  • UDF не вызывает никакой встроенной функции, которая либо зависит от времени (например, GETDATE()), либо имеет побочные эффекты3 (например, NEWSEQUENTIALID()).

  • UDF использует предложение EXECUTE AS CALLER (поведение по умолчанию, если предложение EXECUTE AS не указано).
  • UDF не ссылается на табличные переменные или параметры с табличными значениями.
  • Запрос, вызывающий скалярный UDF, не ссылается на скалярный вызов UDF в своем предложении GROUP BY.
  • UDF не компилируется изначально (взаимодействие поддерживается).
  • UDF не используется в вычисляемом столбце или определении ограничения проверки.
  • UDF не ссылается на пользовательские типы.
  • В UDF не добавлено никаких подписей.
  • UDF не является функцией разбиения на разделы.

Проверка того, является ли функция встроенной:

SELECT OBJECT_NAME([object_id]) AS name, is_inlineable
FROM sys.sql_modules
WHERE [object_id] = OBJECT_ID('schema.function_name')

Включение / выключение функции на уровне базы данных:

ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = ON/OFF;
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top