Columna calculada persistente que causa escaneo

9

La conversión de una columna normal en una columna calculada persistente hace que esta consulta no pueda realizar búsquedas de índice. ¿Por qué?

Probado en varias versiones de SQL Server, incluido 2016 SP1 CU1.

Repros

El problema es con table1, col7.

Las tablas y la consulta son una versión parcial (y simplificada) de los originales. Soy consciente de que la consulta podría reescribirse de manera diferente y, por alguna razón, evitar el problema, pero debemos evitar tocar el código, y la pregunta de por qué table1no se puede buscar sigue en pie.

Como mostró Paul White (¡gracias!), La búsqueda está disponible si es forzada, por lo que la pregunta es: ¿por qué el optimizador no elige la búsqueda y si podemos hacer algo diferente para que la búsqueda suceda como debería, sin cambiar el ¿código?

Para aclarar la parte problemática, aquí está el análisis relevante en el plan de ejecución incorrecta:

plan

Alex Friedman
fuente

Respuestas:

12

Por qué el optimizador no elige la búsqueda


TL: DR La definición de columna computada expandida interfiere con la capacidad del optimizador para reordenar las uniones inicialmente. Con un punto de partida diferente, la optimización basada en costos toma un camino diferente a través del optimizador y termina con una elección de plan final diferente.


Detalles

Para todas las consultas, excepto la más simple, el optimizador no intenta explorar nada como todo el espacio de posibles planes. En cambio, elige un punto de partida de aspecto razonable , luego dedica una cantidad presupuestada de esfuerzo a explorar variaciones lógicas y físicas, en una o más fases de búsqueda, hasta que encuentra un plan razonable.

La razón principal por la que obtiene diferentes planes (con diferentes estimaciones de costos finales) para los dos casos es que hay diferentes puntos de partida. Comenzando desde un lugar diferente, la optimización termina en un lugar diferente (después de su número limitado de iteraciones de exploración e implementación). Espero que esto sea razonablemente intuitivo.

El punto de partida que mencioné se basa en la representación textual de la consulta, pero se realizan cambios en la representación interna del árbol a medida que pasa por las etapas de análisis, enlace, normalización y simplificación de la compilación de la consulta.

Es importante destacar que el punto de partida exacto depende en gran medida del orden de unión inicial seleccionado por el optimizador. Esta elección se realiza antes de cargar las estadísticas y antes de que se hayan derivado estimaciones de cardinalidad. Sin embargo, se conoce la cardinalidad total (número de filas) en cada tabla, que se obtuvo de los metadatos del sistema.

Por lo tanto, el orden de unión inicial se basa en la heurística . Por ejemplo, el optimizador intenta reescribir el árbol de modo que las tablas más pequeñas se unan antes que las más grandes, y las uniones internas vienen antes que las uniones externas (y las uniones cruzadas).

La presencia de la columna calculada interfiere con este proceso, más específicamente con la capacidad del optimizador de empujar las uniones externas hacia abajo en el árbol de consulta. Esto se debe a que la columna calculada se expande a su expresión subyacente antes de que se produzca el reordenamiento de unión, y mover una unión más allá de una expresión compleja es mucho más difícil que moverla más allá de una referencia de columna simple.

Los árboles involucrados son bastante grandes, pero para ilustrar, el árbol de consulta inicial de la columna no calculada comienza con: (observe las dos uniones externas en la parte superior)

LogOp_Select
    LogOp_Apply (x_jtLeftOuter) 
        LogOp_LeftOuterJoin
            LogOp_NAryJoin
                LogOp_LeftAntiSemiJoin
                    LogOp_NAryJoin
                        LogOp_Get TBL: dbo.table1 (alias TBL: a4)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table6 (alias TBL: a3)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a3] .col18
                                ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table1 (alias TBL: a1)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a1] .col2
                                ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                        LogOp_Select
                            LogOp_Get TBL: dbo.table5 (alias TBL: a2)
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [a2] .col2
                                ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a4] .col2
                            ScaOp_Identifier QCOL: [a3] .col19
                    LogOp_Select
                        LogOp_Get TBL: dbo.table7 (alias TBL: a7)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a7] .col22
                            ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a7] .col23
                LogOp_Select
                    LogOp_Get TBL: table1 (alias TBL: cdc)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdc] .col6
                        ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, No propiedad, Valor = 4)
                LogOp_Get TBL: dbo.table5 (alias TBL: a5) 
                LogOp_Get TBL: table2 (alias TBL: cdt)  
                ScaOp_Logical x_lopAnd
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a5] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [cdc] .col2
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [cdt] .col1
                        ScaOp_Identifier QCOL: [cdc] .col1
            LogOp_Get TBL: table3 (alias TBL: ahcr)
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [ahcr] .col9
                ScaOp_Identifier QCOL: [cdt] .col1

El mismo fragmento de la consulta de columna calculada es: (observe la unión externa mucho más abajo, la definición de columna computada expandida y algunas otras diferencias sutiles en el orden de unión (interna))

