18

Me encuentro con un problema de rendimiento interesante con Entity Framework. Estoy usando Code First.Entity Framework Performance Issue

Aquí es la estructura de mis entidades:

Un libro puede tener muchas críticas. Una revisión está asociada a un solo libro. Una revisión puede tener uno o muchos comentarios. Un comentario está asociado con una revisión.

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; } 
} 

He llenado mi base de datos con una gran cantidad de datos y he añadido los índices adecuados. Estoy tratando de recuperar un solo libro que tiene 10.000 comentarios con esta consulta:

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

Este libro en particular tiene 10.000 comentarios. El rendimiento de esta consulta es de alrededor de 4 segundos. Ejecutar exactamente la misma consulta (a través del Analizador de SQL) realmente regresa en muy poco tiempo. Usé la misma consulta y un SqlDataAdapter y objetos personalizados para recuperar los datos y ocurre en menos de 500 milisegundos.

Usando HORMIGAS Performance Profiler se ve como una mayor parte del tiempo se gasta haciendo algunas cosas diferentes:

El método equals se está llamando 50 millones de veces.

¿Alguien sabe por qué tendría que llamar esto 50 millones de veces y cómo podría aumentar el rendimiento de esto?

+0

¿De verdad buscaba ver qué consulta genera su declaración o supone que es la consulta óptima? –

+1

Prueba EF Profiler. –

+1

El problema no es la consulta que he indicado. Tomé la consulta exacta que genera EF y la utilicé en un Adaptador de Datos Sql usando ADO.net regular, cargando los mismos objetos manualmente. Se ejecuta en menos de un segundo. – Dismissile

Respuesta

20

¿Por qué se iguala a 50M veces?

Suena bastante sospechoso. Tienes 10.000 comentarios y 50.000.000 de llamadas al Equals. Supongamos que esto es causado por un mapa de identidad implementado internamente por EF. El mapa de identidad garantiza que el contexto solo rastree cada entidad con clave única, por lo que si el contexto ya tiene instancia con la misma clave que registro cargado de la base de datos, no materializará la nueva instancia y en su lugar utilizará la existente. ¿Ahora cómo esto puede coincidir con esos números? Mi conjetura aterradora:

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

Eso significa que cada nuevo registro se compara con cada registro existente en la aplicación identidad. Mediante la aplicación de las matemáticas para calcular la suma de toda comparación podemos utilizar algo que se llama "secuencia aritmética":

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 

espero que no hice error en mis suposiciones o cálculos. ¡Espere! Espero haber cometido un error porque esto no parece bueno.

Intenta desactivar el seguimiento de cambios = con suerte, desactivar la comprobación del mapa de identidad.

Puede ser complicado.Inicio con:

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

Pero hay una gran posibilidad de que su propiedad de navegación no estará poblado (ya que es manejado por el control de cambios). En tal caso, utilice este enfoque:

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

De todos modos, ¿puede ver qué tipo de objeto se pasa a Igual? Creo que debería comparar solo las claves primarias e incluso las comparaciones enteras de 50M no deberían ser un problema.

Como nota al margen EF es lento, es un hecho bien conocido. También utiliza la reflexión internamente cuando se materializan entidades, así que simplemente 10.000 registros pueden tomar "algún tiempo". A menos que ya lo haya hecho, también puede desactivar la creación de proxy dinámico (db.Configuration.ProxyCreationEnabled).

+0

Awesome analysis! De acuerdo con las pruebas (entidad simple sin propiedades de navegación) que hice hace algún tiempo, 'AsNoTracking' reduce el tiempo de materialización al 50%. Me podría imaginar que la creación de instantáneas para entidades cargadas como rastreados es más costosa que llamar a 'Iguales' en el mapa de identidad. Si llama a la misma consulta por segunda vez (ambas como rastreadas) en el mismo contexto, regresa rápidamente (menos de 1/10 de la primera llamada), mucho más rápido que cargar sin rastrear, lo que me permite adivinar que el control 'Igual' en el mapa de identidad es relativamente barato. – Slauma

+0

BTW: 'Include' también funciona con' AsNoTracking() ', la colección de navegación se llena. (¿O quiso decir que la propiedad de navegación inversa 'Review.Book' no se completará?) – Slauma

1

Sé que esto suena poco convincente, pero ¿ha intentado a la inversa, por ejemplo:

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

he notado veces mejor rendimiento de EF cuando se acerque a sus consultas de esta manera (pero no he tenido el momento de descubrir por qué).

+0

Personalmente evitaría esto debido a problemas con puntos muertos. – Skarsnik