Por que uma consulta agregada é significativamente mais rápida com uma cláusula GROUP BY do que sem uma?

12

Só estou curioso para saber por que uma consulta agregada é executada muito mais rápido com uma GROUP BYcláusula do que sem uma.

Por exemplo, esta consulta leva quase 10 segundos para ser executada

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1

Enquanto este leva menos de um segundo

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1
GROUP BY CreatedDate

Há apenas um CreatedDateneste caso, portanto, a consulta agrupada retorna os mesmos resultados que a consulta não agrupada.

Percebi que os planos de execução para as duas consultas são diferentes - a segunda consulta usa paralelismo, enquanto a primeira consulta não.

Plano de Execução Query1 Plano de Execução Query2

É normal que o SQL Server avalie uma consulta agregada de maneira diferente se não tiver uma cláusula GROUP BY? E há algo que eu possa fazer para melhorar o desempenho da 1ª consulta sem usar umGROUP BY cláusula?

Editar

Acabei de aprender que posso usar OPTION(querytraceon 8649)para definir o custo adicional do paralelismo como 0, o que faz com que a consulta use algum paralelismo e reduz o tempo de execução para 2 segundos, embora eu não saiba se há alguma desvantagem em usar essa dica de consulta.

SELECT MIN(CreatedDate)
FROM MyTable
WHERE SomeIndexedValue = 1
OPTION(querytraceon 8649)

insira a descrição da imagem aqui

Eu ainda preferiria um tempo de execução mais curto, pois a consulta deve preencher um valor na seleção do usuário, portanto, idealmente, deve ser instantânea como a consulta agrupada. No momento, estou apenas encapsulando minha consulta, mas sei que essa não é realmente uma solução ideal.

SELECT Min(CreatedDate)
FROM
(
    SELECT Min(CreatedDate) as CreatedDate
    FROM MyTable WITH (NOLOCK) 
    WHERE SomeIndexedValue = 1
    GROUP BY CreatedDate
) as T

Editar # 2

Em resposta ao pedido de Martin para obter mais informações :

Ambos CreatedDatee SomeIndexedValuetêm um índice não exclusivo e não agrupado em separado.SomeIndexedValueé na verdade um campo varchar (7), mesmo que armazene um valor numérico que aponte para o PK (int) de outra tabela. O relacionamento entre as duas tabelas não está definido no banco de dados. Não devo alterar o banco de dados e só posso escrever consultas que consultam dados.

MyTablecontém mais de 3 milhões de registros e a cada registro é atribuído um grupo ao qual ele pertence ( SomeIndexedValue). Os grupos podem ter de 1 a 200.000 registros

Rachel
fonte

Respostas:

8

Parece que provavelmente está seguindo um índice CreatedDatena ordem do menor para o mais alto e fazendo pesquisas para avaliar o SomeIndexedValue = 1predicado.

Quando encontra a primeira linha correspondente, ela é executada, mas pode muito bem estar fazendo muito mais pesquisas do que espera antes de encontrar essa linha (pressupõe que as linhas correspondentes ao predicado sejam distribuídas aleatoriamente de acordo com a data).

Veja minha resposta aqui para um problema semelhante

O índice ideal para esta consulta seria um SomeIndexedValue, CreatedDate. Supondo que você não possa adicionar isso ou, pelo menos, tornar seu índice existente na SomeIndexedValuecapa CreatedDatecomo uma coluna incluída, tente reescrever a consulta da seguinte maneira

SELECT MIN(DATEADD(DAY, 0, CreatedDate)) AS CreatedDate
FROM MyTable
WHERE SomeIndexedValue = 1

para impedir que ele use esse plano específico.

Martin Smith
fonte
2

Podemos controlar o MAXDOP e escolher uma tabela conhecida, por exemplo, AdventureWorks.Production.TransactionHistory?

Quando repito sua configuração usando

--#1
SELECT MIN(TransactionDate) 
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
OPTION( MAXDOP 1) ;

--#2
SELECT MIN(TransactionDate) 
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO 

os custos são idênticos.

Como um aparte, eu esperaria (faça acontecer) uma busca de índice em seu valor indexado; caso contrário, você provavelmente verá correspondências de hash em vez de agregações de fluxo. Você pode melhorar o desempenho com índices não agrupados em cluster que incluem os valores agregados e / ou criar uma exibição indexada que define suas agregações como colunas. Em seguida, você alcançaria um índice em cluster, que contém suas agregações, por um ID indexado. No SQL Standard, você pode apenas criar a exibição e usar a dica WITH (NOEXPAND).

Um exemplo (eu não uso MIN, pois ele não funciona em modos de exibição indexados):

USE AdventureWorks ;
GO

-- Covering Index with Include
CREATE INDEX IX_CoverAndInclude
ON Production.TransactionHistory(TransactionDate) 
INCLUDE (Quantity) ;
GO

-- Indexed View
CREATE VIEW dbo.SumofQtyByTransDate
    WITH SCHEMABINDING
AS
SELECT 
      TransactionDate 
    , COUNT_BIG(*) AS NumberOfTransactions
    , SUM(Quantity) AS TotalTransactions
FROM Production.TransactionHistory
GROUP BY TransactionDate ;
GO

CREATE UNIQUE CLUSTERED INDEX SumofAllChargesIndex 
    ON dbo.SumofQtyByTransDate (TransactionDate) ;  
GO


--#1
SELECT SUM(Quantity) 
FROM AdventureWorks.Production.TransactionHistory 
WITH (INDEX(0))
WHERE TransactionID = 100001 
OPTION( MAXDOP 1) ;

--#2
SELECT SUM(Quantity)  
FROM AdventureWorks.Production.TransactionHistory 
WITH (INDEX(IX_CoverAndInclude))
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO 

--#3
SELECT SUM(Quantity)  
FROM AdventureWorks.Production.TransactionHistory
WHERE TransactionID = 100001 
GROUP BY TransactionDate
OPTION( MAXDOP 1) ;
GO
ooutwire
fonte
MAXDOPdefine o grau máximo de paralelismo, que limita o número de processadores que a consulta pode usar. Isso basicamente faria com que a 2ª consulta fosse tão lenta quanto a 1ª, pois está removendo seus recursos para usar o paralelismo, o que não é o que eu quero.
Rachel
@ Rachel Eu concordo; mas não podemos comparar nada, a menos que definamos algumas regras básicas. Não consigo comparar facilmente um processo paralelo em execução em 64 núcleos com um único thread em execução em um. No final, espero que todas as nossas máquinas têm pelo menos uma CPU lógico = -)
ooutwire
0

Na minha opinião, o motivo do problema é que o otimizador de servidor sql não está procurando o plano BEST, mas sim um bom plano, como é evidente pelo fato de que, após forçar o paralelismo, a consulta foi executada muito mais rapidamente, algo que o otimizador tinha não feito por si próprio.

Também vi muitas situações em que reescrever a consulta em um formato diferente era a diferença entre paralelizar (por exemplo, embora a maioria dos artigos sobre SQL recomende a parametrização, eu achei que algumas vezes causava paralelismo, mesmo quando os parâmetros farejavam eram os mesmos que não - um paralelizado ou combinando duas consultas com UNION ALL às vezes pode eliminar a paralelização).

Como tal, a solução correta pode estar tentando maneiras diferentes de escrever a consulta, como tentar tabelas temporárias, variáveis ​​de tabela, cte, tabelas derivadas, parametrizar etc. e também brincar com os índices, visualizações indexadas ou índices filtrados em para obter o melhor plano.

yoel halb
fonte