GROUP BY com MAX versus apenas MAX

8

Eu sou um programador, lidando com uma grande tabela com o seguinte esquema:

UpdateTime, PK, datetime, notnull
Name, PK, char(14), notnull
TheData, float

Há um índice clusterizado em Name, UpdateTime

Fiquei me perguntando o que deveria ser mais rápido:

SELECT MAX(UpdateTime)
FROM [MyTable]

ou

SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM [MyTable]
    group by [UpdateTime]
   ) as t

As inserções nesta tabela estão em pedaços de 50.000 linhas com a mesma data . Por isso, pensei que agrupar por poderia facilitar o MAXcálculo.

Em vez de tentar encontrar no máximo 150.000 linhas, agrupar por 3 linhas e calcular MAXmais seria mais rápido? Minha suposição é correta ou agrupa também é onerosa?

Ofiris
fonte

Respostas:

12

Criei a tabela big_table de acordo com seu esquema

create table big_table
(
    updatetime datetime not null,
    name char(14) not null,
    TheData float,
    primary key(Name,updatetime)
)

Em seguida, preenchi a tabela com 50.000 linhas com este código:

DECLARE @ROWNUM as bigint = 1
WHILE(1=1)
BEGIN
    set @rownum  = @ROWNUM + 1
    insert into big_table values(getdate(),'name' + cast(@rownum as CHAR), cast(@rownum as float))
    if @ROWNUM > 50000
        BREAK;  
END

Usando o SSMS, testei as duas consultas e percebi que, na primeira consulta, você está procurando o MAX do TheData e, na segunda, o MAX do updatetime

Assim, modifiquei a primeira consulta para obter também o MAX do tempo de atualização

set statistics time on -- execution time
set statistics io on -- io stats (how many pages read, temp tables)

-- query 1
SELECT MAX([UpdateTime])
FROM big_table

-- query 2
SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM big_table
    group by [UpdateTime]
   ) as t


set statistics time off
set statistics io off

Usando o tempo de estatística , recebo de volta o número de milissegundos necessário para analisar, compilar e executar cada instrução

Usando o Statistics IO , recebo informações sobre a atividade do disco

ESTATÍSTICAS TEMPO e ESTATÍSTICAS IO fornecem informações úteis. Como as tabelas temporárias usadas (indicadas pela mesa de trabalho). Além disso, quantas páginas lógicas lidas foram lidas, o que indica o número de páginas do banco de dados lidas no cache.

Ativei o plano de execução com CTRL + M (ativa o plano de execução atual) e depois executo com F5.

Isso fornecerá uma comparação das duas consultas.

Aqui está a saída da guia Mensagens

- Consulta 1

Tabela 'big_table'. Contagem de varreduras 1, leituras lógicas 543 , leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de lob 0.

Tempos de execução do SQL Server: tempo de CPU = 16 ms, tempo decorrido = 6 ms .

- Consulta 2

Tabela ' Mesa de trabalho '. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.

Tabela 'big_table'. Contagem de varreduras 1, leituras lógicas 543 , leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de lob 0.

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 35 ms .

Ambas as consultas resultam em 543 leituras lógicas, mas a segunda consulta tem um tempo decorrido de 35ms, sendo que a primeira possui apenas 6ms. Você também notará que a segunda consulta resulta no uso de tabelas temporárias no tempdb, indicadas pela palavra tabela de trabalho . Embora todos os valores da tabela de trabalho estejam em 0, o trabalho ainda foi realizado no tempdb.

Depois, há a saída da guia Plano de execução real ao lado da guia Mensagens

insira a descrição da imagem aqui

De acordo com o plano de execução fornecido pelo MSSQL, a segunda consulta que você forneceu tem um custo total de lote de 64%, enquanto a primeira custa apenas 36% do lote total, portanto, a primeira consulta exige menos trabalho.

Usando o SSMS, você pode testar e comparar suas consultas e descobrir exatamente como o MSSQL está analisando suas consultas e quais objetos: tabelas, índices e / ou estatísticas, se houver alguma que esteja sendo usada para satisfazer essas consultas.

Uma observação adicional a ter em mente quando o teste estiver limpando o cache antes do teste, se possível. Isso ajuda a garantir que as comparações sejam precisas e isso é importante quando se pensa em atividade do disco. Começo com DBCC DROPCLEANBUFFERS e DBCC FREEPROCCACHE para limpar todo o cache. Cuidado para não usar esses comandos em um servidor de produção realmente em uso, pois você forçará efetivamente o servidor a ler tudo do disco para a memória.

Aqui está a documentação relevante.

  1. Limpe o cache do plano com DBCC FREEPROCCACHE
  2. Limpe tudo do buffer pool com DBCC DROPCLEANBUFFERS

O uso desses comandos pode não ser possível, dependendo de como seu ambiente é usado.

Atualizado 10/28 12:46 pm

Correções na imagem do plano de execução e na saída de estatísticas.

