Comment implémenter un algorithme basé sur un ensemble / UDF

13

J'ai un algorithme que je dois exécuter contre chaque ligne d'une table avec 800K lignes et 38 colonnes. L'algorithme est implémenté dans VBA et fait un tas de mathématiques en utilisant les valeurs de certaines colonnes pour manipuler d'autres colonnes.

J'utilise actuellement Excel (ADO) pour interroger SQL et utiliser VBA avec des curseurs côté client pour appliquer l'algorithme en boucle sur chaque ligne. Il fonctionne mais prend 7 heures pour fonctionner.

Le code VBA est suffisamment complexe pour qu'il soit beaucoup de travail de le recoder en T-SQL.

J'ai lu sur l'intégration du CLR et les UDF comme itinéraires possibles. J'ai également pensé à mettre le code VBA dans une tâche de script SSIS pour se rapprocher de la base de données, mais je suis sûr qu'il existe une méthodologie experte pour ce type de problème de performances.

Idéalement, je serais en mesure d'exécuter l'algorithme sur autant de lignes (toutes?) Que possible d'une manière basée sur un ensemble parallèle.

Toute aide dépendait grandement de la manière d'obtenir les meilleures performances avec ce type de problème.

--Éditer

Merci pour les commentaires, j'utilise MS SQL 2014 Enterprise, voici quelques détails supplémentaires:

L'algorithme trouve des modèles caractéristiques dans les données de séries chronologiques. Les fonctions de l'algorithme effectuent un lissage polynomial, un fenêtrage et trouvent des régions d'intérêt en fonction de critères d'entrée, renvoyant une douzaine de valeurs et quelques résultats booléens.

Ma question concerne plus la méthodologie que l'algorithme réel: si je veux réaliser un calcul parallèle sur plusieurs lignes à la fois, quelles sont mes options.

Je vois que le recodage en T-SQL est recommandé, ce qui est beaucoup de travail mais possible, mais le développeur de l'algorithme fonctionne en VBA et il change fréquemment, donc je devrais rester synchronisé avec la version T-SQL et revalider chaque changement.

T-SQL est-il le seul moyen d'implémenter des fonctions basées sur des ensembles?

medwar19
la source
3
SSIS peut offrir une parallélisation native en supposant que vous conceviez bien votre flux de données. C'est la tâche que vous recherchez, car vous devez effectuer ce calcul ligne par ligne. Mais cela dit, à moins que vous ne puissiez nous donner des détails (schéma, calculs impliqués et ce que ces calculs espèrent accomplir), il est impossible de vous aider à optimiser. Ils disent que l'écriture en assembleur peut rendre le code le plus rapide, mais si, comme moi, vous le croyez horriblement, cela ne sera pas efficace du tout
billinkc
2
Si vous traitez chaque ligne indépendamment, vous pouvez diviser 800 Ko de lignes en Nlots et exécuter des Ninstances de votre algorithme sur Ndes processeurs / ordinateurs distincts. D'un autre côté, quel est votre principal goulot d'étranglement - le transfert des données de SQL Server vers Excel ou des calculs réels? Si vous modifiez la fonction VBA pour renvoyer immédiatement un résultat fictif, combien de temps le processus prendra-t-il? Si cela prend encore des heures, le goulot d'étranglement est dans le transfert de données. Si cela prend quelques secondes, vous devez optimiser le code VBA qui effectue les calculs.
Vladimir Baranov du
C'est le filtre qui est appelé en tant que procédure stockée: 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 dans Management Studio, cette fonction qui est appelée pour chacune des lignes prend 50 ms
medwar19
1
Ainsi, la requête qui prend 50 ms et s'exécute 800000 fois (11 heures) est ce qui prend du temps. Le @FileID est-il unique pour chaque ligne ou existe-t-il des doublons afin que vous puissiez minimiser le nombre de fois que vous devez exécuter la requête? Vous pouvez également pré-calculer la moyenne mobile de tous les ID de fichier dans une table intermédiaire en une seule fois (utiliser la partition sur FileID), puis interroger cette table sans avoir besoin d'une fonction de fenêtrage pour chaque ligne. La meilleure configuration pour la table intermédiaire semble être celle avec un index clusterisé (FileID, RowID).
Mikael Eriksson
1
Le mieux serait que vous puissiez en quelque sorte supprimer la nécessité de toucher la base de données pour chaque ligne. Cela signifie que vous devez soit aller TSQL et probablement vous joindre à la requête AVG continue ou récupérer suffisamment d'informations pour chaque ligne, de sorte que tout ce dont l'algorithme a besoin se trouve juste sur la ligne, peut-être codé d'une manière ou d'une autre si plusieurs lignes enfants sont impliquées (xml) .
Mikael Eriksson du

