SQL Server nvarchar (max) vs nvarchar (n) afecta el rendimiento

16

Este es SQL Server 2008 R2 SP2. Tengo 2 mesas. Ambos son idénticos (datos e indexación), excepto que la primera tabla tiene una columna VALOR como nvarchar(max)y la segunda tiene la misma columna que nvarchar(800). Esta columna se incluye en un índice no agrupado. También creé un índice agrupado en ambas tablas. También he reconstruido los índices. La longitud máxima de la cadena en esta columna es 650.

Si ejecuto la misma consulta en ambas, la nvarchar(800)tabla es consistentemente más rápida, muchas veces el doble de rápido. Seguro parece que está derrotando el propósito de "varchar". La tabla contiene más de 800,000 filas. La consulta debe mirar alrededor de 110,000 filas (que es lo que estima el plan).

Según las estadísticas de io, no hay lecturas de lóbulos, por lo que todo parece estar en fila. Los planes de ejecución son los mismos, excepto que hay una ligera diferencia en el porcentaje de costo entre las dos tablas y el tamaño de fila estimado es mayor con elnvarchar(max) (91 bytes frente a 63 bytes). El número de lecturas también es prácticamente el mismo.

¿Por qué la diferencia?

===== Esquema ======

 CREATE TABLE [dbo].[table1](
        [ID] [bigint] IDENTITY(1,1) NOT NULL,
        [ProductID] [bigint] NOT NULL,
        [ProductSkeletonID] [bigint] NOT NULL,
        [Value] [nvarchar](max) NOT NULL,
        [IsKeywordSearchable] [bit] NULL,
        [ValueInteger] [bigint] NULL,
        [ValueDecimal] [decimal](18, 2) NULL,
        [ValueDate] [datetime] NULL,
        [TypeOfData] [nvarchar](20) NOT NULL,
     CONSTRAINT [PK_table1] PRIMARY KEY CLUSTERED 
    (
        [ID] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

    CREATE NONCLUSTERED INDEX [IX_table1_productskeletonid] ON [dbo].[table1] 
    (
        [ProductSkeletonID] ASC
    )
    INCLUDE ( [ProductID],
    [Value]) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

    CREATE TABLE [dbo].[table2](
        [ID] [bigint] IDENTITY(1,1) NOT NULL,
        [ProductID] [bigint] NOT NULL,
        [ProductSkeletonID] [bigint] NOT NULL,
        [Value] [nvarchar](800) NOT NULL,
        [IsKeywordSearchable] [bit] NULL,
        [ValueInteger] [bigint] NULL,
        [ValueDecimal] [decimal](18, 2) NULL,
        [ValueDate] [datetime] NULL,
        [TypeOfData] [nvarchar](20) NOT NULL,
     CONSTRAINT [PK_table2] PRIMARY KEY CLUSTERED 
    (
        [ID] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]

    CREATE NONCLUSTERED INDEX [IX_table2_productskeletonid] ON [dbo].[table2] 
    (
        [ProductSkeletonID] ASC
    )
    INCLUDE ( [ProductID],
    [Value]) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]


CREATE TABLE [dbo].[table_results](
    [SearchID] [bigint] NOT NULL,
    [RowNbr] [int] NOT NULL,
    [ProductID] [bigint] NOT NULL,
    [PermissionList] [varchar](250) NULL,
    [SearchWeight] [int] NULL,
 CONSTRAINT [PK_table_results] PRIMARY KEY NONCLUSTERED 
(
    [SearchID] ASC,
    [RowNbr] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_table_results_SearchID] ON [dbo].[cart_product_searches_results] 
(
    [SearchID] ASC
)
INCLUDE ( [ProductID]) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

===== Tabla1 consulta ======

    SELECT cppev.ProductSkeletonID, cppev.Value, COUNT(*) AS Value FROM table1 cppev
    JOIN search_results cpsr ON cppev.ProductID = cpsr.ProductID AND cpsr.SearchID = 227568 
    WHERE cppev.ProductSkeletonID in (3191, 3160, 3158, 3201)
    GROUP BY cppev.ProductSkeletonID, cppev.Value

    Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
    Table 'table1'. Scan count 4, logical reads 582, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
    Table 'table_results'. Scan count 1, logical reads 82, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    SQL Server Execution Times:
       CPU time = 1373 ms,  elapsed time = 1576 ms.

 |--Compute Scalar(DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1008],0)))
       |--Stream Aggregate(GROUP BY:([cppev].[Value], [cppev].[ProductSkeletonID]) DEFINE:([Expr1008]=Count(*)))
            |--Sort(ORDER BY:([cppev].[Value] ASC, [cppev].[ProductSkeletonID] ASC))
                 |--Hash Match(Inner Join, HASH:([cpsr].[ProductID])=([cppev].[ProductID]), RESIDUAL:([dbo].[table1].[ProductID] as [cppev].[ProductID]=[dbo].[table_results].[ProductID] as [cpsr].[ProductID]))
                      |--Index Seek(OBJECT:([dbo].[table_results].[IX_table_results_SearchID] AS [cpsr]), SEEK:([cpsr].[SearchID]=(227568)) ORDERED FORWARD)
                      |--Index Seek(OBJECT:([dbo].[table1].[IX_table1_productskeletonid] AS [cppev]), SEEK:([cppev].[ProductSkeletonID]=(3158) OR [cppev].[ProductSkeletonID]=(3160) OR [cppev].[ProductSkeletonID]=(3191) OR [cppev].[ProductSkeletonID]=(3201)) ORDERED FORWARD)

