Rendimiento de bcp / BULK INSERT frente a parámetros con valores de tabla

84

Estoy a punto de tener que volver a escribir un código bastante antiguo usando el BULK INSERTcomando de SQL Server porque el esquema ha cambiado y se me ocurrió que tal vez debería pensar en cambiar a un procedimiento almacenado con un TVP, pero me pregunto qué efecto podría tener sobre el rendimiento.

Alguna información de fondo que podría ayudar a explicar por qué hago esta pregunta:

  • Los datos realmente llegan a través de un servicio web. El servicio web escribe un archivo de texto en una carpeta compartida en el servidor de la base de datos que a su vez realiza un BULK INSERT. Este proceso se implementó originalmente en SQL Server 2000, y en ese momento realmente no había otra alternativa que arrojar algunos cientos de INSERTdeclaraciones en el servidor, que en realidad era el proceso original y era un desastre de rendimiento.

  • Los datos se insertan de forma masiva en una tabla de preparación permanente y luego se fusionan en una tabla mucho más grande (después de lo cual se eliminan de la tabla de preparación).

  • La cantidad de datos para insertar es "grande", pero no "enorme", por lo general unos cientos de filas, tal vez entre 5 y 10 mil filas en raras ocasiones. Por tanto, mi primera impresión es que BULK INSERTser una operación no registrada no hará que diferencia un gran (pero por supuesto que no estoy seguro, por lo tanto, la pregunta).

  • La inserción es en realidad parte de un proceso por lotes en canalización mucho más grande y debe ocurrir muchas veces seguidas; por lo tanto, el rendimiento es fundamental.

Las razones por las que me gustaría reemplazar el BULK INSERTcon un TVP son:

  • Escribir el archivo de texto sobre NetBIOS probablemente ya esté costando algo de tiempo, y es bastante espantoso desde una perspectiva arquitectónica.

  • Creo que la tabla de etapas puede (y debe) eliminarse. La razón principal por la que está ahí es que los datos insertados deben usarse para un par de actualizaciones más al mismo tiempo de la inserción, y es mucho más costoso intentar la actualización desde la tabla de producción masiva que usar una preparación casi vacía mesa. Con un TVP, el parámetro básicamente es la tabla de preparación, puedo hacer lo que quiera con él antes / después de la inserción principal.

  • Podría acabar con la verificación de duplicados, el código de limpieza y toda la sobrecarga asociada con las inserciones masivas.

  • No hay necesidad de preocuparse por la contención de bloqueo en la tabla de preparación o tempdb si el servidor obtiene algunas de estas transacciones a la vez (intentamos evitarlo, pero sucede).

Obviamente voy a perfilar esto antes de poner algo en producción, pero pensé que sería una buena idea preguntar primero antes de pasar todo ese tiempo, ver si alguien tiene alguna advertencia severa que emitir sobre el uso de TVP para este propósito.

Entonces, para cualquiera que esté lo suficientemente cómodo con SQL Server 2008 como para haber intentado o al menos investigado esto, ¿cuál es el veredicto? Para inserciones de, digamos, unos cientos o miles de filas, que ocurren con bastante frecuencia, ¿los TVP cortan la mostaza? ¿Existe una diferencia significativa en el rendimiento en comparación con los insertos a granel?


Actualización: ¡Ahora con un 92% menos de interrogantes!

(AKA: Resultados de la prueba)

El resultado final está ahora en producción después de lo que parece un proceso de implementación de 36 etapas. Ambas soluciones fueron ampliamente probadas:

  • Extraer el código de la carpeta compartida y usar la SqlBulkCopyclase directamente;
  • Cambio a un procedimiento almacenado con TVP.

