web-dev-qa-db-ja.com

マルチステートメントTVFとインラインTVFパフォーマンス

回文の質問 (回答を削除したため、10,000人以上のユーザーのみ)の回答の一部を比較すると、混乱する結果になります。

私は multi-statement、schema-bound TVF を提案しました。これは、標準の関数を実行するよりも高速だと思いました。また、以下に示すように、マルチステートメントTVFは「インライン化」されるという印象もありましたが、その点については間違っています。この質問は、TVFのこれら2つのスタイルのパフォーマンスの違いについてです。まず、コードを確認する必要があります。

マルチステートメント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_テーブルに挿入されます。

2つのバリエーションをテストするには、_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、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。
テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
テーブル '#Words'。スキャンカウント1、論理読み取り88、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。

SQL Server実行時間:
CPU時間= 1700 ms、経過時間= 2022 ms。
SQL Serverの解析およびコンパイル時間:
CPU時間= 0 ms、経過時間= 0 ms。

インラインTVF:

テーブル「番号」。スキャンカウント1、論理読み取り1272030、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
テーブル '#Words'。スキャンカウント1、論理読み取り88、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。

SQL Server実行時間:
CPU時間= 137874 ms、経過時間= 139415 ms。
SQL Serverの解析およびコンパイル時間:
CPU時間= 0 ms、経過時間= 0 ms。

実行計画は次のようになります。

enter image description here

enter image description here

この場合、インラインバリアントがマルチステートメントバリアントよりもはるかに遅いのはなぜですか?

@AaronBertrandによるコメントに応じて、CTEによって返される行を入力Wordの長さに一致するように制限するように_dbo.InlineIsPalindrome_関数を変更しました。

_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、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。
テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
テーブル '#Words'。スキャンカウント1、論理読み取り90、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

SQL Server実行時間:
CPU時間= 1763ミリ秒、経過時間= 2192ミリ秒。

dbo.FunctionIsPalindrome(w.Word)

(17424行が影響を受けました)
テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
テーブル '#Words'。スキャンカウント1、論理読み取り90、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

SQL Server実行時間:
CPU時間= 328ミリ秒、経過時間= 424ミリ秒。

CROSS APPLY dbo.InlineIsPalindrome(w.Word) p

(17424行が影響を受けました)
表「番号」。スキャンカウント1、論理読み取り237100、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。
テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。
テーブル '#Words'。スキャンカウント1、論理読み取り90、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

SQL Server実行時間:
CPU時間= 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;
_
18
Max Vernon

数値テーブルはヒープであり、毎回完全にスキャンされる可能性があります。

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

それらの変更が適切に行われると、それは私のために飛びます(228msかかります)

enter image description here

12
Martin Smith