Partilhar via


Consulta eficiente

Consultar eficientemente é um assunto vasto, que abrange assuntos tão abrangentes quanto índices, estratégias de carregamento de entidades relacionadas e muitos outros. Esta seção detalha alguns temas comuns para tornar suas consultas mais rápidas e armadilhas que os usuários normalmente encontram.

Use os índices corretamente

O principal fator decisivo para saber se uma consulta é executada rapidamente ou não é se ela utilizará adequadamente os índices quando apropriado: os bancos de dados geralmente são usados para armazenar grandes quantidades de dados e as consultas que atravessam tabelas inteiras geralmente são fontes de sérios problemas de desempenho. Problemas de indexação não são fáceis de detetar, porque não é imediatamente óbvio se uma determinada consulta usará um índice ou não. Por exemplo:

// Matches on start, so uses an index (on SQL Server)
var posts1 = await context.Posts.Where(p => p.Title.StartsWith("A")).ToListAsync();
// Matches on end, so does not use the index
var posts2 = await context.Posts.Where(p => p.Title.EndsWith("A")).ToListAsync();

Uma boa maneira de detetar problemas de indexação é primeiro identificar uma consulta lenta e, em seguida, examinar seu plano de consulta por meio da ferramenta favorita do seu banco de dados; Consulte a página Diagnóstico de desempenho para obter mais informações sobre como fazer isso. O plano de consulta exibe se a consulta atravessa a tabela inteira ou usa um índice.

Como regra geral, não há nenhum conhecimento especial de EF para usar índices ou diagnosticar problemas de desempenho relacionados a eles; o conhecimento geral da base de dados relacionado com índices é tão relevante para aplicações EF como para aplicações que não utilizam EF. A seguir estão listadas algumas diretrizes gerais a serem lembradas ao usar índices:

  • Embora os índices acelerem as consultas, eles também atrasam as atualizações, uma vez que precisam ser mantidos up-toatualizados. Evite definir índices que não são necessários e considere o uso de filtros de índice para limitar o índice a um subconjunto das linhas, reduzindo assim essa sobrecarga.
  • Os índices compostos podem acelerar consultas que filtram em várias colunas, mas também podem acelerar consultas que não filtram todas as colunas do índice - dependendo da ordem. Por exemplo, um índice nas colunas A e B acelera a filtragem de consultas por A e B, bem como consultas filtrando apenas por A, mas não acelera consultas filtrando apenas por B.
  • Se uma consulta filtra por uma expressão em uma coluna (por exemplo price / 2), um índice simples não pode ser usado. No entanto, você pode definir uma coluna persistente armazenada para sua expressão e criar um índice sobre isso. Alguns bancos de dados também suportam índices de expressão, que podem ser usados diretamente para acelerar a filtragem de consultas por qualquer expressão.
  • Bancos de dados diferentes permitem que os índices sejam configurados de várias maneiras e, em muitos casos, os provedores EF Core os expõem por meio da API Fluent. Por exemplo, o provedor do SQL Server permite configurar se um índice está clusterizado ou definir seu fator de preenchimento. Consulte a documentação do seu fornecedor para obter mais informações.

Projete apenas as propriedades de que você precisa

O EF Core facilita muito a consulta de instâncias de entidade e, em seguida, o uso dessas instâncias no código. No entanto, as instâncias de entidade ao serem consultadas podem frequentemente extrair mais dados do que o necessário da sua base de dados. Considere o seguinte:

await foreach (var blog in context.Blogs.AsAsyncEnumerable())
{
    Console.WriteLine("Blog: " + blog.Url);
}

Embora este código só precise realmente da propriedade Url de cada Blog, toda a entidade Blog é carregada, e colunas supérfluas são transferidas do banco de dados.

SELECT [b].[BlogId], [b].[CreationDate], [b].[Name], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]

Isso pode ser otimizado usando Select para informar ao EF quais colunas projetar:

await foreach (var blogName in context.Blogs.Select(b => b.Url).AsAsyncEnumerable())
{
    Console.WriteLine("Blog: " + blogName);
}

O SQL resultante recupera apenas as colunas necessárias:

SELECT [b].[Url]
FROM [Blogs] AS [b]

