Многозначная табличная функция с функцией встроенной таблицы


198

Несколько примеров, чтобы показать, только в случае:

Встроенная таблица оценена

CREATE FUNCTION MyNS.GetUnshippedOrders()
RETURNS TABLE
AS 
RETURN SELECT a.SaleId, a.CustomerID, b.Qty
    FROM Sales.Sales a INNER JOIN Sales.SaleDetail b
        ON a.SaleId = b.SaleId
        INNER JOIN Production.Product c ON b.ProductID = c.ProductID
    WHERE a.ShipDate IS NULL
GO

Многозначная таблица значений

CREATE FUNCTION MyNS.GetLastShipped(@CustomerID INT)
RETURNS @CustomerOrder TABLE
(SaleOrderID    INT         NOT NULL,
CustomerID      INT         NOT NULL,
OrderDate       DATETIME    NOT NULL,
OrderQty        INT         NOT NULL)
AS
BEGIN
    DECLARE @MaxDate DATETIME

    SELECT @MaxDate = MAX(OrderDate)
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = @CustomerID

    INSERT @CustomerOrder
    SELECT a.SalesOrderID, a.CustomerID, a.OrderDate, b.OrderQty
    FROM Sales.SalesOrderHeader a INNER JOIN Sales.SalesOrderHeader b
        ON a.SalesOrderID = b.SalesOrderID
        INNER JOIN Production.Product c ON b.ProductID = c.ProductID
    WHERE a.OrderDate = @MaxDate
        AND a.CustomerID = @CustomerID
    RETURN
END
GO

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

Чтение о них и преимуществах / различиях действительно не было объяснено.


Также одно из огромных преимуществ встроенной функции заключается в том, что вы можете выбирать столбцы ROWID (TIMESTAMP), в то время как вы не можете вставлять данные TIMESTAMP в таблицу возврата в многослойной функции!
Артру

3
Спасибо за отличную ветку. Я многому научился. Однако следует помнить одну вещь: при изменении функции, которая была ITV для MSTV, профилировщик считает, что вы изменяете ITV. Независимо от того, что вы делаете, чтобы получить правильный синтаксис с точки зрения MSTV, перекомпиляция всегда завершается неудачно, обычно вокруг первого оператора после BEGIN. Единственный способ обойти это - УДАЛИТЬ старую функцию и СОЗДАТЬ новую как MSTV.
Fandango68

Ответы:


141

Исследуя комментарий Мэтта, я пересмотрел свое первоначальное утверждение. Он прав, будет разница в производительности между встроенной табличной функцией (ITVF) и многозначной табличной функцией (MSTVF), даже если они оба просто выполняют оператор SELECT. SQL Server будет рассматривать ITVF какVIEWв том смысле, что он рассчитает план выполнения, используя последние статистические данные по рассматриваемым таблицам. MSTVF эквивалентно вставке всего содержимого вашего оператора SELECT в табличную переменную и затем присоединению к ней. Таким образом, компилятор не может использовать какую-либо статистику таблиц для таблиц в MSTVF. Таким образом, при прочих равных условиях (что они редко бывают), ITVF будет работать лучше, чем MSTVF. В моих тестах разница в производительности во время завершения была незначительной, однако с точки зрения статистики это было заметно.

В вашем случае две функции не являются функционально эквивалентными. Функция MSTV выполняет дополнительный запрос при каждом вызове и, что наиболее важно, фильтрует идентификатор клиента. В большом запросе оптимизатор не сможет воспользоваться преимуществами других типов объединений, так как он должен будет вызывать функцию для каждого переданного customerId. Однако, если вы переписали свою функцию MSTV следующим образом:

CREATE FUNCTION MyNS.GetLastShipped()
RETURNS @CustomerOrder TABLE
    (
    SaleOrderID    INT         NOT NULL,
    CustomerID      INT         NOT NULL,
    OrderDate       DATETIME    NOT NULL,
    OrderQty        INT         NOT NULL
    )
AS
BEGIN
    INSERT @CustomerOrder
    SELECT a.SalesOrderID, a.CustomerID, a.OrderDate, b.OrderQty
    FROM Sales.SalesOrderHeader a 
        INNER JOIN Sales.SalesOrderHeader b
            ON a.SalesOrderID = b.SalesOrderID
        INNER JOIN Production.Product c 
            ON b.ProductID = c.ProductID
    WHERE a.OrderDate = (
                        Select Max(SH1.OrderDate)
                        FROM Sales.SalesOrderHeader As SH1
                        WHERE SH1.CustomerID = A.CustomerId
                        )
    RETURN
END
GO

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

ITVF должны быть предпочтительнее, чем MSTVF, когда это возможно, потому что типы данных, обнуляемость и сопоставление из столбцов в таблице, в то время как вы объявляете эти свойства в табличной функции с несколькими утверждениями и, что важно, вы получите лучшие планы выполнения от ITVF. По моему опыту, я не нашел много обстоятельств, когда ITVF был лучшим вариантом, чем VIEW, но пробег может отличаться.

