Consultas muy similares, rendimiento muy diferente.

9

Tengo dos consultas muy similares.

Primera consulta:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Resultado: 267479

Plan: https://www.brentozar.com/pastetheplan/?id=BJWTtILyS


Segunda consulta:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Resultado: 25650

Plan: https://www.brentozar.com/pastetheplan/?id=S1v79U8kS


La primera consulta tarda aproximadamente un segundo en completarse, mientras que la segunda consulta tarda unos 20 segundos. Esto es completamente contra-intuitivo para mí porque la primera consulta tiene un conteo mucho más alto que la segunda. Esto está en el servidor SQL 2012

¿Por qué hay tanta diferencia? ¿Cómo puedo acelerar la segunda consulta para que sea tan rápida como la primera?


Aquí está el script Crear tabla para ambas tablas:

CREATE TABLE [dbo].[AuditRelatedIds](
    [AuditId] [bigint] NOT NULL,
    [RelatedId] [uniqueidentifier] NOT NULL,
    [AuditTargetTypeId] [smallint] NOT NULL,
 CONSTRAINT [PK_AuditRelatedIds] PRIMARY KEY CLUSTERED 
(
    [AuditId] ASC,
    [RelatedId] 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_AuditRelatedIdsRelatedId_INCLUDES] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC
)
INCLUDE (   [AuditId]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id] FOREIGN KEY([AuditId])
REFERENCES [dbo].[Audits] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([AuditTargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id]

CREATE TABLE [dbo].[Audits](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [TargetTypeId] [smallint] NOT NULL,
    [TargetId] [nvarchar](40) NOT NULL,
    [TargetName] [nvarchar](max) NOT NULL,
    [Action] [tinyint] NOT NULL,
    [ActionOverride] [tinyint] NULL,
    [Date] [datetime] NOT NULL,
    [UserDisplayName] [nvarchar](max) NOT NULL,
    [DescriptionData] [nvarchar](max) NULL,
    [IsNotification] [bit] NOT NULL,
 CONSTRAINT [PK_Audits] 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]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetId] ON [dbo].[Audits]
