Come implementare un algoritmo / UDF basato su set


13

Ho un algoritmo che devo eseguire su ogni riga di una tabella con 800K righe e 38 colonne. L'algoritmo è implementato in VBA e fa un sacco di matematica usando i valori di alcune colonne per manipolare altre colonne.

Attualmente sto usando Excel (ADO) per interrogare SQL e usare VBA con i cursori sul lato client per applicare l'algoritmo in loop attraverso ogni riga. Funziona ma richiede 7 ore per l'esecuzione.

Il codice VBA è abbastanza complesso da richiedere molto lavoro per ricodificarlo in T-SQL.

Ho letto sull'integrazione CLR e UDF come possibili percorsi. Ho anche pensato di inserire il codice VBA in un'attività di script SSIS per avvicinarmi al database, ma sono sicuro che esiste una metodologia esperta per questo tipo di problema di prestazioni.

Idealmente sarei in grado di eseguire l'algoritmo su quante più righe (tutte?) Possibile in un modo basato su set parallelo.

Qualsiasi aiuto è fortemente basato su come ottenere le migliori prestazioni con questo tipo di problema.

--Modificare

Grazie per i commenti, sto usando MS SQL 2014 Enterprise, ecco alcuni dettagli:

L'algoritmo trova schemi caratteristici nei dati delle serie temporali. Le funzioni all'interno dell'algoritmo eseguono il livellamento polinomiale, il windowing e trovano le aree di interesse in base a criteri di input, restituendo una dozzina di valori e alcuni risultati booleani.

La mia domanda riguarda più la metodologia che l'algoritmo reale: se voglio ottenere un calcolo parallelo su più righe contemporaneamente, quali sono le mie opzioni.

Vedo che è raccomandato ricodificare in T-SQL, che è molto impegnativo ma possibile, tuttavia lo sviluppatore dell'algoritmo funziona in VBA e cambia frequentemente, quindi dovrei rimanere sincronizzato con la versione T-SQL e riconvalidare ogni modificare.

T-SQL è l'unico modo per implementare funzioni basate su set?


3
SSIS è in grado di offrire un po 'di parallelismo nativo supponendo che si progetta bene il flusso di dati. Questa è l'attività che dovresti cercare poiché devi eseguire questo calcolo riga per riga. Detto questo, a meno che tu non possa darci specifiche (schema, calcoli coinvolti e ciò che questi calcoli sperano di realizzare) è impossibile aiutarti a ottimizzare. Dicono che scrivere cose nell'assemblaggio può rendere il codice più veloce ma se, come me, fai schifo in modo orribile, non sarà affatto efficiente
billinkc

2
Se si elabora ciascuna riga in modo indipendente, è possibile dividere 800.000 righe in Nbatch ed eseguire Nistanze dell'algoritmo su Nprocessori / computer separati. D'altra parte, qual è il tuo collo di bottiglia principale: trasferire i dati da SQL Server a Excel o calcoli effettivi? Se si modifica la funzione VBA per restituire immediatamente un risultato fittizio, quanto tempo impiegherebbe l'intero processo? Se ci vogliono ancora ore, il collo di bottiglia è in fase di trasferimento dei dati. Se impiega pochi secondi, è necessario ottimizzare il codice VBA che esegue i calcoli.
Vladimir Baranov,

È il filtro che viene chiamato come procedura memorizzata: 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 in Management Studio questa funzione che viene chiamata per ciascuna delle righe richiede 50mS
medwar19

1
Quindi la query che richiede 50 ms ed esegue 800000 volte (11 ore) è ciò che richiede tempo. @FileID è univoco per ogni riga o ci sono duplicati in modo da ridurre al minimo il numero di volte necessario per eseguire la query? È inoltre possibile pre calcolare il rotolamento avg per tutti i fileid su una tabella di gestione temporanea in una volta (utilizzare la partizione su FileID) e quindi eseguire una query su quella tabella senza la necessità di una funzione di windowing per ogni riga. La migliore configurazione per la tabella di gestione temporanea sembra che dovrebbe essere attivata con un indice cluster (FileID, RowID).
Mikael Eriksson,

