INSERTAR en una sola fila ... SELECCIONAR mucho más lento que separar

18

Dada la siguiente tabla de montón con 400 filas numeradas del 1 al 400:

DROP TABLE IF EXISTS dbo.N;
GO
SELECT 
    SV.number
INTO dbo.N 
FROM master.dbo.spt_values AS SV
WHERE 
    SV.[type] = N'P'
    AND SV.number BETWEEN 1 AND 400;

y la siguiente configuración:

SET NOCOUNT ON;
SET STATISTICS IO, TIME OFF;
SET STATISTICS XML OFF;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

La siguiente SELECTdeclaración se completa en unos 6 segundos ( demostración , plan ):

DECLARE @n integer = 400;

SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Nota: @La OPTIMIZE FORcláusula es solo para producir una reproducción de tamaño razonable que capture los detalles esenciales del problema real, incluida una desestimación de la cardinalidad que puede surgir por una variedad de razones.

Cuando la salida de una fila se escribe en una tabla, demora 19 segundos ( demostración , plan ):

DECLARE @T table (c bigint NOT NULL);

DECLARE @n integer = 400;

INSERT @T
    (c)
SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Los planes de ejecución aparecen idénticos aparte de la inserción de una fila.

Todo el tiempo extra parece ser consumido por el uso de la CPU.

¿Por qué la INSERTdeclaración es mucho más lenta?

Paul White reinstala a Monica
fuente

Respuestas:

21

SQL Server elige escanear las tablas de montón en el lado interno de las uniones de bucles utilizando bloqueos de nivel de fila. Una exploración completa normalmente elegiría el bloqueo de nivel de página, pero una combinación del tamaño de la tabla y el predicado significa que el motor de almacenamiento elige bloqueos de fila, ya que esa parece ser la estrategia más barata.

La desestimación de la cardinalidad se introdujo deliberadamente por el OPTIMIZE FORmedio de que los montones se escanean muchas más veces de lo que el optimizador espera, y no introduce un carrete como lo haría normalmente.

Esta combinación de factores significa que el rendimiento es muy sensible a la cantidad de bloqueos necesarios en tiempo de ejecución.

La SELECTdeclaración se beneficia de una optimización que permite omitir los bloqueos compartidos a nivel de fila (tomando solo bloqueos a nivel de página con intención compartida) cuando no hay peligro de leer datos no confirmados y no hay datos fuera de la fila.

La INSERT...SELECTdeclaración no se beneficia de esta optimización, por lo que se toman y liberan millones de bloqueos RID cada segundo en el segundo caso, junto con los bloqueos de nivel de página con intención compartida.

La enorme cantidad de actividad de bloqueo representa la CPU adicional y el tiempo transcurrido.

La solución más natural es garantizar que el optimizador (y el motor de almacenamiento) obtengan estimaciones de cardinalidad decentes para que puedan tomar buenas decisiones.

Si eso no es práctico en el caso de uso real, las declaraciones INSERTy SELECTpodrían separarse, con el resultado de la SELECTcontenida en una variable. Esto permitirá que la SELECTdeclaración se beneficie de la optimización de omisión de bloqueo.

También se puede hacer que el cambio del nivel de aislamiento funcione, ya sea al no tomar bloqueos compartidos o al garantizar que la escalada de bloqueo se realice rápidamente.

Como último punto de interés, se puede hacer que la consulta se ejecute incluso más rápido que el SELECTcaso optimizado al forzar el uso de carretes utilizando el indicador de rastreo no documentado 8691.

Paul White reinstala a Monica
fuente