Cómo implementar un algoritmo basado en conjuntos / UDF

13

Tengo un algoritmo que necesito ejecutar en cada fila de una tabla con 800K filas y 38 columnas. El algoritmo se implementa en VBA y hace un montón de matemáticas usando valores de algunas columnas para manipular otras columnas.

Actualmente estoy usando Excel (ADO) para consultar SQL y usar VBA con cursores del lado del cliente para aplicar el algoritmo por bucle a través de cada fila. Funciona pero tarda 7 horas en ejecutarse.

El código VBA es lo suficientemente complejo como para que sea mucho trabajo recodificarlo en T-SQL.

He leído sobre la integración CLR y las UDF como posibles rutas. También pensé en poner el código VBA en una tarea de script SSIS para acercarme a la base de datos, pero estoy seguro de que existe una metodología experta para este tipo de problema de rendimiento.

Lo ideal sería poder ejecutar el algoritmo contra tantas filas (¿todas?) Como sea posible de forma paralela basada en conjuntos.

Cualquier ayuda se basa en gran medida en cómo obtener el mejor rendimiento con este tipo de problema.

--Editar

Gracias por los comentarios, estoy usando MS SQL 2014 Enterprise, aquí hay más detalles:

El algoritmo encuentra patrones característicos en datos de series de tiempo. Las funciones dentro del algoritmo realizan suavizado polinómico, ventanas y encuentran regiones de interés basadas en criterios de entrada, devolviendo una docena de valores y algunos resultados booleanos.

Mi pregunta es más sobre la metodología que el algoritmo real: si quiero lograr el cálculo paralelo en muchas filas a la vez, ¿cuáles son mis opciones?

Veo que se recomienda volver a codificar en T-SQL, que es mucho trabajo pero posible, sin embargo, el desarrollador del algoritmo trabaja en VBA y cambia con frecuencia, por lo que necesitaría mantener la sincronización con la versión de T-SQL y volver a validar cada cambio.

¿Es T-SQL la única forma de implementar funciones basadas en conjuntos?

medwar19
fuente
3
SSIS puede ofrecer una paralelización nativa suponiendo que diseñe bien su flujo de datos. Esa es la tarea que estaría buscando, ya que necesita hacer este cálculo fila por fila. Pero dicho esto, a menos que pueda darnos detalles (esquema, cálculos involucrados y lo que estos cálculos esperan lograr) es imposible ayudarlo a optimizar. Dicen que escribir cosas en ensamblado puede ser el código más rápido, pero si, como yo, apestas horriblemente, no va a ser eficiente en absoluto
billinkc
2
Si procesa cada fila de forma independiente, puede dividir 800K filas en Nlotes y ejecutar Ninstancias de su algoritmo en Nprocesadores / computadoras separadas. Por otro lado, ¿cuál es su principal cuello de botella: transferir los datos de SQL Server a Excel o los cálculos reales? Si cambia la función VBA para devolver algún resultado ficticio de inmediato, ¿cuánto tiempo llevará todo el proceso? Si todavía lleva horas, entonces el cuello de botella está en la transferencia de datos. Si lleva segundos, entonces necesita optimizar el código VBA que hace los cálculos.
Vladimir Baranov
Es el filtro que se llama como un procedimiento almacenado: SELECT AVG([AD_Sensor_Data]) OVER (ORDER BY [RowID] ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING) as 'AD_Sensor_Data' FROM [AD_Points] WHERE [FileID] = @FileID ORDER BY [RowID] ASC en Management Studio, esta función que se llama para cada una de las filas tarda 50 ms
medwar19
1
Entonces, la consulta que toma 50 ms y se ejecuta 800000 veces (11 horas) es lo que está tomando tiempo. ¿El @FileID es único para cada fila o hay duplicados para que pueda minimizar la cantidad de veces que necesita ejecutar la consulta? También puede calcular previamente el promedio móvil de todos los fileid en una tabla de etapas de una sola vez (use la partición en FileID) y luego consultar esa tabla sin la necesidad de una función de ventana para cada fila. Parece que la mejor configuración para la tabla de etapas debería ser con un índice agrupado activado (FileID, RowID).
Mikael Eriksson
1
Lo mejor de todo sería si de alguna manera pudieras eliminar la necesidad de tocar la base de datos para cada fila. Eso significa que debe ir a TSQL y probablemente unirse a la consulta avg continua o obtener suficiente información para cada fila, de modo que todo lo que el algoritmo necesita esté justo allí en la fila, tal vez codificado de alguna manera si hay varias filas secundarias involucradas (xml) .
Mikael Eriksson

Respuestas:

