¿Por qué hay diferencias en el plan de ejecución entre OFFSET ... FETCH y el antiguo esquema ROW_NUMBER?

15

El nuevo OFFSET ... FETCHmodelo que se presenta con SQL Server 2012 ofrece una paginación simple y más rápida. ¿Por qué hay alguna diferencia considerando que las dos formas son semánticamente idénticas y muy comunes?

Uno supondría que el optimizador reconoce ambos y los optimiza (trivialmente) al máximo.

Aquí hay un caso muy simple donde OFFSET ... FETCHes ~ 2 veces más rápido de acuerdo con la estimación de costos.

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

offset-fetch.png

Se puede variar este caso de prueba creando un CI en object_ido agregando filtros, pero es imposible eliminar todas las diferencias de planes. OFFSET ... FETCHsiempre es más rápido porque hace menos trabajo en tiempo de ejecución.

usr
fuente
No estoy muy seguro, así que lo puse como comentario, pero supongo que es porque tiene el mismo orden por condición para la numeración de filas y el conjunto de resultados final. Como en la segunda condición, el optimizador lo sabe, no necesita ordenar los resultados nuevamente. Sin embargo, en el primer caso, debe asegurarse de que los resultados de la selección externa estén ordenados, así como la numeración de las filas en el resultado interno. Crear un índice apropiado en #objects debería resolver el problema
Akash

Respuestas:

13

Los ejemplos en la pregunta no producen los mismos resultados (el OFFSETejemplo tiene un error off-by-one). Los formularios actualizados a continuación solucionan ese problema, eliminan la clasificación adicional para el ROW_NUMBERcaso y usan variables para hacer que la solución sea más general:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

El ROW_NUMBERplan tiene un costo estimado de 0.0197935 :

Plan de número de fila

El OFFSETplan tiene un costo estimado de 0.0196955 :

Plan de compensación

Eso es un ahorro de 0.000098 unidades de costo estimado (aunque el OFFSETplan requeriría operadores adicionales si desea devolver un número de fila para cada fila). El OFFSETplan seguirá siendo un poco más barato, en general, pero recuerde que los costos estimados son exactamente eso: aún se requieren pruebas reales. La mayor parte del costo en ambos planes es el costo del tipo completo del conjunto de entrada, por lo que los índices útiles beneficiarían a ambas soluciones.

Cuando se utilizan valores literales constantes (p. Ej., OFFSET 30En el ejemplo original), el optimizador puede usar una clasificación TopN en lugar de una clasificación completa seguida de una clasificación Top. Cuando las filas necesarias del TopN Sort son un literal constante y <= 100 (la suma de OFFSETy FETCH) el motor de ejecución puede usar un algoritmo de ordenación diferente que puede funcionar más rápido que el TopN generalizado. Los tres casos tienen características de rendimiento diferentes en general.

En cuanto a por qué el optimizador no transforma automáticamente el ROW_NUMBERpatrón de sintaxis para usar OFFSET, hay una serie de razones:

  1. Es casi imposible escribir una transformación que coincida con todos los usos existentes
  2. Tener algunas consultas de paginación se transforman automáticamente y otras no pueden ser confusas.
  3. No OFFSETse garantiza que el plan sea mejor en todos los casos.

Un ejemplo para el tercer punto anterior ocurre cuando el conjunto de paginación es bastante amplio. Puede ser mucho más eficiente buscar las claves necesarias utilizando un índice no agrupado y buscar manualmente en el índice agrupado en comparación con escanear el índice con OFFSETo ROW_NUMBER. Hay problemas adicionales a considerar si la aplicación de paginación necesita saber cuántas filas o páginas hay en total. Hay otra buena discusión sobre los méritos relativos de la tecla 'buscar' y 'compensar' métodos aquí .

En general, probablemente sea mejor que las personas tomen una decisión informada de cambiar sus consultas de paginación para usar OFFSET, si corresponde, después de una prueba exhaustiva.

Paul White reinstala a Monica
fuente
1
Entonces, la razón por la que la transformación no se realiza en casos comunes es probablemente que fue demasiado difícil encontrar una compensación de ingeniería aceptable. Usted proporcionó buenas razones de por qué ese podría haber sido el caso .; Debo decir que esta es una buena respuesta. Muchas ideas y nuevos pensamientos. Dejaré la pregunta abierta por un momento y luego elegiré la mejor respuesta.
usr
5

Con un ligero toque de su consulta, obtengo una estimación de costo igual (50/50) y estadísticas de E / S iguales:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

Esto evita la ordenación adicional que aparece en su versión al ordenar en rlugar de object_id.

Mark Storey-Smith
fuente
Gracias por esta vision. Ahora que pienso en esto, he visto que el optimizador no comprende la naturaleza ordenada de la salida ROW_NUMBER antes. Considera que el conjunto no está ordenado por object_id. O al menos no ordenado por r y object_id.
usr
2
@usr el ORDER BY que ROW_NUMBER () usa define cómo asigna los números. No hace nada para prometer el orden de salida, eso está separado. Sucede que a menudo coincide, pero no está garantizado.
Aaron Bertrand
@AaronBertrand Entiendo que ROW_NUMBER no ordena la salida. Pero si ROW_NUMBER está ordenado por las mismas columnas que la salida, entonces el mismo orden está garantizado, ¿verdad? Entonces el optimizador de consultas podría hacer uso de ese hecho. Por lo tanto, dos operaciones de clasificación siempre son innecesarias en esta consulta.
usr
1
@usr has encontrado un caso de uso común que el optimizador no tiene en cuenta, pero no es el único caso de uso. Considere los casos en que el orden dentro de ROW_NUMBER () es esa columna y algo más. O cuando el orden externo por ordena secundaria en otra columna. O cuando quieres ordenar descendente. O por algo completamente distinto. Me gusta ordenar por la expresión en rlugar de la columna base, aunque solo sea porque coincide con lo que haría en una consulta no anidada y ordenar por una expresión: usaría el alias asignado a la expresión en lugar de repetir la expresión.
Aaron Bertrand
44
@ usr Y para el punto de Paul, habrá casos en los que puede encontrar brechas en la funcionalidad en el optimizador. Si no se van a solucionar, y conoce una mejor manera de escribir la consulta, use la mejor manera. Paciente: "Doctor, me duele cuando hago x". Doctor: "No hagas x". :-)
Aaron Bertrand
-3

Modificaron el optimizador de consultas para agregar esta característica. Esto significa que implementaron mecanismos específicamente para admitir el comando offset ... fetch. En otras palabras, para la consulta principal, SQL Server tiene que hacer mucho más trabajo. De ahí la diferencia en los planes de consulta.

Lixiviación Brandon
fuente