Para que los lectores puedan tener una idea de qué se probó exactamente, para disipar cualquier duda sobre la confiabilidad de estos datos, aquí hay una explicación más detallada de lo que realmente hace este proceso de importación :

  1. Empiece con una secuencia de datos temporal que normalmente tiene entre 20 y 50 puntos de datos (aunque a veces puede llegar a unos pocos cientos);

  2. Realice un montón de procesamiento loco que sea en su mayoría independiente de la base de datos. Este proceso está en paralelo, por lo que aproximadamente 8-10 de las secuencias en (1) se procesan al mismo tiempo. Cada proceso paralelo genera 3 secuencias adicionales.

  3. Tome las 3 secuencias y la secuencia original y combínelas en un lote.

  4. Combine los lotes de las 8-10 tareas de procesamiento ahora terminadas en un gran superlote.

  5. Impórtelo utilizando la BULK INSERTestrategia (consulte el paso siguiente) o la estrategia TVP (vaya al paso 8).

  6. Utilice la SqlBulkCopyclase para volcar todo el super-lote en 4 tablas de preparación permanentes.

  7. Ejecute un procedimiento almacenado que (a) realiza una serie de pasos de agregación en 2 de las tablas, incluidas varias JOINcondiciones, y luego (b) realiza una MERGEen 6 tablas de producción utilizando los datos agregados y no agregados. (Terminado)

    O

  8. Genere 4 DataTableobjetos que contengan los datos a fusionar; 3 de ellos contienen tipos CLR que, lamentablemente, no son compatibles con los TVP de ADO.NET, por lo que deben introducirse como representaciones de cadenas, lo que perjudica un poco el rendimiento.

  9. Alimente los TVP a un procedimiento almacenado, que realiza esencialmente el mismo procesamiento que (7), pero directamente con las tablas recibidas. (Terminado)

Los resultados fueron razonablemente cercanos, pero el enfoque de TVP finalmente se desempeñó mejor en promedio, incluso cuando los datos excedieron las 1000 filas por una pequeña cantidad.

Tenga en cuenta que este proceso de importación se ejecuta miles de veces seguidas, por lo que fue muy fácil obtener un tiempo promedio simplemente contando cuántas horas (sí, horas) se necesitaron para finalizar todas las fusiones.

Originalmente, una fusión promedio tardaba casi exactamente 8 segundos en completarse (con carga normal). Eliminar el kludge de NetBIOS y cambiar a SqlBulkCopyredujo el tiempo a casi exactamente 7 segundos. El cambio a TVP redujo aún más el tiempo a 5,2 segundos por lote. Eso es una mejora del 35% en el rendimiento de un proceso cuyo tiempo de ejecución se mide en horas, por lo que no está nada mal. También es una mejora de ~ 25% SqlBulkCopy.

De hecho, estoy bastante seguro de que la verdadera mejora fue significativamente mayor que esto. Durante las pruebas se hizo evidente que la fusión final ya no era el camino crítico; en cambio, el servicio web que estaba haciendo todo el procesamiento de datos estaba comenzando a fallar por el número de solicitudes que ingresaban. Ni la CPU ni la E / S de la base de datos estaban realmente al máximo y no había actividad de bloqueo significativa. En algunos casos, observamos un intervalo de unos segundos inactivos entre fusiones sucesivas. Hubo un pequeño espacio, pero mucho más pequeño (medio segundo más o menos) cuando se usa SqlBulkCopy. Pero supongo que se convertirá en un cuento para otro día.

Conclusión: Los parámetros con valores de tabla realmente funcionan mejor que las BULK INSERToperaciones para procesos complejos de importación + transformación que operan en conjuntos de datos de tamaño medio.


Me gustaría agregar otro punto, solo para calmar cualquier aprensión por parte de la gente que está a favor de las tablas de preparación. En cierto modo, todo este servicio es un proceso de puesta en escena gigante. Cada paso del proceso está fuertemente auditado, por lo que no necesitamos una tabla de preparación para determinar por qué falló una combinación en particular (aunque en la práctica casi nunca ocurre). Todo lo que tenemos que hacer es establecer una bandera de depuración en el servicio y se romperá con el depurador o volcará sus datos en un archivo en lugar de en la base de datos.

En otras palabras, ya tenemos información más que suficiente sobre el proceso y no necesitamos la seguridad de una mesa de preparación; La única razón por la que teníamos la tabla de preparación en primer lugar fue para evitar golpear todas las declaraciones INSERTy UPDATEque hubiéramos tenido que usar de otra manera. En el proceso original, los datos de preparación solo vivían en la tabla de preparación durante fracciones de segundo de todos modos, por lo que no agregaban valor en términos de mantenimiento / mantenibilidad.

