Multi-declaración TVF vs Inline TVF Performance


18

Comparando algunas de las respuestas en la pregunta de Palindrome (solo 10k + usuarios, ya que he eliminado la respuesta), obtengo resultados confusos.

Propuse un TVF de múltiples enunciados y vinculado a un esquema que pensé que sería más rápido que ejecutar una función estándar, que es. También tenía la impresión de que el TVF de varias declaraciones estaría "en línea", aunque me equivoco en ese aspecto, como verá a continuación. Esta pregunta es sobre la diferencia de rendimiento de esos dos estilos de TVF. Primero, necesitarás ver el código.

Aquí está el TVF multi-declaración:

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

El TVF en línea:

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

La Numberstabla en la función anterior se define como:

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

Nota: La tabla de números no tiene índices ni clave principal, y contiene 1,000,000 de filas.

Una mesa temporal de banco de pruebas:

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;

En mi sistema de prueba, lo anterior INSERTda como resultado que se inserten 16.900 filas en la #Wordstabla.

Para probar las dos variaciones, SET STATISTICS IO, TIME ON;utilizo lo siguiente:

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;

Esperaba que la InlineIsPalindromeversión fuera significativamente más rápida, sin embargo, los siguientes resultados no respaldan esa suposición.

TVF multi-declaración:

Tabla '# A1CE04C3'. Cuenta de escaneo 16896, lecturas lógicas 16900, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tabla 'Tabla de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0.
Tabla '#Palabras'. Cuenta de escaneo 1, lecturas lógicas 88, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tiempos de ejecución de SQL Server:
tiempo de CPU = 1700 ms, tiempo transcurrido = 2022 ms.
Tiempo de análisis y compilación de SQL Server: tiempo de
CPU = 0 ms, tiempo transcurrido = 0 ms.

TVF en línea:

Tabla 'Números'. Cuenta de escaneo 1, lecturas lógicas 1272030, lecturas físicas 0, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'Mesa de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0.
Tabla '#Palabras'. Cuenta de escaneo 1, lecturas lógicas 88, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tiempos de ejecución de SQL Server:
tiempo de CPU = 137874 ms, tiempo transcurrido = 139415 ms.
Tiempo de análisis y compilación de SQL Server: tiempo de
CPU = 0 ms, tiempo transcurrido = 0 ms.

Los planes de ejecución se ven así:

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

¿Por qué la variante en línea es mucho más lenta que la variante de varias instrucciones, en este caso?

En respuesta a un comentario de @AaronBertrand, he modificado la dbo.InlineIsPalindromefunción para limitar las filas devueltas por el CTE para que coincida con la longitud de la palabra de entrada:

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

Como sugirió @MartinSmith, agregué una clave primaria y un índice agrupado a la dbo.Numberstabla, lo que ciertamente ayuda y estaría más cerca de lo que uno esperaría ver en un entorno de producción.

Volver a ejecutar las pruebas anteriores ahora da como resultado las siguientes estadísticas:

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

(17424 filas afectadas)
Tabla '# B1104853'. Recuento de escaneos 17420, lecturas lógicas 17424, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tabla 'Mesa de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0.
Tabla '#Palabras'. Cuenta de escaneo 1, lecturas lógicas 90, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0.

Tiempos de ejecución de SQL Server:
tiempo de CPU = 1763 ms, tiempo transcurrido = 2192 ms.

dbo.FunctionIsPalindrome(w.Word):

(17424 filas afectadas)
Tabla 'Mesa de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0.
Tabla '#Palabras'. Cuenta de escaneo 1, lecturas lógicas 90, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0.

Tiempos de ejecución de SQL Server:
tiempo de CPU = 328 ms, tiempo transcurrido = 424 ms.

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

(17424 filas afectadas)
Tabla 'Números'. Cuenta de escaneo 1, lecturas lógicas 237100, lecturas físicas 0, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla 'Tabla de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0.
Tabla '#Palabras'. Cuenta de escaneo 1, lecturas lógicas 90, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0.

Tiempos de ejecución de SQL Server:
tiempo de CPU = 17737 ms, tiempo transcurrido = 17946 ms.

Estoy probando esto en SQL Server 2012 SP3, v11.0.6020, Developer Edition.

Aquí está la definición de mi tabla de números, con la clave primaria y el índice agrupado:

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;

Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White reinstala a Monica

Respuestas:


12

Su tabla de números es un montón y potencialmente se está escaneando completamente cada vez.

Agregue una clave principal en clúster Numbere intente lo siguiente con una forceseekpista para obtener la búsqueda deseada.

Por lo que puedo decir, esta sugerencia es necesaria ya que SQL Server solo estima que el 27% de la tabla coincidirá con el predicado (30% para el <=y reducido al 27% por el <>). Y por lo tanto, solo tendrá que leer 3-4 filas antes de encontrar una que coincida y pueda salir de la semiunión. Entonces, la opción de escaneo tiene un costo muy bajo. Pero, de hecho, si existen palíndromos, entonces tendrá que leer toda la tabla, por lo que este no es un buen plan.

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

Con esos cambios en su lugar, vuela para mí (toma 228 ms)

ingrese la descripción de la imagen aquí

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.