1
La cosa migliore sarebbe se in qualche modo fosse possibile rimuovere la necessità di toccare il db per ogni riga. Ciò significa che devi andare su TSQL e probabilmente unirti alla query rolling avg o recuperare informazioni sufficienti per ogni riga, quindi tutto l'algoritmo necessario è proprio lì sulla riga, forse codificato in qualche modo se ci sono più righe figlio coinvolte (xml) .
Mikael Eriksson,

Risposte:


8

Per quanto riguarda la metodologia, credo che abbai il b-tree sbagliato ;-).

Quello che sappiamo:

Innanzitutto, consolidiamo e rivediamo ciò che sappiamo della situazione:

  • È necessario eseguire calcoli un po 'complessi:
    • Questo deve accadere su ogni riga di questa tabella.
    • L'algoritmo cambia frequentemente.
    • L'algoritmo ... [usa] i valori di alcune colonne per manipolare altre colonne
    • Il tempo di elaborazione attuale è di 7 ore
  • La tavola:
    • contiene 800.000 righe.
    • ha 38 colonne.
  • Il back-end dell'applicazione:
  • Il database è SQL Server 2014, Enterprise Edition.
  • Esiste una procedura memorizzata che viene chiamata per ogni riga:

    • Questa operazione richiede 50 ms (su avg, presumo) per l'esecuzione.
    • Restituisce circa 4000 righe.
    • La definizione (almeno in parte) è:

      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

Cosa possiamo supporre:

Successivamente, possiamo esaminare tutti questi punti di dati insieme per vedere se siamo in grado di sintetizzare dettagli aggiuntivi che ci aiuteranno a trovare uno o più colli di bottiglia, o puntare verso una soluzione, o almeno escludere alcune possibili soluzioni.

L'attuale direzione del pensiero nei commenti è che il problema principale è il trasferimento di dati tra SQL Server ed Excel. È davvero così? Se la procedura memorizzata viene chiamata per ciascuna delle 800.000 righe e richiede 50 ms per ogni chiamata (ovvero per ogni riga), ciò aggiunge fino a 40.000 secondi (non ms). E questo equivale a 666 minuti (hhmm ;-), o poco più di 11 ore. Eppure si diceva che l'intero processo richiedesse solo 7 ore per essere eseguito. Abbiamo già trascorso 4 ore nel tempo totale e abbiamo persino aggiunto il tempo necessario per eseguire i calcoli o salvare i risultati in SQL Server. Quindi qualcosa non è qui.

Guardando la definizione della Stored Procedure, c'è solo un parametro di input per @FileID; non c'è alcun filtro attivo @RowID. Quindi sospetto che stia accadendo uno dei seguenti due scenari:

  • Questa procedura memorizzata non viene effettivamente chiamata per ogni riga, ma per ciascuna @FileID, che sembra estendersi per circa 4000 righe. Se le 4000 righe dichiarate restituite sono un importo abbastanza coerente, allora ci sono solo 200 di quelle raggruppate nelle 800.000 righe. E 200 esecuzioni che richiedono 50 ms ciascuna equivalgono a soli 10 secondi su quelle 7 ore.
  • Se questa procedura memorizzata viene effettivamente chiamata per ogni riga, la prima volta che una nuova @FileIDviene passata non richiederebbe un po 'più di tempo per estrarre nuove righe nel pool di buffer, ma le successive 3999 esecuzioni in genere tornerebbero più velocemente a causa di essere già cache, vero?

Penso che concentrarsi su questo "filtro" Stored Procedure, o su qualsiasi trasferimento di dati da SQL Server a Excel, sia un'aringa rossa .

Per il momento, penso che gli indicatori più rilevanti delle prestazioni scarse siano:

  • Ci sono 800.000 righe
  • L'operazione funziona su una riga alla volta
  • I dati vengono salvati su SQL Server, quindi "[usa] i valori di alcune colonne per manipolare altre colonne " [la mia fase è ;-)]

Sospetto che:

  • mentre esiste un certo margine di miglioramento per il recupero e i calcoli dei dati, rendere questi migliori non equivarrebbe a una riduzione significativa dei tempi di elaborazione.
  • il principale collo di bottiglia sta emettendo 800.000 UPDATEdichiarazioni separate , ovvero 800.000 transazioni separate.

