GROUP BY con MAX versus solo MAX

8

Soy un programador, que trata con una gran tabla que el siguiente esquema:

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

Hay un índice agrupado en Name, UpdateTime

Me preguntaba qué debería ser más rápido:

SELECT MAX(UpdateTime)
FROM [MyTable]

o

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

Las inserciones de esta tabla están en trozos de 50,000 filas con la misma fecha . Entonces pensé que agrupar por podría facilitar el MAXcálculo.

En lugar de tratar de encontrar un máximo de 150,000 filas, agrupando por 3 filas, y luego el cálculo de MAXsería más rápido. ¿Mi suposición es correcta o agrupar por también es costosa?

Ofiris
fuente

Respuestas:

12

Creé la tabla big_table de acuerdo con su esquema

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

Luego llené la tabla con 50,000 filas con 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 SSMS, probé ambas consultas y me di cuenta de que en la primera consulta está buscando el MAX de TheData y en el segundo, el MAX del tiempo de actualización

Modifiqué así la primera consulta para obtener también el MAX del tiempo de actualización

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 Statistics Time obtengo la cantidad de milisegundos necesarios para analizar, compilar y ejecutar cada instrucción

Usando Statistics IO obtengo información sobre la actividad del disco

ESTADÍSTICAS TIEMPO y ESTADÍSTICAS IO proporcionan información útil. Tal como se usaron las tablas temporales (indicado por la tabla de trabajo). También cuántas páginas lógicas leídas se leyeron, lo que indica el número de páginas de la base de datos leídas de la memoria caché.

Luego activo el plan de ejecución con CTRL + M (activa mostrar el plan de ejecución real) y luego ejecuto con F5.

Esto proporcionará una comparación de ambas consultas.

Aquí está la salida de la pestaña Mensajes

- Consulta 1

Tabla 'tabla_grande'. Cuenta de escaneo 1, lecturas lógicas 543 , lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 16 ms, tiempo transcurrido = 6 ms .

- Consulta 2

Mesa 'Mesa de trabajo '. Recuento de exploración 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tabla 'tabla_grande'. Cuenta de escaneo 1, lecturas lógicas 543 , lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 35 ms .

Ambas consultas dan como resultado 543 lecturas lógicas, pero la segunda consulta tiene un tiempo transcurrido de 35 ms, mientras que la primera tiene solo 6 ms. También notará que la segunda consulta da como resultado el uso de tablas temporales en tempdb, indicadas por la palabra tabla de trabajo . Aunque todos los valores de la tabla de trabajo están en 0, el trabajo todavía se realizó en tempdb.

Luego está la salida de la pestaña del plan de ejecución real al lado de la pestaña Mensajes

ingrese la descripción de la imagen aquí

De acuerdo con el plan de ejecución proporcionado por MSSQL, la segunda consulta que proporcionó tiene un costo de lote total del 64%, mientras que la primera solo cuesta el 36% del lote total, por lo que la primera consulta requiere menos trabajo.

Con SSMS, puede probar y comparar sus consultas y descubrir exactamente cómo MSSQL analiza sus consultas y qué objetos: tablas, índices y / o estadísticas, si se están utilizando, para satisfacer esas consultas.

Una nota adicional adicional a tener en cuenta cuando se realizan pruebas es limpiar el caché antes de realizar las pruebas, si es posible. Esto ayuda a garantizar que las comparaciones sean precisas y esto es importante cuando se piensa en la actividad del disco. Comienzo con DBCC DROPCLEANBUFFERS y DBCC FREEPROCCACHE para limpiar todo el caché. Sin embargo, tenga cuidado de no utilizar estos comandos en un servidor de producción realmente en uso, ya que obligará efectivamente al servidor a leer todo desde el disco a la memoria.

Aquí está la documentación relevante.

  1. Borre el caché del plan con DBCC FREEPROCCACHE
  2. Elimine todo de la agrupación de almacenamientos intermedios con DBCC DROPCLEANBUFFERS

El uso de estos comandos puede no ser posible dependiendo de cómo se use su entorno.

