Buscando ayuda para mejorar el rendimiento de esta consulta.
SQL Server 2008 R2 Enterprise , RAM máxima 16 GB, CPU 40, Grado máximo de paralelismo 4.
SELECT DsJobStat.JobName AS JobName
, AJF.ApplGroup AS GroupName
, DsJobStat.JobStatus AS JobStatus
, AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
, AVG(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG
FROM DsJobStat, AJF
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo
AND DsJobStat.Odate=AJF.Odate
AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )
GROUP BY DsJobStat.JobName
, AJF.ApplGroup
, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;
Mensaje de ejecución,
(0 row(s) affected)
Table 'AJF'. Scan count 11, logical reads 45, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 2, logical reads 1926, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 1, logical reads 3831235, physical reads 85, read-ahead reads 3724396, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row(s) affected)
SQL Server Execution Times:
CPU time = 67268 ms, elapsed time = 90206 ms.
Estructura de tablas:
-- 212271023 rows
CREATE TABLE [dbo].[DsJobStat](
[OrderID] [nvarchar](8) NOT NULL,
[JobNo] [int] NOT NULL,
[Odate] [datetime] NOT NULL,
[TaskType] [nvarchar](255) NULL,
[JobName] [nvarchar](255) NOT NULL,
[StartTime] [datetime] NULL,
[EndTime] [datetime] NULL,
[NodeID] [nvarchar](255) NULL,
[GroupName] [nvarchar](255) NULL,
[CompStat] [int] NULL,
[RerunCounter] [int] NOT NULL,
[JobStatus] [nvarchar](255) NULL,
[CpuMSec] [int] NULL,
[ElapsedSec] [int] NULL,
[StatusReason] [nvarchar](255) NULL,
[NumericOrderNo] [int] NULL,
CONSTRAINT [PK_DsJobStat] PRIMARY KEY CLUSTERED
( [OrderID] ASC,
[JobNo] ASC,
[Odate] ASC,
[JobName] ASC,
[RerunCounter] ASC
));
-- 48992126 rows
CREATE TABLE [dbo].[AJF](
[JobName] [nvarchar](255) NOT NULL,
[JobNo] [int] NOT NULL,
[OrderNo] [int] NOT NULL,
[Odate] [datetime] NOT NULL,
[SchedTab] [nvarchar](255) NULL,
[Application] [nvarchar](255) NULL,
[ApplGroup] [nvarchar](255) NULL,
[GroupName] [nvarchar](255) NULL,
[NodeID] [nvarchar](255) NULL,
[Memlib] [nvarchar](255) NULL,
[Memname] [nvarchar](255) NULL,
[CreationTime] [datetime] NULL,
CONSTRAINT [AJF$PrimaryKey] PRIMARY KEY CLUSTERED
( [JobName] ASC,
[JobNo] ASC,
[OrderNo] ASC,
[Odate] ASC
));
-- 413176 rows
CREATE TABLE [dbo].[DsAvg](
[JobName] [nvarchar](255) NULL,
[GroupName] [nvarchar](255) NULL,
[JobStatus] [nvarchar](255) NULL,
[ElapsedSecAVG] [float] NULL,
[CpuMSecAVG] [float] NULL
);
CREATE NONCLUSTERED INDEX [DJS_Dashboard_2] ON [dbo].[DsJobStat]
( [JobName] ASC,
[Odate] ASC,
[StartTime] ASC,
[EndTime] ASC
)
INCLUDE ( [OrderID],
[JobNo],
[NodeID],
[GroupName],
[JobStatus],
[CpuMSec],
[ElapsedSec],
[NumericOrderNo]) ;
CREATE NONCLUSTERED INDEX [Idx_Dashboard_AJF] ON [dbo].[AJF]
( [OrderNo] ASC,
[Odate] ASC
)
INCLUDE ( [SchedTab],
[Application],
[ApplGroup]) ;
CREATE NONCLUSTERED INDEX [DsAvg$JobName] ON [dbo].[DsAvg]
( [JobName] ASC
)
Plan de ejecución:
https://www.brentozar.com/pastetheplan/?id=rkUVhMlXM
Actualizar después de recibir respuesta
Muchas gracias @ Joe Obbish
Tiene razón sobre el tema de esta consulta que se trata entre DsJobStat y DsAvg. No se trata mucho de cómo UNIRSE y no usar NOT IN.
De hecho, hay una mesa como has adivinado.
CREATE TABLE [dbo].[DSJobNames](
[JobName] [nvarchar](255) NOT NULL,
CONSTRAINT [DSJobNames$PrimaryKey] PRIMARY KEY CLUSTERED
( [JobName] ASC
) );
Intenté tu sugerencia,
SELECT DsJobStat.JobName AS JobName
, AJF.ApplGroup AS GroupName
, DsJobStat.JobStatus AS JobStatus
, AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
, Avg(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG
FROM DsJobStat
INNER JOIN DSJobNames jn
ON jn.[JobName]= DsJobStat.[JobName]
INNER JOIN AJF
ON DsJobStat.Odate=AJF.Odate
AND DsJobStat.NumericOrderNo=AJF.OrderNo
WHERE NOT EXISTS ( SELECT 1 FROM [DsAvg] WHERE jn.JobName = [DsAvg].JobName )
GROUP BY DsJobStat.JobName, AJF.ApplGroup, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;
Mensaje de ejecución:
(0 row(s) affected)
Table 'DSJobNames'. Scan count 5, logical reads 1244, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 5, logical reads 2129, physical reads 0, read-ahead reads 24, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 8, logical reads 84, physical reads 0, read-ahead reads 83, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
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 'AJF'. Scan count 5, logical reads 757999, physical reads 944, read-ahead reads 757311, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
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.
(1 row(s) affected)
SQL Server Execution Times:
CPU time = 21776 ms, elapsed time = 33984 ms.
Plan de ejecución: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f
Respuestas:
Comencemos por considerar el orden de unión. Tiene tres referencias de tabla en la consulta. ¿Qué orden de unión podría darle el mejor rendimiento? El optimizador de consultas cree que la combinación de
DsJobStat
aDsAvg
eliminará casi todas las filas (las estimaciones de cardinalidad caen de 212195000 a 1 fila). El plan real nos muestra que la estimación es bastante cercana a la realidad (11 filas sobreviven a la unión). Sin embargo, la unión se implementa como una unión anti semi-fusión correcta, por lo queDsJobStat
se escanean los 212 millones de filas de la tabla solo para producir 11 filas. Eso ciertamente podría estar contribuyendo al largo tiempo de ejecución de la consulta, pero no puedo pensar en un mejor operador físico o lógico para esa unión que hubiera sido mejor. Estoy seguro de que elDJS_Dashboard_2
index se usa para otras consultas, pero todas las claves adicionales y las columnas incluidas solo requerirán más IO para esta consulta y lo ralentizarán. Por lo tanto, potencialmente tiene un problema de acceso a la tabla con el escaneo del índice en laDsJobStat
tabla.Asumiré que la unión a
AJF
no es muy selectiva. Actualmente no es relevante para los problemas de rendimiento que está viendo en la consulta, por lo que lo ignoraré para el resto de esta respuesta. Eso podría cambiar si los datos en la tabla cambian.El otro problema que se desprende del plan es el operador de carrete de recuento de filas. Este es un operador muy liviano pero se ejecuta más de 200 millones de veces. El operador está allí porque la consulta se escribe con
NOT IN
. Si hay una sola fila NULL enDsAvg
todas las filas deben eliminarse. El spool es la implementación de esa verificación. Probablemente esa no sea la lógica que desea, por lo que sería mejor escribir esa parte para usarNOT EXISTS
. El beneficio real de esa reescritura dependerá de su sistema y datos.Me burlé de algunos datos basados en el plan de consulta para probar algunas reescrituras de consultas. Las definiciones de mi tabla son significativamente diferentes a las suyas porque habría sido demasiado esfuerzo simular datos para cada columna. Incluso con las estructuras de datos abreviadas, pude reproducir el problema de rendimiento que estás experimentando.
Según el plan de consulta, podemos ver que hay alrededor de 200000
JobName
valores únicos en laDsAvg
tabla. Según el número real de filas después de la unión a esa tabla, podemos ver que casi todos losJobName
valoresDsJobStat
también están en laDsAvg
tabla. Por lo tanto, laDsJobStat
tabla tiene 200001 valores únicos para laJobName
columna y 1000 filas por valor.Creo que esta consulta representa el problema de rendimiento:
Todas las demás cosas en su plan de consulta (
GROUP BY
,HAVING
combinación de estilo antiguo, etc.) suceden después de que el conjunto de resultados se haya reducido a 11 filas. Actualmente no importa desde el punto de vista del rendimiento de la consulta, pero podría haber otras preocupaciones que podrían ser reveladas por los datos modificados en sus tablas.Estoy probando en SQL Server 2017, pero obtengo la misma forma de plan básico que usted:
En mi máquina, esa consulta requiere 62219 ms de tiempo de CPU y 65576 ms de tiempo transcurrido para ejecutarse. Si reescribo la consulta para usar
NOT EXISTS
:El spool ya no se ejecuta 212 millones de veces y probablemente tenga el comportamiento previsto del proveedor. Ahora la consulta se ejecuta en 34516 ms de tiempo de CPU y 41132 ms de tiempo transcurrido. La mayor parte del tiempo se dedica a escanear 212 millones de filas del índice.
Esa exploración de índice es muy desafortunada para esa consulta. En promedio, tenemos 1000 filas por valor único de
JobName
, pero sabemos después de leer la primera fila si necesitaremos las 1000 filas anteriores. Casi nunca necesitamos esas filas, pero aún necesitamos escanearlas de todos modos. Si sabemos que las filas no son muy densas en la tabla y que la unión eliminará casi todas ellas, podemos imaginar un patrón de E / S posiblemente más eficiente en el índice. ¿Qué sucede si SQL Server lee la primera fila por valor único deJobName
, verifica si ese valor estaba dentroDsAvg
y simplemente pasa al siguiente valor deJobName
si estaba? En lugar de escanear 212 millones de filas, se podría hacer un plan de búsqueda que requiera alrededor de 200k ejecuciones.Esto se puede lograr principalmente mediante el uso de la recursión junto con una técnica que Paul White fue pionera y que se describe aquí . Podemos usar la recursividad para hacer el patrón IO que describí anteriormente:
Esa consulta tiene mucho que ver, por lo que recomiendo examinar cuidadosamente el plan real . Primero hacemos 200002 busca el índice contra el índice
DsJobStat
para obtener todos losJobName
valores únicos . Luego nos unimosDsAvg
y eliminamos todas las filas menos una. Para la fila restante, vuelva a unirseDsJobStat
y obtenga todas las columnas necesarias.El patrón IO cambia totalmente. Antes de tener esto:
Con la consulta recursiva obtenemos esto:
En mi máquina, la nueva consulta se ejecuta en solo 6891 ms de tiempo de CPU y 7107 ms de tiempo transcurrido. Tenga en cuenta que la necesidad de utilizar la recursividad de esta manera sugiere que falta algo en el modelo de datos (o tal vez simplemente no se mencionó en la pregunta publicada). Si hay una tabla relativamente pequeña que contiene todo lo posible
JobNames
, será mucho mejor usar esa tabla en lugar de la recursividad en la tabla grande. Lo que se reduce a esto es que si tiene un conjunto de resultados que contiene todo loJobNames
que necesita, puede usar el índice para obtener el resto de las columnas que faltan. Sin embargo, no puede hacerlo con un conjunto de resultadosJobNames
que NO necesita.fuente
NOT EXISTS
. Ya respondieron con "Ya probé ambos, unirme y no existe, antes de publicar una pregunta. No hay mucha diferencia".Vea lo que sucede si reescribe la condición,
A
También considere reescribir su combinación SQL89 porque ese estilo es horrible.
En vez de
Tratar
También sospecho que esta condición se puede escribir mejor, pero tendríamos que saber más sobre lo que está sucediendo.
¿Realmente tienes que saber que el promedio no es cero, o solo ese elemento del grupo no es cero?
fuente