La mia raccomandazione (basata sulle informazioni attualmente disponibili):

  1. La tua più grande area di miglioramento sarebbe quella di aggiornare più righe contemporaneamente (cioè in una transazione). È necessario aggiornare il processo affinché funzioni in termini di ciascuno FileIDanziché di ciascuno RowID. Così:

    1. leggi in tutte le 4000 righe di un particolare FileIDin un array
    2. l'array deve contenere elementi che rappresentano i campi manipolati
    3. scorrere l'array, elaborando ogni riga come si fa attualmente
    4. una volta FileIDcalcolate tutte le righe dell'array (ovvero per questo particolare ):
      1. avviare una transazione
      2. chiama ogni aggiornamento per ciascuno RowID
      3. se non ci sono errori, eseguire il commit della transazione
      4. se si è verificato un errore, eseguire il rollback e gestirlo in modo appropriato
  2. Se il tuo indice cluster non è già stato definito come (FileID, RowID)allora, dovresti considerarlo (come suggerito da @MikaelEriksson in un commento sulla domanda). Non aiuterà questi AGGIORNAMENTI singleton, ma migliorerebbe almeno leggermente le operazioni di aggregazione, come ad esempio ciò che si sta facendo in quella procedura memorizzata "filtro" poiché sono tutti basati su FileID.

  3. Dovresti considerare di spostare la logica in un linguaggio compilato. Suggerirei di creare un'app .NET WinForms o persino un'app console. Preferisco l'app console in quanto è facile pianificare tramite SQL Agent o le attività pianificate di Windows. Non dovrebbe importare se viene eseguito in VB.NET o C #. VB.NET potrebbe essere più adatto per il tuo sviluppatore, ma ci sarà ancora qualche curva di apprendimento.

    Non vedo alcun motivo a questo punto per passare a SQLCLR. Se l'algoritmo cambia frequentemente, ciò potrebbe diventare fastidioso ridistribuire l'Assemblea per tutto il tempo. Ricostruire un'app console e far sì che .exe venga collocato nella cartella condivisa appropriata sulla rete in modo tale da eseguire lo stesso programma e che sia sempre aggiornato, dovrebbe essere abbastanza facile da fare.

    Non penso che spostare completamente l'elaborazione in T-SQL sarebbe di aiuto se il problema è quello che sospetto e stai facendo un AGGIORNAMENTO alla volta.

  4. Se l'elaborazione viene spostata in .NET, è quindi possibile utilizzare parametri a valori di tabella (TVP) in modo tale da passare l'array in una Stored Procedure che chiamerebbe un UPDATEJOIN alla variabile della tabella TVP ed è quindi una singola transazione . Il TVP dovrebbe essere più veloce di 4000 INSERTs raggruppati in un'unica transazione. Ma il guadagno derivante dall'utilizzo di TVP oltre 4000 INSERTs in 1 transazione probabilmente non sarà significativo quanto il miglioramento visto passando da 800.000 transazioni separate a solo 200 transazioni di 4000 righe ciascuna.

    L'opzione TVP non è disponibile in modo nativo per il lato VBA, ma qualcuno ha escogitato una soluzione alternativa che potrebbe valere la pena testare:

    Come posso migliorare le prestazioni del database passando da VBA a SQL Server 2008 R2?

  5. SE il proc del filtro sta usando solo FileIDnella WHEREclausola e SE quel proc viene realmente chiamato per ogni riga, puoi risparmiare un po 'di tempo di elaborazione memorizzando nella cache i risultati della prima esecuzione e utilizzandoli per il resto delle righe FileID, giusto?

  6. Una volta che il trattamento è fatto per FileID , allora si può iniziare a parlare di elaborazione parallela. Ma potrebbe non essere necessario a quel punto :). Dato che hai a che fare con 3 parti non ideali piuttosto importanti: transazioni Excel, VBA e 800k, qualsiasi discorso su SSIS o parallelogrammi, o chissà cosa, si tratta di ottimizzazione prematura / roba di tipo carrello prima del cavallo . Se riusciamo a ottenere questo processo di 7 ore fino a 10 minuti o meno, continueresti a pensare ad altri modi per renderlo più veloce? C'è un tempo di completamento target che hai in mente? Tenere presente che una volta eseguita l'elaborazione su un ID file di base, se avessi un'app console VB.NET (cioè .EXE da riga di comando), non ci sarebbe nulla che ti impedisca di eseguire alcuni di questi FileID alla volta :), sia tramite il passaggio CmdExec di SQL Agent che le attività pianificate di Windows, eccetera.

