¿Puedes explicar este plan de ejecución?

20

Estaba investigando algo más cuando me encontré con esto. Estaba generando tablas de prueba con algunos datos y ejecutando diferentes consultas para descubrir cómo las diferentes formas de escribir consultas afectan el plan de ejecución. Aquí está el script que utilicé para generar datos de prueba aleatorios:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID('t') AND type in (N'U'))
DROP TABLE t
GO

CREATE TABLE t 
(
 c1 int IDENTITY(1,1) NOT NULL 
,c2 int NULL
) 
GO

insert into t
select top 1000000 a from
(select t1.number*2048 + t2.number a, newid() b
from [master]..spt_values t1 
cross join  [master]..spt_values t2
where t1.[type] = 'P' and t2.[type] = 'P') a
order by b
GO

update t set c2 = null
where c2 < 2048 * 2048 / 10
GO


CREATE CLUSTERED INDEX pk ON [t] (c1)
GO

CREATE NONCLUSTERED INDEX i ON t (c2)
GO

Ahora, dada esta información, invoqué la siguiente consulta:

select * 
from t 
where 
      c2 < 1048576 
   or c2 is null
;

Para mi gran sorpresa, el plan de ejecución que se generó para esta consulta fue este . (Perdón por el enlace externo, es demasiado grande para caber aquí).

¿Puede alguien explicarme qué pasa con todos estos " Escaneos constantes " y " Escalares computacionales "? ¿Qué esta pasando?

Plan

  |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1010], [Expr1011], [Expr1012]))
       |--Merge Interval
       |    |--Sort(TOP 2, ORDER BY:([Expr1013] DESC, [Expr1014] ASC, [Expr1010] ASC, [Expr1015] DESC))
       |         |--Compute Scalar(DEFINE:([Expr1013]=((4)&[Expr1012]) = (4) AND NULL = [Expr1010], [Expr1014]=(4)&[Expr1012], [Expr1015]=(16)&[Expr1012]))
       |              |--Concatenation
       |                   |--Compute Scalar(DEFINE:([Expr1005]=NULL, [Expr1006]=NULL, [Expr1004]=(60)))
       |                   |    |--Constant Scan
       |                   |--Compute Scalar(DEFINE:([Expr1008]=NULL, [Expr1009]=(1048576), [Expr1007]=(10)))
       |                        |--Constant Scan
       |--Index Seek(OBJECT:([t].[i]), SEEK:([t].[c2] > [Expr1010] AND [t].[c2] < [Expr1011]) ORDERED FORWARD)
Andrew Savinykh
fuente

Respuestas:

29

Los escaneos constantes producen cada uno una fila en memoria sin columnas. El escalar de cómputo superior genera una sola fila con 3 columnas

Expr1005    Expr1006    Expr1004
----------- ----------- -----------
NULL        NULL        60

El escalar de proceso inferior genera una sola fila con 3 columnas

Expr1008    Expr1009    Expr1007
----------- ----------- -----------
NULL        1048576        10

El operador de concatenación une estas 2 filas y genera las 3 columnas, pero ahora se renombra

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

La Expr1012columna es un conjunto de indicadores utilizados internamente para definir ciertas propiedades de búsqueda para Storage Engine .

El siguiente cálculo escalar a lo largo de las salidas 2 filas

Expr1010    Expr1011    Expr1012    Expr1013    Expr1014    Expr1015
----------- ----------- ----------- ----------- ----------- -----------
NULL        NULL        60          True        4           16            
NULL        1048576     10          False       0           0      

Las últimas tres columnas se definen de la siguiente manera y solo se usan para fines de clasificación antes de presentarlas al Operador del intervalo de fusión

[Expr1013] = Scalar Operator(((4)&[Expr1012]) = (4) AND NULL = [Expr1010]), 
[Expr1014] = Scalar Operator((4)&[Expr1012]), 
[Expr1015] = Scalar Operator((16)&[Expr1012])

Expr1014y Expr1015solo pruebe si ciertos bits están activados en la bandera. Expr1013parece devolver una columna booleana verdadera si tanto el bit for 4está activado como si Expr1010está NULLactivado

Al probar otros operadores de comparación en la consulta obtengo estos resultados

+----------+----------+----------+-------------+----+----+---+---+---+---+
| Operator | Expr1010 | Expr1011 | Flags (Dec) |       Flags (Bin)       |
|          |          |          |             | 32 | 16 | 8 | 4 | 2 | 1 |
+----------+----------+----------+-------------+----+----+---+---+---+---+
| >        | 1048576  | NULL     |           6 |  0 |  0 | 0 | 1 | 1 | 0 |
| >=       | 1048576  | NULL     |          22 |  0 |  1 | 0 | 1 | 1 | 0 |
| <=       | NULL     | 1048576  |          42 |  1 |  0 | 1 | 0 | 1 | 0 |
| <        | NULL     | 1048576  |          10 |  0 |  0 | 1 | 0 | 1 | 0 |
| =        | 1048576  | 1048576  |          62 |  1 |  1 | 1 | 1 | 1 | 0 |
| IS NULL  | NULL     | NULL     |          60 |  1 |  1 | 1 | 1 | 0 | 0 |
+----------+----------+----------+-------------+----+----+---+---+---+---+

