Мульти-оператор TVF против Inline TVF Performance

18

Сравнивая некоторые ответы на вопрос о Палиндроме (только 10 000 пользователей, поскольку я удалил ответ), я получаю запутанные результаты.

Я предложил TVF с привязкой к нескольким операторам, который, по моему мнению, будет быстрее, чем запуск стандартной функции. У меня также сложилось впечатление, что TVF с несколькими утверждениями будет «встроенным», хотя я ошибаюсь в этом отношении, как вы увидите ниже. Этот вопрос о разнице в производительности этих двух стилей TVF. Сначала вам нужно увидеть код.

Вот многократное заявление TVF:

IF OBJECT_ID('dbo.IsPalindrome') IS NOT NULL
DROP FUNCTION dbo.IsPalindrome;
GO
CREATE FUNCTION dbo.IsPalindrome
(
    @Word NVARCHAR(500)
) 
RETURNS @t TABLE
(
    IsPalindrome BIT NOT NULL
)
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @IsPalindrome BIT;
    DECLARE @LeftChunk NVARCHAR(250);
    DECLARE @RightChunk NVARCHAR(250);
    DECLARE @StrLen INT;
    DECLARE @Pos INT;
    SET @RightChunk = '';
    SET @IsPalindrome = 0;
    SET @StrLen = LEN(@Word) / 2;
    IF @StrLen % 2 = 1 SET @StrLen = @StrLen - 1;
    SET @Pos = LEN(@Word);
    SET @LeftChunk = LEFT(@Word, @StrLen);
    WHILE @Pos > (LEN(@Word) - @StrLen)
    BEGIN
        SET @RightChunk = @RightChunk + SUBSTRING(@Word, @Pos, 1)
        SET @Pos = @Pos - 1;
    END
    IF @LeftChunk = @RightChunk SET @IsPalindrome = 1;
    INSERT INTO @t VALUES (@IsPalindrome);
    RETURN
END
GO

Встроенный TVF:

IF OBJECT_ID('dbo.InlineIsPalindrome') IS NOT NULL
DROP FUNCTION dbo.InlineIsPalindrome;
GO
CREATE FUNCTION dbo.InlineIsPalindrome
(
    @Word NVARCHAR(500)
)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN (
    WITH Nums AS
    (
      SELECT
        N = number
      FROM
        dbo.Numbers
    )
    SELECT
      IsPalindrome =
        CASE
          WHEN EXISTS
          (
            SELECT N
            FROM Nums
            WHERE N <= L / 2
              AND SUBSTRING(S, N, 1) <> SUBSTRING(S, 1 + L - N, 1)
          )
          THEN 0
          ELSE 1
        END
    FROM
      (SELECT LTRIM(RTRIM(@Word)), LEN(@Word)) AS v (S, L)
);
GO

NumbersТаблица в приведенной выше функции определяются следующим образом:

CREATE TABLE dbo.Numbers
(
    Number INT NOT NULL 
);

Примечание. Таблица чисел не имеет индексов и первичного ключа и содержит 1 000 000 строк.

Временный стол испытательного стенда:

IF OBJECT_ID('tempdb.dbo.#Words') IS NOT NULL
DROP TABLE #Words;
GO
CREATE TABLE #Words 
(
    Word VARCHAR(500) NOT NULL
);

INSERT INTO #Words(Word) 
SELECT o.name + REVERSE(w.name)
FROM sys.objects o
CROSS APPLY (
    SELECT o.name
    FROM sys.objects o
) w;

В моей тестовой системе вышеприведенные INSERTрезультаты приводят к добавлению 16 900 строк в #Wordsтаблицу.

Чтобы проверить два варианта, я SET STATISTICS IO, TIME ON;и использую следующее:

SELECT w.Word
    , p.IsPalindrome
FROM #Words w
    CROSS APPLY dbo.IsPalindrome(w.Word) p
ORDER BY w.Word;


SELECT w.Word
    , p.IsPalindrome
FROM #Words w
    CROSS APPLY dbo.InlineIsPalindrome(w.Word) p
ORDER BY w.Word;

Я ожидал, что InlineIsPalindromeверсия будет значительно быстрее, однако следующие результаты не поддерживают это предположение.

Мульти-оператор TVF:

Таблица «# A1CE04C3». Число сканирований 16896, логическое чтение 16900, физическое чтение 0, чтение с опережением 0, логическое чтение с
бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с
бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «#Words». Сканирование 1, логическое чтение 88, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server:
время ЦП = 1700 мс, прошедшее время = 2022 мс.
Время анализа и компиляции SQL Server: время
ЦП = 0 мс, прошедшее время = 0 мс.

Встроенный TVF:

Таблица «Числа». Сканирование 1, логическое чтение 1272030, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.
Таблица «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с
бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «#Words». Сканирование 1, логическое чтение 88, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server:
время ЦП = 137874 мс, прошедшее время = 139415 мс.
Время анализа и компиляции SQL Server: время
ЦП = 0 мс, прошедшее время = 0 мс.

