Eu tenho duas consultas muito semelhantes
Primeira 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
Plano: 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
Plano: https://www.brentozar.com/pastetheplan/?id=S1v79U8kS
A primeira consulta leva cerca de um segundo para ser concluída, enquanto a segunda consulta leva cerca de 20 segundos. Isso é completamente contra-intuitivo para mim, porque a primeira consulta tem uma contagem muito maior que a segunda. Isso está no SQL Server 2012
Por que há tanta diferença? Como acelerar a segunda consulta para ser tão rápida quanto a primeira?
Aqui está o script Criar tabela para ambas as tabelas:
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 os planos são diferentes porque esse valor realmente distorce a quantidade de dados (espera-se que seja) retornada.Respostas:
Tl; dr na parte inferior
Por que o plano ruim foi escolhido
A principal razão para escolher um plano em detrimento do outro é a
Estimated total subtree
custo.Esse custo foi menor para o plano ruim do que para o plano com melhor desempenho.
O custo total estimado da subárvore para o plano incorreto:
O custo total estimado da subárvore para seu plano com melhor desempenho
O operador estimou os custos
Certos operadores podem levar a maior parte desse custo e pode ser um motivo para o otimizador escolher um caminho / plano diferente.
Em nosso plano de melhor desempenho, a maior parte
Subtreecost
é calculada naindex seek
&nested loops operator
execução da junção:Embora para o nosso plano de consultas incorreto, o
Clustered index seek
custo do operador seja menorO que deve explicar por que o outro plano poderia ter sido escolhido.
(E adicionando o parâmetro
30
aumentando o custo do plano incorreto, onde ele subiu acima do871.510000
custo estimado). Estimativa estimada ™O plano com melhor desempenho
O plano ruim
Para onde isso nos leva?
Essas informações nos levam a uma maneira de forçar o plano de consultas incorretas em nosso exemplo (consulte o DML para quase replicar o problema do OP para obter os dados usados para replicar o problema)
Adicionando uma
INNER LOOP JOIN
dica de junçãoÉ mais próximo, mas tem algumas diferenças na ordem de junção:
Reescrever
Minha primeira tentativa de reescrever poderia ser armazenar todos esses números em uma tabela temporária:
E então adicionando um em
JOIN
vez do grandeIN()
Nosso plano de consulta é diferente, mas ainda não foi corrigido:
com um enorme custo estimado do operador em
AuditRelatedIds
cima da mesaAqui é onde eu notei que
O motivo pelo qual não posso recriar diretamente seu plano é a filtragem de bitmap otimizada.
Posso recriar seu plano desativando os filtros de bitmap otimizados usando traceflags
7497
e7498
Mais informações sobre filtros de bitmap otimizados aqui .
Isso significa que, sem os filtros de bitmap, o otimizador considera melhor ingressar primeiro na
#number
tabela e depois ingressar noAuditRelatedIds
tabela.Ao forçar a ordem
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);
, podemos ver o porquê:&
Não é bom
Removendo a capacidade de ficar paralelo ao maxdop 1
Ao adicionar
MAXDOP 1
a consulta, o desempenho é mais rápido e único.E adicionando este índice
Enquanto estiver usando uma junção de mesclagem.
O mesmo ocorre quando removemos a dica de consulta de ordem de força ou não usamos a tabela #Numbers e usamos a alternativa
IN()
.Meu conselho seria procurar adicionar
MAXDOP(1)
e ver se isso ajuda na sua consulta, com uma reescrita, se necessário.Claro que você também deve ter em mente que, do meu lado, ele tem um desempenho ainda melhor devido à filtragem de bitmap otimizada e ao uso de vários threads com bom efeito:
TL; DR
Os custos estimados definirão o plano escolhido. Consegui replicar o comportamento e vi que
optimized bitmap filters
+parallellism
operadores onde adicionados no meu fim de realizar a consulta de forma performant e rápido.Você pode adicionar
MAXDOP(1)
a sua consulta como uma maneira de obter o mesmo resultado controlado a cada vez, com ummerge join
e não 'ruim'parallellism
.Atualizar para uma versão mais recente e usar uma versão mais alta do estimador de cardinalidade
CardinalityEstimationModelVersion="70"
também pode ajudar.Uma tabela temporária de números para fazer a filtragem de vários valores também pode ajudar.
DML para quase replicar o problema do OP
Passei mais tempo nisso do que gostaria de admitir
fonte
MAXDOP 0
parece ter corrigido. Muito obrigado!Pelo que posso dizer, a principal diferença entre os dois planos é a diferença no que é o "Filtro Principal".
Com a primeira versão, derivava o filtro principal, que
Audit.ID
é relacionado aoari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
filtro dessa lista para aqueles queAudit.TargetTypeID
estavam na lista.Com a segunda versão, derivava o filtro principal,
Audit.ID
relacionado à lista deAudit.TargetTypeID
.Desde que a adição de
Audit.TargetTypeID = 30
parecia aumentar drasticamente a contagem de registros (267.479 e 25.650, respectivamente, de acordo com a pergunta original). É provavelmente por isso que os planos de execução são diferentes. (Pelo que entendi) O SQL tentará primeiro executar a função mais seletiva e depois aplicar o restante das regras depois disso. Com a primeira versão, a consultaAuditRelatedID.RelatedID
para encontrarAudit.ID
foi provavelmente mais seletiva do que tentar usarAudit.TargetTypeID
para encontrarAudit.ID
.Para crédito do ypercube. Certamente, você pode atualizar
[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]
para ter ambosRelatedID
eAuditID
como parte do índice em vez de terAuditID
como parte de umINCLUDE
. Não deve ocupar espaço adicional no índice e permitiria o uso de ambas as colunas emJOIN
cláusulas. Isso pode ajudar o Query Optimizer a criar o mesmo plano de execução para as duas consultas.Operando com uma lógica semelhante, pode haver algum benefício em um índice
Audit
que contenhaTargetTypeID ASC, ID ASC
os nós ordenados / de filtragem reais (não como parte doINCLUDE
). Isso deve permitir que o otimizador de consultas filtreAudit.TargetTypeID
e entre rapidamenteAuditReferenceIds.AuditID
. Agora, isso pode acabar com as duas consultas escolhendo o plano menos eficiente, então eu só daria uma chance depois de tentar a recomendação do ypercube.fonte