8

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:

  • Se deben realizar cálculos algo complejos:
    • Esto debe suceder en cada fila de esta tabla.
    • El algoritmo cambia con frecuencia.
    • El algoritmo ... [usa] valores de algunas columnas para manipular otras columnas
    • El tiempo de procesamiento actual es: 7 horas
  • La mesa:
    • contiene 800,000 filas.
    • Tiene 38 columnas.
  • El back-end de la aplicación:
  • La base de datos es SQL Server 2014, Enterprise Edition.
  • Hay un procedimiento almacenado que se llama para cada fila:

    • Esto toma 50 ms (en promedio, supongo) para ejecutarse.
    • Devuelve aproximadamente 4000 filas.
    • La definición (al menos en parte) es:

      SELECT AVG([AD_Sensor_Data])
                 OVER (ORDER BY [RowID] ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING)
                 as 'AD_Sensor_Data'
      FROM   [AD_Points]
      WHERE  [FileID] = @FileID
      ORDER BY [RowID] ASC

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 @FileIDse 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 UPDATEextractos, que son 800,000 transacciones separadas.

Mi recomendación (basada en la información disponible actualmente):

  1. 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 FileIDlugar de cada uno RowID. Entonces:

    1. leer en todas las 4000 filas de un particular FileIDen una matriz
    2. la matriz debe contener elementos que representen los campos que se manipulan
    3. recorrer la matriz, procesando cada fila como lo haces actualmente
    4. una vez que todas las filas en la matriz (es decir, para este particular FileID se han calculado ):
      1. comenzar una transacción
      2. llame a cada actualización por cada RowID
      3. si no hay errores, confirme la transacción
      4. Si ocurrió un error, retroceda y maneje adecuadamente
  2. 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.

  3. 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.

  4. 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 UPDATEque 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 INSERTs agrupados en una sola transacción. Pero la ganancia proveniente del uso de TVP durante 4000 INSERTs 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?

  5. SI el proceso de filtro solo se usa FileIDen la WHEREclá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?

  6. 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 FileIDy, 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.

Solomon Rutzky
fuente
Hay un buen enfoque de "intermediario" sobre lo que sugiere srutzky, y es usar PowerShell para obtener los datos que necesita de SQL Server, llamar a su script VBA para trabajar los datos y luego llamar a un SP de actualización en SQL Server , pasando las claves y los valores actualizados de nuevo al servidor SQL. De esta manera, combina un enfoque basado en conjuntos con lo que ya tiene.
Steve Mangiameli
@SteveMangiameli Hola Steve y gracias por el comentario. Hubiera respondido antes, pero he estado enfermo. Tengo curiosidad por saber cómo su idea es tan diferente de lo que estoy sugiriendo. Todo indica que Excel todavía es necesario para ejecutar el VBA. ¿O está sugiriendo que PowerShell reemplazaría ADO, y si fuera mucho más rápido en la E / S, valdría la pena incluso si solo reemplazara la E / S?
Solomon Rutzky
1
No te preocupes, me alegro de que te sientas mejor. No sé que sería mejor. No sabemos lo que no sabemos y has hecho un gran análisis, pero aún tienes que hacer algunas suposiciones. La E / S puede ser lo suficientemente significativa como para reemplazarla por sí misma; Simplemente no lo sabemos. Solo quería presentar otro enfoque que pueda ser útil con las cosas que ha sugerido.
Steve Mangiameli
@SteveMangiameli Gracias. Y gracias por aclarar eso. No estaba seguro de su dirección exacta y pensé que era mejor no asumir. Sí, estoy de acuerdo en que tener más opciones es mejor ya que no sabemos qué restricciones hay sobre qué cambios se pueden hacer :).
Solomon Rutzky
Hola srutzky, gracias por los pensamientos detallados! He vuelto a probar en el lado de SQL para optimizar los índices y las consultas y tratar de encontrar los cuellos de botella. He invertido en un servidor adecuado ahora, 36 núcleos, SSD PCIe despojados de 1 TB cuando IO se estaba estancando. Ahora, al llamar al código VB directamente en SSIS, que parece abrir múltiples subprocesos para ejecuciones paralelas.
medwar19
2

En mi humilde opinión y trabajando desde el supuesto de que no es posible volver a codificar el sub VBA en SQL, ¿ha considerado permitir que el script VBA termine de evaluar en el archivo Excel y luego escribir los resultados en el servidor SQL a través de SSIS?

Puede hacer que el sub VBA comience y termine volteando un indicador en un objeto de sistema de archivos o en el servidor (si ya ha configurado la conexión para volver a escribir en el servidor) y luego use una expresión SSIS para verificar este indicador para disablepropiedad de una tarea determinada dentro de su solución SSIS (para que el proceso de importación espere hasta que el sub VBA se complete si le preocupa que sobrepase su programación).

Además, puede hacer que el script de VBA se inicie mediante programación (un poco inestable, pero he usado la workbook_open()propiedad para activar tareas de "disparar y olvidar" de esta naturaleza en el pasado).

Si el tiempo de evaluación de la secuencia de comandos VB comienza a convertirse en un problema, puede ver si su desarrollador de VB está dispuesto y es capaz de transferir su código a una tarea de secuencia de comandos VB dentro de la solución SSIS; en mi experiencia, la aplicación de Excel genera una gran carga cuando trabajando con datos en este volumen.

Peter Vandivier
fuente