Réponses:

8

En ce qui concerne la méthodologie, je crois que vous aboyez le mauvais arbre b ;-).

Ce que nous savons:

Tout d'abord, consolidons et examinons ce que nous savons de la situation:

  • Des calculs quelque peu complexes doivent être effectués:
    • Cela doit se produire sur chaque ligne de ce tableau.
    • L'algorithme change fréquemment.
    • L'algorithme ... [utilise] les valeurs de certaines colonnes pour manipuler d'autres colonnes
    • Le temps de traitement actuel est de: 7 heures
  • La table:
    • contient 800 000 lignes.
    • a 38 colonnes.
  • Le back-end de l'application:
  • La base de données est SQL Server 2014, Enterprise Edition.
  • Il existe une procédure stockée qui est appelée pour chaque ligne:

    • Cela prend 50 ms (en moyenne, je suppose) pour fonctionner.
    • Il renvoie environ 4000 lignes.
    • La définition (au moins en partie) est:

      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

Ce que nous pouvons supposer:

Ensuite, nous pouvons examiner tous ces points de données ensemble pour voir si nous pouvons synthétiser des détails supplémentaires qui nous aideront à trouver un ou plusieurs goulots d'étranglement, et soit pointer vers une solution, soit au moins exclure certaines solutions possibles.

L'orientation actuelle de la réflexion dans les commentaires est que le principal problème est le transfert de données entre SQL Server et Excel. Est-ce vraiment le cas? Si la procédure stockée est appelée pour chacune des 800 000 lignes et prend 50 ms par chaque appel (c'est-à-dire par chaque ligne), cela ajoute jusqu'à 40 000 secondes (et non ms). Et cela équivaut à 666 minutes (hhmm ;-), soit un peu plus de 11 heures. Pourtant, l'ensemble du processus ne durerait que 7 heures. Nous avons déjà 4 heures sur le temps total, et nous avons même ajouté du temps pour faire les calculs ou sauvegarder les résultats dans SQL Server. Donc, quelque chose ne va pas ici.

En regardant la définition de la procédure stockée, il n'y a qu'un paramètre d'entrée pour @FileID; il n'y a pas de filtre @RowID. Je soupçonne donc que l'un des deux scénarios suivants se produit:

  • Cette procédure stockée n'est pas réellement appelée pour chaque ligne, mais plutôt pour chaque ligne @FileID, qui semble s'étendre sur environ 4 000 lignes. Si les 4000 lignes retournées sont un montant assez cohérent, alors il n'y a que 200 de ces groupes dans les 800 000 lignes. Et 200 exécutions de 50 ms chacune ne représentent que 10 secondes sur ces 7 heures.
  • Si cette procédure stockée est effectivement appelée pour chaque ligne, la première fois qu'une nouvelle @FileIDest passée ne prendrait pas un peu plus de temps pour tirer de nouvelles lignes dans le pool de tampons, mais les 3999 prochaines exécutions reviendraient généralement plus rapidement car elles sont déjà mis en cache, non?

Je pense que se concentrer sur cette procédure stockée «filtre», ou tout transfert de données de SQL Server vers Excel, est un redingue .

Pour le moment, je pense que les indicateurs les plus pertinents de performances médiocres sont:

  • Il y a 800 000 lignes
  • L'opération fonctionne sur une ligne à la fois
  • Les données sont enregistrées sur SQL Server, d'où "[utilise] les valeurs de certaines colonnes pour manipuler d'autres colonnes " [mes phases sont ;-)]

Je soupçonne que:

  • bien qu'il y ait une marge d'amélioration sur la récupération et les calculs des données, les améliorer ne représenterait pas une réduction significative du temps de traitement.
  • le principal goulot d'étranglement est l'émission de 800 000 UPDATErelevés distincts , soit 800 000 transactions distinctes.