===== Tabla2 consulta ======

    SELECT cppev.ProductSkeletonID, cppev.Value, COUNT(*) AS Value FROM table2 cppev
    JOIN table_results cpsr ON cppev.ProductID = cpsr.ProductID AND cpsr.SearchID = 227568 
    WHERE cppev.ProductSkeletonID in (3191, 3160, 3158, 3201)
    GROUP BY cppev.ProductSkeletonID, cppev.Value

    Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
    Table 'table2'. Scan count 4, logical reads 584, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
    Table 'table_results'. Scan count 1, logical reads 82, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

    SQL Server Execution Times:
       CPU time = 484 ms,  elapsed time = 796 ms.

  |--Compute Scalar(DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1008],0)))
       |--Stream Aggregate(GROUP BY:([cppev].[Value], [cppev].[ProductSkeletonID]) DEFINE:([Expr1008]=Count(*)))
            |--Sort(ORDER BY:([cppev].[Value] ASC, [cppev].[ProductSkeletonID] ASC))
                 |--Hash Match(Inner Join, HASH:([cpsr].[ProductID])=([cppev].[ProductID]), RESIDUAL:([auctori_core_v40_D].[dbo].[table2].[ProductID] as [cppev].[ProductID]= [dbo].[table2].[ProductID] as [cpsr].[ProductID]))
                      |--Index Seek(OBJECT:([dbo].[table_results].[IX_table_results_SearchID] AS [cpsr]), SEEK:([cpsr].[SearchID]=(227568)) ORDERED FORWARD)
                      |--Index Seek(OBJECT:([dbo].[table2].[IX_table2_productskeletonid] AS [cppev]), SEEK:([cppev].[ProductSkeletonID]=(3158) OR [cppev].[ProductSkeletonID]=(3160) OR [cppev].[ProductSkeletonID]=(3191) OR [cppev].[ProductSkeletonID]=(3201)) ORDERED FORWARD)
Brian Bohl
fuente
44
Consultas, esquema de tabla, muestra o datos indicativos y los planes de ejecución para cada consulta, por favor. "No creo ..." no es lo mismo que "Definitivamente no hay ...".
Mark Storey-Smith
¿Qué versión de SQL Server tienes?
Max Vernon
Consulte technet.microsoft.com/en-us/library/ms189087(v=SQL.105).aspx para obtener detalles sobre el almacenamiento en fila para los campos nvarchar (max). ¿Qué tan grandes son los datos reales en esos campos?
Max Vernon
Actualicé la publicación para abordar los comentarios anteriores.
Brian Bohl el

Respuestas:

14

Estás viendo los costos generales del uso de MAXtipos.

Si bien NVARCHAR(MAX)es idéntico a NVARCHAR(n)TSQL y puede almacenarse en fila, el motor de almacenamiento lo maneja por separado porque puede ser empujado fuera de fila. Cuando está fuera de la fila, es una LOB_DATAunidad de asignación, en lugar de una unidad de ROW_OVERFLOW_DATAasignación, y podemos deducir de sus observaciones que esto conlleva una sobrecarga.

Puede ver que los dos tipos se almacenan internamente de manera diferente con un poco de espeleología de DBCC PAGE . Mark Rasmussen publicó volcados de página de ejemplo que muestran las diferencias en ¿Cuál es el tamaño del puntero LOB para tipos (MAX) como Varchar, Varbinary, Etc?

Probablemente podemos suponer que es GROUP BYen la MAXcolumna que causa la diferencia de rendimiento en su caso. No he probado otras operaciones en un MAXtipo, pero podría ser interesante hacerlo y ver si se observan resultados similares.

Mark Storey-Smith
fuente
¿Entonces está diciendo que hay un procesamiento adicional al leer un [BLOB Inline Data] frente a un simple 'ol varchar? Esperaba una sobrecarga significativa si salía de la fila, pero todos estos datos están en línea (se usa dbcc ind). ¿Y por qué crees que el grupo saca esto a relucir?
Brian Bohl el
Un poco de gastos generales para leerlo, mucho para cualquier cálculo, por ejemplo GROUP BY. @RemusRusanu probablemente podría ofrecer una idea (con suerte verá el ping).
Mark Storey-Smith
Encontré otro artículo que documenta el mismo comportamiento, incluso en iguales y similares. Me pregunto si nvarchar (max) usa un algoritmo menos eficiente.
Brian Bohl el