E, puoi sempre adottare un approccio "graduale" e apportare alcuni miglioramenti alla volta. Ad esempio iniziare con l'esecuzione degli aggiornamenti per FileIDe quindi l'utilizzo di una transazione per quel gruppo. Quindi, vedi se riesci a far funzionare TVP. Quindi vedi come prendere quel codice e spostarlo su VB.NET (e i TVP funzionano in .NET, quindi eseguiranno il port).


Quello che non sappiamo che potrebbe ancora aiutare:

  • La "stored procedure" filtro viene eseguita per RowID o per FileID ? Abbiamo persino la definizione completa di quella Stored Procedure?
  • Schema completo della tabella. Quanto è grande questa tabella? Quanti campi di lunghezza variabile ci sono? Quanti campi sono NULLable? Se ce ne sono NULLable, quanti contengono NULL?
  • Indici per questa tabella. È partizionato? Viene utilizzata la compressione ROW o PAGE?
  • Quanto è grande questa tabella in termini di MB / GB?
  • Come viene gestita la manutenzione dell'indice per questa tabella? Quanto sono frammentati gli indici? Come sono aggiornate le statistiche?
  • Altri processi scrivono in questa tabella mentre è in corso questo processo di 7 ore? Possibile fonte di contesa.
  • Altri processi vengono letti da questa tabella mentre è in corso questo processo di 7 ore? Possibile fonte di contesa.

AGGIORNAMENTO 1:

** Sembra esserci un po 'di confusione su cosa VBA (Visual Basic, Applications Edition) e cosa si può fare con esso, quindi questo è solo per assicurarsi che siamo tutti sulla stessa pagina web:


AGGIORNAMENTO 2:

Un altro punto da considerare: come vengono gestite le connessioni? Il codice VBA apre e chiude la connessione per ogni operazione o apre la connessione all'inizio del processo e la chiude alla fine del processo (ovvero 7 ore dopo)? Anche con il pool di connessioni (che, per impostazione predefinita, dovrebbe essere abilitato per ADO), dovrebbe esserci comunque un notevole impatto tra l'apertura e la chiusura una volta anziché l'apertura e la chiusura di 800.200 o 1.600.000 volte. Tali valori si basano su almeno 800.000 AGGIORNAMENTI più 200 o 800k EXEC (a seconda della frequenza con cui viene effettivamente eseguita la stored procedure di filtro).

Questo problema di troppe connessioni è mitigato automaticamente dalla raccomandazione che ho delineato sopra. Creando una transazione ed eseguendo tutti gli AGGIORNAMENTI all'interno di quella transazione, manterrai aperta quella connessione e la riutilizzerai per ciascuna UPDATE. Il fatto che la connessione sia mantenuta aperta dalla chiamata iniziale per ottenere le 4000 righe per l'operazione specificata FileID, o chiusa dopo tale operazione "get" e riaperta per gli AGGIORNAMENTI, ha un impatto molto minore poiché stiamo parlando di una differenza di 200 o 400 connessioni totali attraverso l'intero processo.

AGGIORNAMENTO 3:

Ho fatto alcuni test rapidi. Si prega di tenere presente che si tratta di un test su scala piuttosto ridotta e non della stessa identica operazione (INSERT puro vs EXEC + AGGIORNAMENTO). Tuttavia, le differenze nei tempi relative al modo in cui vengono gestite le connessioni e le transazioni sono ancora rilevanti, quindi le informazioni possono essere estrapolate per avere un impatto relativamente simile qui.