(
    [TargetId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetTypeIdAction_INCLUDES] ON [dbo].[Audits]
(
    [TargetTypeId] ASC,
    [Action] ASC
)
INCLUDE (   [TargetId],
    [UserDisplayName]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100) ON [PRIMARY]

ALTER TABLE [dbo].[Audits]  WITH CHECK ADD  CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([TargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[Audits] CHECK CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id]
Chocoman
fuente
3
¿Podríamos obtener algunos esquemas de tabla y detalles de índice? Como estoy seguro que notó, los planes son un poco diferentes, pero evidentemente está haciendo una gran diferencia. Si podemos obtener esos detalles, tal vez podamos ver qué opciones tenemos.
Kirk Saunders
2
Como un consejo muy rápido, en lugar de usar IN, cree una tabla temporal con una sola columna TINYINT / INT (agrupada) con los números que desee, y luego INNER JOIN a ella. Aparte de eso, es probable que necesitemos información DDL como @KirkSaunders mencionó anteriormente
George.Palacios
2
¿Hay algo especial en esto TargetTypeId = 30? Parece que los planes son diferentes porque este valor realmente sesga la cantidad de datos (que se espera que sean) devueltos.
Aaron Bertrand
Me doy cuenta de que es terriblemente pedante, pero la afirmación "la primera consulta devuelve muchas más filas que la segunda". no es correcto. Ambos devuelven 1 fila;)
ypercubeᵀᴹ
1
Actualicé

Respuestas:

8

Tl; dr en la parte inferior

¿Por qué se eligió el mal plan?

La razón principal para elegir un plan sobre el otro es el Estimated total subtreecosto.

Este costo fue menor para el mal plan que para el plan de mejor desempeño.

El costo total estimado del subárbol para el mal plan:

ingrese la descripción de la imagen aquí

El costo total estimado del subárbol para su plan de mejor desempeño

ingrese la descripción de la imagen aquí


El operador estima los costos

Ciertos operadores pueden asumir la mayor parte de este costo, y podrían ser una razón para que el optimizador elija una ruta / plan diferente.

En nuestro plan de mejor desempeño, la mayor parte Subtreecostse calcula en index seek& nested loops operatorrealizando la unión:

ingrese la descripción de la imagen aquí

Mientras que para nuestro plan de consulta incorrecta, el Clustered index seekcosto del operador es menor

ingrese la descripción de la imagen aquí

Lo que debería explicar por qué el otro plan podría haber sido elegido.

(Y agregando el parámetro 30aumentando el costo del mal plan donde ha aumentado por encima del 871.510000costo estimado). Estimated guess ™

El mejor plan de desempeño

ingrese la descripción de la imagen aquí

El mal plan

ingrese la descripción de la imagen aquí


¿A dónde nos lleva esto?

Esta información nos lleva a una forma de forzar el plan de consulta incorrecto en nuestro ejemplo (consulte DML para casi replicar el problema de OP para los datos utilizados para replicar el problema)

Al agregar una INNER LOOP JOINsugerencia de unión

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

Está más cerca, pero tiene algunas diferencias de orden de unión:

ingrese la descripción de la imagen aquí


Reescribiendo

Mi primer intento de reescritura podría ser almacenar todos estos números en una tabla temporal:

CREATE TABLE #Numbers(Numbering INT)
INSERT INTO #Numbers(Numbering)
VALUES
(1),(2),(3),(4),(5),(6),(7),(8),(9),(11),(12),(13),(14),(15),(16),(17),(18),(19),
(21),(22),(23),(24),(25),(26),(27),(28),(29),(30),(31),(32),(33),(34),(35),
(36),(37),(38),(39),(41),(42),(43),(44),(45),(46),(47),(48),(49),(51),(52),
(53),(54),(55),(56),(57),(58),(59),(61),(62),(63),(64),(65),(66),(67),(68),
(69),(71),(72),(73),(74),(75),(76),(77),(78),(79);

Y luego agregando un en JOINlugar del grandeIN()

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1';

Nuestro plan de consultas es diferente pero aún no está solucionado:

ingrese la descripción de la imagen aquí

con un enorme costo estimado del operador sobre la AuditRelatedIdsmesa

ingrese la descripción de la imagen aquí


Aquí es donde noté que

La razón por la que no puedo recrear directamente su plan es el filtrado optimizado de mapas de bits.

Puedo recrear su plan deshabilitando los filtros de mapa de bits optimizados usando traceflags 7497&7498

SELECT count(*)
FROM Audits a 
   INNER JOIN AuditRelatedIds  ari ON a.Id = ari.AuditId 
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498);

Más información sobre filtros de mapa de bits optimizados aquí .

ingrese la descripción de la imagen aquí

Esto significa que sin los filtros de mapa de bits, el optimizador considera que es mejor unirse primero a la #numbertabla y luego unirse a la AuditRelatedIdstabla.

Al forzar el pedido OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);podemos ver por qué:

ingrese la descripción de la imagen aquí

Y ingrese la descripción de la imagen aquí

No está bien


Eliminar la capacidad de ir en paralelo con maxdop 1

Al agregar MAXDOP 1la consulta se realiza más rápido, un solo subproceso.

Y agregando este índice

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_AuditId] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC,
    [AuditId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];

ingrese la descripción de la imagen aquí

Mientras usa una combinación de combinación. ingrese la descripción de la imagen aquí

Lo mismo es cierto cuando eliminamos la sugerencia de consulta de orden de fuerza o no usamos la tabla #Numbers y usamos el IN()lugar.

Mi consejo sería considerar agregar MAXDOP(1)y ver si eso ayuda a su consulta, con una reescritura si es necesario.

