sql >> Base de Datos >  >> RDS >> Database

Mantener un MAX (o MIN) en ejecución agrupado

Nota:esta publicación se publicó originalmente solo en nuestro libro electrónico, Técnicas de alto rendimiento para SQL Server, Volumen 3. Puede obtener más información sobre nuestros libros electrónicos aquí.

Un requisito que veo ocasionalmente es que se devuelva una consulta con pedidos agrupados por cliente, que muestre el total máximo adeudado visto para cualquier pedido hasta la fecha (un "máximo acumulado"). Así que imagina estas filas de muestra:

Id. de pedido de venta ID de cliente Fecha del pedido Total adeudado
12 2 2014-01-01 37,55
23 1 2014-01-02 45.29
31 2 2014-01-03 24.56
32 2 2014-01-04 89.84
37 1 2014-01-05 32.56
44 2 2014-01-06 45.54
55 1 2014-01-07 99.24
62 2 2014-01-08 12.55

Algunas filas de datos de muestra

Los resultados deseados de los requisitos establecidos son los siguientes:en términos sencillos, ordene los pedidos de cada cliente por fecha y enumere cada pedido. Si ese es el valor TotalDue más alto para todos los pedidos vistos hasta esa fecha, imprima el total de ese pedido; de lo contrario, imprima el valor TotalDue más alto de todos los pedidos anteriores:

Id. de pedido de venta ID de cliente Fecha del pedido Total adeudado MaxTotalDue
12 1 2014-01-02 45.29 45.29
23 1 2014-01-05 32.56 45.29
31 1 2014-01-07 99.24 99.24
32 2 2014-01-01 37,55 37,55
37 2 2014-01-03 24,56 37,55
44 2 2014-01-04 89,84 89,84
55 2 2014-01-06 45.54 89,84
62 2 2014-01-08 12.55 89,84

Ejemplos de resultados deseados

Mucha gente instintivamente querría usar un cursor o un ciclo while para lograr esto, pero existen varios enfoques que no involucran estas construcciones.

Subconsulta correlacionada

Este enfoque parece ser el más simple y directo para el problema, pero se ha demostrado una y otra vez que no escala, ya que las lecturas crecen exponencialmente a medida que la tabla se hace más grande:

SELECT /* Correlated Subquery */ SalesOrderID, CustomerID, OrderDate, TotalDue,
  MaxTotalDue = (SELECT MAX(TotalDue) 
     FROM Sales.SalesOrderHeader
     WHERE CustomerID = h.CustomerID
     AND SalesOrderID <= h.SalesOrderID)
  FROM Sales.SalesOrderHeader AS h
  ORDER BY CustomerID, SalesOrderID;

Aquí está el plan contra AdventureWorks2014, usando SQL Sentry Plan Explorer:

Plan de ejecución para la subconsulta correlacionada (haga clic para ampliar)

APLICACIÓN CRUZADA autorreferencial

Este enfoque es casi idéntico al enfoque de subconsulta correlacionada, en términos de sintaxis, forma del plan y rendimiento a escala.

SELECT /* CROSS APPLY */ h.SalesOrderID, h.CustomerID, h.OrderDate, h.TotalDue, x.MaxTotalDue
FROM Sales.SalesOrderHeader AS h
CROSS APPLY
(
  SELECT MaxTotalDue = MAX(TotalDue)
    FROM Sales.SalesOrderHeader AS i
    WHERE i.CustomerID = h.CustomerID
    AND i.SalesOrderID <= h.SalesOrderID
) AS x
ORDER BY h.CustomerID, h.SalesOrderID;

El plan es bastante similar al plan de subconsultas correlacionadas, con la única diferencia de la ubicación de un tipo:

Plan de ejecución para CROSS APPLY (haga clic para ampliar)

CTE recursivo

Detrás de escena, esto usa bucles, pero hasta que realmente lo ejecutemos, podemos fingir que no lo hace (aunque fácilmente es la pieza de código más complicada que me gustaría escribir para resolver este problema en particular):

