SQL Server nvarchar (max) vs nvarchar (n) wpływa na wydajność

16

To jest SQL Server 2008 R2 SP2. Mam 2 stoły. Oba są identyczne (dane i indeksowanie), z tą różnicą, że pierwsza tabela ma kolumnę WARTOŚĆ, nvarchar(max)a druga ma taką samą kolumnę jak nvarchar(800). Ta kolumna jest uwzględniona w indeksie nieklastrowanym. Utworzyłem również indeks klastrowy na obu tabelach. Odbudowałem również indeksy. Maksymalna długość ciągu w tej kolumnie wynosi 650.

Jeśli uruchomię to samo zapytanie dla obu nvarchar(800)tabel, jest to konsekwentnie szybsze, wiele razy dwa razy szybsze. Pewnie wydaje się, że pokonuje cel „varchara”. Tabela zawiera ponad 800 000 wierszy. Kwerenda powinna obejmować około 110 000 wierszy (co szacuje plan).

Według statystyk io nie ma odczytów lob, więc wszystko wydaje się być w rzędzie. Plany wykonania są takie same, z tą różnicą, że istnieje niewielka różnica w odsetku kosztów między dwiema tabelami, a szacowany rozmiar wiersza jest większy w przypadku nvarchar(max)(91 bajtów w porównaniu z 63 bajtami). Liczba odczytów jest prawie taka sama.

Skąd ta różnica?

===== Schemat ======

 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]

===== Tabela 1 kwerenda ======

    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)

===== Tabela 2 ======

    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
źródło
4
Zapytania, schemat tabeli, dane przykładowe lub orientacyjne oraz plany wykonania dla każdego zapytania proszę. „Nie sądzę ...” to nie to samo, co „Na pewno nie ma…”.
Mark Storey-Smith
Jaką wersję SQL Server posiadasz?
Max Vernon,
Zobacz technet.microsoft.com/en-us/library/ms189087(v=SQL.105).aspx, aby uzyskać szczegółowe informacje na temat przechowywania w wierszu dla pól nvarchar (max). Jak duże są rzeczywiste dane w tych polach?
Max Vernon,
Zaktualizowałem post, aby odpowiedzieć na powyższe opinie.
Brian Bohl,

Odpowiedzi:

14

Widzisz koszty ogólne związane z używaniem MAXtypów.

Chociaż NVARCHAR(MAX)jest identyczny jak NVARCHAR(n)w TSQL i może być przechowywany w wierszu, jest obsługiwany osobno przez silnik pamięci, ponieważ można go wypchnąć z wiersza. Poza rzędem jest to LOB_DATAjednostka alokacji, a nie ROW_OVERFLOW_DATAjednostka alokacji, a na podstawie twoich obserwacji możemy założyć, że pociąga to za sobą koszty ogólne.

Widać, że te dwa typy są wewnętrznie przechowywane inaczej z niewielkim spelunkowaniem STRONY DBCC . Mark Rasmussen opublikował przykładowe zrzuty stron, które pokazują różnice w tym, jaki jest rozmiar wskaźnika LOB dla typów (MAX), takich jak Varchar, Varbinary, itp.?

Możemy chyba założyć, że to GROUP BYna MAXkolumnie, która powoduje różnicę wydajności w Twoim przypadku. Nie testowałem innych operacji na MAXtypie, ale może to być interesujące i sprawdzić, czy podobne wyniki są widoczne.

Mark Storey-Smith
źródło
Więc mówisz, że istnieje dodatkowe przetwarzanie odczytu [BLOB Inline Data] w porównaniu do zwykłego varchara? Spodziewałem się znacznego narzutu, jeśli wypadłby z wiersza, ale wszystkie te dane są wbudowane (używane dbcc ind). Jak myślisz, dlaczego grupa to wyciąga?
Brian Bohl,
Trochę narzutów za czytanie, dużo do obliczeń np GROUP BY. @RemusRusanu prawdopodobnie może zaoferować pewien wgląd (mam nadzieję, że zobaczy ping).
Mark Storey-Smith,
Znalazłem inny artykuł, który dokumentuje to samo zachowanie, nawet na równych i podobnych. Zastanawiam się, czy nvarchar (max) używa mniej wydajnego algorytmu.
Brian Bohl,