¿Por qué una subconsulta reduce la estimación de fila a 1?


26

Considere la siguiente consulta artificial pero simple:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP;

Esperaría que la estimación de la fila final para esta consulta sea igual al número de filas en la X_HEAPtabla. Lo que sea que esté haciendo en la subconsulta no debería importar para la estimación de fila porque no puede filtrar ninguna fila. Sin embargo, en SQL Server 2016 veo que la estimación de fila se redujo a 1 debido a la subconsulta:

mala consulta

¿Por qué pasó esto? ¿Qué puedo hacer al respecto?

Es muy fácil reproducir este problema con la sintaxis correcta. Aquí hay un conjunto de definiciones de tabla que lo harán:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL)
CREATE TABLE dbo.X_OTHER_TABLE (ID INT NOT NULL);
CREATE TABLE dbo.X_OTHER_TABLE_2 (ID INT NOT NULL);

INSERT INTO dbo.X_HEAP WITH (TABLOCK)
SELECT TOP (1000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values;

CREATE STATISTICS X_HEAP__ID ON X_HEAP (ID) WITH FULLSCAN;

enlace de violín db .

Respuestas:


22

Este problema de estimación de cardinalidad (CE) aparece cuando:

  1. La unión es un exterior se unen con un paso a través predicado
  2. Se estima que la selectividad del predicado de transferencia es exactamente 1 .

Nota: La calculadora particular utilizada para determinar la selectividad no es importante.


Detalles

El CE calcula la selectividad de la unión externa como la suma de:

  • La selectividad de unión interna con el mismo predicado
  • La selectividad anti unirse con el mismo predicado

La única diferencia entre una combinación externa e interna es que una combinación externa también devuelve filas que no coinciden en el predicado de combinación. La combinación anti proporciona exactamente esta diferencia. La estimación de la cardinalidad para la unión interna y anti es más fácil que para la unión externa directamente.

El proceso de estimación de selectividad de unión es muy sencillo:

  • Primero, se evalúa la selectividad del predicado de paso. SPT
    • Esto se hace usando la calculadora que sea apropiada para las circunstancias.
    • El predicado es todo, incluido cualquier IsFalseOrNullcomponente de negación .
  • Selectividad de unión interna: = 1 - SPT
  • Anti selectividad de unión: = SPT

La combinación anti representa filas que 'atravesarán' la combinación. La unión interna representa filas que no 'pasarán'. Tenga en cuenta que 'pasar a través' significa filas que fluyen a través de la unión sin ejecutar el lado interno en absoluto. Para enfatizar: todas las filas serán devueltas por la unión, la distinción es entre filas que corren el lado interno de la unión antes de emerger, y aquellas que no lo hacen.

Claramente, agregar a siempre debería dar una selectividad total de 1, lo que significa que todas las filas son devueltas por la unión, como se esperaba.1 - SPTSPT

De hecho, el cálculo anterior funciona exactamente como se describe para todos los valores de excepto 1 .SPT

Cuando = 1, se estima que las selectividades de unión interna y antiunión son cero, lo que da como resultado una estimación de cardinalidad (para la unión como un todo) de una fila. Por lo que puedo decir, esto no es intencional y debe informarse como un error.SPT


Un problema relacionado

Es más probable que este error se manifieste de lo que uno podría pensar, debido a una limitación de CE por separado. Esto surge cuando la CASEexpresión usa una EXISTScláusula (como es común). Por ejemplo, la siguiente consulta modificada de la pregunta no encuentra la estimación de cardinalidad inesperada:

-- This is fine
SELECT 
    CASE
        WHEN XH.ID = 1
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

La introducción de un trivial EXISTShace que surja el problema:

-- This is not fine
SELECT 
    CASE
        WHEN EXISTS (SELECT 1 WHERE XH.ID = 1)
        THEN (SELECT TOP (1) XOT.ID FROM dbo.X_OTHER_TABLE AS XOT) 
    END
FROM dbo.X_HEAP AS XH;

El uso EXISTSintroduce una semiunión (resaltada) en el plan de ejecución:

Plan de semi unión

La estimación para la semiunión está bien. El problema es que el CE trata la columna de sonda asociada como una proyección simple, con una selectividad fija de 1:

Semijoin with probe column treated as a Project.

Selectivity of probe column = 1

Esto cumple automáticamente una de las condiciones requeridas para que se manifieste este problema de CE, independientemente del contenido de la EXISTScláusula.


Para obtener información importante sobre antecedentes, consulte Subconsultas en CASEexpresiones de Craig Freedman.


22

Esto definitivamente parece un comportamiento involuntario. Es cierto que las estimaciones de cardinalidad no necesitan ser consistentes en cada paso de un plan, pero este es un plan de consulta relativamente simple y la estimación de cardinalidad final es inconsistente con lo que está haciendo la consulta. Una estimación de cardinalidad tan baja podría dar como resultado malas elecciones para los tipos de unión y los métodos de acceso para otras tablas posteriores en un plan más complicado.

A través de prueba y error, podemos encontrar algunas consultas similares para las cuales el problema no aparece:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

También podemos generar más consultas para las que aparece el problema:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP;

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP;

Parece haber un patrón: si hay una expresión dentro de la CASEque no se espera que se ejecute y la expresión del resultado es una subconsulta en una tabla, la estimación de la fila cae a 1 después de esa expresión.

Si escribo la consulta en una tabla con un índice agrupado, las reglas cambian un poco. Podemos usar los mismos datos:

CREATE TABLE dbo.X_CI (ID INT NOT NULL, PRIMARY KEY (ID))

INSERT INTO dbo.X_CI WITH (TABLOCK)
SELECT * FROM dbo.X_HEAP;

UPDATE STATISTICS X_CI WITH FULLSCAN;

Esta consulta tiene una estimación final de 1000 filas:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI;

Pero esta consulta tiene una estimación final de 1 fila:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI;

Para profundizar más en esto, podemos usar el indicador de seguimiento no documentado 2363 para obtener información sobre cómo el optimizador de consultas realizó cálculos de selectividad. Me pareció útil emparejar ese indicador de seguimiento con el indicador de seguimiento no documentado 8606 . TF 2363 parece dar cálculos de selectividad tanto para el árbol simplificado como para el árbol después de la normalización del proyecto. Tener ambas marcas de rastreo habilitadas deja en claro qué cálculos se aplican a qué árbol.

Probémoslo para la consulta original publicada en la pregunta:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Aquí hay parte de la parte del resultado que creo que es relevante junto con algunos comentarios:

Plan for computation:

  CSelCalcColumnInInterval -- this is the type of calculator used

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID -- this is the column used for the calculation

Pass-through selectivity: 0 -- all rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- the row estimate after the join will still be 1000

      CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

      CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1 -- no rows are expected to have a true value for the case expression

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter) -- the row estimate after the join will still be 1

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter) -- here is the row estimate after the previous join

          CStCollBaseTable(ID=1, CARD=1000 TBL: X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: X_OTHER_TABLE_2)

