Почему UDF работает намного медленнее, чем подзапрос?
-
21-08-2019 - |
Вопрос
У меня есть случай, когда мне нужно перевести (выполнить поиск) несколько значений из одной таблицы.Первым способом, которым я это написал, было использование подзапросов:
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 таблиц с этими полями), и мне, возможно, потребуется добавить еще немного кода к подзапросу (например, "И 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, чтобы мне не приходилось использовать так много подзапросов?
Редактировать:
Просто еще немного объясню, когда это полезно.Этот простой запрос (то есть получение идентификатора пользователя) становится намного сложнее, когда я хочу получить текст для пользователя, поскольку мне нужно подключиться к profile, чтобы получить язык, к компании, чтобы узнать, следует ли извлекать язык оттуда, и к таблице перевода, чтобы получить переведенный текст.И для большинства из этих запросов производительность является второстепенной проблемой по сравнению с удобочитаемостью и ремонтопригодностью.
Решение
UDF - это черный ящик для оптимизатора запросов, поэтому он выполняется для каждой строки.Вы перемещаете курсор по строкам.Для каждой строки в активе найдите идентификатор три раза в другой таблице.Это происходит, когда вы используете скалярные или многопроцессорные UDF (встроенные 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 начинает устранять эту проблему.
Другие советы
Как предполагалось в других постерах, использование объединений, безусловно, обеспечит вам наилучшую общую производительность.
Однако, поскольку вы заявили, что вам не нужна головная боль от поддержания 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, что сделает его намного более удобным в обслуживании.
Чтобы получить тот же результат (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)
Я что-то упускаю?Почему это не может сработать?Вы только выбираете идентификатор, который у вас уже есть в таблице:
select created_by as creator, updated_by as updater,
owned_by as owner, [name]
from asset
Кстати, при проектировании вам действительно следует избегать ключевых слов, таких как name
, как имена полей.