Tengo una visión muy importante, muy lenta, que incluye algunas condiciones realmente feas como esta en su cláusula where. También soy consciente de que las uniones son uniones gruesas y lentas en varchar(13)
lugar de campos de identidad enteros, pero me gustaría mejorar la consulta simple a continuación que utiliza esta vista:
CREATE VIEW [dbo].[vwReallySlowView] AS
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.tblItemtran AS I
INNER JOIN -- booking_no = varchar(13)
dbo.tblbookings AS B ON B.booking_no = I.booking_no_v32 -- string inner-join
INNER JOIN -- product_code = varchar(13)
dbo.tblInvmas AS M ON I.product_code_v42 = M.product_code -- string inner-join
WHERE (I.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)) AND (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) OR
(I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (B.BookingProgressStatus = 1) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut = 0) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut > 0) AND (I.trans_qty - (I.QtyCheckedOut - I.QtyReturned) > 0)
Esta vista generalmente se usa así:
select * from vwReallySlowView
where product_code_v42 = 'LIGHTBULB100W' -- find "100 watt lightbulb" rows
Cuando lo ejecuto me sale este artículo plan de ejecución que cuesta de 20 a 80% del costo total del lote, con predicado CONVERT_IMPLICIT( .... &(4))
mostrando que parece ser muy lento al hacer esto bitwise boolean tests
como (I.ibitfield & 4 = 0)
.
No soy un experto en MS SQL o en el trabajo de tipo DBA en general, ya que soy un desarrollador de software que no es SQL la mayor parte del tiempo. Pero sospecho que tales combinaciones bit a bit son una mala idea, y que hubiera sido mejor tener campos booleanos discretos.
¿Podría de alguna manera mejorar este índice que tengo, para manejar mejor esta vista sin cambiar el esquema (que ya está en producción en miles de ubicaciones) o debo cambiar la tabla subyacente que tiene varios valores booleanos empaquetados en un entero bit_field_v41
, para solucionar este problema? ?
Aquí está mi índice agrupado en el tblItemtran
que se está escaneando en este plan de ejecución:
-- goal: speed up select * from vwReallySlowView where productcode = 'X'
CREATE CLUSTERED INDEX [idxtblItemTranProductCodeAndTransType] ON [dbo].[tblItemtran]
(
[product_code_v42] ASC, -- varchar(13)
[trans_type_v41] ASC -- int
)WITH ( PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
Aquí está el plan de ejecución, para uno de los otros productos que resulta en un costo del 27% en este CONVERT_IMPLICIT
predicado. actualización Tenga en cuenta que en este caso, mi peor nodo ahora es "coincidencia hash" en un inner join
, que cuesta un 34%. Creo que este es un costo que no puedo evitar a menos que pueda evitar hacer uniones en cadenas que actualmente no puedo deshacerse de. Ambas INNER JOIN
operaciones en la vista anterior están en varchar(13)
campos.
Zoom en la esquina inferior derecha:
Todo el plan de ejecución como .sqlplan está disponible en skydrive. Esta imagen es solo una descripción visual. Haga clic aquí para ver la imagen sola.
La actualización publicó todo el plan de ejecución. Parece que no puedo encontrar qué product_code
valor era patológicamente malo, pero una forma de hacerlo es en select count(*) from view
lugar de hacer un solo producto. Pero los productos que solo se usan en el 5% de los registros en la tabla subyacente o menos parecen mostrar costos mucho más bajos en la CONVERT_IMPLICIT
operación. Si iba a arreglar el SQL aquí, creo que tomaría la WHERE
cláusula bruta en la vista, y calcularía y almacenaría el resultado de esa condición de cláusula where gigante como un campo de bits "IncludeMeInTheView", en la tabla subyacente . Presto, problema resuelto, ¿verdad?
fuente
product_code
valor que usé para generar ese87%
caso patológico con mis datos. Las imágenes ahora muestran27%
. Nuevamente, disculpas por la confusión debido a mis ediciones.Respuestas:
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 residualbit_field_v41 & 4 = 0
aplicado. Hay una conversión implícita debit_field_v41
su tipo base (tinyint
osmallint
) ainteger
.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 elbit_field_v41
estinyint
sin conversiones se producen en absoluto. Del mismo modo,CONVERT(smallint, 4)
podría usarse sibit_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):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ésDBCC 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: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):
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 labooking_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: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 .
La vista existente modificada para usar la vista indizada anterior:
Ejemplo de consulta y plan de ejecución:
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.
fuente