;WITH /* Recursive CTE */ cte AS 
(
  SELECT  SalesOrderID, CustomerID, OrderDate, TotalDue, MaxTotalDue 
    FROM 
	(
	  SELECT SalesOrderID, CustomerID, OrderDate, TotalDue, MaxTotalDue = TotalDue, 
	    rn = ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY SalesOrderID)
	  FROM Sales.SalesOrderHeader
	) AS x
  WHERE rn = 1
  UNION ALL
  SELECT r.SalesOrderID, r.CustomerID, r.OrderDate, r.TotalDue,
    MaxTotalDue = CASE 
	  WHEN r.TotalDue > cte.MaxTotalDue THEN r.TotalDue 
	  ELSE cte.MaxTotalDue 
	END
  FROM cte
  CROSS APPLY
  (
    SELECT  SalesOrderID, CustomerID, OrderDate, TotalDue,
      rn = ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY SalesOrderID)
    FROM Sales.SalesOrderHeader AS h
    WHERE h.CustomerID = cte.CustomerID
    AND h.SalesOrderID > cte.SalesOrderID
  ) AS r
  WHERE r.rn = 1
)
SELECT SalesOrderID, CustomerID, OrderDate, TotalDue, MaxTotalDue
FROM cte
ORDER BY CustomerID, SalesOrderID
OPTION (MAXRECURSION 0);

Inmediatamente puede ver que el plan es más complejo que los dos anteriores, lo cual no es sorprendente dada la consulta más compleja:

Plan de ejecución para CTE recursivo (haga clic para ampliar)

Debido a algunas malas estimaciones, vemos una búsqueda de índice con una búsqueda de clave adjunta que probablemente debería haber sido reemplazada por un solo escaneo, y también obtenemos una operación de ordenación que finalmente debe pasar a tempdb (puede ver esto en la información sobre herramientas si pasa el cursor sobre el operador de clasificación con el icono de advertencia):

MAX() SOBRE (FILAS SIN LÍMITES)

Esta es una solución que solo está disponible en SQL Server 2012 y superior, ya que utiliza extensiones recientemente introducidas para funciones de ventana.

SELECT /* MAX() OVER() */ SalesOrderID, CustomerID, OrderDate, TotalDue,
  MaxTotalDue = MAX(TotalDue) OVER 
  (
    PARTITION BY CustomerID ORDER BY SalesOrderID 
    ROWS UNBOUNDED PRECEDING
  )
FROM Sales.SalesOrderHeader
ORDER BY CustomerID, SalesOrderID;

El plan muestra exactamente por qué escala mejor que todos los demás; solo tiene una operación de escaneo de índice agrupado, a diferencia de dos (o la mala elección de un escaneo y una búsqueda + búsqueda en el caso del CTE recursivo):

Plan de ejecución para MAX() OVER() (haga clic para ampliar)

Comparación de rendimiento

Los planes ciertamente nos llevan a creer que el nuevo MAX() OVER() La capacidad en SQL Server 2012 es un verdadero ganador, pero ¿qué hay de las métricas de tiempo de ejecución tangibles? Así es como se compararon las ejecuciones:

Las dos primeras consultas fueron casi idénticas; mientras que en este caso el CROSS APPLY fue mejor en términos de duración general por un pequeño margen, la subconsulta correlacionada a veces lo supera un poco. El CTE recursivo es sustancialmente más lento cada vez, y puede ver los factores que contribuyen a eso, a saber, las malas estimaciones, la gran cantidad de lecturas, la búsqueda de clave y la operación de clasificación adicional. Y como he demostrado antes con los totales acumulados, la solución SQL Server 2012 es mejor en casi todos los aspectos.

Conclusión

Si está en SQL Server 2012 o superior, definitivamente querrá familiarizarse con todas las extensiones de las funciones de ventana que se introdujeron por primera vez en SQL Server 2005; es posible que le brinden mejoras de rendimiento bastante importantes al revisar el código que aún se está ejecutando. la vieja manera". Si desea obtener más información sobre algunas de estas nuevas capacidades, le recomiendo el libro de Itzik Ben-Gan, Microsoft SQL Server 2012 High-Performance T-SQL Using Window Functions.

Si aún no está en SQL Server 2012, al menos en esta prueba, puede elegir entre CROSS APPLY y la subconsulta correlacionada. Como siempre, debe probar varios métodos con los datos de su hardware.