Se precisar de projetar mais de uma coluna, projete para um tipo anónimo em C# com as propriedades desejadas.

Observe que essa técnica é muito útil para consultas somente leitura, mas as coisas ficam mais complicadas se você precisar atualizar os blogs buscados, já que o controle de alterações do EF só funciona com instâncias de entidade. É possível realizar atualizações sem carregar entidades inteiras anexando uma instância modificada do Blog e informando ao EF quais propriedades foram alteradas, mas essa é uma técnica mais avançada que pode não valer a pena.

Limitar o tamanho do conjunto de resultados

Por padrão, uma consulta retorna todas as linhas que correspondem aos seus filtros:

var blogsAll = await context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .ToListAsync();

Como o número de linhas retornadas depende dos dados reais em seu banco de dados, é impossível saber quantos dados serão carregados do banco de dados, quanta memória será ocupada pelos resultados e quanta carga adicional será gerada ao processar esses resultados (por exemplo, enviando-os para um navegador do usuário pela rede). Crucialmente, os bancos de dados de teste frequentemente contêm poucos dados, de modo que tudo funciona bem durante o teste, mas problemas de desempenho aparecem de repente quando a consulta começa a ser executada em dados do mundo real e muitas linhas são retornadas.

Como resultado, geralmente vale a pena pensar em limitar o número de resultados:

var blogs25 = await context.Posts
    .Where(p => p.Title.StartsWith("A"))
    .Take(25)
    .ToListAsync();

No mínimo, sua interface do usuário pode mostrar uma mensagem indicando que mais linhas podem existir no banco de dados (e permitir recuperá-las de alguma outra maneira). Uma solução completa implementaria paginação, onde sua interface do usuário mostra apenas um certo número de linhas de cada vez, e permitiria que os usuários avançassem para a próxima página conforme necessário; Consulte a próxima seção para obter mais detalhes sobre como implementar isso de forma eficiente.

Paginação eficiente

Paginação refere-se à recuperação de resultados em páginas, em vez de todos de uma só vez; Isso geralmente é feito para grandes conjuntos de resultados, onde uma interface do usuário é mostrada que permite que o usuário navegue até a página seguinte ou anterior dos resultados. Uma maneira comum de implementar paginação com bancos de dados é usar os Skip operadores e Take (OFFSET e LIMIT em SQL), embora esta seja uma implementação intuitiva, também é bastante ineficiente. Para uma paginação que permita mover uma página de cada vez (em vez de saltar para páginas arbitrárias), considere usar keyset pagination.

Para obter mais informações, consulte a página de documentação sobre paginação.

Em bancos de dados relacionais, todas as entidades relacionadas são carregadas introduzindo JOINs em uma única consulta.

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId], [p].[PostId]

Se um blog típico tiver várias postagens relacionadas, as linhas para essas postagens duplicarão as informações do blog. Esta duplicação conduz ao chamado problema da "explosão cartesiana". À medida que mais relações um-para-muitos são carregadas, a quantidade de dados duplicados pode crescer e afetar negativamente o desempenho da sua aplicação.

O EF permite evitar este efeito através do uso de "consultas divididas", que carregam as entidades relacionadas através de consultas separadas. Para obter mais informações, leia a documentação sobre consultas divididas e únicas.

Observação

A implementação atual de consultas divididas executa uma viagem de ida e volta para cada consulta. Planejamos melhorar isso no futuro e executar todas as consultas em uma única viagem de ida e volta.

Recomenda-se a leitura da página dedicada sobre entidades relacionadas antes de continuar com esta seção.

Ao lidar com entidades relacionadas, geralmente sabemos com antecedência o que precisamos carregar: um exemplo típico seria carregar um determinado conjunto de Blogs, juntamente com todos os seus Posts. Nesses cenários, é sempre melhor usar eager loading, para que o EF possa buscar todos os dados necessários num único ciclo. O recurso de inclusão filtrada também permite limitar quais entidades relacionadas você gostaria de carregar, mantendo o processo de carregamento ansioso e, portanto, factível em uma única viagem de ida e volta:

