Varias declaraciones INSERT frente a un solo INSERT con varios VALORES

119

Estoy ejecutando una comparación de rendimiento entre el uso de 1000 declaraciones INSERT:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0)
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1)
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999)

.. versus el uso de una sola declaración INSERT con 1000 valores:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
VALUES 
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0),
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1),
...
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999)

Para mi gran sorpresa, los resultados son opuestos a lo que pensaba:

  • 1000 declaraciones INSERT: 290 mseg.
  • 1 instrucción INSERT con 1000 VALORES: 2800 mseg.

La prueba se ejecuta directamente en MSSQL Management Studio con SQL Server Profiler usado para la medición (y obtuve resultados similares ejecutándolo desde el código C # usando SqlClient, lo cual es aún más sorprendente considerando todos los recorridos de ida y vuelta de las capas DAL)

¿Puede esto ser razonable o explicarse de alguna manera? ¿Cómo es que un método supuestamente más rápido da como resultado un rendimiento 10 veces (!) Peor ?

Gracias.

EDITAR: Adjuntar planes de ejecución para ambos: Planes ejecutivos

Borka
fuente
1
estas son pruebas limpias, nada se ejecuta en paralelo, no hay datos repetidos (cada consulta es con datos diferentes, por supuesto, para evitar el almacenamiento en caché simple)
Borka
1
¿hay algún desencadenante involucrado?
AK
2
Convertí un programa a TVP para superar el límite de 1000 valores y obtuve una gran ganancia de rendimiento. Haré una comparación.
paparazzo
1
relevante: simple-talk.com/sql/performance/…
desconocido

Respuestas:

126

Además: SQL Server 2012 muestra un rendimiento mejorado en esta área, pero no parece abordar los problemas específicos que se indican a continuación. ¡ Aparentemente, esto debería solucionarse en la próxima versión principal después de SQL Server 2012!

Su plan muestra que las inserciones individuales están usando procedimientos parametrizados (posiblemente auto parametrizados) por lo que el tiempo de análisis / compilación para estos debe ser mínimo.

Pensé en investigar esto un poco más, así que configuré un bucle ( script ) e intenté ajustar el número de VALUEScláusulas y registrar el tiempo de compilación.

Luego dividí el tiempo de compilación por el número de filas para obtener el tiempo de compilación promedio por cláusula. Los resultados están debajo

Grafico

Hasta 250 VALUEScláusulas presentes, el tiempo de compilación / número de cláusulas tiene una ligera tendencia al alza pero nada demasiado dramático.

Grafico

Pero luego hay un cambio repentino.

Esa sección de los datos se muestra a continuación.

+------+----------------+-------------+---------------+---------------+
| Rows | CachedPlanSize | CompileTime | CompileMemory | Duration/Rows |
+------+----------------+-------------+---------------+---------------+
|  245 |            528 |          41 |          2400 | 0.167346939   |
|  246 |            528 |          40 |          2416 | 0.162601626   |
|  247 |            528 |          38 |          2416 | 0.153846154   |
|  248 |            528 |          39 |          2432 | 0.157258065   |
|  249 |            528 |          39 |          2432 | 0.156626506   |
|  250 |            528 |          40 |          2448 | 0.16          |
|  251 |            400 |         273 |          3488 | 1.087649402   |
|  252 |            400 |         274 |          3496 | 1.087301587   |
|  253 |            400 |         282 |          3520 | 1.114624506   |
|  254 |            408 |         279 |          3544 | 1.098425197   |
|  255 |            408 |         290 |          3552 | 1.137254902   |
+------+----------------+-------------+---------------+---------------+

El tamaño del plan en caché que había estado creciendo linealmente cae repentinamente, pero CompileTime aumenta 7 veces y CompileMemory se dispara. Este es el punto de corte entre el plan que es auto parametrizado (con 1000 parámetros) y uno no parametrizado. A partir de entonces, parece volverse linealmente menos eficiente (en términos de número de cláusulas de valor procesadas en un tiempo determinado).

No estoy seguro de por qué debería ser esto. Presumiblemente, cuando está compilando un plan para valores literales específicos, debe realizar alguna actividad que no se escala linealmente (como ordenar).

No parece afectar el tamaño del plan de consulta en caché cuando probé una consulta que consta completamente de filas duplicadas y tampoco afecta el orden de la salida de la tabla de las constantes (y mientras lo inserta en un montón, el tiempo dedicado a ordenar sería inútil de todos modos incluso si lo hiciera).

Además, si se agrega un índice agrupado a la tabla, el plan aún muestra un paso de clasificación explícito, por lo que no parece estar ordenando en tiempo de compilación para evitar una clasificación en tiempo de ejecución.

Plan

Traté de ver esto en un depurador, pero los símbolos públicos para mi versión de SQL Server 2008 no parecen estar disponibles, así que en su lugar tuve que buscar la UNION ALLconstrucción equivalente en SQL Server 2005.

A continuación se muestra un rastro de pila típico

sqlservr.exe!FastDBCSToUnicode()  + 0xac bytes  
sqlservr.exe!nls_sqlhilo()  + 0x35 bytes    
sqlservr.exe!CXVariant::CmpCompareStr()  + 0x2b bytes   
sqlservr.exe!CXVariantPerformCompare<167,167>::Compare()  + 0x18 bytes  
sqlservr.exe!CXVariant::CmpCompare()  + 0x11f67d bytes  
sqlservr.exe!CConstraintItvl::PcnstrItvlUnion()  + 0xe2 bytes   
sqlservr.exe!CConstraintProp::PcnstrUnion()  + 0x35e bytes  
sqlservr.exe!CLogOp_BaseSetOp::PcnstrDerive()  + 0x11a bytes    
sqlservr.exe!CLogOpArg::PcnstrDeriveHandler()  + 0x18f bytes    
sqlservr.exe!CLogOpArg::DeriveGroupProperties()  + 0xa9 bytes   
sqlservr.exe!COpArg::DeriveNormalizedGroupProperties()  + 0x40 bytes    
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x18a bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!CQuery::PqoBuild()  + 0x3cb bytes  
sqlservr.exe!CStmtQuery::InitQuery()  + 0x167 bytes 
sqlservr.exe!CStmtDML::InitNormal()  + 0xf0 bytes   
sqlservr.exe!CStmtDML::Init()  + 0x1b bytes 
sqlservr.exe!CCompPlan::FCompileStep()  + 0x176 bytes   
sqlservr.exe!CSQLSource::FCompile()  + 0x741 bytes  
sqlservr.exe!CSQLSource::FCompWrapper()  + 0x922be bytes    
sqlservr.exe!CSQLSource::Transform()  + 0x120431 bytes  
sqlservr.exe!CSQLSource::Compile()  + 0x2ff bytes   

Entonces, al eliminar los nombres en el seguimiento de la pila, parece pasar mucho tiempo comparando cadenas.

Este artículo de KB indica que DeriveNormalizedGroupPropertiesestá asociado con lo que solía llamarse la etapa de normalización del procesamiento de consultas

Esta etapa ahora se llama enlace o algebrización y toma la salida del árbol de análisis sintáctico de la etapa anterior y genera un árbol de expresión algebrizado (árbol del procesador de consultas) para avanzar a la optimización (optimización del plan trivial en este caso) [ref] .

Probé un experimento más ( Script ) que consistía en volver a ejecutar la prueba original pero observando tres casos diferentes.

  1. Nombre y apellido Cadenas de 10 caracteres de longitud sin duplicados.
  2. Nombre y apellido Cadenas de 50 caracteres de longitud sin duplicados.
  3. Nombre y apellido Cadenas de 10 caracteres de longitud con todos los duplicados.

Grafico

Se puede ver claramente que cuanto más largas son las cadenas, peor se ponen las cosas y que, por el contrario, cuanto más duplicados, mejores son las cosas. Como se mencionó anteriormente, los duplicados no afectan el tamaño del plan almacenado en caché, por lo que supongo que debe haber un proceso de identificación duplicada al construir el árbol de expresión algebrizado.

Editar

@Lieven muestra un lugar donde se aprovecha esta información aquí

SELECT * 
FROM (VALUES ('Lieven1', 1),
             ('Lieven2', 2),
             ('Lieven3', 3))Test (name, ID)
ORDER BY name, 1/ (ID - ID) 

Debido a que en el momento de la compilación puede determinar que la Namecolumna no tiene duplicados, omite el orden por la 1/ (ID - ID)expresión secundaria en el tiempo de ejecución (la ordenación en el plan solo tiene una ORDER BYcolumna) y no se genera ningún error de división por cero. Si se agregan duplicados a la tabla, el operador de clasificación muestra dos orden por columnas y se genera el error esperado.

Martin Smith
fuente
6
El número mágico que tiene es NumberOfRows / ColumnCount = 250. Cambie su consulta para usar solo tres columnas y el cambio ocurrirá en 333. El número mágico 1000 podría ser algo así como el número máximo de parámetros usados ​​en un plan en caché. Parece ser "más fácil" generar un plan con <ParameterList>uno que con una <ConstantScan><Values><Row>lista.
Mikael Eriksson
1
@MikaelEriksson - De acuerdo. La fila 250 con 1000 valores se parametriza automáticamente, la fila 251 no, por lo que parece ser la diferencia. Aunque no estoy seguro de por qué. Quizás dedica tiempo a ordenar los valores literales en busca de duplicados o algo cuando los tiene.
Martin Smith
1
Este es un problema bastante loco, simplemente me entristeció. Esta es una gran respuesta gracias
No amado
1
@MikaelEriksson ¿Quiere decir que el número mágico es NumberOfRows * ColumnCount = 1000?
paparazzo
1
@Blam - Sí. Cuando el número total de elementos es más de 1000 (NumberOfRows * ColumnCount), el plan de consulta cambia para usar en <ConstantScan><Values><Row>lugar de <ParameterList>.
Mikael Eriksson
23

No es demasiado sorprendente: el plan de ejecución para la pequeña inserción se calcula una vez y luego se reutiliza 1000 veces. Analizar y preparar el plan es rápido, porque solo tiene cuatro valores para eliminar. Un plan de 1000 filas, por otro lado, debe manejar 4000 valores (o 4000 parámetros si parametrizó sus pruebas de C #). Esto podría consumir fácilmente el ahorro de tiempo que gana al eliminar 999 viajes de ida y vuelta a SQL Server, especialmente si su red no es demasiado lenta.

dasblinkenlight
fuente
9

El problema probablemente tenga que ver con el tiempo que lleva compilar la consulta.

Si desea acelerar las inserciones, lo que realmente debe hacer es envolverlas en una transacción:

BEGIN TRAN;
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0);
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1);
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999);
COMMIT TRAN;