También tenga en cuenta que hemos no sustituido cada BULK INSERToperación con TVP. Varias operaciones que tratan con grandes cantidades de datos y / o no necesitan hacer nada especial con los datos que no sean arrojarlos a la base de datos todavía se usan SqlBulkCopy. No estoy sugiriendo que los TVP sean una panacea del desempeño, solo que tuvieron éxito SqlBulkCopyen esta instancia específica que involucra varias transformaciones entre la puesta en escena inicial y la fusión final.

Así que ahí lo tienes. El punto va a TToni por encontrar el enlace más relevante, pero también aprecio las otras respuestas. ¡Gracias de nuevo!

Aaronaught
fuente
Esta es una pregunta asombrosa en sí misma, creo que la parte de actualización debería estar en una respuesta;)
Marc.2377

Respuestas:

10

Realmente no tengo experiencia con TVP todavía, sin embargo, hay una buena tabla de comparación de rendimiento frente a BULK INSERT en MSDN aquí .

Dicen que BULK INSERT tiene un costo de inicio más alto, pero es más rápido a partir de entonces. En un escenario de cliente remoto, trazan la línea en alrededor de 1000 filas (para una lógica de servidor "simple"). A juzgar por su descripción, diría que debería estar bien con el uso de TVP. El impacto en el rendimiento, si lo hay, es probablemente insignificante y los beneficios arquitectónicos parecen muy buenos.

Editar: en una nota al margen, puede evitar el archivo local del servidor y aún usar la copia masiva usando el objeto SqlBulkCopy. Simplemente rellene un DataTable e introdúzcalo en el método "WriteToServer" de una instancia de SqlBulkCopy. Fácil de usar y muy rápido.

TToni
fuente
Gracias por el enlace, que en realidad es bastante útil ya que MS parece recomendar TVP cuando los datos alimentan una lógica compleja (lo cual es así) y también tenemos la capacidad de marcar hacia arriba o hacia abajo el tamaño del lote para no ir demasiado lejos del Punto de dolor de 1k filas. En base a esto, puede valer la pena el tiempo para al menos probar y ver, incluso si termina siendo demasiado lento.
Aaronaught
Sí, el enlace es interesante. @Aaronaught: en situaciones como esta, siempre vale la pena explorar y analizar el rendimiento de enfoques potenciales, ¡así que me interesaría escuchar sus hallazgos!
AdaTheDev
7

El cuadro mencionado con respecto al enlace proporcionado en la respuesta de @ TToni debe tomarse en contexto. No estoy seguro de cuánta investigación real se dedicó a esas recomendaciones (también tenga en cuenta que el cuadro parece estar disponible solo en las versiones 2008y 2008 R2de esa documentación).

Por otro lado, está este documento técnico del equipo asesor de clientes de SQL Server: Maximización del rendimiento con TVP

He estado usando TVP desde 2009 y he descubierto, al menos en mi experiencia, que para cualquier otra cosa que no sea una simple inserción en una tabla de destino sin necesidades lógicas adicionales (que rara vez es el caso), los TVP suelen ser la mejor opción.

Tiendo a evitar las tablas de ensayo, ya que la validación de datos debe realizarse en la capa de la aplicación. Al usar TVP, eso se acomoda fácilmente y la variable de tabla TVP en el procedimiento almacenado es, por su propia naturaleza, una tabla de preparación localizada (por lo tanto, no hay conflicto con otros procesos que se ejecutan al mismo tiempo como se obtiene cuando se usa una tabla real para la preparación ).

