Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Problemas de desempenho com consultas únicas
Ao trabalhar em bancos de dados relacionais, o EF carrega entidades relacionadas introduzindo JOINs em uma única consulta. Embora os JOINs sejam bastante padrão ao usar SQL, eles podem criar problemas de desempenho significativos se usados incorretamente. Esta página descreve esses problemas de desempenho e mostra uma maneira alternativa de carregar entidades relacionadas que os contornam.
Explosão cartesiana
Vamos examinar a seguinte consulta LINQ e seu equivalente SQL traduzido:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title], [c].[Id], [c].[BlogId], [c].[FirstName], [c].[LastName]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Contributors] AS [c] ON [b].[Id] = [c].[BlogId]
ORDER BY [b].[Id], [p].[Id]
Neste exemplo, como tanto Posts quanto Contributors são navegações de coleções de Blog - encontram-se no mesmo nível - os bancos de dados relacionais retornam um produto cruzado: cada linha de Posts é unida a cada linha de Contributors. Isso significa que, se um determinado blog tiver 10 postagens e 10 contribuidores, o banco de dados retornará 100 linhas para esse único blog. Esse fenômeno - às vezes chamado de explosão cartesiana - pode fazer com que enormes quantidades de dados sejam transferidas involuntariamente para o cliente, especialmente à medida que mais JOINs irmãos são adicionados à consulta; Isso pode ser um grande problema de desempenho em aplicativos de banco de dados.
Observe que a explosão cartesiana não ocorre quando as duas JOINs não estão no mesmo nível:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Title], [t].[Id0], [t].[Content], [t].[PostId]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Comment] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY [b].[Id], [t].[Id]
Nesta consulta, Comments é uma navegação de coleção de Post, ao contrário Contributors da consulta anterior, que era uma navegação de coleção de Blog. Nesse caso, uma única linha é retornada para cada comentário que um blog tem (através de suas postagens), e um produto cruzado não ocorre.
Duplicação de dados
As JOINs podem criar outro tipo de problema de desempenho. Vamos examinar a seguinte consulta, que carrega apenas uma única navegação de coleção:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.ToListAsync();
SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]
Examinando as colunas projetadas, cada linha retornada por esta consulta contém propriedades das tabelas Blogs e Posts, o que significa que as propriedades do blog são duplicadas para cada postagem que o blog contém. Embora isso geralmente seja normal e não cause problemas, se a Blogs tabela tiver uma coluna muito grande (por exemplo, dados binários ou um texto enorme), essa coluna será duplicada e enviada de volta ao cliente várias vezes. Isso pode aumentar significativamente o tráfego de rede e afetar negativamente o desempenho do seu aplicativo.
Se você realmente não precisa da coluna enorme, é fácil simplesmente não consultá-la:
var blogs = await ctx.Blogs
.Select(b => new
{
b.Id,
b.Name,
b.Posts
})
.ToListAsync();
Usando uma projeção para escolher explicitamente quais colunas você deseja, você pode omitir colunas grandes e melhorar o desempenho; Observe que essa é uma boa ideia, independentemente da duplicação de dados, portanto, considere fazê-lo mesmo quando não estiver carregando uma navegação de coleção. No entanto, como isso projeta o blog para um tipo anônimo, o blog não é rastreado pelo EF e as alterações nele não podem ser salvas como de costume.
Vale a pena notar que, ao contrário da explosão cartesiana, a duplicação de dados causada por JOINs normalmente não é significativa, pois o tamanho dos dados duplicados é insignificante; Isso normalmente é algo para se preocupar apenas se você tiver colunas grandes em sua tabela principal.
Dividir consultas
Para contornar os problemas de desempenho descritos acima, o EF permite especificar que uma determinada consulta LINQ deve ser dividida em várias consultas SQL. Em vez de JOINs, consultas divididas geram uma consulta SQL adicional para cada navegação de coleção incluída:
using (var context = new BloggingContext())
{
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSplitQuery()
.ToListAsync();
}
Ele produzirá o seguinte SQL:
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]
SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]
Advertência
Ao usar consultas divididas com Skip/Take em versões EF anteriores a 10, preste especial atenção para tornar sua ordem de consulta totalmente exclusiva; Não fazer isso pode fazer com que dados incorretos sejam retornados. Por exemplo, se os resultados são ordenados apenas por data, mas pode haver vários resultados com a mesma data, então cada uma das consultas divididas pode obter resultados diferentes do banco de dados. Encomendar por data e ID (ou qualquer outra propriedade exclusiva ou combinação de propriedades) torna a encomenda totalmente única e evita este problema. Observe que os bancos de dados relacionais não aplicam nenhuma ordenação por padrão, mesmo na chave primária.
Observação
As entidades relacionadas individualmente são sempre carregadas por meio de JOINs na mesma consulta, pois isso não tem impacto no desempenho.
Habilitando consultas divididas globalmente
Você também pode configurar consultas divididas como padrão para o contexto do seu aplicativo:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}
Quando as consultas divididas são configuradas como padrão, ainda é possível configurar consultas específicas para serem executadas como consultas únicas:
using (var context = new SplitQueriesBloggingContext())
{
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSingleQuery()
.ToListAsync();
}
O EF Core usa o modo de consulta única por padrão na ausência de qualquer configuração. Como pode causar problemas de desempenho, o EF Core gera um aviso sempre que as seguintes condições são atendidas:
- O EF Core deteta que a consulta carrega várias coleções.
- O usuário não configurou o modo de divisão de consulta globalmente.
- O usuário não usou
AsSingleQuery/AsSplitQueryo operador na consulta.
Para desativar o aviso, configure o modo de divisão de consulta globalmente ou no nível da consulta para um valor apropriado.
Características das consultas divididas
Embora a consulta dividida evite os problemas de desempenho associados a JOINs e explosão cartesiana, ela também tem algumas desvantagens:
- Embora a maioria dos bancos de dados garanta a consistência dos dados para consultas únicas, essas garantias não existem para consultas múltiplas. Se o banco de dados for atualizado simultaneamente ao executar suas consultas, os dados resultantes podem não ser consistentes. Você pode mitigar isso encapsulando as consultas em uma transação do tipo serializável ou de instantâneo, embora isso possa criar novos problemas de desempenho. Para obter mais informações, consulte a documentação do banco de dados.
- Cada consulta implica atualmente uma viagem de ida e volta de rede adicional para o seu banco de dados. Várias viagens de ida e volta de rede podem degradar o desempenho, especialmente quando a latência para o banco de dados é alta (por exemplo, serviços em nuvem).
- Enquanto alguns bancos de dados permitem consumir os resultados de várias consultas ao mesmo tempo (SQL Server com MARS, Sqlite), a maioria permite que apenas uma única consulta esteja ativa em um determinado ponto. Portanto, todos os resultados de consultas anteriores devem ser armazenados em buffer na memória do seu aplicativo antes de executar consultas posteriores, o que leva a um aumento dos requisitos de memória.
- Ao incluir navegações de referência, bem como navegações de coleção, cada uma das consultas divididas incluirá junções às navegações de referência. Isso pode prejudicar o desempenho, especialmente se houver muitas navegações de referência. Por favor, vote #29182 se isso é algo que você gostaria de ver corrigido.
Infelizmente, não há uma estratégia para carregar entidades relacionadas que se adapte a todos os cenários. Considere cuidadosamente as vantagens e desvantagens de consultas únicas e divididas para selecionar a que atende às suas necessidades.