Планы выполнения выглядят так:

введите описание изображения здесь

введите описание изображения здесь

Почему встроенный вариант в этом случае намного медленнее, чем вариант с несколькими утверждениями?

В ответ на комментарий @AaronBertrand я изменил dbo.InlineIsPalindromeфункцию, чтобы ограничить строки, возвращаемые CTE, для соответствия длине входного слова:

CREATE FUNCTION dbo.InlineIsPalindrome
(
    @Word NVARCHAR(500)
)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN (
    WITH Nums AS
    (
      SELECT
        N = number
      FROM
        dbo.Numbers
      WHERE 
        number <= LEN(@Word)
    )
    SELECT
      IsPalindrome =
        CASE
          WHEN EXISTS
          (
            SELECT N
            FROM Nums
            WHERE N <= L / 2
              AND SUBSTRING(S, N, 1) <> SUBSTRING(S, 1 + L - N, 1)
          )
          THEN 0
          ELSE 1
        END
    FROM
      (SELECT LTRIM(RTRIM(@Word)), LEN(@Word)) AS v (S, L)
);

Как предложил @MartinSmith, я добавил в dbo.Numbersтаблицу первичный ключ и кластеризованный индекс , что, безусловно, помогает и будет ближе к тому, что можно ожидать в производственной среде.

Повторное выполнение тестов, приведенных выше, теперь приводит к следующей статистике:

CROSS APPLY dbo.IsPalindrome(w.Word) p:

(Затронуто 17424 строк)
Таблица '# B1104853'. Число сканирований 17420, логическое чтение 17424, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0.
Таблица «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с
бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «#Words». Сканирование 1, логическое чтение 90, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server:
время ЦП = 1763 мс, прошедшее время = 2192 мс.

dbo.FunctionIsPalindrome(w.Word):

(Затронуто 17424 строк)
Таблица 'Рабочий стол'. Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с
бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «#Words». Сканирование 1, логическое чтение 90, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server:
время ЦП = 328 мс, прошедшее время = 424 мс.

CROSS APPLY dbo.InlineIsPalindrome(w.Word) p:

(Затронуты 17424 строки)
Таблица «Числа». Сканирование 1, логическое чтение 237100, физическое чтение 0, чтение с опережением 0, чтение логического объекта 0, физическое чтение 1, чтение с опережением 0.
Таблица «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с
бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «#Words». Сканирование 1, логическое чтение 90, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server:
время ЦП = 17737 мс, прошедшее время = 17946 мс.

Я тестирую это на SQL Server 2012 SP3, v11.0.6020, Developer Edition.

Вот определение моей таблицы чисел с первичным ключом и кластерным индексом:

CREATE TABLE dbo.Numbers
(
    Number INT NOT NULL 
        CONSTRAINT PK_Numbers
        PRIMARY KEY CLUSTERED
);

;WITH n AS
(
    SELECT v.n 
    FROM (
        VALUES (1) 
            ,(2) 
            ,(3) 
            ,(4) 
            ,(5) 
            ,(6) 
            ,(7) 
            ,(8) 
            ,(9) 
            ,(10)
        ) v(n)
)
INSERT INTO dbo.Numbers(Number)
SELECT ROW_NUMBER() OVER (ORDER BY n1.n)
FROM n n1
    , n n2
    , n n3
    , n n4
    , n n5
    , n n6;
Макс Вернон
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Пол Уайт восстановил Монику

Ответы:

12

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

Добавьте кластерный первичный ключ Numberи попробуйте следующее с forceseekподсказкой, чтобы получить желаемый поиск.

Насколько я могу судить, эта подсказка необходима, поскольку SQL Server просто оценивает, что 27% таблицы будет соответствовать предикату (30% для <=и уменьшено до 27% для <>). И поэтому ему нужно будет только прочитать 3-4 строки, прежде чем найти ту, которая соответствует, и он может выйти из полу соединения. Таким образом, опция сканирования стоит очень дешево. Но на самом деле, если какие-то палиндромы существуют, тогда придется читать всю таблицу, так что это не очень хороший план.

CREATE FUNCTION dbo.InlineIsPalindrome
(
    @Word NVARCHAR(500)
)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN (
    WITH Nums AS
    (
      SELECT
        N = number
      FROM
        dbo.Numbers WITH(FORCESEEK)
    )
    SELECT
      IsPalindrome =
        CASE
          WHEN EXISTS
          (
            SELECT N
            FROM Nums
            WHERE N <= L / 2
              AND SUBSTRING(S, N, 1) <> SUBSTRING(S, 1 + L - N, 1)
          )
          THEN 0
          ELSE 1
        END
    FROM
      (SELECT LTRIM(RTRIM(@Word)), LEN(@Word)) AS v (S, L)
);
GO

С этими изменениями он летит для меня (занимает 228 мс)

введите описание изображения здесь

Мартин Смит
источник