2009-12-18 10 views
65

Sto eseguendo un'importazione che avrà 1000 di record per ogni esecuzione. Solo in cerca di qualche conferma sulla mia ipotesi:Quando devo chiamare SaveChanges() quando si creano 1000 di oggetti Entity Framework? (come durante un'importazione)

Quale di questi ha più senso:

  1. Run SaveChanges() ogni AddToClassName() chiamata.
  2. Corsa SaveChanges() ogni n numero di chiamate AddToClassName().
  3. Eseguire SaveChanges() dopo tutte le delle chiamate AddToClassName().

La prima opzione è probabilmente lenta, giusto? Dal momento che sarà necessario analizzare gli oggetti EF in memoria, generare SQL, ecc.

Suppongo che la seconda opzione sia la migliore di entrambi i mondi, dal momento che possiamo eseguire un tentativo di cattura attorno alla chiamata SaveChanges() e perdere solo n numero di record alla volta, se uno di questi fallisce. Forse memorizzare ogni partita in un elenco <>. Se la chiamata SaveChanges() ha esito positivo, eliminare l'elenco. Se fallisce, registra gli articoli.

L'ultima opzione probabilmente finirà per essere anche molto lenta, dal momento che ogni singolo oggetto EF dovrebbe essere in memoria fino a quando non viene chiamato SaveChanges(). E se il salvataggio fallisse, non verrebbe commesso nulla, giusto?

risposta

47

ho iniziato ad esaminare in primo luogo per essere sicuro. Le prestazioni non devono essere così male.

Se è necessario immettere tutte le righe in una transazione, chiamarla dopo tutta la classe AddToClassName. Se le righe possono essere inserite indipendentemente, salvare le modifiche dopo ogni riga. La consistenza del database è importante.

Seconda opzione non mi piace. Sarebbe fonte di confusione per me (dal punto di vista dell'utente finale) se avessi importato nel sistema e avrebbe declinato 10 righe su 1000, solo perché 1 è negativo. Puoi provare a importare 10 e se fallisce, prova uno per uno e poi loggati.

Test se richiede molto tempo. Non scrivere "propably". Non lo sai ancora. Solo quando è effettivamente un problema, pensa ad un'altra soluzione (marc_s).

EDIT

Ho fatto alcuni test (tempo in millisecondi):

10000 righe:

SaveChanges() dopo 1 fila: 18510,534
SaveChanges() dopo 100 righe: 4350,3075
SaveChanges() dopo 10000 righe: 5233,0635

50000 righe:

SaveChanges() dopo il 1 ° fila: 78496,929
SaveChanges() dopo 500 righe: 22302,2835
SaveChanges() dopo 50000 Righe: 24022,8765

quindi è effettivamente più veloce di commettere dopo n file che dopo tutto.

mia raccomandazione è di:

  • SaveChanges() dopo n righe.
  • Se un commit fallisce, provatelo uno per uno per trovare una riga difettosa.

classi di test:

TABELLA:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL, 
    [SomeInt] [int] NOT NULL, 
    [SomeVarchar] [varchar](100) NOT NULL, 
    [SomeOtherVarchar] [varchar](50) NOT NULL, 
    [SomeOtherInt] [int] NULL, 
CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
) ON [PRIMARY] 

Classe:

public class TestController : Controller 
{ 
    // 
    // GET: /Test/ 
    private readonly Random _rng = new Random(); 
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 

    private string RandomString(int size) 
    { 
     var randomSize = _rng.Next(size); 

     char[] buffer = new char[randomSize]; 

     for (int i = 0; i < randomSize; i++) 
     { 
      buffer[i] = _chars[_rng.Next(_chars.Length)]; 
     } 
     return new string(buffer); 
    } 


    public ActionResult EFPerformance() 
    { 
     string result = ""; 

     TruncateTable(); 
     result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>"; 
     TruncateTable(); 
     result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>"; 
     TruncateTable(); 
     result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>"; 
     TruncateTable(); 
     result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>"; 
     TruncateTable(); 
     result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>"; 
     TruncateTable(); 
     result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>"; 
     TruncateTable(); 

     return Content(result); 
    } 

    private void TruncateTable() 
    { 
     using (var context = new CamelTrapEntities()) 
     { 
      var connection = ((EntityConnection)context.Connection).StoreConnection; 
      connection.Open(); 
      var command = connection.CreateCommand(); 
      command.CommandText = @"TRUNCATE TABLE TestTable"; 
      command.ExecuteNonQuery(); 
     } 
    } 

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows) 
    { 
     var startDate = DateTime.Now; 

     using (var context = new CamelTrapEntities()) 
     { 
      for (int i = 1; i <= noOfRows; ++i) 
      { 
       var testItem = new TestTable(); 
       testItem.SomeVarchar = RandomString(100); 
       testItem.SomeOtherVarchar = RandomString(50); 
       testItem.SomeInt = _rng.Next(10000); 
       testItem.SomeOtherInt = _rng.Next(200000); 
       context.AddToTestTable(testItem); 

       if (i % commitAfterRows == 0) context.SaveChanges(); 
      } 
     } 

     var endDate = DateTime.Now; 

     return endDate.Subtract(startDate); 
    } 
} 
+0

La ragione per cui ho scritto "probabilmente" è che ho fatto un'ipotesi plausibile. Per rendere più chiaro che "Non sono sicuro", ho fatto una domanda. Inoltre, penso che abbia perfettamente senso pensare a potenziali problemi PRIMA di incontrarli. Questa è la ragione per cui ho fatto questa domanda. Speravo che qualcuno sapesse quale metodo sarebbe più efficiente, e potrei andare con quello, subito. –

