2012-08-30 18 views
7

Sto lavorando a un'applicazione esistente. Questa applicazione legge i dati da un file enorme e quindi, dopo aver eseguito alcuni calcoli, memorizza i dati in un'altra tabella.Ciclo foreach molto lento

Ma il ciclo che esegue questa operazione (vedere di seguito) richiede molto tempo. Poiché il file contiene talvolta 1.000 di record, l'intero processo richiede giorni.

Posso sostituire questo ciclo foreach con qualcos'altro? Ho provato a utilizzare Parallel.ForEach e ha aiutato. Sono nuovo a questo, quindi apprezzerò il tuo aiuto.

foreach (record someredord Somereport.r) 
{ 
    try 
    { 
     using (var command = new SqlCommand("[procname]", sqlConn)) 
     { 
      command.CommandTimeout = 0; 
      command.CommandType = CommandType.StoredProcedure; 
      command.Parameters.Add(…); 

      IAsyncResult result = command.BeginExecuteReader(); 
      while (!result.IsCompleted) 
      { 
       System.Threading.Thread.Sleep(10); 
      } 
      command.EndExecuteReader(result); 
     } 
    } 
    catch (Exception e) 
    { 
     … 
    } 
} 

Dopo aver esaminato le risposte, ho rimosso Async e utilizzato modificato il codice come di seguito. Ma questo non ha migliorato le prestazioni.

using (command = new SqlCommand("[sp]", sqlConn)) 
{ 
    command.CommandTimeout = 0; 
    command.CommandType = CommandType.StoredProcedure; 
    foreach (record someRecord in someReport.) 
    { 
     command.Parameters.Clear(); 
     command.Parameters.Add(....) 
     command.Prepare();        

     using (dr = command.ExecuteReader()) 
     { 
      while (dr.Read()) 
      { 
       if() 
       { 

       } 
       else if() 
       { 

       } 
      } 
     }        
    }       
} 
+7

Due pensieri: in primo luogo, stai sbagliando in modo asincrono e, di conseguenza, stai dormendo per MOLTI articoli nel ciclo. Secondo, puoi riutilizzare l'oggetto SqlCommand per l'intero ciclo invece di crearne/distruggerne ogni volta? – n8wrl

+3

Se ci dici di più su cosa stai cercando di realizzare, potremmo potenzialmente mostrarti una soluzione in SQL che esegue diversi ordini di grandezza più velocemente, ed evita del tutto l'intera azienda asincrona/parallela. –

+1

@ user1110790: il codice che hai postato era pieno di errori (e ne ha ancora almeno uno), quindi l'ho ripulito un po '. Posso umilmente suggerire che quando pubblichi su SO, assicurati che il tuo codice sia OK; altrimenti potresti semplicemente ricevere molti commenti concentrati su questo, anziché sul problema reale. – stakx

risposta

7

Invece di looping la connessione SQL tante volte, mai preso in considerazione l'estrazione l'intero insieme di dati fuori dal server sql ed elaborare i dati tramite il set di dati?