using (var context = new BloggingContext())
{
    var filteredBlogs = await context.Blogs
        .Include(
            blog => blog.Posts
                .Where(post => post.BlogId == 1)
                .OrderByDescending(post => post.Title)
                .Take(5))
        .ToListAsync();
}

Em outros cenários, podemos não saber de qual entidade relacionada precisaremos antes de obter sua entidade principal. Por exemplo, ao carregar algum Blog, podemos precisar consultar alguma outra fonte de dados - possivelmente um webservice - para saber se estamos interessados nas Postagens desse Blog. Nesses casos, o carregamento explícito ou lento pode ser usado para buscar entidades relacionadas separadamente e preencher a navegação de Postagens do Blog. Observe que, como esses métodos não são ansiosos, eles exigem viagens de ida e volta adicionais ao banco de dados, o que é fonte de lentidão; dependendo do seu cenário específico, pode ser mais eficiente carregar sempre todos os Posts, em vez de executar as viagens de ida e volta adicionais e obter seletivamente apenas os Posts que você precisa.

Cuidado com o carregamento preguiçoso

O carregamento lento muitas vezes parece ser uma maneira muito útil de escrever a lógica do banco de dados, já que o EF Core carrega automaticamente entidades relacionadas do banco de dados à medida que elas são acessadas pelo seu código. Isso evita o carregamento de entidades relacionadas que não são necessárias (como o carregamento explícito) e aparentemente libera o programador de ter que lidar com entidades relacionadas. No entanto, o carregamento preguiçoso é particularmente propenso a produzir viagens de ida e volta extras desnecessárias, o que pode retardar a aplicação.

Considere o seguinte:

foreach (var blog in await context.Blogs.ToListAsync())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Este pedaço de código aparentemente inocente itera através de todos os blogs e seus posts, imprimindo-os. Ativar o log de instruções do EF Core revela o seguinte:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[BlogId], [b].[Rating], [b].[Url]
      FROM [Blogs] AS [b]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (5ms) [Parameters=[@__p_0='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='2'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@__p_0='3'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title]
      FROM [Post] AS [p]
      WHERE [p].[BlogId] = @__p_0

... and so on

O que se passa aqui? Por que todas essas consultas estão sendo enviadas para os loops simples acima? Com o lazy loading, as Postagens de um Blog só são carregadas quando a propriedade Posts é acessada. Como resultado, cada iteração no foreach interno dispara uma consulta adicional ao banco de dados, em um ciclo de ida e volta próprio. Como resultado, após a consulta inicial carregar todos os blogs, temos então outra consulta por blog, carregando todos os seus posts; isso às vezes é chamado de problema N+1 , e pode causar problemas de desempenho muito significativos.

Supondo que vamos precisar de todas as publicações dos blogs, faz sentido usar o carregamento antecipado aqui. Podemos usar o operador Include para realizar o carregamento, mas como só precisamos dos URLs dos blogs (e só devemos carregar o que é necessário). Então, vamos usar uma projeção:

await foreach (var blog in context.Blogs.Select(b => new { b.Url, b.Posts }).AsAsyncEnumerable())
{
    foreach (var post in blog.Posts)
    {
        Console.WriteLine($"Blog {blog.Url}, Post: {post.Title}");
    }
}

Isso fará com que o EF Core busque todos os Blogs - juntamente com seus Posts - em uma única consulta. Em alguns casos, também pode ser útil evitar efeitos de explosão cartesiana usando consultas divididas.

Advertência

Como o carregamento preguiçoso torna extremamente fácil desencadear inadvertidamente o problema N+1, recomenda-se evitá-lo. O carregamento ansioso ou explícito deixa muito claro no código-fonte quando ocorre uma viagem de ida e volta do banco de dados.

Buffering e streaming

Buffering refere-se ao carregamento de todos os resultados das consultas na memória, enquanto que streaming significa que o EF fornece à aplicação um único resultado de cada vez, sem nunca conter todo o conjunto de resultados na memória. Em princípio, os requisitos de memória de uma consulta de streaming são fixos - eles são os mesmos se a consulta retorna 1 linha ou 1000; Uma consulta de buffer, por outro lado, requer mais memória quanto mais linhas são retornadas. Para consultas que produzem grandes conjuntos de resultados, isto pode ser um fator de desempenho importante.

Se uma consulta é feita em buffer ou em fluxo depende de como é avaliada:

// ToList and ToArray cause the entire resultset to be buffered:
var blogsList = await context.Posts.Where(p => p.Title.StartsWith("A")).ToListAsync();
var blogsArray = await context.Posts.Where(p => p.Title.StartsWith("A")).ToArrayAsync();

// Foreach streams, processing one row at a time:
await foreach (var blog in context.Posts.Where(p => p.Title.StartsWith("A")).AsAsyncEnumerable())
{
    // ...
}

// AsAsyncEnumerable also streams, allowing you to execute LINQ operators on the client-side:
var doubleFilteredBlogs = context.Posts
    .Where(p => p.Title.StartsWith("A")) // Translated to SQL and executed in the database
    .AsAsyncEnumerable()
    .Where(p => SomeDotNetMethod(p)); // Executed at the client on all database results

Se suas consultas retornarem apenas alguns resultados, então você provavelmente não precisa se preocupar com isso. No entanto, se a sua consulta puder retornar um grande número de linhas, vale a pena considerar a transmissão em vez de usar um buffer.

Observação

Evite usar ToList ou ToArray se você pretende usar outro operador LINQ no resultado - isso irá armazenar desnecessariamente todos os resultados em buffer na memória. Utilize AsEnumerable em substituição.

Bufferização interna por EF

Em determinadas situações, o próprio EF armazenará o conjunto de resultados em buffer internamente, independentemente de como você avalia sua consulta. Os dois casos em que isso acontece são:

  • Quando uma estratégia de execução de nova tentativa está em vigor. Isso é feito para garantir que os mesmos resultados sejam retornados se a consulta for repetida mais tarde.
  • Quando a consulta dividida é usada, os conjuntos de resultados de todas as consultas, exceto a última, são armazenados em buffer - a menos que o MARS (Multiple Ative Result Sets) esteja habilitado no SQL Server. Isso ocorre porque geralmente é impossível ter vários conjuntos de resultados de consulta ativos ao mesmo tempo.

Observe que esse buffering interno ocorre além de qualquer buffering causado por meio de operadores LINQ. Por exemplo, se você usar ToList em uma consulta e uma estratégia de execução repetitiva estiver em vigor, o conjunto de resultados será carregado na memória duas vezes: uma vez internamente pelo EF e outra pelo ToList.

Rastreamento, não rastreamento e resolução de identidade

Recomenda-se ler a página dedicada sobre rastreamento e não rastreamento antes de continuar com esta seção.

O EF rastreia instâncias de entidade por padrão, para que as alterações nelas sejam detetadas e persistam quando SaveChanges são chamadas. Outro efeito das consultas de rastreamento é que o EF deteta se uma instância já foi carregada para seus dados e retornará automaticamente essa instância rastreada em vez de retornar uma nova; Isso é chamado de resolução de identidade. Do ponto de vista do desempenho, o controlo de alterações significa o seguinte:

  • O EF mantém internamente um dicionário de instâncias monitoradas. Quando novos dados são carregados, o EF verifica o dicionário para ver se uma instância já está rastreada para a chave dessa entidade (resolução de identidade). A manutenção do dicionário e as pesquisas levam algum tempo ao carregar os resultados da consulta.
  • Antes de entregar uma instância carregada à aplicação, o EF faz instantâneos dessa instância e mantém o instantâneo internamente. Quando SaveChanges é chamada, a instância do aplicativo é comparada com o instantâneo para descobrir as alterações a serem persistidas. O snapshot ocupa mais memória, e o processo de snapshotting em si leva tempo; Às vezes, é possível especificar um comportamento de snapshotting diferente, possivelmente mais eficiente, por meio de comparadores de valor, ou usar proxies de controle de alterações para ignorar completamente o processo de snapshot (embora isso venha com seu próprio conjunto de desvantagens).

Em cenários somente leitura em que as alterações não são salvas de volta no banco de dados, a sobrecarga acima pode ser evitada usando consultas sem rastreamento de alterações. No entanto, como as consultas sem rastreamento não executam a resolução de identidade, uma linha de banco de dados que é referenciada por várias outras linhas carregadas será materializada como instâncias diferentes.

Para ilustrar, suponha que estamos carregando um grande número de Posts do banco de dados, bem como o Blog referenciado por cada Post. Se 100 Posts fizerem referência ao mesmo Blog, uma consulta de acompanhamento detetará isso por meio da resolução de identidade, e todas as instâncias de Post referenciarão a mesma instância de Blog eliminada de duplicação. Uma consulta sem rastreamento, em contraste, duplica o mesmo Blog 100 vezes - e o código do aplicativo deve ser escrito de acordo.

Aqui estão os resultados para um benchmark comparando o comportamento de rastreamento versus não rastreamento para uma consulta carregando 10 Blogs com 20 Posts cada. O código-fonte está disponível aqui, sinta-se à vontade para usá-lo como base para suas próprias medições.

Método NumBlogs NúmeroDePostsPorBlog Média Erro StdDev Mediana Proporção RácioSD Geração 0 Geração 1 Geração 2 Atribuído
AsTracking 10 20 1.414,7 US 27,20 EUA 45,44 EUA 1.405,5 US 1,00 0.00 60,5469 13.6719 - 380,11 KB
AsNoTracking 10 20 993,3 EUA 24.04 EUA 65,40 EUA 966,2 EUA 0.71 0.05 37.1094 6.8359 - 232,89 KB

Finalmente, é possível executar atualizações sem a sobrecarga do controle de alterações, utilizando uma consulta sem controle e, em seguida, anexando a instância retornada ao contexto, especificando quais alterações devem ser feitas. Isso transfere o ônus do controle de alterações do EF para o usuário e só deve ser tentado se a sobrecarga de controle de alterações tiver se mostrado inaceitável por meio de criação de perfis ou benchmarking.

Usando consultas SQL

Em alguns casos, existe um SQL mais otimizado para sua consulta, que o EF não gera. Isso pode acontecer quando a construção SQL é uma extensão específica para seu banco de dados que não é suportada, ou simplesmente porque o EF ainda não se traduz para ela. Nesses casos, escrever SQL manualmente pode fornecer um aumento substancial de desempenho, e o EF oferece suporte a várias maneiras de fazer isso.

  • Use consultas SQL diretamente em sua consulta, por exemplo, via FromSqlRaw. O EF até permite que você componha sobre o SQL com consultas LINQ regulares, permitindo que você expresse apenas uma parte da consulta em SQL. Esta é uma boa técnica quando o SQL só precisa ser usado em uma única consulta em sua base de código.
  • Defina uma função definida pelo usuário (UDF) e, em seguida, chame-a a partir de suas consultas. Observe que o EF permite que UDFs retornem conjuntos de resultados completos - eles são conhecidos como funções com valor de tabela (TVFs) - e também permite mapear a DbSet para uma função, tornando-a parecida com apenas outra tabela.
  • Defina uma exibição de banco de dados e consulte a partir dela em suas consultas. Observe que, ao contrário das funções, as exibições não podem aceitar parâmetros.

Observação

O SQL bruto geralmente deve ser usado como último recurso, depois de se certificar de que o EF não pode gerar o SQL desejado e quando o desempenho é importante o suficiente para que a consulta dada o justifique. O uso de SQL bruto traz desvantagens consideráveis de manutenção.

Programação assíncrona

Como regra geral, para que seu aplicativo seja escalável, é importante sempre usar APIs assíncronas em vez de síncronas (por exemplo SaveChangesAsync , em vez de SaveChanges). As APIs síncronas bloqueiam a thread durante a E/S do banco de dados, aumentando a necessidade de threads e o número de mudanças de contexto de thread que devem ocorrer.

Para obter mais informações, consulte a página sobre programação assíncrona.

Advertência

Evite misturar código síncrono e assíncrono na mesma aplicação - é muito fácil acionar inadvertidamente problemas subtis de exaustão do pool de threads.

Advertência

A implementação assíncrona do Microsoft.Data.SqlClient infelizmente tem alguns problemas conhecidos (por exemplo, #593, #601 e outros). Se você estiver vendo problemas de desempenho inesperados, tente usar a execução de comando de sincronização, especialmente ao lidar com texto grande ou valores binários.

Recursos adicionais