Ahora probémoslo para una consulta similar que no tenga el problema. Voy a usar este:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT -1) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Salida de depuración al final:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollConstTable(ID=4, CARD=1) -- this is different than before because we select a constant instead of from a table

Probemos con otra consulta para la que está presente la estimación de fila incorrecta:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Al final, la estimación de cardinalidad cae a 1 fila, nuevamente después de la selectividad de transferencia = 1. La estimación de cardinalidad se conserva después de una selectividad de 0.501 y 0.499.

Plan for computation:

 CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.501

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

...

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=12, CARD=1 x_jtLeftOuter) -- this is associated with the ELSE expression

      CStCollOuterJoin(ID=11, CARD=1000 x_jtLeftOuter)

          CStCollOuterJoin(ID=10, CARD=1000 x_jtLeftOuter)

              CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

              CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

          CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=4, CARD=1 TBL: X_OTHER_TABLE)

Cambiemos nuevamente a otra consulta similar que no tenga el problema. Voy a usar este:

SELECT 
  ID
, CASE
    WHEN ID < 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    WHEN ID >= 500 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END AS ID2
FROM dbo.X_HEAP
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

En la salida de depuración nunca hay un paso que tenga una selectividad de transferencia de 1. La estimación de cardinalidad se mantiene en 1000 filas.

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_HEAP].ID