LogOp_Select
    LogOp_Apply (x_jtLeftOuter)
        LogOp_NAryJoin
            LogOp_LeftAntiSemiJoin
                LogOp_NAryJoin
                    LogOp_Get TBL: dbo.table1 (alias TBL: a4)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table6 (alias TBL: a3)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a3] .col18
                            ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table1 (alias TBL: a1
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a1] .col2
                            ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                    LogOp_Select
                        LogOp_Get TBL: dbo.table5 (alias TBL: a2)
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [a2] .col2
                            ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a4] .col2
                        ScaOp_Identifier QCOL: [a3] .col19
                LogOp_Select
                    LogOp_Get TBL: dbo.table7 (alias TBL: a7) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [a7] .col22
                        ScaOp_Const TI (clasificación varchar 53256, Var, Trim, ML = 16)
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [a7] .col23
            LogOp_Project
                LogOp_LeftOuterJoin
                    LogOp_Join
                        LogOp_Select
                            LogOp_Get TBL: table1 (alias TBL: cdc) 
                            ScaOp_Comp x_cmpEq
                                ScaOp_Identifier QCOL: [cdc] .col6
                                ScaOp_Const TI (smallint, ML = 2) XVAR (smallint, No propiedad, Valor = 4)
                        LogOp_Get TBL: table2 (alias TBL: cdt) 
                        ScaOp_Comp x_cmpEq
                            ScaOp_Identifier QCOL: [cdc] .col1
                            ScaOp_Identifier QCOL: [cdt] .col1
                    LogOp_Get TBL: table3 (alias TBL: ahcr) 
                    ScaOp_Comp x_cmpEq
                        ScaOp_Identifier QCOL: [ahcr] .col9
                        ScaOp_Identifier QCOL: [cdt] .col1
                AncOp_PrjList 
                    AncOp_PrjEl QCOL: [cdc] .col7
                        ScaOp_Convert char colate 53256, Null, Trim, ML = 6
                            ScaOp_IIF varchar colate 53256, Null, Var, Trim, ML = 6
                                ScaOp_Comp x_cmpEq
                                    ScaOp_Intrinsic isnumeric
                                        ScaOp_Intrinsic right
                                            ScaOp_Identifier QCOL: [cdc] .col4
                                            ScaOp_Const TI (int, ML = 4) XVAR (int, no propiedad, valor = 4)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, no propiedad, valor = 0)
                                ScaOp_Const TI (varchar colate 53256, Var, Trim, ML = 1) XVAR (varchar, Owned, Value = Len, Data = (0,))
                                Subcadena ScaOp_Intrinsic
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, no propiedad, valor = 6)
                                    ScaOp_Const TI (int, ML = 4) XVAR (int, no propiedad, valor = 1)
                                    ScaOp_Identifier QCOL: [cdc] .col4
            LogOp_Get TBL: dbo.table5 (alias TBL: a5)
            ScaOp_Logical x_lopAnd
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a5] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2
                ScaOp_Comp x_cmpEq
                    ScaOp_Identifier QCOL: [a4] .col2
                    ScaOp_Identifier QCOL: [cdc] .col2

Las estadísticas se cargan y se realiza una estimación de cardinalidad inicial en el árbol justo después de establecer el orden de unión inicial. Tener las uniones en diferentes órdenes también afecta estas estimaciones y, por lo tanto, tiene un efecto secundario durante la optimización posterior basada en el costo.

Finalmente para esta sección, tener una unión externa atascada en el medio del árbol puede evitar que coincidan algunas reglas de reordenamiento de unión durante la optimización basada en costos.


El uso de una guía de plan (o, de manera equivalente, una USE PLANpista, ejemplo para su consulta ) cambia la estrategia de búsqueda a un enfoque más orientado a objetivos, guiado por la forma general y las características de la plantilla suministrada. Esto explica por qué el optimizador puede encontrar el mismo table1plan de búsqueda contra esquemas de columna calculados y no calculados, cuando se utiliza una guía o sugerencia de plan.

Si podemos hacer algo diferente para que la búsqueda suceda

Esto es algo de lo que solo debe preocuparse si el optimizador no encuentra un plan con características de rendimiento aceptables por sí mismo.

Todas las herramientas de ajuste normales son potencialmente aplicables. Puede, por ejemplo, dividir la consulta en partes más simples, revisar y mejorar la indexación disponible, actualizar o crear nuevas estadísticas ... y así sucesivamente.

Todas estas cosas pueden afectar las estimaciones de cardinalidad, la ruta del código tomada a través del optimizador e influir en las decisiones basadas en costos de manera sutil.

En última instancia, puede recurrir al uso de sugerencias (o una guía de plan), pero esa no suele ser la solución ideal.


Preguntas adicionales de comentarios

Estoy de acuerdo en que es mejor simplificar la consulta, etc., pero ¿hay alguna forma (indicador de seguimiento) para hacer que el optimizador continúe con la optimización y alcance el mismo resultado?

No, no hay una marca de seguimiento para realizar una búsqueda exhaustiva, y no desea una. El posible espacio de búsqueda es enorme, y los tiempos de compilación que exceden la edad del universo no serían bien recibidos. Además, el optimizador no conoce todas las transformaciones lógicas posibles (nadie lo sabe).

Además, ¿por qué se necesita la expansión compleja, ya que la columna persiste? ¿Por qué el optimizador no puede evitar expandirlo, tratarlo como una columna normal y alcanzar el mismo punto de partida?

Las columnas calculadas se expanden (al igual que las vistas) para permitir oportunidades de optimización adicionales. La expansión puede coincidir, por ejemplo, con una columna o índice persistente más adelante en el proceso, pero esto sucede después de que se arregla el orden de unión inicial .

Paul White 9
fuente