Partilhar via


Modelagem para desempenho

Em muitos casos, a maneira como você modela pode ter um impacto profundo no desempenho do seu aplicativo; Embora um modelo devidamente normalizado e "correto" seja geralmente um bom ponto de partida, em aplicações do mundo real alguns compromissos pragmáticos podem ajudar muito a alcançar um bom desempenho. Como é muito difícil alterar seu modelo quando um aplicativo está sendo executado em produção, vale a pena ter o desempenho em mente ao criar o modelo inicial.

Desnormalização e armazenamento em cache

A desnormalização é a prática de adicionar dados redundantes ao seu esquema, geralmente para eliminar junções ao consultar. Por exemplo, para um modelo com Blogs e Posts, em que cada Post tem uma Classificação, você pode ser solicitado a mostrar frequentemente a classificação média do Blog. A abordagem simples para isso seria agrupar os Posts por seu Blog e proceder ao cálculo da média como parte da consulta; mas isso requer uma junção dispendiosa entre as duas tabelas. A desnormalização adicionaria a média calculada de todos os posts a uma nova coluna no Blog, para que ela fique imediatamente acessível, sem ingressar ou calcular.

O acima pode ser visto como uma forma de cache - informações agregadas dos Posts são armazenadas em cache em seu Blog; E, como acontece com qualquer cache, o problema é como manter o valor armazenado em cache atualizado com os dados que ele está armazenando em cache. Em muitos casos, não há problema em que os dados armazenados em cache atrasem um pouco; Por exemplo, no exemplo acima, geralmente é razoável que a classificação média do blog não esteja completamente atualizada em um determinado momento. Se for esse o caso, você pode recalculá-lo de vez em quando; caso contrário, um sistema mais elaborado deve ser configurado para manter os valores armazenados em cache atualizados.

O seguinte detalha algumas técnicas de desnormalização e armazenamento em cache no EF Core e aponta para as seções relevantes na documentação.

Colunas computadas armazenadas

Se os dados a serem armazenados em cache forem um produto de outras colunas na mesma tabela, uma coluna computada armazenada pode ser uma solução perfeita. Por exemplo, um Customer pode ter FirstName e LastName colunas, mas podemos precisar pesquisar pelo nome completo do cliente. Uma coluna computada armazenada é mantida automaticamente pelo banco de dados - que a recalcula sempre que a linha é alterada - e você pode até definir um índice sobre ela para acelerar as consultas.

Atualizar colunas de cache quando as entradas são alteradas

Se a coluna em cache precisar fazer referência a entradas de fora da linha da tabela, não será possível usar colunas computadas. No entanto, ainda é possível recalcular a coluna sempre que sua entrada muda; por exemplo, você pode recalcular a classificação média do Blog sempre que uma publicação for alterada, adicionada ou removida. Certifique-se de identificar as condições exatas quando o recálculo for necessário, caso contrário, o valor armazenado em cache ficará fora de sincronia.

Uma maneira de fazer isso é executar a atualização por conta própria, por meio da API EF Core regular. SaveChanges Eventos ou intercetadores podem ser usados para verificar automaticamente se algum Post está a ser atualizado e para realizar o recálculo dessa forma. Observe que isso normalmente implica viagens de ida e volta adicionais ao banco de dados, pois comandos adicionais devem ser enviados.

Para aplicativos mais sensíveis a perf, os gatilhos de banco de dados podem ser definidos para executar automaticamente o recálculo no banco de dados. Isso salva as viagens de ida e volta extras do banco de dados, ocorre automaticamente dentro da mesma transação da atualização principal e pode ser mais simples de configurar. O EF não fornece nenhuma API específica para criar ou manter gatilhos, mas não há problema em criar uma migração vazia e adicionar a definição de gatilho via SQL bruto.

Visualizações materializadas/indexadas

As visualizações materializadas (ou indexadas) são semelhantes às exibições regulares, exceto que seus dados são armazenados em disco ("materializados"), em vez de calculados toda vez que a exibição é consultada. Tais visualizações são conceitualmente semelhantes às colunas computadas armazenadas, pois armazenam em cache os resultados de cálculos potencialmente caros; no entanto, eles armazenam em cache o conjunto de resultados de uma consulta inteira em vez de uma única coluna. As visualizações materializadas podem ser consultadas como qualquer tabela normal e, como são armazenadas em cache no disco, essas consultas são executadas de forma muito rápida e barata, sem ter que executar constantemente os cálculos caros da consulta que define a exibição.

