¿Un pobre cálculo de cardinalidad descalifica a INSERT de un registro mínimo?

11

¿Por qué la segunda INSERTdeclaración es ~ 5 veces más lenta que la primera?

Por la cantidad de datos de registro generados, creo que el segundo no califica para un registro mínimo. Sin embargo, la documentación en la Guía de rendimiento de carga de datos indica que ambas inserciones deben poder registrarse mínimamente. Entonces, si el registro mínimo es la diferencia clave de rendimiento, ¿por qué la segunda consulta no califica para un registro mínimo? ¿Qué se puede hacer para mejorar la situación?


Consulta # 1: Insertar filas de 5MM usando INSERT ... WITH (TABLOCK)

Considere la siguiente consulta, que inserta filas de 5MM en un montón. Esta consulta se ejecuta 1 secondy genera 64MBdatos de registro de transacciones según lo informado por sys.dm_tran_database_transactions.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbers
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Consulta # 2: Insertar los mismos datos, pero SQL subestima el número de filas

Ahora considere esta consulta muy similar, que opera exactamente con los mismos datos pero que se basa en una tabla (o SELECTdeclaración compleja con muchas combinaciones en mi caso de producción real) donde la estimación de cardinalidad es demasiado baja. Esta consulta se ejecuta 5.5 secondsy genera 461MBdatos de registro de transacciones.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that produces 5MM rows but SQL estimates just 1000 rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Guión completo

Consulte este Pastebin para obtener un conjunto completo de scripts para generar los datos de prueba y ejecutar cualquiera de estos escenarios. Tenga en cuenta que debe usar una base de datos que esté en el SIMPLE modelo de recuperación .


Contexto empresarial

Con frecuencia, nos movemos alrededor de millones de filas de datos, y es importante que estas operaciones sean lo más eficientes posible, tanto en términos del tiempo de ejecución como de la carga de E / S del disco. Inicialmente teníamos la impresión de que crear una tabla de almacenamiento dinámico y usarlo INSERT...WITH (TABLOCK)era una buena manera de hacerlo, pero ahora nos hemos vuelto menos seguros dado que observamos la situación demostrada anteriormente en un escenario de producción real (aunque con consultas más complejas, no el versión simplificada aquí).

Geoff Patterson
fuente

Respuestas:

7

¿Por qué es que la segunda consulta no califica para un registro mínimo?

El registro mínimo está disponible para la segunda consulta, pero el motor elige no usarlo en tiempo de ejecución.

Hay un umbral mínimo por INSERT...SELECTdebajo del cual elige no utilizar las optimizaciones de carga masiva. Hay un costo involucrado en la configuración de una operación masiva de conjuntos de filas, y la inserción masiva de unas pocas filas no daría como resultado una utilización eficiente del espacio.

¿Qué se puede hacer para mejorar la situación?

Utilice uno de los muchos otros métodos (p SELECT INTO. Ej. ) Que no tiene este umbral. Alternativamente, puede reescribir la consulta fuente de alguna manera para aumentar el número estimado de filas / páginas por encima del umbral INSERT...SELECT.

Consulte también la respuesta automática de Geoff para obtener más información útil.


Curiosidades posiblemente interesantes: SET STATISTICS IO informa las lecturas lógicas para la tabla de destino solo cuando no se utilizan optimizaciones de carga masiva .

Paul White 9
fuente
5

Pude recrear el problema con mi propio equipo de prueba:

USE test;

