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]
TargetTypeId = 30
? Parece que los planes son diferentes porque este valor realmente sesga la cantidad de datos (que se espera que sean) devueltos.Respuestas:
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 subtree
costo.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:
El costo total estimado del subárbol para su plan de mejor desempeño
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
Subtreecost
se calcula enindex seek
&nested loops operator
realizando la unión:Mientras que para nuestro plan de consulta incorrecta, el
Clustered index seek
costo del operador es menorLo que debería explicar por qué el otro plan podría haber sido elegido.
(Y agregando el parámetro
30
aumentando el costo del mal plan donde ha aumentado por encima del871.510000
costo estimado). Estimated guess ™El mejor plan de desempeño
El mal plan
¿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 JOIN
sugerencia de uniónEstá más cerca, pero tiene algunas diferencias de orden de unión:
Reescribiendo
Mi primer intento de reescritura podría ser almacenar todos estos números en una tabla temporal:
Y luego agregando un en
JOIN
lugar del grandeIN()
Nuestro plan de consultas es diferente pero aún no está solucionado:
con un enorme costo estimado del operador sobre la
AuditRelatedIds
mesaAquí 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
Más información sobre filtros de mapa de bits optimizados aquí .
Esto significa que sin los filtros de mapa de bits, el optimizador considera que es mejor unirse primero a la
#number
tabla y luego unirse a laAuditRelatedIds
tabla.Al forzar el pedido
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);
podemos ver por qué:Y
No está bien
Eliminar la capacidad de ir en paralelo con maxdop 1
Al agregar
MAXDOP 1
la consulta se realiza más rápido, un solo subproceso.Y agregando este índice
Mientras usa una combinación de combinación.
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:
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, conmerge join
y 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
fuente
MAXDOP 0
parece haberlo solucionado. ¡Muchas gracias!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.ID
yari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
luego se filtraba esa lista a aquellos queAudit.TargetTypeID
estaban en la lista.Con la segunda versión se derivaba el filtro principal que
Audit.ID
está relacionado con la lista deAudit.TargetTypeID
.Dado que la adición de
Audit.TargetTypeID = 30
parecí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 consultasAuditRelatedID.RelatedID
para encontrar luegoAudit.ID
fueron probablemente más selectivas que tratar de usarAudit.TargetTypeID
para encontrarAudit.ID
.Para crédito de ypercube. Ciertamente puede actualizar
[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]
para tener ambosRelatedID
yAuditID
como parte de un índice en lugar de tenerAuditID
como parte de unINCLUDE
. No debería ocupar ningún espacio de índice adicional y le permitiría usar ambas columnas enJOIN
clá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
Audit
que contieneTargetTypeID ASC, ID ASC
los nodos de filtrado / orden reales (no como parte delINCLUDE
). Esto debería permitir que el optimizador de consultas filtre yAudit.TargetTypeID
luego se una rápidamenteAuditReferenceIds.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.fuente