Classificar derramamentos em tempdb devido a varchar (max)

10

Em um servidor com 32 GB, estamos executando o SQL Server 2014 SP2 com uma memória máxima de 25 GB, temos duas tabelas, aqui você encontra uma estrutura simplificada de ambas as tabelas:

CREATE TABLE [dbo].[Settings](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [resourceId] [int] NULL,
    [typeID] [int] NULL,
    [remark] [varchar](max) NULL,
    CONSTRAINT [PK_Settings] PRIMARY KEY CLUSTERED ([id] ASC)
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[Resources](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [resourceUID] [int] NULL,
 CONSTRAINT [PK_Resources] PRIMARY KEY CLUSTERED ([id] ASC)
) ON [PRIMARY]
GO

com os seguintes índices não agrupados em cluster:

CREATE NONCLUSTERED INDEX [IX_UID] ON [dbo].[Resources]
(
    [resourceUID] ASC
)

CREATE NONCLUSTERED INDEX [IX_Test] ON [dbo].[Settings]
(
    [resourceId] ASC,
    [typeID] ASC
)

O banco de dados está configurado com compatibility level120.

Quando executo essa consulta, há derramamentos para tempdb. É assim que executo a consulta:

exec sp_executesql N'
select r.id,remark
FROM Resources r
inner join Settings on resourceid=r.id
where resourceUID=@UID
ORDER BY typeID',
N'@UID int',
@UID=38

Se não selecionar o [remark]campo, nenhum derramamento ocorrerá. Minha primeira reação foi que os derramamentos ocorreram devido ao baixo número de linhas estimadas no operador de loop aninhado.

Portanto, adiciono 5 colunas datetime e 5 inteiros à tabela de configurações e as adiciono à minha instrução select. Quando executo a consulta, não ocorrem derramamentos.

Por que os derramamentos acontecem apenas quando [remark]é selecionado? Provavelmente tem algo a ver com o fato de que este é um varchar(max). O que posso fazer para evitar derramamento tempdb?

Adicionar OPTION (RECOMPILE)à consulta não faz diferença.

Frederik Vanderhaegen
fonte
Pode ser que você possa tentar select r.id, LEFT(remark, 512)(ou qualquer que seja o tamanho sensível da substring).
mustaccio
@ Forrest: Estou tentando reproduzir os dados necessários para simular o problema. À primeira vista, isso tem a ver com a baixa estimativa do loop aninhado. Nos meus dados fictícios, o número estimado de linhas é muito maior e nenhum derramamento está acontecendo #
Frederik Vanderhaegen

Respostas:

10

Haverá várias soluções possíveis aqui.

Você pode ajustar manualmente a concessão de memória, embora eu provavelmente não siga esse caminho.

Você também pode usar um CTE e TOP para empurrar a classificação para baixo, antes de pegar a coluna de comprimento máximo. Será algo como abaixo.

WITH CTE AS (
SELECT TOP 1000000000 r.ID, s.ID AS ID2, s.typeID
FROM Resources r
inner join Settings s on resourceid=r.id
where resourceUID=@UID
ORDER BY s.typeID
)
SELECT c.ID, ca.remark
FROM CTE c
CROSS APPLY (SELECT remark FROM dbo.Settings s WHERE s.id = c.ID2) ca(remark)
ORDER BY c.typeID

Dbfiddle da prova de conceito aqui . Os dados de amostra ainda seriam apreciados!

Se você quiser ler uma excelente análise de Paul White, leia aqui.

Forrest
fonte
7

Por que os derramamentos estão acontecendo apenas quando [observação] é selecionado?

O derramamento ocorre quando você inclui essa coluna, porque você não recebe uma concessão de memória grande o suficiente para os dados de string grandes que estão sendo classificados.

Você não recebe uma concessão de memória grande o suficiente porque o número real de linhas é 10 vezes maior que o número estimado de linhas (1.302 reais versus 126 estimadas).

Por que a estimativa está desativada? Por que o SQL Server acha que há apenas uma linha no dbo.Settings com um resourceidde 38?

Pode ser um problema de estatística, que você pode verificar executando DBCC SHOW_STATISTICS('dbo.Settings', 'IX_Test')e ver as contagens dessa etapa do histograma. Mas o plano de execução parece indicar que as estatísticas são as mais completas e atualizadas possível.

Como as estatísticas não estão ajudando, sua melhor aposta é provavelmente uma reescrita da consulta - que Forrest abordou em sua resposta.

Josh Darnell
fonte
3

Para mim, parece que a wherecláusula na consulta está fornecendo o problema e é a causa das baixas estimativas, mesmo que OPTION(RECOMPILE)sejam usadas.

Criei alguns dados de teste e, no final, criei duas soluções, armazenando o IDcampo resourcesem uma variável (se sempre for única) ou em uma tabela temporária, se pudermos ter mais de uma ID.

Registros de teste base

SET NOCOUNT ON
DECLARE @i int= 1;
WHILE @i <= 10000
BEGIN
INSERT INTO [dbo].[Settings]([resourceId],[typeID],remark)
VALUES(@i,@i,'KEPT THESE VALUES OUT BECAUSE IT WOULD CLUTTER THE EXAMPLES, VALUES OVER 8000 Chars entered here'); -- 23254 character length on each value
INSERT INTO  [dbo].[Resources](resourceUID)
VALUES(@i);
SET @i += 1;
END

Insira os valores 'Procurar', para obter o mesmo conjunto de resultados aproximado que OP (1300 registros)

INSERT INTO  [dbo].[Settings]([resourceId],[typeID],remark)
VALUES(38,38,'KEPT THESE VALUES OUT BECAUSE IT WOULD CLUTTER THE EXAMPLES, VALUES OVER 8000 Chars entered here')
GO 1300

Altere as estatísticas compat & Update para corresponder ao OP

ALTER DATABASE StackOverflow SET COMPATIBILITY_LEVEL = 120;
UPDATE STATISTICS settings WITH FULLSCAN;
UPDATE STATISTICS resources WITH FULLSCAN;

Consulta original

exec sp_executesql N'
select r.id
FROM Resources r
inner join Settings on resourceid=r.id
where resourceUID=@UID
ORDER BY typeID',
N'@UID int',
@UID=38

Minhas estimativas são ainda piores , com uma linha estimada, enquanto 1300 são retornadas. E, como o OP afirmou, não importa se eu adicionarOPTION(RECOMPILE)

Uma coisa importante a ser observada é que, quando nos livramos da cláusula where, as estimativas estão 100% corretas, o que é esperado, pois estamos usando todos os dados nas duas tabelas.

Forcei os índices apenas para garantir que usamos os mesmos da consulta anterior, para provar o ponto

exec sp_executesql N'
select r.id,remark
FROM Resources r with(index([IX_UID]))
inner join Settings WITH(INDEX([IX_Test])) 
on resourceid=r.id
ORDER BY typeID',
N'@UID int',
@UID=38

Como esperado, boas estimativas.

Então, o que poderíamos mudar para obter melhores estimativas, mas ainda buscar nossos valores?

Se @UID for único, como no exemplo que o OP deu, poderíamos colocar o único idretornado resourcesem uma variável e procurar nessa variável com um OPTION (RECOMPILE)

DECLARE @UID int =38 , @RID int;
SELECT @RID=r.id from 
Resources r where resourceUID = @UID;

SELECT @uid, remark 
from Settings 
where resourceId = @uid 
Order by typeID
OPTION(RECOMPILE);

O que fornece estimativas 100% precisas

Mas e se houver vários resourceUIDs em recursos?

adicione alguns dados de teste

INSERT INTO Resources(ResourceUID)
VALUES (38);
go 50

Isso pode ser resolvido com uma tabela temporária

CREATE TABLE #RID (id int)
DECLARE @UID int =38 
INSERT INTO #RID
SELECT r.id 
from 
Resources r where resourceUID = @UID

SELECT @uid, remark 
from Settings  s
INNER JOIN #RID r
ON r.id =s.resourceId
Order by typeID
OPTION(RECOMPILE)

DROP TABLE #RID

Novamente com estimativas precisas .

Isso foi feito com meu próprio conjunto de dados, YMMV.


Escrito com sp_executesql

Com uma variável

exec sp_executesql N'
DECLARE  @RID int;
    SELECT @RID=r.id from 
    Resources r where resourceUID = @UID;

    SELECT @uid, remark 
    from Settings 
    where resourceId = @uid 
    Order by typeID
    OPTION(RECOMPILE);',
N'@UID int',
@UID=38

Com uma tabela temporária

exec sp_executesql N'

CREATE TABLE #RID (id int)

INSERT INTO #RID
SELECT r.id 
from 
Resources r where resourceUID = @UID

SELECT @uid, remark 
from Settings  s
INNER JOIN #RID r
ON r.id =s.resourceId
Order by typeID
OPTION(RECOMPILE)

DROP TABLE #RID',
N'@UID int',
@UID=38

Ainda estimativas 100% corretas no meu teste

Randi Vertongen
fonte