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 PLAN
pista, 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 table1
plan 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 .