CREATE TABLE dbo.SourceGood
(
    SourceGoodID INT NOT NULL
        CONSTRAINT PK_SourceGood
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.SourceBad
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_SourceBad
        PRIMARY KEY CLUSTERED
        IDENTITY(-2147483647,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.InsertTest
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_InsertTest
        PRIMARY KEY CLUSTERED
    , SomeData VARCHAR(384) NOT NULL
);
GO

INSERT INTO dbo.SourceGood WITH (TABLOCK) (SomeData) 
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS OFF;
GO

INSERT INTO dbo.SourceBad WITH (TABLOCK) (SomeData)
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS ON;
GO

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceGood;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472 
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;


BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count   
5000003 
database_transaction_log_bytes_used
642699256
*/

COMMIT TRANSACTION;

Esto plantea la pregunta, ¿por qué no "solucionar" el problema actualizando las estadísticas en las tablas de origen antes de ejecutar la operación mínimamente registrada?

TRUNCATE TABLE dbo.InsertTest;
UPDATE STATISTICS dbo.SourceBad;

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;
Max Vernon
fuente
2
En el código real, hay una SELECTdeclaración compleja con numerosas combinaciones que genera el conjunto de resultados para INSERT. Estas uniones producen estimaciones de cardinalidad deficientes para el operador de inserción de la tabla final (que he simulado en el script de repro a través de la UPDATE STATISTICSllamada incorrecta ) y, por lo tanto, no es tan simple como emitir un UPDATE STATISTICScomando para solucionar el problema. Estoy totalmente de acuerdo en que simplificar la consulta para que sea más fácil de entender para el Estimador de cardinalidad podría ser un buen enfoque, pero no es una buena opción implementar una lógica comercial compleja.
Geoff Patterson
No tengo una instancia de SQL Server 2014 para probar esto, sin embargo, la identificación de problemas del nuevo estimador de cardinalidad de SQL Server 2014 y la mejora del Service Pack 1 hablan de habilitar el indicador de seguimiento 4199, entre otros, para habilitar el nuevo estimador de cardinalidad. ¿Has intentado eso?
Max Vernon
Buena idea, pero no ayudó. Acabo de probar TF 4199, TF 610 (afloja las condiciones mínimas de registro), y ambos juntos (oye, ¿por qué no?), Pero no hay cambios para la segunda consulta de prueba.
Geoff Patterson
4

Reescribe la consulta de origen de alguna manera para aumentar el número estimado de filas

Ampliando la idea de Paul, una solución alternativa si está realmente desesperado es agregar una tabla ficticia que garantice que el número estimado de filas para el inserto será lo suficientemente alto como para la calidad de las optimizaciones de carga masiva. Confirmé que esto obtiene un registro mínimo y mejora el rendimiento de la consulta.

-- Create a dummy table that SQL Server thinks has a million rows
CREATE TABLE dbo.emptyTableWithMillionRowEstimate (
    n INT PRIMARY KEY
)
GO
UPDATE STATISTICS dbo.emptyTableWithMillionRowEstimate
WITH ROWCOUNT = 1000000
GO

-- Concatenate this table into the final rowset:
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Add in dummy rowset to ensure row estimate is high enough for bulk load optimization
UNION ALL
SELECT NULL FROM dbo.emptyTableWithMillionRowEstimate
OPTION (MAXDOP 1)

Conclusiones finales

  1. Úselo SELECT...INTOpara operaciones de inserción únicas si se requiere un registro mínimo. Como señala Paul, esto garantizará un registro mínimo independientemente de la estimación de la fila
  2. Siempre que sea posible, escriba consultas de una manera simple que el optimizador de consultas pueda razonar de manera efectiva. Es posible dividir una consulta en varias partes, por ejemplo, para permitir que las estadísticas se construyan en una tabla intermedia.
  3. Si tiene acceso a SQL Server 2014, pruébelo en su consulta; en mi caso de producción real, simplemente lo probé y el nuevo Estimador de Cardinalidad arrojó una estimación mucho más alta (y mejor); La consulta se registró mínimamente. Pero esto puede no ser útil si necesita admitir SQL 2012 y versiones anteriores.
  4. ¡Si estás desesperado, pueden aplicarse soluciones extravagantes como esta!

Un artículo relacionado

La publicación de blog de Paul White en mayo de 2019 El registro mínimo con INSERTAR ... SELECCIONAR en las tablas del montón cubre parte de esta información con más detalle.

Geoff Patterson
fuente