Con respecto a las pruebas realizadas en la Pregunta, creo que podría demostrarse que es incluso más rápido de lo que se encontró originalmente:

  1. No debe utilizar un DataTable, a menos que su aplicación tenga un uso para él fuera de enviar los valores al TVP. El uso de la IEnumerable<SqlDataRecord>interfaz es más rápido y usa menos memoria, ya que no está duplicando la colección en la memoria solo para enviarla a la base de datos. Tengo esto documentado en los siguientes lugares:
  2. Los TVP son variables de tabla y, como tales, no mantienen estadísticas. Es decir, informan que solo tienen 1 fila en el Optimizador de consultas. Entonces, en su proceso, ya sea:
    • Use la recompilación a nivel de declaración en cualquier consulta usando el TVP para cualquier cosa que no sea un simple SELECT: OPTION (RECOMPILE)
    • Cree una tabla temporal local (es decir, única #) y copie el contenido del TVP en la tabla temporal
Salomón Rutzky
fuente
4

Creo que todavía me quedaría con un enfoque de inserción masiva. Puede encontrar que tempdb todavía se golpea usando un TVP con un número razonable de filas. Este es mi presentimiento, no puedo decir que haya probado el rendimiento del uso de TVP (aunque también estoy interesado en escuchar las opiniones de otros)

No menciona si usa .NET, pero el enfoque que tomé para optimizar las soluciones anteriores fue hacer una carga masiva de datos usando la clase SqlBulkCopy ; no es necesario que escriba los datos en un archivo antes. cargando, simplemente déle a la clase SqlBulkCopy (por ejemplo) un DataTable; esa es la forma más rápida de insertar datos en la base de datos. 5-10K filas no es mucho, he usado esto para hasta 750K filas. Sospecho que, en general, con unos pocos cientos de filas no haría una gran diferencia usando un TVP. Pero la ampliación sería limitada en mi humilde opinión.

¿Quizás la nueva funcionalidad MERGE en SQL 2008 le beneficiaría?

Además, si su tabla de preparación existente es una única tabla que se utiliza para cada instancia de este proceso y le preocupa la contención, etc., ¿ha considerado crear una nueva tabla de preparación "temporal" pero física cada vez, y luego descartarla cuando sea necesario? ¿acabado con?

Tenga en cuenta que puede optimizar la carga en esta tabla de preparación completándola sin índices. Luego, una vez completado, agregue los índices requeridos en ese punto (FILLFACTOR = 100 para un rendimiento de lectura óptimo, ya que en este punto no se actualizará).

AdaTheDev
fuente
Yo uso .NET, y el proceso es anterior SqlBulkCopyy simplemente nunca se ha cambiado. Gracias por recordarme eso, puede que valga la pena volver a visitarlo. MERGEtambién ya se usa ampliamente, y las tablas temporales se probaron una vez antes, pero resultaron ser más lentas y más difíciles de administrar. ¡Gracias por el aporte!
Aaronaught
-2

¡Las tablas de preparación son buenas! Realmente no quisiera hacerlo de otra manera. ¿Por qué? Porque las importaciones de datos pueden cambiar inesperadamente (y a menudo de formas que no se pueden prever, como el momento en que las columnas todavía se llamaban nombre y apellido pero tenían los datos del nombre en la columna del apellido, por ejemplo, para elegir un ejemplo no al azar.) Fácil de investigar el problema con una tabla de preparación para que pueda ver exactamente qué datos estaban en las columnas que manejó la importación. Creo que es más difícil de encontrar cuando usas una tabla en memoria. Conozco a mucha gente que se gana la vida con las importaciones como yo y todos recomiendan usar tablas de preparación. Sospecho que hay una razón para esto.

Es más fácil y requiere menos tiempo corregir un pequeño cambio de esquema en un proceso de trabajo que rediseñar el proceso. Si está funcionando y nadie está dispuesto a pagar horas para cambiarlo, solo corrija lo que deba arreglarse debido al cambio de esquema. Al cambiar todo el proceso, introduce muchos más errores potenciales nuevos que si realiza un pequeño cambio en un proceso de trabajo existente y probado.

¿Y cómo va a acabar con todas las tareas de limpieza de datos? Es posible que los esté haciendo de manera diferente, pero aún deben hacerse. Nuevamente, cambiar el proceso de la forma en que lo describe es muy arriesgado.

Personalmente, me parece que te ofende usar técnicas antiguas en lugar de tener la oportunidad de jugar con juguetes nuevos. Parece que no tiene una base real para querer cambiar que no sea la inserción masiva es tan 2000.

HLGEM
fuente
27
SQL 2008 ha existido durante 2 años y este proceso ha existido durante años, y esta es la primera vez que considero cambiarlo. ¿Era realmente necesario el comentario sarcástico al final?
Aaronaught