Desde C #, también puede considerar usar un parámetro con valores de tabla. Emitir varios comandos en un solo lote, separándolos con punto y coma, es otro enfoque que también ayudará.

RickNZ
fuente
1
Re: "Emitir varios comandos en un solo lote": eso ayuda un poco, pero no mucho. Pero definitivamente estoy de acuerdo con las otras dos opciones de envolver en una TRANSACCIÓN (¿realmente funciona TRANS o debería ser simplemente TRAN?) O usar un TVP.
Solomon Rutzky
1

Me encontré con una situación similar al intentar convertir una tabla con varias filas de 100k con un programa C ++ (MFC / ODBC).

Dado que esta operación tomó mucho tiempo, pensé en agrupar múltiples inserciones en una (hasta 1000 debido a las limitaciones de MSSQL ). Supongo que muchas declaraciones de inserción únicas crearían una sobrecarga similar a la que se describe aquí .

Sin embargo, resulta que la conversión tomó un poco más de tiempo:

        Method 1       Method 2     Method 3 
        Single Insert  Multi Insert Joined Inserts
Rows    1000           1000         1000
Insert  390 ms         765 ms       270 ms
per Row 0.390 ms       0.765 ms     0.27 ms

Entonces, 1000 llamadas individuales a CDatabase :: ExecuteSql, cada una con una sola instrucción INSERT (método 1) son aproximadamente el doble de rápidas que una sola llamada a CDatabase :: ExecuteSql con una instrucción INSERT de varias líneas con 1000 tuplas de valor (método 2).

Actualización: Entonces, lo siguiente que intenté fue agrupar 1000 declaraciones INSERT separadas en una sola cadena y hacer que el servidor lo ejecute (método 3). Resulta que esto es incluso un poco más rápido que el método 1.

Editar: estoy usando Microsoft SQL Server Express Edition (64 bits) v10.0.2531.0

uceumern
fuente