2012-04-09 23 views
16

Tratando de refactorizar un código que se ha vuelto realmente lento recientemente y encontré un bloque de código que está tardando más de 5 segundos en ejecutarse.Entity Framework + LINQ + "Contiene" == Súper lento?

El código consta de 2 estados:

IEnumerable<int> StudentIds = _entities.Filters 
        .Where(x => x.TeacherId == Profile.TeacherId.Value && x.StudentId != null) 
        .Select(x => x.StudentId) 
        .Distinct<int>(); 

y

_entities.StudentClassrooms 
        .Include("ClassroomTerm.Classroom.School.District") 
        .Include("ClassroomTerm.Teacher.Profile") 
        .Include("Student") 
        .Where(x => StudentIds.Contains(x.StudentId) 
        && x.ClassroomTerm.IsActive 
        && x.ClassroomTerm.Classroom.IsActive 
        && x.ClassroomTerm.Classroom.School.IsActive 
        && x.ClassroomTerm.Classroom.School.District.IsActive).AsQueryable<StudentClassroom>(); 

Así que es un poco desordenado pero primero me sale una lista distinta de la identificación del de una tabla (filtros), entonces yo consulto otra mesa usándolo.

Estas son tablas relativamente pequeñas, pero aún son más de 5 segundos de tiempo de consulta.

Puse esto en LINQPad y mostró que estaba haciendo primero la consulta de abajo y luego ejecutando 1000 consultas "distintas" después.

Por un capricho, cambié el código "StudentIds" simplemente agregando .ToArray() al final. Esto mejoró la velocidad 1000x ... ahora lleva 100ms completar la misma consulta.

¿Cuál es el problema? ¿Qué estoy haciendo mal?

+2

'¿Qué estoy haciendo mal?' Erm ... ¿No está haciendo una matriz de StudentID? :) –

+1

En otras noticias, ¿no es Linqpad una herramienta genial? –

+0

Este es uno de esos casos en los que 'var' hubiera hecho mágicamente que el código fuera más rápido al identificar que StudentIds podría ser IQueryable. –

Respuesta

24

Este es uno de los peligros de la ejecución diferida en Linq: en su primer acercamiento StudentIds es realmente un IQueryable, no una colección en memoria. Eso significa que usarlo en la segunda consulta ejecutará la consulta nuevamente en la base de datos, todas y cada una de las veces.

Forzar la ejecución de la primera consulta utilizando ToArray() hace StudentIds una colección en memoria y la parte Contains en su segunda consulta se ejecutará sobre esta colección que contiene una secuencia fija de artículos - Esto se asigna a algo equivalente a un SQL where StudentId in (1,2,3,4) consulta.

Esta consulta, por supuesto, será mucho más rápida ya que ha determinado esta secuencia una vez por adelantado, y no cada vez que se ejecuta la cláusula Where. Su segunda consulta sin usar ToArray() (yo pensaría) sería mapeada a una consulta SQL con una subconsulta where exists (...) que se evalúa para cada fila.

+0

Gracias, no pensé lo suficiente en cuanto a por qué nunca querría hacer una colección en memoria primero. Acabo de asumir y sabes lo que sucede cuando asumes :-P –

+2

No lo entiendo :(Estoy de acuerdo con el argumento de ejecución diferida * si * la segunda consulta sería LINQ a Objetos. Pero aparentemente es LINQ a Entidades La segunda consulta se traduce a SQL (solo una vez) y luego se ejecuta el SQL. También para la expresión, el tipo * declarado * de 'StudentIds' es importante, no el tipo de tiempo de ejecución. Es una diferencia si usa' IEnumerable ' o si usa 'var' (=' IQueryable '). En el primer caso, la primera consulta se ejecuta una vez y la segunda consulta se traduce en' IN', el segundo caso es 'exists (subquery)'. No lo hago saber de dónde vienen 1000 consultas. – Slauma

+0

@Slauma: mirándolo con ojos frescos hoy, creo que tienes razón, solo debería haber dos casos, ya sea la consulta 'where in..' o' where exists (subquery) '. Lo posterior puede ser menos eficiente (es decir, faltan algunos índices) por lo que explica por qué el uso de 'IQueryable' tendrá un peor rendimiento que' IEnumerable' en este caso. Sin embargo, no explica la diferencia que se ve al usar 'ToArray()'. Los resultados de sincronización de OP también parecen indicar que cada una de las 3 versiones (IEnumerable, IQueryable, IEnumerable + ToArray) tiene características de rendimiento muy diferentes. Tal vez un experto puede participar. – BrokenGlass

4

ToArray() Materializa la consulta inicial en la memoria del servidor.

Supongo que el proveedor de consultas no puede analizar la expresión StudentIds.Contains(x.StudentId). Por lo tanto, probablemente piense que el studentIds es una matriz ya cargada en la memoria. Probablemente esté consultando la base de datos una y otra vez durante la fase de análisis. La única manera de saberlo con certeza es configurar el generador de perfiles.

Si necesita hacer esto en el servidor de db, use una unión, en lugar de "contiene". Si necesita usar contiene para hacer lo que parece un problema de combinación, es probable que le falte una clave primaria sustituta o una clave externa en alguna parte.

También podría declarar studentIds como IQueryable en lugar de IEnumerable. Esto podría dar al proveedor de consultas la pista que necesita para interpretar el studentIds como expresión aka. datos aún no cargados en la memoria. De alguna manera lo dudo, pero vale la pena intentarlo.

Si falla todo lo demás, use ToArray(). Esto cargará el studentIds inicial en la memoria.

+0

+1 para la recomendación de reestructurar esto en una unión. Cuantos más identificadores de estudiantes tengan, menos contenido tendrá. – Devin

Cuestiones relacionadas