ACTUALIZACIÓN 3: De acuerdo con este anuncio , esto ha sido abordado por el equipo EF en EF6 alpha 2.
ACTUALIZACIÓN 2: He creado una sugerencia para solucionar este problema. Para votar por él, vaya aquí .
Considere una base de datos SQL con una tabla muy simple.
CREATE TABLE Main (Id INT PRIMARY KEY)
Completo la tabla con 10,000 registros.
WITH Numbers AS
(
SELECT 1 AS Id
UNION ALL
SELECT Id + 1 AS Id FROM Numbers WHERE Id <= 10000
)
INSERT Main (Id)
SELECT Id FROM Numbers
OPTION (MAXRECURSION 0)
Construyo un modelo EF para la tabla y ejecuto la siguiente consulta en LINQPad (estoy usando el modo "C # Statements" para que LINQPad no cree un volcado automáticamente).
var rows =
Main
.ToArray();
El tiempo de ejecución es de ~ 0,07 segundos. Ahora agrego el operador Contiene y vuelvo a ejecutar la consulta.
var ids = Main.Select(a => a.Id).ToArray();
var rows =
Main
.Where (a => ids.Contains(a.Id))
.ToArray();
¡El tiempo de ejecución para este caso es 20,14 segundos (288 veces más lento)!
Al principio sospeché que el T-SQL emitido para la consulta tardaba más en ejecutarse, así que intenté cortarlo y pegarlo desde el panel SQL de LINQPad en SQL Server Management Studio.
SET NOCOUNT ON
SET STATISTICS TIME ON
SELECT
[Extent1].[Id] AS [Id]
FROM [dbo].[Primary] AS [Extent1]
WHERE [Extent1].[Id] IN (1,2,3,4,5,6,7,8,...
Y el resultado fue
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 88 ms.
Luego sospeché que LINQPad estaba causando el problema, pero el rendimiento es el mismo si lo ejecuto en LINQPad o en una aplicación de consola.
Entonces, parece que el problema está en algún lugar dentro de Entity Framework.
¿Estoy haciendo algo mal aquí? Esta es una parte de mi código en la que el tiempo es crítico, entonces, ¿hay algo que pueda hacer para acelerar el rendimiento?
Estoy usando Entity Framework 4.1 y Sql Server 2008 R2.
ACTUALIZACIÓN 1:
En la discusión a continuación, hubo algunas preguntas sobre si el retraso ocurrió mientras EF estaba construyendo la consulta inicial o mientras analizaba los datos que recibió. Para probar esto, ejecuté el siguiente código,
var ids = Main.Select(a => a.Id).ToArray();
var rows =
(ObjectQuery<MainRow>)
Main
.Where (a => ids.Contains(a.Id));
var sql = rows.ToTraceString();
lo que obliga a EF a generar la consulta sin ejecutarla en la base de datos. El resultado fue que este código requirió ~ 20 segundos para ejecutarse, por lo que parece que se toma casi todo el tiempo para construir la consulta inicial.
CompiledQuery al rescate entonces? No tan rápido ... CompiledQuery requiere que los parámetros pasados a la consulta sean tipos fundamentales (int, string, float, etc.). No acepta matrices o IEnumerable, por lo que no puedo usarlo para una lista de ID.
fuente
var qry = Main.Where (a => ids.Contains(a.Id)); var rows = qry.ToArray();
ver qué parte de la consulta se está demorando?parent._recompileRequired = () => true;
sucede con todas las consultas que contienen un parámetro IEnumerable <T>. ¡Abucheo!Respuestas:
ACTUALIZACIÓN: Con la incorporación de InExpression en EF6, el rendimiento del procesamiento de Enumerable.Contains mejoró drásticamente. El enfoque descrito en esta respuesta ya no es necesario.
Tiene razón en que la mayor parte del tiempo se dedica a procesar la traducción de la consulta. El modelo de proveedor de EF no incluye actualmente una expresión que represente una cláusula IN, por lo que los proveedores de ADO.NET no pueden admitir IN de forma nativa. En cambio, la implementación de Enumerable.Contains lo traduce a un árbol de expresiones OR, es decir, para algo que en C # se ve así:
new []{1, 2, 3, 4}.Contains(i)
... generaremos un árbol DbExpression que podría representarse así:
((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))
(Los árboles de expresión deben estar equilibrados porque si tuviéramos todos los OR en una sola columna larga, habría más posibilidades de que el visitante de la expresión golpeara un desbordamiento de pila (sí, en realidad lo hicimos en nuestras pruebas))
Luego enviamos un árbol como este al proveedor de ADO.NET, que puede tener la capacidad de reconocer este patrón y reducirlo a la cláusula IN durante la generación de SQL.
Cuando agregamos soporte para Enumerable.Contains en EF4, pensamos que era deseable hacerlo sin tener que introducir soporte para expresiones IN en el modelo de proveedor, y sinceramente, 10,000 es mucho más que la cantidad de elementos que anticipamos que los clientes pasarían. Enumerable Contiene. Dicho esto, entiendo que esto es una molestia y que la manipulación de árboles de expresiones encarece demasiado las cosas en su escenario particular.
Hablé de esto con uno de nuestros desarrolladores y creemos que en el futuro podríamos cambiar la implementación agregando soporte de primera clase para IN. Me aseguraré de que esto se agregue a nuestro trabajo pendiente, pero no puedo prometer cuándo lo hará, dado que hay muchas otras mejoras que nos gustaría hacer.
A las soluciones ya sugeridas en el hilo, agregaría lo siguiente:
Considere la posibilidad de crear un método que equilibre el número de viajes de ida y vuelta a la base de datos con el número de elementos que pasa a Contiene. Por ejemplo, en mis propias pruebas observé que calcular y ejecutar contra una instancia local de SQL Server la consulta con 100 elementos toma 1/60 de segundo. Si puede escribir su consulta de tal manera que la ejecución de 100 consultas con 100 conjuntos diferentes de ID le dé un resultado equivalente a la consulta con 10,000 elementos, entonces puede obtener los resultados en aproximadamente 1,67 segundos en lugar de 18 segundos.
Los diferentes tamaños de fragmentos deberían funcionar mejor según la consulta y la latencia de la conexión de la base de datos. Para ciertas consultas, es decir, si la secuencia pasada tiene duplicados o si Enumerable.Contains se usa en una condición anidada, puede obtener elementos duplicados en los resultados.
Aquí hay un fragmento de código (lo siento si el código utilizado para dividir la entrada en trozos parece demasiado complejo. Hay formas más simples de lograr lo mismo, pero estaba tratando de encontrar un patrón que preserva la transmisión de la secuencia y No pude encontrar nada parecido en LINQ, así que probablemente exagere esa parte :)):
Uso:
var list = context.GetMainItems(ids).ToList();
Método para contexto o repositorio:
public partial class ContainsTestEntities { public IEnumerable<Main> GetMainItems(IEnumerable<int> ids, int chunkSize = 100) { foreach (var chunk in ids.Chunk(chunkSize)) { var q = this.MainItems.Where(a => chunk.Contains(a.Id)); foreach (var item in q) { yield return item; } } } }
Métodos de extensión para cortar secuencias enumerables:
public static class EnumerableSlicing { private class Status { public bool EndOfSequence; } private static IEnumerable<T> TakeOnEnumerator<T>(IEnumerator<T> enumerator, int count, Status status) { while (--count > 0 && (enumerator.MoveNext() || !(status.EndOfSequence = true))) { yield return enumerator.Current; } } public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> items, int chunkSize) { if (chunkSize < 1) { throw new ArgumentException("Chunks should not be smaller than 1 element"); } var status = new Status { EndOfSequence = false }; using (var enumerator = items.GetEnumerator()) { while (!status.EndOfSequence) { yield return TakeOnEnumerator(enumerator, chunkSize, status); } } } }
¡Espero que esto ayude!
fuente
!(status.EndOfSequence = true)
en el método TakeOnEnumerator <T>: Por lo tanto, el efecto secundario de esta asignación de expresión siempre será! Verdadero, por lo que no afectará a la expresión general. Básicamente, marcastats.EndOfSequence
comotrue
solo cuando quedan elementos por recuperar, pero ha llegado al final de la enumeración.Enumerable.Contains
mejoró dramáticamente en EF 6 en comparación con las versiones anteriores de EF. Pero, desafortunadamente, todavía está lejos de ser satisfactorio / listo para producción en nuestros casos de uso.Si encuentra un problema de rendimiento que le está bloqueando, no intente pasar mucho tiempo resolviéndolo porque lo más probable es que no tenga éxito y tendrá que comunicarlo directamente con MS (si tiene soporte premium) y se necesita siglos.
Utilice soluciones alternativas en caso de problemas de rendimiento y EF significa SQL directo. No tiene nada de malo. La idea global de que usar EF = dejar de usar SQL es una mentira. Tiene SQL Server 2008 R2, entonces:
Include
lógica de manera óptimaSqlDataReader
para obtener resultados y construir sus entidadesSi el rendimiento es crítico para usted, no encontrará una mejor solución. EF no puede mapear y ejecutar este procedimiento porque la versión actual no admite parámetros con valores de tabla ni conjuntos de resultados múltiples.
fuente
Pudimos resolver el problema de EF Contiene agregando una tabla intermedia y uniéndonos a esa tabla desde la consulta LINQ que necesitaba usar la cláusula Contains. Pudimos obtener resultados asombrosos con este enfoque. Tenemos un modelo EF grande y como "Contiene" no está permitido al precompilar consultas EF, obtuvimos un rendimiento muy bajo para las consultas que usan la cláusula "Contiene".
Un resumen:
Crear una tabla en SQL Server - por ejemplo,
HelperForContainsOfIntType
con elHelperID
deGuid
tipo de datos yReferenceID
deint
columnas de tipo de datos. Cree diferentes tablas con ReferenceID de diferentes tipos de datos según sea necesario.Cree un Entity / EntitySet para
HelperForContainsOfIntType
y otras tablas similares en el modelo EF. Cree diferentes Entity / EntitySet para diferentes tipos de datos según sea necesario.Cree un método auxiliar en código .NET que tome la entrada de an
IEnumerable<int>
y devuelva unGuid
. Este método genera una nuevaGuid
e inserta los valores deIEnumerable<int>
aHelperForContainsOfIntType
lo largo con el generadoGuid
. A continuación, el método devuelve este recién generadoGuid
a la persona que llama. Para una inserción rápida en laHelperForContainsOfIntType
tabla, cree un procedimiento almacenado que tome la entrada de una lista de valores y realice la inserción. Consulte Parámetros con valores de tabla en SQL Server 2008 (ADO.NET) . Cree diferentes ayudantes para diferentes tipos de datos o cree un método de ayuda genérico para manejar diferentes tipos de datos.Cree una consulta compilada de EF que sea similar a algo como a continuación:
static Func<MyEntities, Guid, IEnumerable<Customer>> _selectCustomers = CompiledQuery.Compile( (MyEntities db, Guid containsHelperID) => from cust in db.Customers join x in db.HelperForContainsOfIntType on cust.CustomerID equals x.ReferenceID where x.HelperID == containsHelperID select cust );
Llame al método auxiliar con los valores que se usarán en la
Contains
cláusula y obtenga elGuid
uso en la consulta. Por ejemplo:var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 }); var result = _selectCustomers(_dbContext, containsHelperID).ToList();
fuente
Editando mi respuesta original: existe una posible solución alternativa, según la complejidad de sus entidades. Si conoce el sql que EF genera para poblar sus entidades, puede ejecutarlo directamente usando DbContext.Database.SqlQuery . En EF 4, creo que podrías usar ObjectContext.ExecuteStoreQuery , pero no lo probé.
Por ejemplo, usando el código de mi respuesta original a continuación para generar la declaración SQL usando a
StringBuilder
, pude hacer lo siguientevar rows = db.Database.SqlQuery<Main>(sql).ToArray();
y el tiempo total pasó de aproximadamente 26 segundos a 0,5 segundos.
Seré el primero en decir que es feo y, con suerte, se presentará una mejor solución.
actualizar
Después de pensar un poco más, me di cuenta de que si usa una combinación para filtrar sus resultados, EF no tiene que crear esa larga lista de identificadores. Esto podría ser complejo según la cantidad de consultas simultáneas, pero creo que podría usar identificadores de usuario o identificadores de sesión para aislarlos.
Para probar esto, creé una
Target
tabla con el mismo esquema queMain
. Luego uséStringBuilder
para crearINSERT
comandos para completar laTarget
tabla en lotes de 1,000, ya que esa es la mayor cantidad que SQL Server aceptará en una solaINSERT
. La ejecución directa de las declaraciones sql fue mucho más rápido que pasar por EF (aproximadamente 0,3 segundos frente a 2,5 segundos), y creo que estaría bien ya que el esquema de la tabla no debería cambiar.Finalmente, seleccionar usando un
join
resultó en una consulta mucho más simple y se ejecutó en menos de 0.5 segundos.ExecuteStoreCommand("DELETE Target"); var ids = Main.Select(a => a.Id).ToArray(); var sb = new StringBuilder(); for (int i = 0; i < 10; i++) { sb.Append("INSERT INTO Target(Id) VALUES ("); for (int j = 1; j <= 1000; j++) { if (j > 1) { sb.Append(",("); } sb.Append(i * 1000 + j); sb.Append(")"); } ExecuteStoreCommand(sb.ToString()); sb.Clear(); } var rows = (from m in Main join t in Target on m.Id equals t.Id select m).ToArray(); rows.Length.Dump();
Y el sql generado por EF para la unión:
SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] INNER JOIN [dbo].[Target] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
(respuesta original)
Esta no es una respuesta, pero quería compartir información adicional y es demasiado larga para caber en un comentario. Pude reproducir sus resultados y tengo algunas otras cosas que agregar:
SQL Profiler muestra que el retraso se produce entre la ejecución de la primera consulta (
Main.Select
) y la segundaMain.Where
consulta, por lo que sospeché que el problema estaba en generar y enviar una consulta de ese tamaño (48,980 bytes).Sin embargo, construir la misma declaración sql en T-SQL dinámicamente toma menos de 1 segundo, y tomar
ids
de suMain.Select
declaración, construir la misma declaración sql y ejecutarla usandoSqlCommand
0.112 segundos, y eso incluye el tiempo para escribir el contenido en la consola. .En este punto, sospecho que EF está realizando algún análisis / procesamiento para cada uno de los 10,000
ids
mientras construye la consulta. Ojalá pudiera proporcionar una respuesta y una solución definitivas :(.Aquí está el código que probé en SSMS y LINQPad (por favor, no critique con demasiada dureza, tengo prisa por dejar el trabajo):
declare @sql nvarchar(max) set @sql = 'SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN (' declare @count int = 0 while @count < 10000 begin if @count > 0 set @sql = @sql + ',' set @count = @count + 1 set @sql = @sql + cast(@count as nvarchar) end set @sql = @sql + ')' exec(@sql)
var ids = Mains.Select(a => a.Id).ToArray(); var sb = new StringBuilder(); sb.Append("SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN ("); for(int i = 0; i < ids.Length; i++) { if (i > 0) sb.Append(","); sb.Append(ids[i].ToString()); } sb.Append(")"); using (SqlConnection connection = new SqlConnection("server = localhost;database = Test;integrated security = true")) using (SqlCommand command = connection.CreateCommand()) { command.CommandText = sb.ToString(); connection.Open(); using(SqlDataReader reader = command.ExecuteReader()) { while(reader.Read()) { Console.WriteLine(reader.GetInt32(0)); } } }
fuente
No estoy familiarizado con Entity Framework, pero ¿el rendimiento es mejor si hace lo siguiente?
En lugar de esto:
var ids = Main.Select(a => a.Id).ToArray(); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
qué tal esto (asumiendo que la ID es un int):
var ids = new HashSet<int>(Main.Select(a => a.Id)); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
fuente
Se corrigió en Entity Framework 6 Alpha 2: http://entityframework.codeplex.com/SourceControl/changeset/a7b70f69e551
http://blogs.msdn.com/b/adonet/archive/2012/12/10/ef6-alpha-2-available-on-nuget.aspx
fuente
¿Una alternativa almacenable en caché a Contiene?
Esto solo me mordió, así que agregué mis dos centavos al enlace Sugerencias de funciones de Entity Framework.
El problema es definitivamente al generar el SQL. Tengo un cliente sobre cuyos datos la generación de consultas fue de 4 segundos, pero la ejecución fue de 0,1 segundos.
Me di cuenta de que al usar LINQ y OR dinámicos, la generación de sql tardaba el mismo tiempo, pero generaba algo que podía almacenarse en caché . Entonces, al ejecutarlo nuevamente, se redujo a 0.2 segundos.
Tenga en cuenta que todavía se generó un SQL in.
Solo algo más a considerar si puede soportar el golpe inicial, su recuento de matrices no cambia mucho y ejecuta la consulta mucho. (Probado en LINQ Pad)
fuente
El problema es con la generación de SQL de Entity Framework. No puede almacenar en caché la consulta si uno de los parámetros es una lista.
Para que EF almacene en caché su consulta, puede convertir su lista en una cadena y hacer un .Contains en la cadena.
Entonces, por ejemplo, este código se ejecutaría mucho más rápido ya que EF podría almacenar en caché la consulta:
var ids = Main.Select(a => a.Id).ToArray(); var idsString = "|" + String.Join("|", ids) + "|"; var rows = Main .Where (a => idsString.Contains("|" + a.Id + "|")) .ToArray();
Cuando se genera esta consulta, es probable que se genere utilizando un Me gusta en lugar de un In, por lo que acelerará su C #, pero podría ralentizar su SQL. En mi caso, no noté ninguna disminución del rendimiento en mi ejecución de SQL, y C # se ejecutó significativamente más rápido.
fuente