Craig Efrein
fonte
Obrigado pela resposta profunda, observe minha linha careca no código, cada grupo de 50.000 linhas tem a mesma data que é diferente de outros pedaços. Então, eu deveria mover getdate()para fora do loop
Ofiris
11
Olá @Ofiris. A resposta que dei é realmente apenas para ajudá-lo a fazer a comparação por conta própria. Criei dados aleatórios indesejados apenas para ilustrar o uso dos vários comandos e ferramentas que você pode usar para tirar suas próprias conclusões.
Craig Efrein
11
Nenhum trabalho foi feito no tempdb. A tabela de trabalho é gerenciar partições, caso o agregado de hash tenha que ser derramado no tempdb porque a memória insuficiente foi reservada para ele. Por favor, enfatize que os custos são sempre estimados, mesmo em um plano 'real'. São estimativas do otimizador, que podem não ter muita relação com o desempenho real. Não use% do lote como métrica de ajuste principal. A limpeza de buffers é importante apenas se você quiser testar o desempenho do cache frio.
Paul White 9
11
Olá @PaulWhite. Obrigado pelas informações adicionais. Agradeço sinceramente todas as sugestões sobre como ser mais exato. Quando você redige suas frases: "Não use", isso não pode ser mal interpretado como dando um pedido em vez de oferecer aconselhamento profissional? Cumprimentos.
Craig Efrein
@CraigEfrein Provavelmente. Eu estava sendo breve para caber no espaço de comentários permitido.
Paul White 9
6

As inserções nesta tabela estão em pedaços de 50.000 linhas com a mesma data. Por isso, pensei que agrupar por poderia facilitar o cálculo do MAX.

A reescrita pode ter ajudado se o SQL Server implementou a pular varredura de índice, mas isso não acontece.

A pular varredura de índice permite que um mecanismo de banco de dados busque o próximo valor de índice diferente em vez de varrer todas as duplicatas (ou subchaves irrelevantes) intermediárias. No seu caso, o skip-scan permitiria ao mecanismo encontrar MAX(UpdateTime)o primeiro Name, MAX(UpdateTime)o segundo no segundo Name... e assim por diante. A etapa final seria encontrar os MAX(UpdateTime)candidatos a um por nome.

Você pode simular isso até certo ponto, usando um CTE recursivo, mas é um pouco confuso e não é tão eficiente quanto seria o skip-scan interno:

WITH RecursiveCTE
AS
(
    -- Anchor: MAX UpdateTime for
    -- highest-sorting Name
    SELECT TOP (1)
        BT.Name,
        BT.UpdateTime
    FROM dbo.BigTable AS BT
    ORDER BY
        BT.Name DESC,
        BT.UpdateTime DESC

    UNION ALL

    -- Recursive part
    -- MAX UpdateTime for Name
    -- that sorts immediately lower
    SELECT
        SubQuery.Name,
        SubQuery.UpdateTime
    FROM 
    (
        SELECT
            BT.Name,
            BT.UpdateTime,
            rn = ROW_NUMBER() OVER (
                ORDER BY BT.Name DESC, BT.UpdateTime DESC)
        FROM RecursiveCTE AS R
        JOIN dbo.BigTable AS BT
            ON BT.Name < R.Name
    ) AS SubQuery
    WHERE
        SubQuery.rn = 1
)
-- Final MAX aggregate over
-- MAX(UpdateTime) per Name
SELECT MAX(UpdateTime) 
FROM RecursiveCTE
OPTION (MAXRECURSION 0);

Plano recursivo de CTE

Esse plano realiza uma busca única para cada distinto e Name, em seguida, encontra o mais alto UpdateTimedos candidatos. Seu desempenho em relação a uma verificação completa simples da tabela depende de quantas duplicatas existem por Namee se as páginas tocadas pelas buscas singleton estão na memória ou não.

Soluções alternativas

Se você conseguir criar um novo índice nesta tabela, uma boa opção para esta consulta seria um índice UpdateTimesozinho:

CREATE INDEX IX__BigTable_UpdateTime 
ON dbo.BigTable (UpdateTime);

Este índice permitirá que o mecanismo de execução encontre o mais alto UpdateTimecom uma busca singleton até o final da árvore b do índice:

Novo plano de índice

Esse plano consome apenas algumas E / S lógicas (para navegar nos níveis da árvore b) e é concluído imediatamente. Observe que a Varredura de índice no plano não é uma varredura completa do novo índice - apenas retorna uma linha da 'extremidade' do índice.

Se você não deseja criar um novo índice completo na tabela, considere uma exibição indexada contendo apenas os UpdateTimevalores exclusivos :

CREATE VIEW dbo.BigTableUpdateTimes
WITH SCHEMABINDING AS
SELECT 
    UpdateTime, 
    NumRows = COUNT_BIG(*)
FROM dbo.BigTable AS BT
GROUP BY
    UpdateTime;
GO
CREATE UNIQUE CLUSTERED INDEX cuq
ON dbo.BigTableUpdateTimes (UpdateTime);

Isso tem a vantagem de criar apenas uma estrutura com tantas linhas quanto houver UpdateTimevalores exclusivos , embora toda consulta que altere dados na tabela base tenha operadores adicionais adicionados ao seu plano de execução para manter a exibição indexada. A consulta para encontrar o UpdateTimevalor máximo seria:

SELECT MAX(BTUT.UpdateTime)
FROM dbo.BigTableUpdateTimes AS BTUT
    WITH (NOEXPAND);

Plano de visualização indexada

Paul White 9
fonte