Por supuesto, también debe tener en cuenta que, por mi parte, funciona aún mejor debido al filtrado de mapa de bits optimizado y al uso de múltiples subprocesos con buenos resultados:

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí


TL; DR

Los costos estimados definirán el plan elegido, pude replicar el comportamiento y vi que optimized bitmap filters+parallellism agregaron operadores para realizar la consulta de manera eficiente y rápida.

Podría considerar agregar MAXDOP(1)a su consulta como una forma de obtener el mismo resultado controlado cada vez, con merge joiny sin 'malo' parallellism.

Actualizar a una versión más nueva y usar una versión de estimador de cardinalidad más alta que la CardinalityEstimationModelVersion="70"que también podría ayudar.

Una tabla temporal de números para hacer el filtrado de valores múltiples también puede ayudar.


DML casi replica el problema de OP

Pasé más tiempo en esto de lo que me gustaría admitir

set NOCOUNT ON;
DECLARE @I INT = 0
WHILE @I < 56
BEGIN
INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(500000) CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 END as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;
SET @I +=1;
END

-- 'Bad Query matches'
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])
SELECT
TOP(25650)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') , 
CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 END as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2

-- Extra matches with 30
SELECT MAX([Id]) FROM [dbo].[Audits];
--28000001 Upper value

INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(241829) 30 as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;



;WITH CTE AS
(SELECT
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') as gu , 
30 as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2
CROSS APPLY master.dbo.spt_values spt3
)
--267479 - 25650 = 241829
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])

SELECT TOP(241829) rownum1,gu,rownum2 FROM CTE
WHERE rownum1 > 28000001
ORDER BY rownum1 ASC;
Randi Vertongen
fuente
Muy buena explicación! Agregar MAXDOP 0parece haberlo solucionado. ¡Muchas gracias!
Chocoman
1
MAXDOP 1 ** (error tipográfico)
Chocoman
@Chocoman ¡Genial! Feliz de ayudar :)
Randi Vertongen
1

Por lo que puedo decir, la diferencia principal entre los dos planes es la diferencia en lo que es el "filtro primario".

Con la primera versión, el filtro principal se derivaba Audit.IDy ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'luego se filtraba esa lista a aquellos que Audit.TargetTypeIDestaban en la lista.

Con la segunda versión se derivaba el filtro principal que Audit.IDestá relacionado con la lista de Audit.TargetTypeID.

Dado que la adición de Audit.TargetTypeID = 30parecía aumentar drásticamente el recuento récord (267,479 y 25,650 respectivamente, según la pregunta original). Probablemente por eso los planes de ejecución son diferentes. (Según tengo entendido) SQL intentará hacer la función más selectiva primero y luego aplicará el resto de las reglas después de eso. Con la primera versión, las consultas AuditRelatedID.RelatedIDpara encontrar luego Audit.IDfueron probablemente más selectivas que tratar de usar Audit.TargetTypeIDpara encontrar Audit.ID.

Para crédito de ypercube. Ciertamente puede actualizar [AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]para tener ambos RelatedIDy AuditIDcomo parte de un índice en lugar de tener AuditIDcomo parte de un INCLUDE. No debería ocupar ningún espacio de índice adicional y le permitiría usar ambas columnas en JOINcláusulas. Eso puede ayudar al Optimizador de consultas a crear el mismo plan de ejecución para ambas consultas.

Al operar con una lógica similar, puede haber algún beneficio en un índice Auditque contiene TargetTypeID ASC, ID ASClos nodos de filtrado / orden reales (no como parte del INCLUDE). Esto debería permitir que el optimizador de consultas filtre y Audit.TargetTypeIDluego se una rápidamente AuditReferenceIds.AuditID. Ahora, esto puede terminar con ambas consultas eligiendo el plan menos eficiente, por lo que solo lo probaría después de probar la recomendación de ypercube.

Kirk Saunders
fuente