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
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
é o mesmo que context.Set
, 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));