Por que é .Contém lento? Maneira mais eficiente de obter várias entidades por chave primária?

Qual é a maneira mais eficiente de selecionar várias entidades por chave primária?

public IEnumerable GetImagesById(IEnumerable ids) { //return ids.Select(id => Images.Find(id)); //is this cool? return Images.Where( im => ids.Contains(im.Id)); //is this better, worse or the same? //is there a (better) third way? } 

Eu percebo que eu poderia fazer alguns testes de desempenho para comparar, mas eu estou querendo saber se existe de fato uma maneira melhor do que ambos, e estou procurando por alguma iluminação sobre qual é a diferença entre essas duas perguntas, se houver alguma, ‘traduzido’.

ATUALIZAÇÃO: Com a adição do InExpression no EF6, o desempenho do processamento Enumerable.Contains melhorou drasticamente. A análise nesta resposta é ótima, mas em grande parte obsoleta desde 2013.

Usar Contains no Entity Framework é realmente muito lento. É verdade que se traduz em uma cláusula IN no SQL e que a própria consulta SQL é executada rapidamente. Mas o problema e o gargalo de desempenho está na tradução da sua consulta LINQ para o SQL. A tree de expressão que será criada é expandida em uma longa cadeia de concatenações OR porque não há expressão nativa que represente uma IN . Quando o SQL é criado, essa expressão de muitos OR s é reconhecida e recolhida de volta na cláusula SQL IN .

Isso não significa que usar Contains seja pior do que emitir uma consulta por elemento na sua coleção de ids (sua primeira opção). Provavelmente ainda é melhor – pelo menos para collections não muito grandes. Mas para grandes collections é muito ruim. Lembro-me de ter testado há algum tempo uma consulta Contains com cerca de 12.000 elementos que funcionavam mas demoravam cerca de um minuto, embora a consulta em SQL fosse executada em menos de um segundo.

Pode valer a pena testar o desempenho de uma combinação de várias viagens de ida e volta ao database com um número menor de elementos em uma expressão Contains para cada viagem de ida e volta.

Essa abordagem e também as limitações de usar o Contains with Entity Framework são mostradas e explicadas aqui:

Por que o operador Contains () prejudica o desempenho do Entity Framework tão drasticamente?

É possível que um comando SQL bruto funcione melhor nessa situação, o que significa que você chama dbContext.Database.SqlQuery(sqlString) ou dbContext.Images.SqlQuery(sqlString) que sqlString é o SQL mostrado na resposta de @ Rune.

Editar

Aqui estão algumas medições:

Eu fiz isso em uma tabela com 550000 registros e 11 colunas (IDs começam de 1 sem lacunas) e escolhi aleatoriamente 20000 ids:

 using (var context = new MyDbContext()) { Random rand = new Random(); var ids = new List(); for (int i = 0; i < 20000; i++) ids.Add(rand.Next(550000)); Stopwatch watch = new Stopwatch(); watch.Start(); // here are the code snippets from below watch.Stop(); var msec = watch.ElapsedMilliseconds; } 

Teste 1

 var result = context.Set() .Where(e => ids.Contains(e.ID)) .ToList(); 

Resultado -> mseg = 85,5 segundos

Teste 2

 var result = context.Set().AsNoTracking() .Where(e => ids.Contains(e.ID)) .ToList(); 

Resultado -> mseg = 84,5 seg

Este pequeno efeito do AsNoTracking é muito incomum. Isso indica que o gargalo não é materialização de objects (e não SQL como mostrado abaixo).

Para ambos os testes, pode ser visto no SQL Profiler que a consulta SQL chega ao database muito tarde. (Eu não medi exatamente, mas foi mais tarde que 70 segundos.) Obviamente, a tradução desta consulta LINQ em SQL é muito cara.

Teste 3

 var values = new StringBuilder(); values.AppendFormat("{0}", ids[0]); for (int i = 1; i < ids.Count; i++) values.AppendFormat(", {0}", ids[i]); var sql = string.Format( "SELECT * FROM [MyDb].[dbo].[MyEntities] WHERE [ID] IN ({0})", values); var result = context.Set().SqlQuery(sql).ToList(); 

Resultado -> mseg = 5,1 seg

Teste 4

 // same as Test 3 but this time including AsNoTracking var result = context.Set().SqlQuery(sql).AsNoTracking().ToList(); 

Resultado -> mseg = 3,8 seg

Desta vez, o efeito de desabilitar o rastreamento é mais perceptível.

Teste 5

 // same as Test 3 but this time using Database.SqlQuery var result = context.Database.SqlQuery(sql).ToList(); 

Resultado -> mseg = 3,7 s

Meu entendimento é que context.Database.SqlQuery(sql) é o mesmo que context.Set().SqlQuery(sql).AsNoTracking() , portanto, não há diferença esperada entre o Teste 4 e o Teste 5.

(O comprimento dos conjuntos de resultados nem sempre foi o mesmo devido a possíveis duplicatas após a seleção de ID random, mas sempre entre 19600 e 19640 elementos.)

Editar 2

Teste 6

Mesmo 20.000 viagens de ida e volta ao database são mais rápidas do que usando Contains :

 var result = new List(); foreach (var id in ids) result.Add(context.Set().SingleOrDefault(e => e.ID == id)); 

Resultado -> mseg = 73,6 seg

Observe que eu usei SingleOrDefault vez de Find . Usar o mesmo código com o Find é muito lento (cancelei o teste após vários minutos) porque o Find chama DetectChanges internamente. Desativar a detecção de alteração automática ( context.Configuration.AutoDetectChangesEnabled = false ) leva aproximadamente ao mesmo desempenho que SingleOrDefault . Usar o AsNoTracking reduz o tempo em um ou dois segundos.

Os testes foram feitos com o cliente de database (aplicativo de console) e o servidor de database na mesma máquina. O último resultado pode piorar significativamente com um database "remoto" devido às muitas viagens de ida e volta.

A segunda opção é definitivamente melhor que a primeira. A primeira opção resultará em ids.Length consultas ao database, enquanto a segunda opção pode usar um operador 'IN' na consulta SQL. Ele basicamente transformará sua consulta LINQ em algo como o seguinte SQL:

 SELECT * FROM ImagesTable WHERE id IN (value1,value2,...) 

onde value1, value2 etc. são os valores da variável ids. Esteja ciente, no entanto, que acho que pode haver um limite superior no número de valores que podem ser serializados em uma consulta dessa maneira. Vou ver se consigo encontrar alguma documentação …

Eu estou usando o Entity Framework 6.1 e descobri usando seu código que, é melhor usar:

 return db.PERSON.Find(id); 

ao invés de:

 return db.PERSONA.FirstOrDefault(x => x.ID == id); 

Desempenho de Find () vs. FirstOrDefault são algumas reflexões sobre isso.

Weel, recentemente, tem um problema semelhante e a melhor maneira que encontrei foi inserir a lista de contém em uma tabela temporária e depois fazer uma junit.

 private List GetFoos(IEnumerable ids) { var sb = new StringBuilder(); sb.Append("DECLARE @Temp TABLE (Id bitint PRIMARY KEY)\n"); foreach (var id in ids) { sb.Append("INSERT INTO @Temp VALUES ('"); sb.Append(id); sb.Append("')\n"); } sb.Append("SELECT f.* FROM [dbo].[Foo] f inner join @Temp t on f.Id = t.Id"); return this.context.Database.SqlQuery(sb.ToString()).ToList(); } 

Não é uma maneira bonita, mas para grandes listas é muito eficaz.

Transformar a lista para uma matriz com toArray () aumenta o desempenho. Você pode fazer assim:

 ids.Select(id => Images.Find(id)); return Images.toArray().Where( im => ids.Contains(im.Id));