Modelagem para desempenho

Em muitos casos, a maneira como você modela pode afetar profundamente o desempenho do seu aplicativo. Embora um modelo adequadamente normalizado e "correto" geralmente seja um bom ponto de partida, em aplicativos reais alguns compromissos pragmáticos podem demorar para alcançar um bom desempenho. Como é muito difícil alterar o modelo depois que um aplicativo está sendo executado em produção, vale a pena considerar o desempenho ao criar o modelo inicial.

Desnormalização e cache

A desnormalização é a prática de adicionar dados redundantes ao esquema, geralmente para eliminar junções durante consultas. Por exemplo, em um modelo com blogs e postagens, no qual cada postagem tem uma classificação, talvez seja necessário mostrar com frequência a classificação média do blog. A abordagem simples para isso seria agrupar as postagens por blogs e calcular a média como parte da consulta, no entanto, isso exige uma junção de alto custo entre as duas tabelas. A desnormalização adicionaria a média calculada de todas as postagens a uma nova coluna no blog, para que assim ela fique imediatamente acessível, sem junção ou cálculo.

O descrito acima pode ser exibido como uma forma de cache – as informações de agregação das postagens são armazenadas em cache no blog; e, assim como em todo 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 tem problema os dados armazenados em cache terem um pouco de atraso. Como no exemplo acima, geralmente é razoável que a classificação média do blog não esteja completamente atualizada em um determinado momento. Se esse for 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 conteúdo a seguir detalha algumas técnicas de desnormalização e armazenamento em cache no EF Core e aponta quais são 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 poderá ser uma solução perfeita. Por exemplo, um Customer pode ter colunas FirstName e LastName, mas pode ser necessário pesquisar pelo nome completo do cliente. Uma coluna computada armazenada é mantida automaticamente pelo banco de dados, que a recalcula sempre que a linha é alterada, além disso, você pode até definir um índice sobre ela a fim de acelerar as consultas.

Atualizar colunas de cache quando as entradas forem alteradas

Se a coluna armazenada em cache precisar usar como referência as entradas de fora da linha da tabela, não será possível usar colunas computadas. No entanto, ainda será possível recalcular a coluna sempre que sua entrada for alterada, por exemplo, você poderá recalcular a classificação média do blog sempre que uma postagem for alterada, adicionada ou removida. Não deixe de identificar as condições exatas quando for necessário recalcular, 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 do EF Core comum. SaveChangesEventos ou interceptores podem ser usados para verificar automaticamente se alguma postagem está sendo atualizada e executar o recálculo dessa forma. Observe, isso normalmente envolve viagens de ida e volta de banco de dados adicionais, pois mais comandos devem ser enviados.

Em aplicativos mais suscetíveis ao desempenho, os gatilhos de banco de dados podem ser definidos para executar automaticamente o recálculo no banco de dados. Isso evita as idas e voltas adicionais do banco de dados, ocorre automaticamente na 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 é ótimo para criar uma migração vazia e adicionar a definição de gatilho por meio de SQL bruta.

Exibições materializadas/indexadas

Exibições materializadas (ou indexadas) são semelhantes a exibições regulares, exceto que seus dados são armazenados em disco ("materializado"), em vez de calculados sempre que a exibição é consultada. Essas exibiçõ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. Exibições materializadas podem ser consultadas da mesma forma que qualquer tabela comum e, como são armazenadas em cache em disco, essas consultas são executadas de forma muito rápida e barata sem precisar executar constantemente os cálculos caros da consulta que define a exibição.

O suporte específico para exibições materializadas varia entre bancos de dados. Em alguns bancos de dados (por exemplo, PostgreSQL), as exibições materializadas devem ser atualizadas manualmente para que seus valores sejam sincronizados com suas tabelas subjacentes. Isso normalmente é feito por meio de um temporizador - em casos em que algum atraso de dados é aceitável - ou por meio de uma chamada de gatilho ou procedimento armazenado em condições específicas. 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 suporte; consulte a documentação para obter mais informações.

Atualmente, o EF não fornece nenhuma API específica para criar ou manter exibições, materializadas/indexadas ou não, mas é perfeitamente possível criar uma migração vazia e adicionar a definição de exibição por meio de SQL bruto.

Mapeamento de herança

É recomendável ler a página dedicada sobre herança antes de continuar nesta seção.

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

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

A escolha da técnica de mapeamento de herança pode afetar consideravelmente o desempenho do aplicativo. É recomendável ponderar cuidadosamente antes de se comprometer com uma escolha.

Intuitivamente, a TPT pode parecer a técnica "mais limpa", pois uma tabela separada para cada tipo .NET faz com que o esquema de banco de dados se pareça com a hierarquia de tipos do .NET. Além disso, como a TPH deve representar toda a hierarquia em uma única tabela, as linhas têm todas as colunas, independentemente do tipo que está sendo mantido na linha e as colunas não relacionadas estão sempre vazias e sem uso. Além de parecer ser uma técnica de mapeamento "poluída", acredita-se que essas colunas vazias ocupam espaço considerável no banco de dados e também podem prejudicar o desempenho.

Dica

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

No entanto, a medição mostra que a TPT é, na maioria dos casos, a técnica de mapeamento inferior do ponto de vista de desempenho, pois enquanto todos os dados na TPH partem de uma única tabela, as consultas da 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. Geralmente, os bancos de dados também tendem a lidar bem com colunas vazias e recursos como as colunas esparsas do SQL Server, podem reduzir ainda mais essa sobrecarga.

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

Para obter um exemplo concreto, confira este parâmetro de comparação que configura um modelo simples com uma hierarquia de sete tipos. Cinco mil linhas são semeadas para cada tipo, totalizando 35 mil linhas e o parâmetro de comparação simplesmente carrega todas as linhas do banco de dados:

Método Média Erro StdDev Geração 0 Gen 1 Alocado
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 pode ser visto, a TPH e a TPC são consideravelmente mais eficientes do que a TPT para esse cenário. Observe, 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 falta de desempenho diferente. O uso desse código de parâmetro de comparação é incentivado como um modelo para testar outras consultas.