No debe confiar demasiado en los porcentajes de costos en los planes de ejecución. Estos son siempre costos estimados , incluso en planes posteriores a la ejecución con números 'reales' para cosas como el recuento de filas. Los costos estimados se basan en un modelo que funciona bastante bien para el propósito para el que está destinado: permitir que el optimizador elija entre diferentes planes de ejecución candidatos para la misma consulta. La información de costos es interesante y un factor a tener en cuenta, pero rara vez debería ser una métrica principal para el ajuste de consultas. La interpretación de la información del plan de ejecución requiere una visión más amplia de los datos presentados.
ItemTran Operador de búsqueda de índice agrupado
Este operador es realmente dos operaciones en una. Primero, una operación de búsqueda de índice encuentra todas las filas que coinciden con el predicado product_code_v42 = 'M10BOLT'
, luego cada fila tiene el predicado residual bit_field_v41 & 4 = 0
aplicado. Hay una conversión implícita de bit_field_v41
su tipo base ( tinyint
o smallint
) a integer
.
La conversión se produce porque el operador AND y bit a bit (&) requiere que ambos operandos sean del mismo tipo. El tipo implícito del valor constante '4' es entero y las reglas de precedencia del tipo de datos significan que el bit_field_v41
valor del campo de menor prioridad se convierte.
El problema (tal como es) se corrige fácilmente escribiendo el predicado como bit_field_v41 & CONVERT(tinyint, 4) = 0
, lo que significa que el valor constante tiene la prioridad más baja y se convierte (durante el plegado constante) en lugar del valor de la columna. Si el bit_field_v41
es tinyint
sin conversiones se producen en absoluto. Del mismo modo, CONVERT(smallint, 4)
podría usarse si bit_field_v41
es así smallint
. Dicho esto, la conversión no es un problema de rendimiento en este caso, pero sigue siendo una buena práctica hacer coincidir los tipos y evitar las conversiones implícitas cuando sea posible.
La mayor parte del costo estimado de esta búsqueda se reduce al tamaño de la tabla base. Si bien la clave de índice agrupada es razonablemente estrecha, el tamaño de cada fila es grande. No se proporciona una definición para la tabla, pero solo las columnas utilizadas en la vista se suman a un ancho de fila significativo. Como el índice agrupado incluye todas las columnas, la distancia entre las claves de índice agrupadas es el ancho de la fila , no el ancho de las claves de índice . El uso de sufijos de versión en algunas columnas sugiere que la tabla real tiene incluso más columnas para versiones anteriores.
Mirando las columnas de búsqueda, predicado residual y salida, el rendimiento de este operador podría verificarse de forma aislada mediante la construcción de la consulta equivalente ( 1 <> 2
es un truco para evitar la autoparamización, el optimizador elimina la contradicción y no aparece en el plan de consulta):
SELECT
it.booking_no_v32,
it.QtyCheckedOut,
it.QtyReturned,
it.Trans_qty,
it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
1 <> 2
AND it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;
El rendimiento de esta consulta con un caché de datos en frío es de interés, ya que la lectura anticipada se vería afectada por la fragmentación de la tabla (índice agrupado). La clave de agrupamiento para esta tabla invita a la fragmentación, por lo que podría ser importante mantener (reorganizar o reconstruir) este índice con regularidad, y usar un apropiado FILLFACTOR
para permitir espacio para nuevas filas entre las ventanas de mantenimiento del índice.
Realicé una prueba del efecto de la fragmentación en la lectura anticipada utilizando datos de muestra generados con el Generador de datos SQL . Usando los mismos recuentos de filas de la tabla como se muestra en el plan de consulta de la pregunta, un índice agrupado altamente fragmentado resultó en SELECT * FROM view
15 segundos después DBCC DROPCLEANBUFFERS
. La misma prueba en las mismas condiciones con un índice agrupado recientemente reconstruido en la tabla ItemTrans completado en 3 segundos.
Si los datos de la tabla generalmente están completamente en caché, el problema de fragmentación es mucho menos importante. Pero, incluso con baja fragmentación, las filas de la tabla ancha pueden significar que el número de lecturas lógicas y físicas es mucho mayor de lo esperado. También podría experimentar agregando y eliminando lo explícito CONVERT
para validar mi expectativa de que el problema de conversión implícita no es importante aquí, excepto como una violación de las mejores prácticas.
Más importante es el número estimado de filas que salen del operador de búsqueda. La estimación del tiempo de optimización es de 165 filas, pero se produjeron 4,226 en el momento de la ejecución. Volveré a este punto más adelante, pero la razón principal de la discrepancia es que la selectividad del predicado residual (que involucra el bit-AND) es muy difícil de predecir por el optimizador; de hecho, recurre a la suposición.
Operador de filtro
Estoy mostrando el predicado de filtro aquí principalmente para ilustrar cómo NOT IN
se combinan, simplifican y luego se expanden las dos listas, y también para proporcionar una referencia para la siguiente discusión de coincidencia hash. La consulta de prueba de la búsqueda se puede ampliar para incorporar sus efectos y determinar el efecto del operador de filtro en el rendimiento:
SELECT
it.booking_no_v32,
it.trans_type_v41,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND
(
(
it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
)
);
El operador Compute Scalar en el plan define la siguiente expresión (el cálculo en sí mismo se difiere hasta que un operador posterior requiera el resultado):
[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))
El operador de Hash Match
Realizar una unión en tipos de datos de caracteres no es la razón del alto costo estimado de este operador. La información sobre herramientas de SSMS muestra solo una entrada de Hash Keys Probe, pero los detalles importantes se encuentran en la ventana Propiedades de SSMS.
El operador Hash Match crea una tabla hash usando los valores de la booking_no_v32
columna (Hash Keys Build) de la tabla ItemTran, y luego sondea las coincidencias usando la booking_no
columna (Hash Keys Probe) de la tabla Bookings. La información sobre herramientas de SSMS también normalmente mostraría un Residual de sonda, pero el texto es demasiado largo para una información sobre herramientas y simplemente se omite.
Un Residual de sonda es similar al Residual visto después de la búsqueda de índice anterior; el predicado residual se evalúa en todas las filas que coinciden con hash para determinar si la fila se debe pasar al operador principal. Encontrar coincidencias de hash en una tabla de hash bien equilibrada es extremadamente rápido, pero la aplicación de un predicado residual complejo a cada fila que coincide es bastante lenta en comparación. La información sobre herramientas Hash Match en Plan Explorer muestra los detalles, incluida la expresión residual de la sonda:
El predicado residual es complejo e incluye la verificación del estado del progreso de la reserva ahora que la columna está disponible en la tabla de reservas. La información sobre herramientas también muestra la misma discrepancia entre los recuentos de filas estimados y reales vistos anteriormente en la búsqueda de índice. Puede parecer extraño que gran parte del filtrado se realice dos veces, pero esto es solo que el optimizador es optimista. No espera que las partes del filtro que se pueden empujar hacia abajo del plan desde el residuo de la sonda eliminen las filas (las estimaciones de recuento de filas son las mismas antes y después del filtro), pero el optimizador sabe que podría estar equivocado al respecto. La posibilidad de filtrar filas temprano (reduciendo el costo de la unión hash) vale el pequeño costo del filtro adicional. No se puede empujar todo el filtro hacia abajo porque incluye una prueba en una columna de la tabla de reservas, pero la mayoría puede serlo.
La subestimación del recuento de filas es un problema para el operador Hash Match porque la cantidad de memoria reservada para la tabla hash se basa en el número estimado de filas. Cuando la memoria es demasiado pequeña para el tamaño de la tabla hash requerida en el tiempo de ejecución (debido al mayor número de filas), la tabla hash se derrama de forma recursiva al almacenamiento físico tempdb , lo que a menudo resulta en un rendimiento muy pobre. En el peor de los casos, el motor de ejecución deja de derramar recursivamente cubos de hash y recurre a un proceso muy lentoalgoritmo de rescate. El derrame de hash (recursivo o rescate) es la causa más probable de los problemas de rendimiento descritos en la pregunta (no columnas de unión de tipo de caracteres o conversiones implícitas). La causa raíz sería que el servidor reservara muy poca memoria para la consulta basada en una estimación incorrecta del conteo de filas (cardinalidad).
Lamentablemente, antes de SQL Server 2012, no hay ninguna indicación en el plan de ejecución de que una operación de hash haya excedido su asignación de memoria (que no puede crecer dinámicamente después de ser reservada antes de que comience la ejecución, incluso si el servidor tiene grandes cantidades de memoria libre) y tuvo que derramarse para tempdb. Es posible monitorear la clase de evento de advertencia de hash utilizando Profiler, pero puede ser difícil correlacionar las advertencias con una consulta en particular.
Corrigiendo los problemas
Los tres problemas son la fragmentación, la sonda compleja residual en el operador de coincidencia hash y la estimación de cardinalidad incorrecta resultante de la suposición en la búsqueda del índice.
Solución recomendada
Verifique la fragmentación y corríjala si es necesario, programando mantenimiento para garantizar que el índice se mantenga aceptablemente organizado. La forma habitual de corregir la estimación de cardinalidad es proporcionar estadísticas. En este caso, el optimizador necesita estadísticas para la combinación ( product_code_v42
, bitfield_v41 & 4 = 0
). No podemos crear estadísticas en una expresión directamente, por lo que primero debemos crear una columna calculada para la expresión del campo de bits y luego crear las estadísticas manuales de varias columnas:
ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);
CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);
La definición de texto de la columna calculada debe coincidir exactamente con el texto de la definición de vista para que se usen las estadísticas, por lo que se debe corregir la vista para eliminar la conversión implícita al mismo tiempo, y se debe tener cuidado para garantizar una coincidencia textual.
Las estadísticas de varias columnas deberían dar lugar a estimaciones mucho mejores, reduciendo en gran medida la posibilidad de que el operador de coincidencia hash utilice el derrame recursivo o el algoritmo de rescate. Agregar la columna calculada (que es una operación de solo metadatos y no ocupa espacio en la tabla ya que no está marcada PERSISTED
) y las estadísticas de varias columnas es mi mejor suposición en una primera solución.
Al resolver problemas de rendimiento de consultas, es importante medir cosas como el tiempo transcurrido, el uso de la CPU, las lecturas lógicas, las lecturas físicas, los tipos de espera y las duraciones ... y así sucesivamente. También puede ser útil ejecutar partes de la consulta por separado para validar las causas sospechosas como se muestra arriba.
En algunos entornos, donde una vista actualizada de los datos no es importante, puede ser útil ejecutar un proceso en segundo plano que materialice toda la vista en una tabla de instantáneas cada cierto tiempo. Esta tabla es solo una tabla base normal y se puede indexar para consultas de lectura sin preocuparse de afectar el rendimiento de la actualización.
Ver indexación
No se sienta tentado a indexar la vista original directamente. El rendimiento de lectura será increíblemente rápido (una sola búsqueda en un índice de vista) pero (en este caso) todos los problemas de rendimiento en los planes de consulta existentes se transferirán a consultas que modifiquen cualquiera de las columnas de la tabla a las que se hace referencia en la vista. Las consultas que cambian las filas de la tabla base se verán muy afectadas.
Solución avanzada con una vista indizada parcial
Hay una solución de vista indexada parcial para esta consulta en particular que corrige las estimaciones de cardinalidad y elimina el filtro y la sonda residual, pero se basa en algunas suposiciones sobre los datos (principalmente mi suposición sobre el esquema) y requiere una implementación experta, especialmente en lo que respecta a índices para soportar los planes de mantenimiento de vista indexada. Comparto el siguiente código por interés, no le propongo que lo implemente sin un análisis y pruebas muy cuidadosos .
-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
it.ID,
it.product_code_v42,
it.trans_type_v41,
it.booking_no_v32,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut,
it.QtyReserved,
it.bit_field_v41,
it.prep_on,
it.From_locn,
it.Trans_to_locn,
it.PDate,
it.FirstDate,
it.PTimeH,
it.PTimeM,
it.RetnDate,
it.BookDate,
it.TimeBookedH,
it.TimeBookedM,
it.TimeBookedS,
it.del_time_hour,
it.del_time_min,
it.return_to_locn,
it.return_time_hour,
it.return_time_min,
it.AssignTo,
it.AssignType,
it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
tb.booking_no = it.booking_no_v32
WHERE
(
it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND tb.BookingProgressStatus = 1
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
);
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO
La vista existente modificada para usar la vista indizada anterior:
CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
I.booking_no_v32 AS bkno,
I.trans_type_v41 AS trantype,
B.Assigned_to_v61 AS Assignbk,
B.order_date AS dateo,
B.HourBooked AS HBooked,
B.MinBooked AS MBooked,
B.SecBooked AS SBooked,
I.prep_on AS Pon,
I.From_locn AS Flocn,
I.Trans_to_locn AS TTlocn,
CASE I.prep_on
WHEN 'Y' THEN I.PDate
ELSE I.FirstDate
END AS PrDate,
I.PTimeH AS PrTimeH,
I.PTimeM AS PrTimeM,
CASE
WHEN I.RetnDate < I.FirstDate
THEN I.FirstDate
ELSE I.RetnDate
END AS RDatev,
I.bit_field_v41 AS bitField,
I.FirstDate AS FDatev,
I.BookDate AS DBooked,
I.TimeBookedH AS TBookH,
I.TimeBookedM AS TBookM,
I.TimeBookedS AS TBookS,
I.del_time_hour AS dth,
I.del_time_min AS dtm,
I.return_to_locn AS rtlocn,
I.return_time_hour AS rth,
I.return_time_min AS rtm,
CASE
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty < I.QtyCheckedOut
THEN 0
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty >= I.QtyCheckedOut
THEN I.Trans_Qty - I.QtyCheckedOut
ELSE
I.trans_qty
END AS trqty,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyCheckedOut
END AS MyQtycheckedout,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyReturned
END AS retqty,
I.ID,
B.BookingProgressStatus AS bkProg,
I.product_code_v42,
I.return_to_locn,
I.AssignTo,
I.AssignType,
I.QtyReserved,
B.DeprepOn,
CASE B.DeprepOn
WHEN 1 THEN B.DeprepDateTime
ELSE I.RetnDate
END AS DeprepDateTime,
I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
I.product_code_v42 = M.product_code;
Ejemplo de consulta y plan de ejecución:
SELECT
vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';
En el nuevo plan, la coincidencia hash no tiene predicado residual , no hay filtro complejo , no hay predicado residual en la búsqueda de vista indexada, y las estimaciones de cardinalidad son exactamente correctas.
Como ejemplo de cómo se verían afectados los planes de inserción / actualización / eliminación, este es el plan para una inserción en la tabla ItemTrans:
La sección resaltada es nueva y necesaria para el mantenimiento de la vista indexada. El carrete de la tabla reproduce las filas de la tabla base insertadas para el mantenimiento de la vista indexada. Cada fila se une a la tabla de reservas mediante una búsqueda de índice agrupado, luego un filtro aplica los WHERE
predicados de cláusula compleja para ver si la fila debe agregarse a la vista. Si es así, se realiza una inserción en el índice agrupado de la vista.
La misma SELECT * FROM view
prueba realizada anteriormente se completó en 150 ms con la vista indexada en su lugar.
Lo último: noto que su servidor 2008 R2 todavía está en RTM. No solucionará sus problemas de rendimiento, pero el Service Pack 2 para 2008 R2 ha estado disponible desde julio de 2012, y hay muchas buenas razones para mantenerse lo más actualizado posible con los service packs.