+0

Tizio fantastico. Esattamente quello che stavo cercando. Grazie per aver dedicato del tempo per testare questo! Immagino di poter immagazzinare ogni lotto in memoria, provare il commit, e poi se fallisce, passa a ciascuno di essi individualmente come hai detto tu. Quindi, una volta completato il batch, rilasciare i riferimenti a quei 100 elementi in modo che possano essere eliminati dalla memoria. Grazie ancora! –

+2

La memoria non verrà liberata, poiché tutti gli oggetti saranno trattenuti da ObjectContext, ma in questo momento il contesto non richiede molto spazio in 50000 o 100000. – LukLed

12

Se è necessario importare migliaia di record, utilizzerei qualcosa come SqlBulkCopy e non Entity Framework.

+9

Odio quando la gente non rispondere alla mia domanda :) Beh, diciamo che ho "bisogno" di utilizzare EF. Cosa poi? –

+3

Bene, se davvero * DEVI * usare EF, allora proverei a eseguire il commit dopo un batch di 500 o 1000 record. Altrimenti, finirai per utilizzare troppe risorse e un errore potrebbe potenzialmente ripristinare tutte le 99999 righe che hai aggiornato quando il 100000 non riesce. –

+0

Con lo stesso problema, ho terminato utilizzando SqlBulkCopy che è molto più performante di EF in quel caso. Anche se non mi piace usare diversi modi per accedere al database. –

16

Ho appena ottimizzato un problema molto simile nel mio codice e vorrei sottolineare un'ottimizzazione quello ha funzionato per me.

Ho trovato che gran parte del tempo nell'elaborazione di SaveChanges, sia che elabori 100 o 1000 record contemporaneamente, è vincolata alla CPU. Quindi, elaborando i contesti con un pattern producer/consumer (implementato con BlockingCollection), sono stato in grado di fare un uso migliore dei core della CPU e ottenuto da un totale di 4000 cambiamenti/secondo (come riportato dal valore di ritorno di SaveChanges) a oltre 14.000 modifiche/secondo. L'utilizzo della CPU è passato da circa il 13% (ho 8 core) a circa il 60%. Anche utilizzando più thread di consumo, ho a malapena addebitato il (molto veloce) sistema di I/O del disco e l'utilizzo della CPU di SQL Server non era superiore al 15%.

Scaricando il salvataggio su più thread, è possibile regolare sia il numero di record prima del commit sia il numero di thread che eseguono le operazioni di commit.

Ho trovato che creando 1 thread di produzione e (# di core CPU) -1 thread di consumo mi consentivano di ottimizzare il numero di record impegnati per batch in modo che il conteggio degli elementi in BlockingCollection oscillava tra 0 e 1 (dopo un il thread del consumatore ha preso un articolo). In questo modo, c'era abbastanza lavoro per far funzionare i thread consumati in modo ottimale.

Questo scenario richiede ovviamente la creazione di un nuovo contesto per ogni batch, che trovo più veloce anche in uno scenario a thread singolo per il mio caso d'uso.

+0

Ciao, @ eric-j potresti per favore elaborare leggermente questa riga "elaborando i contesti con un pattern produttore/consumatore (implementato con BlockingCollection)" in modo che possa provare con il mio codice? –

2

Utilizzare una procedura memorizzata.

  1. Creare un tipo di dati definito dall'utente nel server Sql.
  2. Creare e compilare un array di questo tipo nel codice (molto veloce).
  3. Passa la matrice alla procedura memorizzata con una chiamata (molto veloce).

Credo che questo sarebbe il modo più semplice e veloce per farlo.

+5

In genere su SO, le affermazioni di "questo è il più veloce" devono essere comprovate con codice di test e risultati. –

0

Spiacente, so che questo thread è vecchio, ma penso che questo potrebbe aiutare le altre persone con questo problema.

Ho avuto lo stesso problema, ma c'è la possibilità di convalidare le modifiche prima di eseguirle. Il mio codice appare come questo e sta funzionando bene. Con il chUser.LastUpdated, controllo se si tratta di una nuova voce o di una modifica. Perché non è possibile ricaricare una voce che non è ancora nel database.

// Validate Changes 
var invalidChanges = _userDatabase.GetValidationErrors(); 
foreach (var ch in invalidChanges) 
{ 
    // Delete invalid User or Change 
    var chUser = (db_User) ch.Entry.Entity; 
    if (chUser.LastUpdated == null) 
    { 
     // Invalid, new User 
     _userDatabase.db_User.Remove(chUser); 
     Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey); 
    } 
    else 
    { 
     // Invalid Change of an Entry 
     _userDatabase.Entry(chUser).Reload(); 
     Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey); 
    }      
} 

_userDatabase.SaveChanges(); 
+0

Stai rispondendo alla domanda giusta? –

+0

Sì, si tratta dello stesso problema, giusto? Con questo, è possibile aggiungere tutti i 1000 record e prima di eseguire 'saveChanges()' è possibile eliminare quelli che causerebbero un errore. –

+0

Ma l'enfasi della domanda è sul numero di inserti/aggiornamenti da impegnare in modo efficiente in una chiamata "SaveChanges". Non affronta questo problema. Tieni presente che esistono più potenziali motivi per cui SaveChanges fallisce rispetto agli errori di convalida. A proposito, puoi anche contrassegnare le entità come "Invariato" invece di ricaricarle/eliminarle. –

Problemi correlati