O suporte específico para visualizações materializadas varia entre bancos de dados. Em alguns bancos de dados (por exemplo, PostgreSQL), as visualizações materializadas devem ser atualizadas manualmente para que seus valores sejam sincronizados com suas tabelas subjacentes. Isso geralmente é feito por meio de um temporizador - nos casos em que algum atraso de dados é aceitável - ou através de um gatilho ou chamada de procedimento armazenado em condições específicas. As Exibições Indexadas do SQL Server, por outro lado, são atualizadas automaticamente à medida que suas tabelas subjacentes são modificadas; Isso garante que a exibição sempre mostre os dados mais recentes, ao custo de atualizações mais lentas. Além disso, as Exibições de Índice do SQL Server têm várias restrições sobre o que elas suportam; Consulte a documentação para obter mais informações.

Atualmente, o EF não fornece nenhuma API específica para criar ou manter visualizações, materializadas/indexadas ou de outra forma; mas não há problema em criar uma migração vazia e adicionar a definição de exibição via SQL bruto.

Mapeamento de herança

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

Atualmente, o EF Core oferece suporte a três técnicas para mapear um modelo de herança para um banco de dados relacional:

  • Tabela por hierarquia (TPH), na qual toda uma hierarquia de classes do .NET é mapeada para uma única tabela de banco de dados.
  • Tabela por tipo (TPT), na qual cada tipo na hierarquia do .NET é mapeado para uma tabela diferente no banco de dados.
  • Tabela por tipo de concreto (TPC), na qual cada tipo de concreto na hierarquia do .NET é mapeado para uma tabela diferente no banco de dados, onde cada tabela contém colunas para todas as propriedades do tipo correspondente.

A escolha da técnica de mapeamento de herança pode ter um impacto considerável no desempenho do aplicativo - recomenda-se medir cuidadosamente antes de se comprometer com uma escolha.

Intuitivamente, o TPT pode parecer a técnica "mais limpa"; uma tabela separada para cada tipo .NET faz com que o esquema de banco de dados seja semelhante à hierarquia de tipos .NET. Além disso, como o TPH deve representar toda a hierarquia em uma única tabela, as linhas têm todas as colunas, independentemente do tipo que realmente está representado na linha, e as colunas não relacionadas estão sempre vazias e não são nunca utilizadas. Além de parecer uma técnica de mapeamento "imprecisa" ou "problemática", muitos acreditam que essas colunas vazias ocupam espaço considerável no banco de dados e também podem prejudicar o desempenho.

Sugestão

Se o seu sistema de banco de dados oferecer suporte a ele (e.g. SQL Server), considere usar "colunas esparsas" para colunas TPH que raramente serão preenchidas.

No entanto, a medição mostra que o TPT é, na maioria dos casos, a técnica de mapeamento inferior do ponto de vista do desempenho; onde todos os dados no TPH vêm de uma única tabela, as consultas TPT devem unir várias tabelas e as junções são uma das principais fontes de problemas de desempenho em bancos de dados relacionais. Os bancos de dados também tendem a lidar bem com colunas vazias, e recursos como colunas esparsas do SQL Server podem reduzir ainda mais essa sobrecarga.

O TPC tem características de desempenho semelhantes ao TPH, mas é ligeiramente mais lento ao selecionar entidades de todos os tipos, pois envolve várias tabelas. No entanto, o TPC realmente se destaca ao consultar entidades de um único tipo de folha - a consulta usa apenas uma única tabela e não precisa de filtragem.

Para um exemplo concreto, ver este benchmark que estabelece um modelo simples com uma hierarquia de 7 tipos; 5000 linhas são semeadas para cada tipo - totalizando 35000 linhas - e o benchmark simplesmente carrega todas as linhas do banco de dados:

Método Média Erro StdDev Geração 0 Geração 1 Atribuído
TPH 149,0 ms 3,38 ms 9,80 ms 4000.0000 1000.0000 40 MB
TPT 312,9 ms 6,17 ms 10,81 ms 9000.0000 3000.0000 75 MB
TPC 158,2 ms 3,24 ms 8,88 ms 5000.0000 2000.0000 46 MB

Como se pode observar, TPH e TPC são consideravelmente mais eficientes do que TPT para este cenário. Observe que os resultados reais sempre dependem da consulta específica que está sendo executada e do número de tabelas na hierarquia, portanto, outras consultas podem mostrar uma lacuna de desempenho diferente; Você é incentivado a usar esse código de referência como um modelo para testar outras consultas.