18

Si è verificato un problema di prestazioni interessante con Entity Framework. Sto usando il codice prima.Entity Framework Performance Issue

Ecco la struttura dei miei soggetti:

un libro può avere molte recensioni. Una revisione è associata a un singolo libro. Una revisione può avere uno o più commenti. Un commento è associato a una revisione.

public class Book 
{ 
    public int BookId { get; set; } 
    // ... 
    public ICollection<Review> Reviews { get; set; } 
} 

public class Review 
{ 
    public int ReviewId { get; set; } 
    public int BookId { get; set; } 
    public Book Book { get; set; } 
    public ICollection<Comment> Comments { get; set; } 
} 

public class Comment 
{ 
    public int CommentId { get; set; } 
    public int ReviewId { get; set; } 
    public Review Review { get; set; } 
} 

Ho compilato il mio database con molti dati e aggiunto gli indici corretti. Sto cercando di recuperare un singolo libro che ha 10.000 recensioni su di esso utilizzando questa query:

var bookAndReviews = db.Books.Where(b => b.BookId == id) 
         .Include(b => b.Reviews) 
         .FirstOrDefault(); 

Questo particolare libro ha 10.000 recensioni. Le prestazioni di questa query sono di circa 4 secondi. L'esecuzione della stessa esatta query (tramite SQL Profiler) in realtà ritorna in men che non si dica. Ho usato la stessa query e un oggetto SqlDataAdapter e oggetti personalizzati per recuperare i dati e avviene in meno di 500 millisecondi.

Utilizzando ANTS Profiler performance sembra un grosso del tempo viene speso facendo un paio di cose diverse:

Il Equals metodo viene chiamato 50 milioni di volte.

Qualcuno sa perché sarebbe necessario chiamare questo 50 milioni di volte e come potrei aumentare le prestazioni per questo?

+0

Hai effettivamente cercato di vedere quale query viene generata dalla tua istruzione o stai assumendo che sia la query ottimale? –

+1

Dai una prova a EF Profiler. –

+1

Il problema non è la query come ho affermato. Ho preso la query esatta che EF sta generando e l'ho usata in un Sql Data Adapter usando ADO.net regolare, caricando manualmente gli stessi oggetti. Funziona in meno di un secondo. – Dismissile

risposta

20

Perché equals viene chiamato 50M volte?

Sembra abbastanza sospetto. Hai 10.000 recensioni e 50.000.000 chiamate a Equals. Supponiamo che ciò sia causato dalla mappa delle identità implementata internamente da EF. La mappa di identità garantisce che ogni entità con chiave univoca sia tracciata dal contesto solo una volta, quindi se il contesto ha già un'istanza con la stessa chiave del record caricato dal database, non materializzerà una nuova istanza e utilizzerà invece quella esistente. Ora come questo può coincidere con quei numeri? La mia ipotesi terrificante:

============================================= 
1st  record read | 0  comparisons 
2nd  record read | 1  comparison 
3rd  record read | 2  comparisons 
... 
10.000th record read | 9.999 comparisons 

Ciò significa che ogni nuovo record viene confrontato con tutti i record esistenti nella mappa delle identità. Applicando la matematica per calcolare somma di ogni confronto possiamo utilizzare una cosa chiamata "sequenza aritmetica":

a(n) = a(n-1) + 1 
Sum(n) = (n/2) * (a(1) + a(n)) 
Sum(10.000) = 5.000 * (0 + 9.999) => 5.000 * 10.000 = 50.000.000 

Spero non ho fatto errore nella mia ipotesi o di calcolo. Aspettare! Spero di aver sbagliato perché questo non sembra buono.

Provare a disattivare il rilevamento delle modifiche = sperare di disattivare il controllo della mappa di identità.

Può essere difficile.Inizia con:

var bookAndReviews = db.Books.Where(b => b.BookId == id) 
          .Include(b => b.Reviews) 
          .AsNoTracking() 
          .FirstOrDefault(); 

Ma c'è una grande occasione che la vostra proprietà di navigazione non verrà popolata (perché è gestito da rilevamento delle modifiche). In tal caso, utilizzare questo approccio:

var book = db.Books.Where(b => b.BookId == id).AsNoTracking().FirstOrDefault(); 
book.Reviews = db.Reviews.Where(r => r.BookId == id).AsNoTracking().ToList(); 

In ogni caso è possibile vedere quale tipo di oggetto viene passato a Equals? Penso che dovrebbe confrontare solo le chiavi primarie e anche i confronti in interi 50M non dovrebbero essere un problema del genere.

Come nota a margine EF è lento - è noto infatti. Utilizza anche la riflessione internamente quando si materializzano le entità in modo che solo 10.000 record possano richiedere "un po 'di tempo". Se non lo hai già fatto, puoi anche disattivare la creazione di proxy dinamici (db.Configuration.ProxyCreationEnabled).

+0

Analisi impressionante! Secondo i test (entità semplice senza proprietà nav) che ho fatto qualche tempo fa, 'AsNoTracking' riduce il tempo di materializzazione al 50%. Posso immaginare che la creazione di istantanee per entità caricate come tracciate sia più costosa di quella che si chiama 'Equals' nella mappa delle identità. Se chiami la stessa query una seconda volta (entrambi come tracciati) nello stesso contesto ritorna veloce (meno di 1/10 della prima chiamata), molto più veloce del caricamento senza tracciamento - che mi permette di indovinare che il controllo 'Equals' nella mappa delle identità è relativamente economico. – Slauma

+0

BTW: 'Include' funziona anche con' AsNoTracking() ', la raccolta di navigazione viene popolata. (Oppure intendevi che la proprietà di navigazione inversa 'Review.Book' non verrà popolata?) – Slauma

1

So che questo suona zoppo, ma avete provato il contrario, ad esempio:

var reviewsAndBooks = db.Reviews.Where(r => r.Book.BookId == id) 
         .Include(r => r.Book); 

ho notato a volte la migliore prestazione da EF quando ci si avvicina alle vostre domande in questo modo (ma non ho avuto il tempo di capire perché).

+0

Personalmente lo eviterei a causa di problemi con deadlock. – Skarsnik