Edit: Ha deciso di spiegare ulteriormente ciò che volevo dire .. È possibile effettuare le seguenti operazioni, pseudo codice da seguire

  1. Utilizzare un select * e ottenere tutte le informazioni dal database e memorizzarli in una lista della classe o dizionario.
  2. Esegui foreach (registra un po 'di registrazione in qualche rapporto) e fai come di consueto la condizione corrispondente.
+5

+1. Ma probabilmente sarebbe meglio caricare i dati in una raccolta fortemente tipizzata e quindi usare Linq su quello, invece di usare un DataSet. –

+0

Ho provato a utilizzare un datase, ma per qualche motivo ha rallentato ulteriormente il processo. Stiamo anche registrando ogni singola operazione. Pensi che io possa fare il logging su un thread separato per migliorare le prestazioni? – user1110790

+0

@ user1110790 - Dalla mia esperienza, i set di dati sono generalmente lenti con cui lavorare. Ecco perché ho suggerito una raccolta fortemente tipizzata. Lavorare semplicemente con una collezione IEnumerable in memoria sarà molto veloce. E se stai facendo molte ricerche chiave, potrebbe essere reso ancora più veloce con un 'dizionario'. –

6

Passaggio 1: Abbandonare la prova asincrona. Non è implementato correttamente e tu stai bloccando comunque. Quindi, basta eseguire la procedura e vedere se questo aiuta.

Passaggio 2: Spostare SqlCommand all'esterno del loop e riutilizzarlo per ciascuna iterazione. in questo modo non incorrere nel costo di creazione e distruzione per ogni elemento del tuo ciclo.

Avviso: Assicurarsi di ripristinare/cancellare/rimuovere i parametri non necessari dall'iterazione precedente. Abbiamo fatto qualcosa di simile con parametri opzionali e abbiamo avuto "bleed-thru" dall'iterazione precedente perché non abbiamo ripulito i parametri di cui non avevamo bisogno!

+1

+1 Il secondo passaggio è molto importante, molte persone lo dimenticano. –

+0

Il "Passaggio 2: spostare SqlCommand all'esterno del ciclo ..." è il miglioramento principale !!! – Lester

3

Il tuo più grande problema è che si sta loop su questo:

IAsyncResult result = command.BeginExecuteReader(); 

while (!result.IsCompleted) 
{ 
    System.Threading.Thread.Sleep(10); 
} 

command.EndExecuteReader(result); 

L'intera idea del modello asincrono è che il thread chiamante (quello che fa questo anello) deve essere filatura backup di tutti i asincrono attività che utilizzano il metodo Begin prima di iniziare a lavorare con i risultati con il metodo End. Se stai utilizzando Thread.Sleep() all'interno del tuo thread principale di chiamata per attendere il completamento di un'operazione asincrona (come sei qui), stai sbagliando e quello che succede è che ogni comando, uno alla volta , viene chiamato e quindi aspettato prima che inizi il prossimo.

Invece, provare qualcosa di simile:

public void BeginExecutingCommands(Report someReport) 
{ 
    foreach (record someRecord in someReport.r) 
    { 
     var command = new SqlCommand("[procname]", sqlConn); 

     command.CommandTimeout = 0; 
     command.CommandType = CommandType.StoredProcedure; 
     command.Parameters.Add(…); 

     command.BeginExecuteReader(ReaderExecuted, 
      new object[] { command, someReport, someRecord });     
    } 
} 

void ReaderExecuted(IAsyncResult result) 
{ 
    var state = (object[])result.AsyncState; 
    var command = state[0] as SqlCommand; 
    var someReport = state[1] as Report; 
    var someRecord = state[2] as Record; 

    try 
    { 
     using (SqlDataReader reader = command.EndExecuteReader(result)) 
     { 
      // work with reader, command, someReport and someRecord to do what you need. 
     } 
    } 
    catch (Exception ex) 
    { 
     // handle exceptions that occurred during the async operation here 
    } 
} 
+0

Ho rimosso il modificatore di accessibilità 'public' dal metodo di callback (' ReaderExecuted'). Quelli non dovrebbero essere pubblici, dal momento che non sono operazioni complete, ma solo il "resto" logico di un altro metodo. – stakx

+0

+1 per dimostrare l'uso corretto del pattern asincrono 'Begin ...'/'End ...'. Tuttavia, non sono sicuro al 100% che questo risolva il problema principale. Non sono nemmeno sicuro di quanto bene il pool di thread e il DB gestiranno probabilmente 1000 di richieste quasi simultanee ...? – stakx

+1

Il pool di thread pianificherà un certo numero di essi con la stessa velocità con cui entrano, fino a una soglia "minima". Quindi, produrrà 4 thread al secondo mentre il numero di thread è superiore a tale soglia, fino a quando non raggiunge una soglia massima, a quel punto conterrà qualsiasi nuova richiesta. Le soglie minima e massima sono configurabili. – KeithS

0

Sembra esecuzione tuoi SQL mette comando di blocco su alcune risorse richieste e questo è il motivo forzata di utilizzare Async metodi (la mia ipotesi).

Se il database non è in uso, provare ad accedervi in ​​modo esclusivo. Anche in questo caso, alcune transazioni interne a causa della complessità del modello di dati considerano la consulenza al progettista di database.

+0

Grazie. Il database è infatti in uso, quindi non posso fare come suggerito. – user1110790

1

In SQL all'altra estremità di una scrittura è un (uno) disco. Raramente puoi scrivere più velocemente in parallelo. Di fatto, in parallelo, spesso rallenta a causa della frammentazione dell'indice. Se è possibile ordinare i dati per chiave primaria (in cluster) prima del caricamento. In un grande carico anche disabilitare altre chiavi, caricare le chiavi di ricostruzione dei dati.

Non proprio sicuro di ciò che sta facendo in asynch ma di sicuro non stava facendo quello che ci si aspettava dato che era in attesa su se stesso.

try 
{ 
    using (var command = new SqlCommand("[procname]", sqlConn)) 
    { 
     command.CommandTimeout = 0; 
     command.CommandType = CommandType.StoredProcedure; 

     foreach (record someredord Somereport.r) 
     { 
      command.Parameters.Clear() 
      command.Parameters.Add(…); 

      using (var rdr = command.ExecuteReader()) 
      { 
       while (rdr.Read()) 
       { 
        … 
       } 
      } 
     } 
    } 
} 
catch (…) 
{ 
    … 
} 
+0

@stakx Si prenderà cura di un rdr.Close()? – Paparazzi

+0

IIRC 'rdr.Close()' avrebbe lo stesso effetto di 'rdr.Dispose()' ... ma un blocco 'using' è più semplice del wrapping' rdr.Close() 'in una clausola' finally' (che dovresti fare per la sicurezza delle eccezioni). – stakx

+0

OK, ma al di sopra della creazione di un nuovo rdr ogni ciclo. Quale eccezione non verrebbe catturata sul Catch se rdr fosse riutilizzato? – Paparazzi

1

Mentre stavamo parlando nei commenti, la memorizzazione di questi dati in memoria e lavorare con esso ci può essere un approccio più efficiente.

Quindi, un modo semplice per farlo è iniziare con Entity Framework. Entity Framework genererà automaticamente le classi per te in base allo schema del tuo database. Quindi puoi import a stored procedure che contiene la tua istruzione SELECT. Il motivo per cui suggerisco di importare un processo memorizzato in EF è che questo approccio è generalmente più efficiente rispetto a eseguire le query in LINQ contro EF.

quindi eseguire la stored procedure e memorizzare i dati in un List come questo ...

var data = db.MyStoredProc().ToList();

allora si può fare tutto quello che vuoi con quella data. O come ho già detto, se si sta facendo un sacco di ricerche su chiavi primarie quindi usare ToDictionary() qualcosa di simile ...

var data = db.MyStoredProc().ToDictionary(k => k.MyPrimaryKey);

In entrambi i casi, si lavorerà con il vostro data in memoria a questo punto.