Con respecto a la metodología, creo que estás ladrando el árbol b incorrecto ;-).
Lo que sabemos:
Primero, consolidemos y revisemos lo que sabemos sobre la situación:
Lo que podemos suponer:
Luego, podemos ver todos estos puntos de datos juntos para ver si podemos sintetizar detalles adicionales que nos ayudarán a encontrar uno o más cuellos de botella, y señalar una solución, o al menos descartar algunas posibles soluciones.
La dirección actual de pensamiento en los comentarios es que el problema principal es la transferencia de datos entre SQL Server y Excel. ¿Es realmente el caso? Si se llama al Procedimiento almacenado para cada una de las 800,000 filas y toma 50 ms por cada llamada (es decir, por cada fila), eso suma 40,000 segundos (no ms). Y eso equivale a 666 minutos (hhmm ;-), o poco más de 11 horas. Sin embargo, se dijo que todo el proceso demoraba solo 7 horas en ejecutarse. Ya hemos pasado 4 horas sobre el tiempo total, e incluso hemos agregado a tiempo para hacer los cálculos o guardar los resultados en SQL Server. Entonces algo no está bien aquí.
Mirando la definición del Procedimiento almacenado, solo hay un parámetro de entrada para @FileID
; No hay ningún filtro activado @RowID
. Entonces sospecho que uno de los siguientes dos escenarios está sucediendo:
- Este procedimiento almacenado en realidad no se llama por cada fila, sino por cada una
@FileID
, que parece abarcar aproximadamente 4000 filas. Si las 4000 filas indicadas devueltas son una cantidad bastante consistente, entonces solo hay 200 de esas agrupaciones en las 800,000 filas. Y 200 ejecuciones de 50 ms cada una equivalen a solo 10 segundos de esas 7 horas.
- Si este procedimiento almacenado realmente se llama para cada fila, entonces la primera vez que
@FileID
se pasa una nueva no tomaría un poco más de tiempo para atraer nuevas filas al Buffer Pool, pero luego las siguientes 3999 ejecuciones generalmente regresarían más rápido debido a que ya en caché, ¿verdad?
Creo que centrarse en este procedimiento almacenado de "filtro", o cualquier transferencia de datos desde SQL Server a Excel, es una pista falsa .
Por el momento, creo que los indicadores más relevantes de rendimiento mediocre son:
- Hay 800,000 filas
- La operación funciona en una fila a la vez.
- Los datos se guardan de nuevo en SQL Server, por lo tanto, "[utiliza] valores de algunas columnas para manipular otras columnas " [mi fase es ;-)]
Sospecho que:
- Si bien hay margen de mejora en la recuperación de datos y los cálculos, mejorarlos no equivaldría a una reducción significativa en el tiempo de procesamiento.
- El principal cuello de botella es la emisión de 800,000
UPDATE
extractos, que son 800,000 transacciones separadas.
Mi recomendación (basada en la información disponible actualmente):
Su mayor área de mejora sería actualizar varias filas a la vez (es decir, en una transacción). Debe actualizar su proceso para que funcione en términos de cada uno en FileID
lugar de cada uno RowID
. Entonces:
- leer en todas las 4000 filas de un particular
FileID
en una matriz
- la matriz debe contener elementos que representen los campos que se manipulan
- recorrer la matriz, procesando cada fila como lo haces actualmente
- una vez que todas las filas en la matriz (es decir, para este particular
FileID
se han calculado ):
- comenzar una transacción
- llame a cada actualización por cada
RowID
- si no hay errores, confirme la transacción
- Si ocurrió un error, retroceda y maneje adecuadamente
Si su índice agrupado aún no está definido, (FileID, RowID)
entonces debería considerarlo (como sugirió @MikaelEriksson en un comentario sobre la Pregunta). No ayudará a estas ACTUALIZACIONES de singleton, pero al menos mejoraría ligeramente las operaciones agregadas, como lo que está haciendo en ese procedimiento almacenado de "filtro", ya que todas se basan en ellas FileID
.
Debería considerar mover la lógica a un lenguaje compilado. Sugeriría crear una aplicación .NET WinForms o incluso una aplicación de consola. Prefiero la aplicación de consola, ya que es fácil de programar a través del Agente SQL o las tareas programadas de Windows. No debería importar si se hace en VB.NET o C #. VB.NET puede ser más adecuado para su desarrollador, pero seguirá habiendo cierta curva de aprendizaje.
No veo ninguna razón en este momento para pasar a SQLCLR. Si el algoritmo cambia con frecuencia, sería molesto tener que volver a implementar la Asamblea todo el tiempo. Reconstruir una aplicación de consola y hacer que el .exe se coloque en la carpeta compartida adecuada en la red de modo que simplemente ejecute el mismo programa y siempre esté actualizado, debería ser bastante fácil de hacer.
No creo que mover el procesamiento completamente a T-SQL ayudaría si el problema es lo que sospecho y solo está haciendo una ACTUALIZACIÓN a la vez.
Si el procesamiento se traslada a .NET, puede utilizar los Parámetros con valor de tabla (TVP) de modo que pase la matriz a un Procedimiento almacenado que llame a un UPDATE
que se UNE a la variable de tabla de TVP y, por lo tanto, sea una sola transacción . El TVP debería ser más rápido que hacer 4000 INSERT
s agrupados en una sola transacción. Pero la ganancia proveniente del uso de TVP durante 4000 INSERT
s en 1 transacción probablemente no será tan significativa como la mejora observada al pasar de 800,000 transacciones separadas a solo 200 transacciones de 4000 filas cada una.
La opción TVP no está disponible de forma nativa para el lado de VBA, pero a alguien se le ocurrió una solución que podría valer la pena probar:
¿Cómo mejoro el rendimiento de la base de datos cuando paso de VBA a SQL Server 2008 R2?
SI el proceso de filtro solo se usa FileID
en la WHERE
cláusula, y si realmente se llama a ese proceso por cada fila, entonces puede ahorrar algo de tiempo de procesamiento almacenando en caché los resultados de la primera ejecución y usándolos para el resto de las filas por eso FileID
, ¿Derecha?
Una vez que el procesamiento realizado por FileID , entonces podemos empezar a hablar de procesamiento en paralelo. Pero eso podría no ser necesario en ese momento :). Dado que se trata de 3 partes no ideales bastante importantes: transacciones de Excel, VBA y 800k, cualquier conversación sobre SSIS o paralelogramos, o quién sabe qué, es un tipo de optimización prematura / carro antes del caballo . Si podemos reducir este proceso de 7 horas a 10 minutos o menos, ¿seguiría pensando en formas adicionales de acelerarlo? ¿Hay un tiempo de finalización objetivo que tenga en mente? Tenga en cuenta que una vez que el procesamiento se realiza en un ID de archivo base, si tuviera una aplicación de consola VB.NET (es decir, línea de comandos .EXE), no habría nada que le impidiera ejecutar algunos de esos ID de archivo a la vez :), ya sea a través del paso CmdExec del Agente SQL o Tareas programadas de Windows, etc.
Y, siempre puede adoptar un enfoque "por fases" y hacer algunas mejoras a la vez. Como comenzar con las actualizaciones por FileID
y, por lo tanto, usar una transacción para ese grupo. Luego, vea si puede hacer que el TVP funcione. Luego, vea cómo tomar ese código y moverlo a VB.NET (y los TVP funcionan en .NET, por lo que se portará bien).
Lo que no sabemos que aún podría ayudar:
- ¿El procedimiento almacenado "filtro" se ejecuta por RowID o por FileID ? ¿Tenemos siquiera la definición completa de ese procedimiento almacenado?
- Esquema completo de la tabla. ¿Qué tan ancha es esta mesa? ¿Cuántos campos de longitud variable hay? ¿Cuántos campos son NULLable? Si alguno es NULLable, ¿cuántos contienen NULL?
- Índices para esta tabla. ¿Está dividido? ¿Se está utilizando la compresión ROW o PAGE?
- ¿Qué tamaño tiene esta tabla en términos de MB / GB?
- ¿Cómo se maneja el mantenimiento del índice para esta tabla? ¿Qué tan fragmentados están los índices? ¿Qué tan actualizadas son las estadísticas?
- ¿Algún otro proceso escribe en esta tabla mientras se lleva a cabo este proceso de 7 horas? Posible fuente de contención.
- ¿Leen otros procesos de esta tabla mientras se lleva a cabo este proceso de 7 horas? Posible fuente de contención.
ACTUALIZACIÓN 1:
** Parece haber cierta confusión acerca de qué VBA (Visual Basic para aplicaciones) y qué se puede hacer con él, así que esto es solo para asegurarse de que todos estamos en la misma página web:
ACTUALIZACIÓN 2:
Un punto más a considerar: ¿Cómo se manejan las conexiones? ¿El código VBA abre y cierra la conexión por cada operación, o abre la conexión al comienzo del proceso y la cierra al final del proceso (es decir, 7 horas después)? Incluso con la agrupación de conexiones (que, de forma predeterminada, debería estar habilitada para ADO), todavía debería haber un gran impacto entre abrir y cerrar una vez en lugar de abrir y cerrar 800.200 o 1.600.000 veces. Esos valores se basan en al menos 800,000 ACTUALIZACIONES más 200 u 800k EXEC (dependiendo de con qué frecuencia se ejecute el procedimiento almacenado del filtro).
Este problema de demasiadas conexiones se mitiga automáticamente mediante la recomendación que describí anteriormente. Al crear una transacción y hacer todas las ACTUALIZACIONES dentro de esa transacción, mantendrá esa conexión abierta y la reutilizará para cada una UPDATE
. Si la conexión se mantiene abierta o no desde la llamada inicial para obtener las 4000 filas según lo especificado FileID
, o se cierra después de esa operación "get" y se abre nuevamente para las ACTUALIZACIONES, es mucho menos impactante ya que ahora estamos hablando de una diferencia de 200 o 400 conexiones totales en todo el proceso.
ACTUALIZACIÓN 3:
Hice algunas pruebas rápidas. Tenga en cuenta que esta es una prueba a pequeña escala, y no exactamente la misma operación (INSERT puro vs ACTUALIZACIÓN EXEC +). Sin embargo, las diferencias en el tiempo relacionadas con la forma en que se manejan las conexiones y las transacciones siguen siendo relevantes, por lo tanto, la información puede extrapolarse para tener un impacto relativamente similar aquí.
Parámetros de prueba:
- SQL Server 2012 Developer Edition (64 bits), SP2
Mesa:
CREATE TABLE dbo.ManyInserts
(
RowID INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
InsertTime DATETIME NOT NULL DEFAULT (GETDATE()),
SomeValue BIGINT NULL
);
Operación:
INSERT INTO dbo.ManyInserts (SomeValue) VALUES ({LoopIndex * 12});
- Inserciones totales por cada prueba: 10,000
- Restablecimientos por cada prueba:
TRUNCATE TABLE dbo.ManyInserts;
(dada la naturaleza de esta prueba, hacer el FREEPROCCACHE, FREESYSTEMCACHE y DROPCLEANBUFFERS no parecía agregar mucho valor).
- Modelo de recuperación: SIMPLE (y quizás 1 GB libre en el archivo de registro)
- Las pruebas que usan transacciones solo usan una única conexión, independientemente de cuántas transacciones.
Resultados:
Test Milliseconds
------- ------------
10k INSERTs across 10k Connections 3968 - 4163
10k INSERTs across 1 Connection 3466 - 3654
10k INSERTs across 1 Transaction 1074 - 1086
10k INSERTs across 10 Transactions 1095 - 1169
Como puede ver, incluso si la conexión ADO a la base de datos ya se está compartiendo en todas las operaciones, se garantiza que agruparlos en lotes mediante una transacción explícita (el objeto ADO debería ser capaz de manejar esto) significativamente (es decir, más del doble de mejora) Reducir el tiempo total del proceso.