Ma recommandation (basée sur les informations actuellement disponibles):

  1. Votre principal domaine d'amélioration serait de mettre à jour plusieurs lignes à la fois (c'est-à-dire en une seule transaction). Vous devez mettre à jour votre processus pour travailler en termes de chacun FileIDau lieu de chacun RowID. Donc:

    1. lire dans les 4000 lignes d'un particulier FileIDdans un tableau
    2. le tableau doit contenir des éléments représentant les champs manipulés
    3. parcourir le tableau, en traitant chaque ligne comme vous le faites actuellement
    4. une fois que toutes les lignes du tableau (c'est-à-dire pour ce point particulier FileID) ont été calculées:
      1. démarrer une transaction
      2. appeler chaque mise à jour pour chaque RowID
      3. si aucune erreur, valider la transaction
      4. en cas d'erreur, annulez et gérez correctement
  2. Si votre index cluster n'est pas déjà défini comme (FileID, RowID)alors vous devriez considérer cela (comme l'a suggéré @MikaelEriksson dans un commentaire sur la question). Cela n'aidera pas ces mises à jour singleton, mais cela améliorerait au moins légèrement les opérations d'agrégation, telles que ce que vous faites dans cette procédure stockée de "filtrage" car elles sont toutes basées sur FileID.

  3. Vous devriez envisager de déplacer la logique vers un langage compilé. Je suggère de créer une application .NET WinForms ou même une application console. Je préfère l'application console car elle est facile à planifier via l'agent SQL ou les tâches planifiées de Windows. Peu importe que cela soit fait en VB.NET ou en C #. VB.NET pourrait être un ajustement plus naturel pour votre développeur, mais il y aura toujours une courbe d'apprentissage.

    Je ne vois aucune raison pour l'instant de passer au SQLCLR. Si l'algorithme change fréquemment, cela deviendrait ennuyeux de devoir redéployer l'assembly tout le temps. Reconstruire une application console et placer le fichier .exe dans le dossier partagé approprié sur le réseau de sorte que vous exécutez simplement le même programme et qu'il se trouve qu'il soit toujours à jour, devrait être assez facile à faire.

    Je ne pense pas que le déplacement complet du traitement dans T-SQL aiderait si le problème est ce que je soupçonne et que vous ne faites qu'une seule MISE À JOUR à la fois.

  4. Si le traitement est déplacé dans .NET, vous pouvez ensuite utiliser des paramètres table (TVP) de sorte que vous passiez le tableau dans une procédure stockée qui appellerait un UPDATEqui se joint à la variable de table TVP et est donc une transaction unique . Le TVP devrait être plus rapide que de faire 4000 INSERTs regroupés en une seule transaction. Mais le gain provenant de l'utilisation de TVP de plus de 4000 INSERTs en 1 transaction ne sera probablement pas aussi important que l'amélioration observée lors du passage de 800000 transactions distinctes à seulement 200 transactions de 4000 lignes chacune.

    L'option TVP n'est pas nativement disponible pour le côté VBA, mais quelqu'un a trouvé une solution qui pourrait valoir la peine d'être testée:

    Comment puis-je améliorer les performances de la base de données lors du passage de VBA à SQL Server 2008 R2?

  5. SI le filtre proc n'utilise que FileIDdans la WHEREclause, et si ce proc est réellement appelé pour chaque ligne, vous pouvez gagner du temps de traitement en mettant en cache les résultats de la première exécution et en les utilisant pour le reste des lignes par cela FileID, droite?

  6. Une fois que vous obtenez le traitement fait par FileID , alors nous pouvons commencer à parler de traitement parallèle. Mais cela pourrait ne pas être nécessaire à ce stade :). Étant donné que vous avez affaire à 3 parties non idéales assez importantes: les transactions Excel, VBA et 800k, toute discussion sur SSIS, ou parallélogrammes, ou qui sait quoi, est une optimisation prématurée / des trucs de type chariot avant le cheval . Si nous pouvons réduire ce processus de 7 heures à 10 minutes ou moins, pensez-vous toujours à des moyens supplémentaires pour l'accélérer? Y a-t-il un délai d'achèvement que vous avez en tête? Gardez à l'esprit qu'une fois le traitement effectué sur un ID de fichier Si vous aviez une application console VB.NET (par exemple, la ligne de commande .EXE), rien ne vous empêcherait d'exécuter quelques-uns de ces FileID à la fois :), que ce soit via l'étape SQL Agent CmdExec ou les tâches planifiées Windows, etc.

