Почему UDF намного медленнее, чем подзапрос? - PullRequest
12 голосов
/ 04 февраля 2009

У меня есть случай, когда мне нужно перевести (поиск) несколько значений из одной таблицы. Первый способ, которым я это написал, - это использование подзапросов:

SELECT
    (SELECT id FROM user WHERE user_pk = created_by) AS creator,
    (SELECT id FROM user WHERE user_pk = updated_by) AS updater,
    (SELECT id FROM user WHERE user_pk = owned_by) AS owner,
    [name]
FROM asset

Поскольку я часто использую этот подзапрос (то есть у меня есть около 50 таблиц с этими полями), и мне может понадобиться добавить еще немного кода в подзапрос (например, "AND active = 1"), я думал, что я бы поместил их в пользовательскую функцию UDF и использовал это. Но производительность с использованием этого UDF была ужасной.

CREATE FUNCTION dbo.get_user ( @user_pk INT )
RETURNS INT
AS BEGIN 
    RETURN ( SELECT id
             FROM   ice.dbo.[user]
             WHERE  user_pk = @user_pk )
END

SELECT dbo.get_user(created_by) as creator, [name]
FROM asset

Производительность # 1 составляет менее 1 секунды. Производительность # 2 составляет около 30 секунд ...

Почему, или, что более важно, можно ли как-нибудь кодировать в SQL Server 2008, чтобы мне не приходилось использовать так много подзапросов?

Edit:

Просто небольшое объяснение того, когда это полезно. Этот простой запрос (то есть get userid) становится намного сложнее, когда я хочу получить текст для пользователя, так как мне нужно объединиться с профилем, чтобы получить язык, с компанией, чтобы посмотреть, должен ли язык быть выбранным. ' оттуда вместо этого и с таблицей перевода, чтобы получить переведенный текст. И для большинства этих запросов производительность является второстепенной проблемой для удобочитаемости и удобства обслуживания.

Ответы [ 4 ]

32 голосов
/ 04 февраля 2009

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

Одна из многих статей по этой проблеме: « Скалярные функции, встраивание и производительность: занимательный заголовок для скучного сообщения ».

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

То, что вы действительно хотите, это:

select 
   uc.id as creator,
   uu.id as updater,
   uo.id as owner,
   a.[name]
from
    asset a
    JOIN
    user uc ON uc.user_pk = a.created_by
    JOIN
    user uu ON uu.user_pk = a.updated_by
    JOIN
    user uo ON uo.user_pk = a.owned_by

Обновление февраль 2019

SQL Server 2019 начинает исправлять эту проблему.

12 голосов
/ 04 февраля 2009

Как предлагали другие авторы, использование объединений определенно даст вам наилучшую общую производительность.

Однако, поскольку вы заявили, что вам не нужна головная боль при поддержке 50-ти подобных аналогичных объединений или подзапросов, попробуйте использовать встроенную табличную функцию следующим образом:

CREATE FUNCTION dbo.get_user_inline (@user_pk INT)
RETURNS TABLE AS
RETURN
(
    SELECT TOP 1 id
    FROM ice.dbo.[user]
    WHERE user_pk = @user_pk
        -- AND active = 1
)

Ваш исходный запрос станет примерно таким:

SELECT
    (SELECT TOP 1 id FROM dbo.get_user_inline(created_by)) AS creator,
    (SELECT TOP 1 id FROM dbo.get_user_inline(updated_by)) AS updater,
    (SELECT TOP 1 id FROM dbo.get_user_inline(owned_by)) AS owner,
    [name]
FROM asset

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

Производительность должна быть примерно эквивалентна вашему исходному запросу, но любые будущие изменения могут быть внесены в UDF, что сделает его более удобным для обслуживания.

2 голосов
/ 04 февраля 2009

Чтобы получить тот же результат (NULL, если пользователь удален или не активен).

 select 
    u1.id as creator,
    u2.id as updater,
    u3.id as owner,
    [a.name]
 FROM asset a
        LEFT JOIN user u1 ON (u1.user_pk = a.created_by AND u1.active=1) 
        LEFT JOIN user u2 ON (u2.user_pk = a.created_by AND u2.active=1) 
        LEFT JOIN user u3 ON (u3.user_pk = a.created_by AND u3.active=1) 
0 голосов
/ 04 февраля 2009

Я что-то упустил? Почему это не работает? Вы выбираете только тот идентификатор, который у вас уже есть в таблице:

select created_by as creator, updated_by as updater, 
owned_by as owner, [name]
from asset

Кстати, при проектировании вы действительно должны избегать ключевых слов, таких как name, в качестве имен полей.

...