Ajuste de rendimiento en una consulta

9

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

Wendy
fuente
Si no puede cambiar el código del proveedor, lo mejor que puede hacer es abrir un incidente de soporte con el proveedor, por más doloroso que sea, y vencerlo por tener una consulta que requiere que se completen muchas lecturas. La cláusula NOT IN que se refiere a valores en una tabla con 413 mil filas es, uh, subóptima. El escaneo del índice en DSJobStat está devolviendo 212 millones de filas, que burbujean hasta 212 millones de bucles anidados, y puede ver que el recuento de filas de 212 millones es el 83% del costo. No creo que pueda ayudar esto sin reescribir la consulta o depurar datos ...
Tony Hinkle
No entiendo, por qué la sugerencia de Evan no te ayudó en primer lugar, ambas respuestas son las mismas, excepto la explicación. Además, no veo que hayas implementado completamente lo que estos dos chicos te sugirieron. Joe hizo esta pregunta interesante.
KumarHarsh

Respuestas:

11

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 DsJobStata DsAvgeliminará 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 que DsJobStatse 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_2index 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 la DsJobStattabla.

Asumiré que la unión a AJFno 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 en DsAvgtodas 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 usar NOT 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.

CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL
);

CREATE CLUSTERED INDEX CI_DsAvg ON [DsAvg] (JobName);

INSERT INTO [DsAvg] WITH (TABLOCK)
SELECT TOP (200000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE [dbo].[DsJobStat](
    [JobName] [nvarchar](255) NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
);

CREATE CLUSTERED INDEX CI_JobStat ON DsJobStat (JobName)

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT [JobName], 'ACTIVE'
FROM [DsAvg] ds
CROSS JOIN (
SELECT TOP (1000) 1
FROM master..spt_values t1
) c (t);

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT TOP (1000) '200001', 'ACTIVE'
FROM master..spt_values t1;

Según el plan de consulta, podemos ver que hay alrededor de 200000 JobNamevalores únicos en la DsAvgtabla. Según el número real de filas después de la unión a esa tabla, podemos ver que casi todos los JobNamevalores DsJobStattambién están en la DsAvgtabla. Por lo tanto, la DsJobStattabla tiene 200001 valores únicos para la JobNamecolumna y 1000 filas por valor.

Creo que esta consulta representa el problema de rendimiento:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] );

Todas las demás cosas en su plan de consulta ( GROUP BY, HAVINGcombinació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:

antes del plan

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:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE DsJobStat.JobName = [DsAvg].JobName);

sin carrete

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 de JobName, verifica si ese valor estaba dentro DsAvgy simplemente pasa al siguiente valor de JobNamesi 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:

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        [JobName]
    FROM dbo.DsJobStat AS T
    ORDER BY
        T.[JobName]

    UNION ALL

    -- Recursive
    SELECT R.[JobName]
    FROM
    (
        -- Number the rows
        SELECT 
            T.[JobName],
            rn = ROW_NUMBER() OVER (
                ORDER BY T.[JobName])
        FROM dbo.DsJobStat AS T
        JOIN RecursiveCTE AS R
            ON R.[JobName] < T.[JobName]
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT js.*
FROM RecursiveCTE
INNER JOIN dbo.DsJobStat js ON RecursiveCTE.[JobName]= js.[JobName]
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE RecursiveCTE.JobName = [DsAvg].JobName)
OPTION (MAXRECURSION 0);

Esa consulta tiene mucho que ver, por lo que recomiendo examinar cuidadosamente el plan real . Primero hacemos 200002 busca el índice contra el índice DsJobStatpara obtener todos los JobNamevalores únicos . Luego nos unimos DsAvgy eliminamos todas las filas menos una. Para la fila restante, vuelva a unirse DsJobStaty obtenga todas las columnas necesarias.

El patrón IO cambia totalmente. Antes de tener esto:

Tabla 'DsJobStat'. Cuenta de escaneo 1, lecturas lógicas 1091651, lecturas físicas 13836, lecturas de lectura anticipada 181966

Con la consulta recursiva obtenemos esto:

Tabla 'DsJobStat'. Cuenta de escaneo 200003, lecturas lógicas 1398000, lecturas físicas 1, lecturas de lectura anticipada 7345

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 lo JobNamesque necesita, puede usar el índice para obtener el resto de las columnas que faltan. Sin embargo, no puede hacerlo con un conjunto de resultados JobNamesque NO necesita.

Joe Obbish
fuente
Sugerí NOT EXISTS. Ya respondieron con "Ya probé ambos, unirme y no existe, antes de publicar una pregunta. No hay mucha diferencia".
Evan Carroll
1
Me gustaría saber si la idea recursiva funciona, aunque eso es aterrador.
Evan Carroll
creo que no es necesario tener una cláusula. "ElapsedSec no es nulo" en donde lo hará la cláusula. También creo que CTE recursivo no es obligatorio. Puede usar row_number () over (partición por orden de trabajo por nombre) rn donde no existe (seleccione consulta). ¿Qué tienes que decir sobre mi idea?
KumarHarsh
@ Joe Obbish, actualicé mi publicación. Muchas gracias.
Wendy
sí, el CTE recursivo se realiza realizando row_number () over (partición por nombre de trabajo, orden por nombre) rn por 1 minuto. Pero al mismo tiempo no vi ninguna ganancia adicional en CTE recursivo usando sus datos de muestra.
KumarHarsh
0

Vea lo que sucede si reescribe la condición,

AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         

A

AND NOT EXISTS ( SELECT 1 FROM [DsAvg] AS d WHERE d.JobName = DsJobStat.JobName )

También considere reescribir su combinación SQL89 porque ese estilo es horrible.

En vez de

FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 

Tratar

FROM DsJobStat
INNER JOIN AJF ON (
  DsJobStat.NumericOrderNo=AJF.OrderNo 
  AND DsJobStat.Odate=AJF.Odate
)

También sospecho que esta condición se puede escribir mejor, pero tendríamos que saber más sobre lo que está sucediendo.

HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

¿Realmente tienes que saber que el promedio no es cero, o solo ese elemento del grupo no es cero?

Evan Carroll
fuente
@EvanCarroll. Ya probé ambos, unirse y no existe, antes de publicar una pregunta. No mucha diferencia.
Wendy