Mehrfachaussage TVF vs. Inline TVF Performance

18

Beim Vergleich einiger Antworten auf die Palindrome-Frage (ab 10.000 Benutzern, da ich die Antwort gelöscht habe) erhalte ich verwirrende Ergebnisse.

Ich schlug eine mehranweisungsfähige, schemagebundene TVF vor, die meiner Meinung nach schneller ist als die Ausführung einer Standardfunktion, die es ist. Ich hatte auch den Eindruck, dass die TVF mit mehreren Aussagen "inline" sein würde, obwohl ich in dieser Hinsicht falsch liege, wie Sie weiter unten sehen werden. Bei dieser Frage geht es um den Leistungsunterschied dieser beiden TVF-Stile. Zuerst müssen Sie den Code sehen.

Hier ist die mehrteilige 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

Der Inline-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

Die NumbersTabelle in der obigen Funktion ist wie folgt definiert:

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

Hinweis: Die Zahlentabelle enthält keine Indizes und keinen Primärschlüssel und enthält 1.000.000 Zeilen.

Ein temporärer Tisch auf dem Prüfstand:

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;

Auf meinem Testsystem führen die obigen INSERTErgebnisse dazu, dass 16.900 Zeilen in die #WordsTabelle eingefügt werden .

Zum Testen der beiden Varianten verwende ich SET STATISTICS IO, TIME ON;Folgendes:

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;

Ich habe erwartet, dass die InlineIsPalindromeVersion deutlich schneller sein wird, aber die folgenden Ergebnisse stützen diese Annahme nicht.

Mehrfachaussage TVF:

Tabelle '# A1CE04C3'. Scananzahl 16896, logische Lesevorgänge 16900, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0,
Lobs-Vorauslesevorgänge 0. Tabelle 'Arbeitstabelle'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0,
Vorauslesevorgänge 0, logische Lobs- Lesevorgänge 0, physikalische Lobs -Lesevorgänge 0 , Lobs-Vorauslesevorgänge 0. Tabelle '#Words'. Scananzahl 1, logische Lesevorgänge 88, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten:
CPU-Zeit = 1700 ms, verstrichene Zeit = 2022 ms.
SQL Server-Analyse- und Kompilierungszeit:
CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms.

Inline-TVF:

Tabelle 'Zahlen'. Scananzahl 1, logische Lesevorgänge 1272030, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0,
Lobs-Vorauslesevorgänge 0. Tabelle 'Arbeitstabelle'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0,
Vorauslesevorgänge 0, logische Lobs- Lesevorgänge 0, physikalische Lobs -Lesevorgänge 0 , Lobs-Vorauslesevorgänge 0. Tabelle '#Words'. Scananzahl 1, logische Lesevorgänge 88, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten:
CPU-Zeit = 137874 ms, verstrichene Zeit = 139415 ms.
SQL Server-Analyse- und Kompilierungszeit:
CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms.

Die Ausführungspläne sehen folgendermaßen aus:

Bildbeschreibung hier eingeben

Bildbeschreibung hier eingeben

Warum ist die Inline-Variante in diesem Fall so viel langsamer als die Variante mit mehreren Anweisungen?

Als Antwort auf einen Kommentar von @AaronBertrand habe ich die dbo.InlineIsPalindromeFunktion geändert , um die vom CTE zurückgegebenen Zeilen auf die Länge des Eingabeworts zu beschränken:

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)
);

Wie @MartinSmith vorschlug, habe ich der dbo.NumbersTabelle einen Primärschlüssel und einen Clustered-Index hinzugefügt , was sicherlich hilfreich ist und näher an dem liegt, was man in einer Produktionsumgebung erwarten würde.

Das erneute Ausführen der obigen Tests führt nun zu den folgenden Statistiken:

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

(17424 betroffene Zeile (n))
Tabelle '# B1104853'. Scan-Anzahl 17420, logische Lesevorgänge 17424, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0,
Lobs-Vorauslesevorgänge 0. Tabelle 'Arbeitstabelle'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0,
Vorauslesevorgänge 0, logische Lobs- Lesevorgänge 0, physikalische Lobs -Lesevorgänge 0 , Lobs-Vorauslesevorgänge 0. Tabelle '#Words'. Scananzahl 1, logische Lesevorgänge 90, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten:
CPU-Zeit = 1763 ms, verstrichene Zeit = 2192 ms.

dbo.FunctionIsPalindrome(w.Word):

(17424 betroffene Zeile (n))
Tabelle 'Arbeitstisch'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0,
Vorauslesevorgänge 0, logische Lobs- Lesevorgänge 0, physikalische Lobs -Lesevorgänge 0 , Lobs-Vorauslesevorgänge 0. Tabelle '#Words'. Scananzahl 1, logische Lesevorgänge 90, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten:
CPU-Zeit = 328 ms, verstrichene Zeit = 424 ms.

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

(17424 betroffene Zeile (n))
Tabelle 'Numbers'. Scananzahl 1, logische Lesevorgänge 237100, physische Lesevorgänge 0, Vorlesevorgänge 0, logische Vorlesevorgänge 0, physische
Vorlesevorgänge 0 , Vorlesevorgänge 0. Tabelle 'Arbeitstabelle'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0,
Vorauslesevorgänge 0, logische Lobs- Lesevorgänge 0, physikalische Lobs -Lesevorgänge 0 , Lobs-Vorauslesevorgänge 0. Tabelle '#Words'. Scananzahl 1, logische Lesevorgänge 90, physische Lesevorgänge 0, Vorauslesevorgänge 0, logische Lobs-Lesevorgänge 0, physikalische Lobs-Lesevorgänge 0, Lobs-Vorauslesevorgänge 0.

SQL Server-Ausführungszeiten:
CPU-Zeit = 17737 ms, verstrichene Zeit = 17946 ms.

Ich teste dies auf SQL Server 2012 SP3, v11.0.6020, Developer Edition.

Hier ist die Definition meiner Nummerntabelle mit dem Primärschlüssel und dem Clustered-Index:

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;
Max Vernon
quelle
Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
Paul White sagt GoFundMonica

Antworten:

12

Ihre Zahlentabelle ist ein Haufen und wird möglicherweise jedes Mal vollständig gescannt.

Fügen Sie einen gruppierten Primärschlüssel hinzu Numberund versuchen Sie Folgendes mit einem forceseekHinweis, um die gewünschte Suche zu erhalten.

Soweit ich das beurteilen kann, wird dieser Hinweis benötigt, da SQL Server nur schätzt, dass 27% der Tabelle mit dem Prädikat übereinstimmen (30% für das <=und bis auf 27% für das <>). Und deshalb muss es nur 3-4 Zeilen lesen, bevor es eine passende findet, und es kann den Semi-Join verlassen. Die Scanoption ist also sehr günstig. Wenn Palindrome vorhanden sind, muss die gesamte Tabelle gelesen werden, sodass dies kein guter Plan ist.

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

Mit diesen Änderungen fliegt es für mich (dauert 228ms)

Bildbeschreibung hier eingeben

Martin Smith
quelle