Consultas muito semelhantes, desempenho muito diferente

9

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]
Chocoman
fonte
3
Poderíamos obter alguns detalhes do esquema e do índice da tabela. Como tenho certeza, você notou que os planos são um pouco diferentes, mas evidentemente está fazendo uma grande diferença. Se conseguirmos obter esses detalhes, talvez possamos ver quais opções temos.
Kirk Saunders
2
Como uma dica muito rápida, em vez de usar IN, crie uma TempTable com uma única coluna TINYINT / INT (agrupada) com os números desejados e, em seguida, INNER JOIN. Fora isso, provavelmente precisaremos de informações DDL como @KirkSaunders mencionado acima
George.Palacios
2
Existe algo de especial TargetTypeId = 30? Parece que os planos são diferentes porque esse valor realmente distorce a quantidade de dados (espera-se que seja) retornada.
Aaron Bertrand
Sei que é muito pedante, mas a afirmação "a primeira consulta retorna muito mais linhas que a segunda". não está correto. Ambos retornam 1 linha;)
ypercubeOff
11
Eu atualizei a questão com o CREATE TABLE para tabelas
Chocoman

Respostas:

8

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:

insira a descrição da imagem aqui

O custo total estimado da subárvore para seu plano com melhor desempenho

insira a descrição da imagem aqui


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 na index seek& nested loops operatorexecução da junção:

insira a descrição da imagem aqui

Embora para o nosso plano de consultas incorreto, o Clustered index seekcusto do operador seja menor

insira a descrição da imagem aqui

O que deve explicar por que o outro plano poderia ter sido escolhido.

(E adicionando o parâmetro 30aumentando o custo do plano incorreto, onde ele subiu acima do 871.510000custo estimado). Estimativa estimada ™

O plano com melhor desempenho

insira a descrição da imagem aqui

O plano ruim

insira a descrição da imagem aqui


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 JOINdica de junção

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)

É mais próximo, mas tem algumas diferenças na ordem de junção:

insira a descrição da imagem aqui


Reescrever

Minha primeira tentativa de reescrever poderia ser armazenar todos esses números em uma tabela temporária:

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);

E então adicionando um em JOINvez do 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';

Nosso plano de consulta é diferente, mas ainda não foi corrigido:

insira a descrição da imagem aqui

com um enorme custo estimado do operador em AuditRelatedIdscima da mesa

insira a descrição da imagem aqui


Aqui é 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 7497e7498

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);

Mais informações sobre filtros de bitmap otimizados aqui .

insira a descrição da imagem aqui

Isso significa que, sem os filtros de bitmap, o otimizador considera melhor ingressar primeiro na #numbertabela e depois ingressar noAuditRelatedIds tabela.

Ao forçar a ordem OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);, podemos ver o porquê:

insira a descrição da imagem aqui

& insira a descrição da imagem aqui

Não é bom


Removendo a capacidade de ficar paralelo ao maxdop 1

Ao adicionar MAXDOP 1a consulta, o desempenho é mais rápido e único.

E adicionando 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];

insira a descrição da imagem aqui

Enquanto estiver usando uma junção de mesclagem. insira a descrição da imagem aqui

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:

insira a descrição da imagem aqui

insira a descrição da imagem aqui


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 um merge joine 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

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
fonte
Explicação muito agradável! Adicionar MAXDOP 0parece ter corrigido. Muito obrigado!
Chocoman 19/06/19
11
MAXDOP 1 ** (erro de digitação)
Chocoman
@Chocoman Great! Happy to help :)
Randi Vertongen
1

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 ao ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'filtro dessa lista para aqueles que Audit.TargetTypeIDestavam na lista.

Com a segunda versão, derivava o filtro principal, Audit.IDrelacionado à lista deAudit.TargetTypeID .

Desde que a adição de Audit.TargetTypeID = 30parecia 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 consulta AuditRelatedID.RelatedIDpara encontrar Audit.IDfoi provavelmente mais seletiva do que tentar usar Audit.TargetTypeIDpara encontrar Audit.ID.

Para crédito do ypercube. Certamente, você pode atualizar [AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]para ter ambos RelatedIDe AuditIDcomo parte do índice em vez de ter AuditIDcomo parte de um INCLUDE. 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 Auditque contenha TargetTypeID ASC, ID ASCos nós ordenados / de filtragem reais (não como parte do INCLUDE). Isso deve permitir que o otimizador de consultas filtre Audit.TargetTypeIDe entre rapidamente AuditReferenceIds.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.

Kirk Saunders
fonte