Ordenar derrames a tempdb debido a varchar (max)

10

En un servidor con 32 GB, estamos ejecutando SQL Server 2014 SP2 con una memoria máxima de 25 GB, tenemos dos tablas, aquí encontrará una estructura simplificada de ambas tablas:

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

con los siguientes índices no agrupados:

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

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

La base de datos está configurada con compatibility level120.

Cuando ejecuto esta consulta hay derrames a tempdb. Así es como ejecuto la 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

Si no selecciona el [remark]campo, no se producen derrames. Mi primera reacción fue que los derrames ocurrieron debido al bajo número de filas estimadas en el operador de bucle anidado.

Entonces agrego 5 columnas datetime y 5 enteras a la tabla de configuración y las agrego a mi declaración select. Cuando ejecuto la consulta, no se producen derrames.

¿Por qué los derrames solo ocurren cuando [remark]se selecciona? Probablemente tiene algo que ver con el hecho de que esto es un varchar(max). ¿Qué puedo hacer para evitar derramar tempdb?

Agregar OPTION (RECOMPILE)a la consulta no hace ninguna diferencia.

Frederik Vanderhaegen
fuente
Puede intentarlo select r.id, LEFT(remark, 512)(o cualquier longitud de subcadena razonable).
mustaccio
@Forrest: estoy tratando de reproducir los datos necesarios para simular el problema. A primera vista tiene que ver con la baja estimación del bucle anidado. En mis datos ficticios, el número estimado de filas es mucho mayor y no se produce ningún derrame
Frederik Vanderhaegen

Respuestas:

10

Habrá varias soluciones posibles aquí.

Puede ajustar manualmente la concesión de memoria, aunque probablemente no iría por esa ruta.

También puede usar un CTE y TOP para empujar la clasificación hacia abajo, antes de agarrar la columna de longitud máxima. Se verá algo así como a continuación.

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

Prueba de concepto dbfiddle aquí . ¡Aún se apreciarán datos de muestra!

Si desea leer un excelente análisis de Paul White, lea aquí.

Para descanso
fuente
7

¿Por qué los derrames solo ocurren cuando se selecciona [comentario]?

El derrame ocurre cuando incluye esa columna porque no obtiene una concesión de memoria lo suficientemente grande para los datos de cadena grandes que se ordenan.

No obtiene una concesión de memoria lo suficientemente grande porque el número real de filas es 10 veces más que el número estimado de filas (1.302 reales frente a 126 estimados).

¿Por qué está fuera de la estimación? ¿Por qué SQL Server cree que solo hay una fila en dbo.Settings con un resourceidde 38?

Podría ser un problema de estadísticas, que puede verificar ejecutando DBCC SHOW_STATISTICS('dbo.Settings', 'IX_Test')y ver los recuentos para ese paso del histograma. Pero el plan de ejecución parece indicar que las estadísticas son tan completas y actualizadas como podrían ser.

Dado que las estadísticas no ayudan, su mejor opción es probablemente una reescritura de consultas, que Forrest ha cubierto en su respuesta.

Josh Darnell
fuente
3

Para mí, parece que la wherecláusula en la consulta está dando el problema, y ​​es la causa de las bajas estimaciones, incluso si OPTION(RECOMPILE)se usa.

Creé algunos datos de prueba, y al final se me ocurrieron dos soluciones, almacenando el IDcampo resourcesen una variable (si siempre es única) o en una tabla temporal, si podemos tener más de una ID.

Base de registros de prueba

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

Inserte los valores de 'Búsqueda' para obtener el mismo 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

Cambie las estadísticas de compatibilidad y actualización para que coincidan con el 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

Mis estimaciones son aún peores , con una fila estimada, mientras que se devuelven 1300. Y como OP dijo, no importa si agregoOPTION(RECOMPILE)

Una cosa importante a tener en cuenta es que cuando nos deshacemos de la cláusula where, las estimaciones son 100% correctas, lo que se espera ya que estamos utilizando todos los datos en ambas tablas.

Forcé los índices solo para asegurarme de que usamos los mismos que en la consulta anterior, para probar el punto

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 era de esperar, buenas estimaciones.

Entonces, ¿qué podríamos cambiar para obtener mejores estimaciones pero aún buscar nuestros valores?

SI @UID es único, como en el ejemplo que dio OP, podríamos poner el single idque se devolvió resourcesen una variable, luego buscar esa variable con una 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);

Lo que da estimaciones 100% precisas

Pero, ¿qué pasa si hay múltiples resourceUID en los recursos?

agregar algunos datos de prueba

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

Esto podría resolverse con una tabla temporal

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

De nuevo con estimaciones precisas .

Esto se hizo con mi propio conjunto de datos, YMMV.


Escrito con sp_executesql

Con una variable

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

Con una mesa temporal

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

Todavía estimaciones 100% correctas en mi prueba

Randi Vertongen
fuente