Pass-through selectivity: 0.499

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_HEAP)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

End selectivity computation

¿Qué pasa con la consulta cuando involucra una tabla con un índice agrupado? Considere la siguiente consulta con el problema de estimación de filas:

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

El final de la salida de depuración es similar a lo que ya hemos visto:

Plan for computation:

  CSelCalcColumnInInterval

      Column: QCOL: [SE_DB].[dbo].[X_CI].ID

Pass-through selectivity: 1

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

Sin embargo, la consulta contra el CI sin el problema tiene una salida diferente. Usando esta consulta:

SELECT 
  ID
, CASE
    WHEN ID = 0 
    THEN (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE_2) 
    ELSE (SELECT TOP 1 ID FROM dbo.X_OTHER_TABLE) 
  END
FROM dbo.X_CI
OPTION (QUERYTRACEON 3604, QUERYTRACEON 2363, QUERYTRACEON 8606);

Resultados en diferentes calculadoras que se utilizan. CSelCalcColumnInIntervalya no aparece:

Plan for computation:

  CSelCalcFixedFilter (0.559)

Pass-through selectivity: 0.559

Stats collection generated: 

  CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

      CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

      CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

...

Plan for computation:

  CSelCalcUniqueKeyFilter

Pass-through selectivity: 0.001

Stats collection generated: 

  CStCollOuterJoin(ID=9, CARD=1000 x_jtLeftOuter)

      CStCollOuterJoin(ID=8, CARD=1000 x_jtLeftOuter)

          CStCollBaseTable(ID=1, CARD=1000 TBL: dbo.X_CI)

          CStCollBaseTable(ID=2, CARD=1 TBL: dbo.X_OTHER_TABLE_2)

      CStCollBaseTable(ID=3, CARD=1 TBL: dbo.X_OTHER_TABLE)

En conclusión, parece que obtenemos una estimación de fila incorrecta después de la subconsulta en las siguientes condiciones:

  1. Se CSelCalcColumnInIntervalusa la calculadora de selectividad. No sé exactamente cuándo se usa esto, pero parece aparecer mucho más a menudo cuando la tabla base es un montón.

  2. Selectividad de paso = 1. En otras palabras, CASEse espera que una de las expresiones se evalúe como falsa para todas las filas. No importa si la primera CASEexpresión se evalúa como verdadera para todas las filas.

  3. Hay una unión externa a CStCollBaseTable. En otras palabras, la CASEexpresión del resultado es una subconsulta contra una tabla. Un valor constante no funcionará.

Quizás en esas condiciones, el optimizador de consultas está aplicando involuntariamente la selectividad de paso a la estimación de fila de la tabla externa en lugar del trabajo realizado en la parte interna del bucle anidado. Eso reduciría la estimación de la fila a 1.

Pude encontrar dos soluciones alternativas. No pude reproducir el problema al usar en APPLYlugar de una subconsulta. La salida del indicador de traza 2363 fue muy diferente con APPLY. Aquí hay una forma de reescribir la consulta original en la pregunta:

SELECT 
  h.ID
, a.ID2
FROM X_HEAP h
OUTER APPLY
(
SELECT CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END
) a(ID2);

buena consulta 1

El CE heredado parece evitar el problema también.

SELECT 
  ID
, CASE
    WHEN ID <> 0 
    THEN (SELECT TOP 1 ID FROM X_OTHER_TABLE) 
    ELSE (SELECT TOP 1 ID FROM X_OTHER_TABLE_2) 
  END AS ID2
FROM X_HEAP
OPTION (USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

buena consulta 2

Se envió un elemento de conexión para este problema (con algunos de los detalles que Paul White proporcionó en su respuesta).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.