SQL Server nvarchar (max) vs nvarchar (n) afeta o desempenho

16

Este é o SQL Server 2008 R2 SP2. Eu tenho 2 mesas. Ambos são idênticos (dados e indexação), exceto que a primeira tabela possui uma coluna VALUE nvarchar(max)e a segunda possui a mesma coluna que nvarchar(800). Esta coluna está incluída em um índice não clusterizado. Também criei um índice em cluster nas duas tabelas. Também reconstruí os índices. O comprimento máximo da string nesta coluna é 650.

Se eu executar a mesma consulta em ambas, a nvarchar(800)tabela será consistentemente mais rápida, muitas vezes duas vezes mais rápida. Certamente parece que está derrotando o propósito de "varchar". A tabela contém mais de 800.000 linhas. A consulta deve analisar cerca de 110.000 linhas (que é o que o plano estima).

De acordo com as estatísticas io, não há leituras de lob, então tudo parece estar em linha. Os planos de execução são os mesmos, exceto que há uma pequena diferença na porcentagem de custo entre as duas tabelas e o tamanho estimado da linha é maior com nvarchar(max)(91 bytes vs 63 bytes). O número de leituras também é praticamente o mesmo.

Por que a diferença?

===== 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]

===== Consulta na Tabela1 ======

    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)

===== Consulta na Tabela2 ======

    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
fonte
4
Consultas, esquema de tabela, amostra ou dados indicativos e os planos de execução para cada consulta, por favor. "Eu não acho ..." não é o mesmo que "Definitivamente não existe ...".
Mark-Storey-Smith
Qual versão do SQL Server você possui?
Max Vernon
Consulte technet.microsoft.com/en-us/library/ms189087(v=SQL.105).aspx para obter detalhes sobre o armazenamento em linha nos campos nvarchar (max). Qual é o tamanho dos dados reais nesses campos?
Max Vernon
Atualizei a postagem para abordar os comentários acima.
Brian Bohl

Respostas:

14

Você está vendo o custo adicional do uso de MAXtipos.

Embora NVARCHAR(MAX)seja idêntico ao NVARCHAR(n)TSQL e possa ser armazenado em linha, ele é tratado separadamente pelo mecanismo de armazenamento, pois pode ser empurrado para fora da linha. Quando fora de linha, é uma LOB_DATAunidade de alocação, em vez de uma unidade de ROW_OVERFLOW_DATAalocação, e podemos supor pelas suas observações que isso carrega uma sobrecarga.

Você pode ver que os dois tipos são armazenados internamente de maneira diferente com um pouco de spelunking do DBCC PAGE . Mark Rasmussen postou exemplos de despejos de página que mostram as diferenças em Qual é o tamanho do ponteiro LOB para tipos (MAX) como Varchar, Varbinary, Etc?

Provavelmente, podemos supor que é o GROUP BYda MAXcoluna que faz com que a diferença de desempenho no seu caso. Não testei outras operações em um MAXtipo, mas pode ser interessante fazê-lo e ver se resultados semelhantes são vistos.

Mark Storey-Smith
fonte
Então você está dizendo que há um processamento extra lendo um [BLOB Inline Data] vs um simples 'varchar? Eu esperava uma sobrecarga significativa se ela saiu da linha, mas todos esses dados estão em linha (usado dbcc ind). E por que você acha que o grupo traz isso à tona?
quer
Um pouco de sobrecarga para lê-lo, muito para qualquer cálculo nele, por exemplo GROUP BY. O @RemusRusanu provavelmente poderia oferecer algumas dicas (ele espera ver o ping).
Mark Storey-Smith
Encontrei outro artigo que documenta o mesmo comportamento, mesmo em iguais e iguais. Gostaria de saber se nvarchar (max) usa um algoritmo menos eficiente.
Brian Bohl