Спасибо Мэтту.

прибавление

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

Его оригинальный пост в блоге.

Копировать на SQL Server Central


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

Самое лучшее объяснение, которое я когда-либо нашел, - это первый ответ и соответствующий пост: stackoverflow.com/questions/4109152/… Не пропустите связанный документ, вы можете прочитать его быстро, и это чрезвычайно интересно.
JotaBe

1
Будет ли обновление этого ответа для SQL Server 2017 ?: youtube.com/watch?time_continue=2&v=szTmo6rTUjM
Ральф

29

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

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

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

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

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


2
Хотя он может обрабатывать табличные функции с несколькими операторами аналогично хранимой процедуре, функционально идентичная хранимая процедура намного быстрее табличной функции для больших наборов данных. Я придерживаюсь хранимых процедур над табличными функциями с несколькими операторами.
Kekoa

6
Если вам не нужно объединить эти результаты в другом запросе.
Гильермо Гутьеррес

почему бы не использовать оба? Хранимый процесс, который возвращает результат табличной функции с несколькими операторами. Лучшее из обоих миров.
Робино

13

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


3

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

Я предлагаю использовать самое простое (встроенное) всякий раз, когда это возможно, и использовать несколько утверждений, когда это необходимо (очевидно) или когда личные предпочтения / удобочитаемость делают это дополнительным набором текста.


Спасибо за ответ. Таким образом, по сути, мульти-оператор действительно должен использоваться только тогда, когда функция более сложна, чем это возможно сделать во встроенной функции, для удобства чтения? Есть ли какие-либо преимущества в производительности для нескольких операторов?
AndrewC

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

Большое спасибо еще раз. Я могу посмотреть дальше, когда у меня будет больше времени! :)
AndrewC


0

Я не проверял это, но функция нескольких операторов кэширует набор результатов. Могут быть случаи, когда оптимизатор слишком много встроит функцию. Например, предположим, что у вас есть функция, которая возвращает результат из разных баз данных, в зависимости от того, что вы передаете как «Номер компании». Как правило, вы можете создать представление с объединением all, а затем фильтровать по номеру компании, но я обнаружил, что иногда sql-сервер откатывает весь объединение и не достаточно умен, чтобы вызвать выбранное. Табличная функция может иметь логику для выбора источника.


0

Другим случаем использования многострочной функции было бы обойти сервер sql от нажатия предложения where.

Например, у меня есть таблица с именами таблиц, и некоторые имена таблиц отформатированы как C05_2019 и C12_2018, и все таблицы, отформатированные таким образом, имеют одинаковую схему. Я хотел объединить все эти данные в одну таблицу и разобрать 05 и 12 в столбец CompNo и 2018,2019 в столбец год. Тем не менее, есть другие таблицы, такие как ACA_StupidTable, которые я не могу извлечь CompNo и CompYr и получить ошибку преобразования, если я попытаюсь. Итак, мой запрос состоял из двух частей: внутреннего запроса, который возвращал только таблицы, отформатированные как «C_______», тогда внешний запрос выполнял преобразование подстроки и int. то есть приведение (подстрока (2, 2) как int) как CompNo. Все выглядит хорошо, за исключением того, что сервер sql решил установить функцию Cast до того, как результаты были отфильтрованы, и поэтому я получаю ошибку преобразования. Табличная функция с множеством операторов может предотвратить это,


0

Может быть, в очень сжатой форме. ITVF (встроенный TVF): больше, если вы - человек БД, вид параметризованного представления, возьмите один SELECT st

MTVF (Multi-Statement TVF): разработчик, создает и загружает переменную таблицы.


-2

если вы собираетесь выполнить запрос, вы можете присоединиться к функции Inline Table Valued, например:

SELECT
    a.*,b.*
    FROM AAAA a
        INNER JOIN MyNS.GetUnshippedOrders() b ON a.z=b.z

это понесет небольшие накладные расходы и будет работать нормально.

Если вы попытаетесь использовать таблицу Multi Statement Valued в аналогичном запросе, у вас будут проблемы с производительностью:

SELECT
    x.a,x.b,x.c,(SELECT OrderQty FROM MyNS.GetLastShipped(x.CustomerID)) AS Qty
    FROM xxxx   x

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


Ах, так вы бы сказали, что встроенный намного лучше с точки зрения производительности?
AndrewC

1
Нет, они оба возвращают таблицу, что делает ваш второй SQL недействительным, когда вы пытаетесь поместить таблицу в столбец.
cjk

1
@ck, я обновил запрос, который вы прокомментировали. параметры функции, используемой во второй функции, позволяют использовать ее в качестве подзапроса, что приведет к снижению производительности.
КМ.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.