De lo cual deduzco que el Bit 4 significa "Tiene inicio de rango" (en lugar de no estar limitado) y el Bit 16 significa que el inicio del rango es inclusivo.

Este conjunto de resultados de 6 columnas se emite desde el SORToperador ordenado por Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC. Asumir que Trueestá representado por 1y Falsepor 0el conjunto de resultados representado anteriormente ya está en ese orden.

Según mis supuestos anteriores, el efecto neto de este tipo es presentar los rangos al intervalo de fusión en el siguiente orden

 ORDER BY 
          HasStartOfRangeAndItIsNullFirst,
          HasUnboundedStartOfRangeFirst,
          StartOfRange,
          StartOfRangeIsInclusiveFirst

El operador de intervalo de fusión genera 2 filas

Expr1010    Expr1011    Expr1012
----------- ----------- -----------
NULL        NULL        60
NULL        1048576     10

Para cada fila emitida se realiza una búsqueda de rango

Seek Keys[1]: Start:[dbo].[t].c2 > Scalar Operator([Expr1010]), 
               End: [dbo].[t].c2 < Scalar Operator([Expr1011])

Entonces parecería que se realizan dos búsquedas. Uno aparentemente > NULL AND < NULLy uno > NULL AND < 1048576. Sin embargo, las banderas que se pasan parecen modificar esto IS NULLy < 1048576respectivamente. ¡Ojalá @sqlkiwi pueda aclarar esto y corregir cualquier inexactitud!

Si cambia la consulta ligeramente a

select *
from t 
where 
      c2 > 1048576 
   or c2 = 0
;

Entonces el plan se ve mucho más simple con una búsqueda de índice con múltiples predicados de búsqueda.

El plan muestra Seek Keys

Start: c2 >= 0, End: c2 <= 0, 
Start: c2 > 1048576

SQLKiwi da la explicación de por qué este plan más simple no puede usarse para el caso en el OP en los comentarios a la publicación de blog vinculada anteriormente .

Una búsqueda de índice con múltiples predicados no puede mezclar diferentes tipos de predicados de comparación (es decir, Isy Eqen el caso del OP). Esta es solo una limitación actual del producto (y es presumiblemente la razón por la cual c2 = 0se implementa la prueba de igualdad en la última consulta >=y <=no solo la búsqueda de igualdad directa que obtiene para la consulta c2 = 0 OR c2 = 1048576.

Martin Smith
fuente
No puedo ver nada en el artículo de Paul que explique la diferencia en las banderas de [Expr1012]. ¿Puedes deducir lo que significa el 60/10 aquí?
Mark Storey-Smith
@ MarkStorey-Smith - dice que 62es para una comparación de igualdad. Supongo que 60debe significar que, en lugar de lo > AND < que se muestra en el plan, de hecho se obtiene, a >= AND <=menos que sea una marca explícita IS NULL, tal vez (?) O tal vez el bit 2indique algo más no relacionado y 60siga siendo igual que cuando lo hago set ansi_nulls offy lo cambio a c2 = nullél, todavía permanece en60
Martin Smith
2
@MartinSmith 60 es de hecho para una comparación con NULL. Las expresiones de límite de rango usan NULL para representar 'ilimitado' en cualquier extremo. La búsqueda es siempre exclusiva, es decir, buscar Inicio:> Expr y Fin: <Expr en lugar de incluir usando> = y <=. Gracias por el comentario del blog, publicaré una respuesta o un comentario más largo en respuesta por la mañana (demasiado tarde para hacerle justicia en este momento).
Paul White dice GoFundMonica
@SQLKiwi - Gracias. Eso tiene sentido. Espero haber descubierto algunas de las partes faltantes antes de eso.
Martin Smith
Muchas gracias, todavía estoy absorbiendo esto, pero parece explicar las cosas muy bien, la pregunta principal que queda es la que le preguntas a @SQLKiwi en su blog. Meditaré unos días más en tu respuesta para asegurarme de que no tengo ninguna pregunta de seguimiento y aceptaré tu respuesta. Gracias de nuevo, fue de gran ayuda.
Andrew Savinykh
13

Los escaneos constantes son una forma para que SQL Server cree un depósito en el que colocará algo más adelante en el plan de ejecución. He publicado una explicación más completa de esto aquí . Para comprender para qué sirve la exploración constante, debe profundizar en el plan. En este caso, son los operadores Compute Scalar los que se utilizan para completar el espacio creado por el escaneo constante.

Los operadores de Compute Scalar se están cargando con NULL y el valor 1045876, por lo que claramente se usarán con Loop Join en un esfuerzo por filtrar los datos.

La parte realmente genial es que este plan es trivial. Significa que pasó por un proceso de optimización mínimo. Todas las operaciones conducen al intervalo de fusión. Esto se utiliza para crear un conjunto mínimo de operadores de comparación para una búsqueda de índice ( detalles sobre eso aquí ).

La idea es deshacerse de los valores superpuestos para que luego pueda extraer los datos con pases mínimos. Aunque todavía está usando una operación de bucle, notará que el bucle se ejecuta exactamente una vez, lo que significa que es efectivamente un escaneo.

ADENDA: Esa última oración está apagada. Hubo dos búsquedas. Leí mal el plan. El resto de los conceptos son los mismos y el objetivo, pases mínimos, es el mismo.

Grant Fritchey
fuente