Actualizado 28/10 12:46 pm

Se realizaron correcciones en la imagen del plan de ejecución y en la salida de estadísticas.

Craig Efrein
fuente
Gracias por la respuesta profunda, tenga en cuenta mi línea calva en el código, cada grupo de 50,000 filas tiene la misma fecha que es diferente de otros fragmentos. Así que debería getdate()salir del círculo
Ofiris
1
Hola @Ofiris La respuesta que di es en realidad solo para ayudarte a hacer la comparación por tu cuenta. Creé datos basura aleatorios solo para ilustrar el uso de los diversos comandos y herramientas que puede utilizar para llegar a sus propias conclusiones.
Craig Efrein
1
No se realizó ningún trabajo en tempdb. La mesa de trabajo es administrar particiones en caso de que el agregado de hash tenga que derramarse a tempdb porque no hay suficiente memoria reservada para ello. Por favor enfatice que los costos son siempre estimados, incluso en un plan 'real'. Son las estimaciones del optimizador, que pueden no tener mucha relación con el rendimiento real. No utilice el% de lote como una métrica de ajuste principal. El borrado de búferes solo es importante si desea probar el rendimiento de la memoria caché en frío.
Paul White 9
1
Hola @PaulWhite Gracias por la información adicional, agradezco sinceramente cualquier sugerencia sobre cómo ser más exactos. Sin embargo, cuando redacta sus oraciones: "No usar", ¿no podría interpretarse erróneamente como dar una orden en lugar de ofrecer asesoramiento profesional? Atentamente.
Craig Efrein
@CraigEfrein Probablemente. Estaba siendo breve para caber en el espacio de comentarios permitido.
Paul White 9
6

Las inserciones de esta tabla están en trozos de 50,000 filas con la misma fecha. Entonces pensé que agrupar por podría facilitar el cálculo MAX.

La reescritura podría haber ayudado si SQL Server implementara el escaneo de índice, pero no lo hace.

El escaneo de índice permite que un motor de base de datos busque el siguiente valor de índice diferente en lugar de escanear todos los duplicados (o subclaves irrelevantes) intermedios. En su caso, skip-scan permitiría que el motor encuentre MAX(UpdateTime)el primero Name, salte al MAX(UpdateTime)segundo Name... y así sucesivamente. El último paso sería encontrar los MAX(UpdateTime)candidatos de uno por nombre.

Puede simular esto hasta cierto punto utilizando un CTE recursivo, pero es un poco desordenado y no es tan eficiente como lo sería el omitir escaneo incorporado:

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

Plan CTE recursivo

Ese plan realiza una búsqueda única para cada distintivo Name, luego encuentra el más alto UpdateTimede los candidatos. Su rendimiento en relación con un escaneo completo simple de la tabla depende de cuántos duplicados haya por persona Namey de si las páginas tocadas por las búsquedas individuales están en la memoria o no.

Soluciones alternativas

Si puede crear un nuevo índice en esta tabla, una buena opción para esta consulta sería un índice UpdateTimesolo:

CREATE INDEX IX__BigTable_UpdateTime 
ON dbo.BigTable (UpdateTime);

Este índice permitirá que el motor de ejecución encuentre el más alto UpdateTimecon una búsqueda única hasta el final del índice b-tree:

Nuevo plan de índice

Este plan consume solo unas pocas IO lógicas (para navegar por los niveles de b-tree) y se completa de inmediato. Tenga en cuenta que la exploración de índice en el plan no es una exploración completa del nuevo índice, simplemente devuelve una fila desde el 'final' del índice.

Si no desea crear un nuevo índice completo en la tabla, puede considerar una vista indizada que contenga solo los UpdateTimevalores únicos :

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

Esto tiene la ventaja de crear solo una estructura con tantas filas como UpdateTimevalores únicos , aunque cada consulta que cambie datos en la tabla base tendrá operadores adicionales agregados a su plan de ejecución para mantener la vista indexada. La consulta para encontrar el UpdateTimevalor máximo sería:

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

Plan de vista indexada

Paul White 9
fuente