ET, vous pouvez toujours adopter une approche "progressive" et apporter quelques améliorations à la fois. Telles que commencer par faire les mises à jour par FileIDet donc utiliser une transaction pour ce groupe. Ensuite, voyez si vous pouvez faire fonctionner le TVP. Ensuite, voyez comment prendre ce code et le déplacer vers VB.NET (et les TVP fonctionnent dans .NET pour qu'il soit bien porté).


Ce que nous ne savons pas qui pourrait encore aider:

  • Le « filtre » run procédure stockée par RowID ou par FileID ? Avons-nous même la définition complète de cette procédure stockée?
  • Schéma complet de la table. Quelle est la largeur de cette table? Combien de champs de longueur variable existe-t-il? Combien de champs sont NULLables? Si certains sont NULLable, combien contiennent des NULL?
  • Index de ce tableau. Est-il partitionné? La compression ROW ou PAGE est-elle utilisée?
  • Quelle est la taille de ce tableau en termes de Mo / Go?
  • Comment la maintenance d'index est-elle gérée pour cette table? Dans quelle mesure les index sont-ils fragmentés? Quelle est la mise à jour des statistiques à ce jour?
  • D'autres processus écrivent-ils dans ce tableau pendant ce processus de 7 heures? Source possible de conflit.
  • D'autres processus sont-ils lus dans ce tableau pendant ce processus de 7 heures? Source possible de conflit.

MISE À JOUR 1:

** Il semble y avoir une certaine confusion sur ce que VBA (Visual Basic pour Applications) et ce qui peut être fait avec, donc c'est juste pour nous assurer que nous sommes tous sur la même page Web:


MISE À JOUR 2:

Un autre point à considérer: comment les connexions sont-elles gérées? Le code VBA ouvre-t-il et ferme-t-il la connexion pour chaque opération, ou ouvre-t-il la connexion au début du processus et la ferme-t-il à la fin du processus (c'est-à-dire 7 heures plus tard)? Même avec le regroupement de connexions (qui, par défaut, devrait être activé pour ADO), il devrait toujours y avoir un certain impact entre l'ouverture et la fermeture une fois par opposition à l'ouverture et la fermeture 800 800 ou 1 600 000 fois. Ces valeurs sont basées sur au moins 800 000 MISES À JOUR plus 200 ou 800 000 EXEC (selon la fréquence à laquelle la procédure stockée de filtrage est réellement exécutée).

Ce problème de trop de connexions est automatiquement atténué par la recommandation que j'ai décrite ci-dessus. En créant une transaction et en effectuant toutes les MISES À JOUR au sein de cette transaction, vous allez garder cette connexion ouverte et la réutiliser pour chacune UPDATE. Que la connexion soit maintenue ouverte depuis l'appel initial pour obtenir les 4000 lignes par spécifiée FileID, ou fermée après cette opération "get" et rouverte pour les MISES À JOUR, a beaucoup moins d'impact car nous parlons maintenant d'une différence de 200 ou 400 connexions au total sur l'ensemble du processus.

MISE À JOUR 3:

J'ai fait quelques tests rapides. Veuillez garder à l'esprit qu'il s'agit d'un test à petite échelle, et pas exactement la même opération (INSERT pur vs EXEC + UPDATE). Cependant, les différences de calendrier liées à la façon dont les connexions et les transactions sont traitées sont toujours pertinentes, par conséquent, les informations peuvent être extrapolées pour avoir un impact relativement similaire ici.

Paramètres de test:

  • SQL Server 2012 Developer Edition (64 bits), SP2
  • Table:

     CREATE TABLE dbo.ManyInserts
     (
        RowID INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
        InsertTime DATETIME NOT NULL DEFAULT (GETDATE()),
        SomeValue BIGINT NULL
     );
  • Opération:

    INSERT INTO dbo.ManyInserts (SomeValue) VALUES ({LoopIndex * 12});
  • Inserts totaux pour chaque test: 10 000
  • Réinitialise pour chaque test: TRUNCATE TABLE dbo.ManyInserts;(étant donné la nature de ce test, faire les FREEPROCCACHE, FREESYSTEMCACHE et DROPCLEANBUFFERS ne semblait pas ajouter beaucoup de valeur.)
  • Modèle de récupération: SIMPLE (et peut-être 1 Go gratuit dans le fichier journal)
  • Les tests qui utilisent des transactions n'utilisent qu'une seule connexion, quel que soit le nombre de transactions.

Résultats:

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

Comme vous pouvez le voir, même si la connexion ADO à la base de données est déjà partagée entre toutes les opérations, leur regroupement en lots à l'aide d'une transaction explicite (l'objet ADO devrait être capable de gérer cela) est garanti de manière significative (c'est-à-dire plus de 2x amélioration) réduire le temps de traitement global.

Solomon Rutzky
la source
Il existe une belle approche «intermédiaire» de ce que propose srutzky, à savoir utiliser PowerShell pour obtenir les données dont vous avez besoin de SQL Server, appeler votre script VBA pour travailler les données, puis appeler un SP de mise à jour dans SQL Server , en transmettant les clés et les valeurs mises à jour au serveur SQL. De cette façon, vous combinez une approche basée sur un ensemble avec ce que vous avez déjà.
Steve Mangiameli
@SteveMangiameli Salut Steve et merci pour le commentaire. J'aurais répondu plus tôt mais j'étais malade. Je suis curieux de voir à quel point votre idée est si différente de ce que je suggère. Tout indique que Excel est toujours requis pour exécuter le VBA. Ou suggérez-vous que PowerShell remplacerait ADO, et s'il est beaucoup plus rapide au niveau des E / S, en vaudrait-il la peine, ne serait-ce que pour remplacer uniquement les E / S?
Solomon Rutzky
1
Pas de soucis, content de vous sentir mieux. Je ne sais pas que ce serait mieux. Nous ne savons pas ce que nous ne savons pas et vous avez fait une excellente analyse, mais vous devez encore faire des hypothèses. Les E / S peuvent être suffisamment importantes pour être remplacées seules; nous ne savons tout simplement pas. Je voulais simplement présenter une autre approche qui pourrait être utile avec les choses que vous avez suggérées.
Steve Mangiameli
@SteveMangiameli Merci. Et merci d'avoir clarifié cela. Je n'étais pas sûr de votre direction exacte et j'ai pensé qu'il valait mieux ne pas assumer. Oui, je suis d'accord qu'il est préférable d'avoir plus d'options car nous ne savons pas quelles contraintes il y a sur les changements qui peuvent être apportés :).
Solomon Rutzky
Hé srutzky, merci pour les pensées détaillées! J'ai testé de nouveau du côté SQL pour optimiser les index et les requêtes et essayer de trouver les goulots d'étranglement. J'ai investi dans un serveur approprié maintenant, 36cores, SSD PCIe dépouillés de 1 To alors qu'IO s'embourbait. Passons maintenant à l'appel du code VB directement dans SSIS qui semble ouvrir plusieurs threads pour des exécutions parallèles.
medwar19
2

À mon humble avis et en partant de l'hypothèse qu'il n'est pas possible de recoder le sous-VBA en SQL, avez-vous envisagé de permettre au script VBA de terminer l'évaluation dans le fichier Excel, puis de réécrire les résultats sur le serveur SQL via SSIS?

Vous pouvez faire démarrer et terminer le sous-VBA en retournant un indicateur dans un objet de système de fichiers ou sur le serveur (si vous avez déjà configuré la connexion pour réécrire sur le serveur), puis utilisez une expression SSIS pour vérifier cet indicateur pour le disablepropriété d'une tâche donnée dans votre solution SSIS (de sorte que le processus d'importation attend la fin du sous-VBA si vous craignez qu'il ne dépasse son planning).

De plus, vous pouvez faire démarrer le script VBA par programmation (un peu bancal, mais j'ai utilisé la workbook_open()propriété pour déclencher des tâches de type "tirer et oublier" de cette nature dans le passé).

Si le temps d'évaluation du script VB commence à devenir un problème, vous pouvez voir si votre développeur VB est disposé et capable de porter son code dans une tâche de script VB dans la solution SSIS - d'après mon expérience, l'application Excel tire beaucoup de frais lorsque travailler avec des données à ce volume.

Peter Vandivier
la source