Parametri di prova:

  • SQL Server 2012 Developer Edition (64 bit), SP2
  • Tavolo:

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

    INSERT INTO dbo.ManyInserts (SomeValue) VALUES ({LoopIndex * 12});
  • Inserti totali per ciascun test: 10.000
  • Ripristini per ogni test: TRUNCATE TABLE dbo.ManyInserts;(data la natura di questo test, eseguire FREEPROCCACHE, FREESYSTEMCACHE e DROPCLEANBUFFERS non sembra aggiungere molto valore.)
  • Modello di recupero: SEMPLICE (e forse 1 GB gratuito nel file di registro)
  • I test che utilizzano Transazioni utilizzano solo una singola connessione indipendentemente da quante Transazioni.

risultati:

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

Come puoi vedere, anche se la connessione ADO al DB è già condivisa tra tutte le operazioni, il raggruppamento in batch utilizzando una transazione esplicita (l'oggetto ADO dovrebbe essere in grado di gestirlo) è garantito in modo significativo (cioè con un miglioramento di 2x) ridurre il tempo complessivo del processo.


Esiste un buon approccio "middle man" a ciò che srutzky sta suggerendo, ovvero utilizzare PowerShell per ottenere i dati necessari da SQL Server, chiamare lo script VBA per far funzionare i dati e quindi chiamare un aggiornamento SP in SQL Server , passando le chiavi e i valori aggiornati al server SQL. In questo modo si combina un approccio basato su set con ciò che già si possiede.
Steve Mangiameli,

@SteveMangiameli Ciao Steve e grazie per il commento. Avrei risposto prima ma mi sarei ammalato. Sono curioso di sapere come la tua idea sia molto diversa da quella che sto suggerendo. Tutte le indicazioni sono che Excel è ancora necessario per eseguire VBA. O stai suggerendo che PowerShell sostituirà ADO, e se molto più veloce sull'I / O, varrebbe la pena anche solo per sostituire solo l'I / O?
Solomon Rutzky,

1
Nessun problema, contento che ti senti meglio. Non so che sarebbe meglio. Non sappiamo cosa non sappiamo e hai fatto delle ottime analisi ma dobbiamo ancora fare delle ipotesi. L'I / O può essere abbastanza significativo da sostituire da solo; semplicemente non lo sappiamo. Volevo solo presentare un altro approccio che potrebbe essere utile con le cose che hai suggerito.
Steve Mangiameli,

@SteveMangiameli Grazie. E grazie per averlo chiarito. Non ero sicuro della tua direzione esatta e ho pensato che fosse meglio non presumere. Sì, sono d'accordo sul fatto che avere più opzioni sia meglio poiché non sappiamo quali vincoli ci siano su quali modifiche possono essere apportate :).
Solomon Rutzky,

Ehi srutzky, grazie per i pensieri dettagliati! Sono tornato a testare sul lato SQL per ottenere indici e query ottimizzati e cercare di trovare i colli di bottiglia. Ho investito in un server adeguato ora, 36 core, SSD PCIe spogliati da 1 TB mentre IO stava impantanando. Ora su chiamando il codice VB direttamente in SSIS che sembra aprire più thread per esecuzioni parallele.
medwar19

2

IMHO e lavorando sul presupposto che non è possibile ricodificare il sub VBA in SQL, hai preso in considerazione la possibilità di consentire allo script VBA di completare la valutazione nel file Excel e quindi riscrivere i risultati sul server SQL tramite SSIS?

È possibile che il sub VBA inizi e termini con il capovolgere un indicatore in un oggetto filesystem o nel server (se hai già configurato la connessione per riscrivere nel server) e quindi utilizzare un'espressione SSIS per controllare questo indicatore per il disableproprietà di una determinata attività all'interno della soluzione SSIS (in modo che il processo di importazione sia in attesa fino al completamento del sottotitolo VBA se sei preoccupato che prevalga sulla sua pianificazione).

Inoltre, è possibile avviare lo script VBA in modo programmatico (un po 'traballante, ma in passato ho usato la workbook_open()proprietà per attivare attività "ignora e dimentica" di questa natura).

Se il tempo di valutazione dello script VB inizia a diventare un problema, potresti vedere se lo sviluppatore VB è disposto e in grado di eseguire il porting del suo codice in un'attività di script VB all'interno della soluzione SSIS - nella mia esperienza l'applicazione Excel comporta un sacco di sovraccarico quando lavorare con i dati a questo volume.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.