database-performance

Padrões de acesso a banco de dados para performance. Separação de modelos de leitura e escrita, evitar consultas N+1, usar AsNoTracking, aplicar limites de linhas e nunca fazer joins no lado da aplicação. Funciona com EF Core e Dapper.

O que é database-performance?

Este skill fornece padrões de acesso a banco de dados focados em performance para aplicações que usam EF Core e Dapper. Ele aborda práticas essenciais como separação de modelos de leitura e escrita (CQRS), aplicação de limites de linhas em todas as consultas, uso de AsNoTracking para consultas somente leitura, e evitar consultas N+1. Também cobre tópicos avançados como evitar explosões cartesianas com AsSplitQuery, restringir tamanhos de colunas, e a importância de não construir repositórios genéricos. O skill documenta interfaces de exemplo tanto para leitura quanto para escrita, compara o uso de EF Core vs Dapper com base no cenário, e fornece uma referência rápida de anti-padrões e soluções, tudo com exemplos de código em C#.

Funciona com~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/database-performance

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Padrões de Performance em Banco de Dados

Quando Usar Este Skill

Use este skill quando:

  • Projetando camadas de acesso a dados
  • Otimizando consultas lentas de banco de dados
  • Escolhendo entre EF Core e Dapper
  • Evitando armadilhas comuns de performance

Princípios Fundamentais

  1. Separe modelos de leitura e escrita – Não use os mesmos tipos para ambos
  2. Pense em lotes – Evite consultas N+1
  3. Recupere apenas o que precisa – Nada de SELECT *
  4. Aplique limites de linhas – Sempre tenha um Take/Limit configurável
  5. Faça joins no SQL – Nunca no código da aplicação
  6. AsNoTracking para leituras – O rastreamento de alterações do EF Core é caro

Separação de Modelos de Leitura/Escrita (Padrão CQRS)

Modelos de leitura e escrita são fundamentalmente diferentes – eles têm formas, colunas e propósitos distintos. Não crie uma única entidade "User" e reutilize-a em todos os lugares.

  • Modelos de leitura são desnormalizados, otimizados para eficiência de consulta e retornam múltiplos tipos de projeção (UserProfile, UserSummary, UserDetailForAdmin)
  • Modelos de escrita são normalizados, focados em validação e aceitam comandos fortemente tipados (CreateUserCommand, UpdateUserCommand)

Arquitetura

src/
  MyApp.Data/
    Users/
      # Lado de leitura – múltiplas projeções otimizadas
      IUserReadStore.cs
      PostgresUserReadStore.cs

      # Lado de escrita – handlers de comando
      IUserWriteStore.cs
      PostgresUserWriteStore.cs

      # DTOs de leitura – leves, desnormalizados
      UserProfile.cs
      UserSummary.cs

      # Comandos de escrita – focados em validação
      CreateUserCommand.cs
      UpdateUserCommand.cs
    Orders/
      IOrderReadStore.cs
      IOrderWriteStore.cs
      (estrutura similar...)

Interface do Armazenamento de Leitura

// Modelos de leitura: Múltiplas projeções especializadas otimizadas para diferentes casos de uso
public interface IUserReadStore
{
    // Retorna perfil detalhado para visualização de usuário único
    Task<UserProfile?> GetByIdAsync(UserId id, CancellationToken ct = default);

    // Retorna informações leves para buscas
    Task<UserProfile?> GetByEmailAsync(EmailAddress email, CancellationToken ct = default);

    // Retorna sumários paginados – apenas o que a visualização de lista precisa
    Task<IReadOnlyList<UserSummary>> GetAllAsync(int limit, UserId? cursor = null, CancellationToken ct = default);

    // Consulta booleana – nenhuma entidade necessária
    Task<bool> EmailExistsAsync(EmailAddress email, CancellationToken ct = default);
}

Interface do Armazenamento de Escrita

// Modelo de escrita: Aceita comandos fortemente tipados, valores de retorno mínimos
public interface IUserWriteStore
{
    // Retorna apenas o ID criado – quem chama não precisa da entidade completa
    Task<UserId> CreateAsync(CreateUserCommand command, CancellationToken ct = default);

    // Atualização valida o comando, retorna void (sucesso ou lança exceção)
    Task UpdateAsync(UserId id, UpdateUserCommand command, CancellationToken ct = default);

    // Exclusão é simples e explícita
    Task DeleteAsync(UserId id, CancellationToken ct = default);
}

Diferenças estruturais chave ilustradas:

  • O armazenamento de leitura retorna múltiplos DTOs diferentes (UserProfile, UserSummary, flag bool)
  • O armazenamento de escrita retorna dados mínimos (apenas UserId na criação) ou void
  • Consultas de leitura são projeções sem estado – nenhum rastreamento necessário
  • Operações de escrita focam na validação do comando, não na recuperação de dados posteriormente
  • Diferentes bancos de dados/tabelas podem suportar leitura vs escrita (padrão de consistência eventual)

Sempre Aplique Limites de Linhas

Nunca retorne conjuntos de resultados sem limites. Cada método de leitura deve ter um limite configurável.

Padrão: Parâmetro de Limite

public interface IOrderReadStore
{
    // Limite é obrigatório, não opcional
    Task<IReadOnlyList<OrderSummary>> GetByCustomerAsync(
        CustomerId customerId,
        int limit,
        OrderId? cursor = null,
        CancellationToken ct = default);
}

// Implementação
public async Task<IReadOnlyList<OrderSummary>> GetByCustomerAsync(
    CustomerId customerId,
    int limit,
    OrderId? cursor = null,
    CancellationToken ct = default)
{
    await using var connection = await _dataSource.OpenConnectionAsync(ct);

    const string sql = """
        SELECT id, customer_id, total, status, created_at
        FROM orders
        WHERE customer_id = @CustomerId
        AND (@Cursor IS NULL OR created_at < (SELECT created_at FROM orders WHERE id = @Cursor))
        ORDER BY created_at DESC
        LIMIT @Limit
        """;

    var rows = await connection.QueryAsync<OrderRow>(sql, new
    {
        CustomerId = customerId.Value,
        Cursor = cursor?.Value,
        Limit = limit
    });

    return rows.Select(r => r.ToOrderSummary()).ToList();
}

EF Core com Paginação

public async Task<PaginatedList<OrderSummary>> GetOrdersAsync(
    CustomerId customerId,
    Paginator paginator,
    CancellationToken ct = default)
{
    var query = _context.Orders
        .AsNoTracking()
        .Where(o => o.CustomerId == customerId.Value)
        .OrderByDescending(o => o.CreatedAt);

    var totalCount = await query.CountAsync(ct);

    var orders = await query
        .Skip((paginator.PageNumber - 1) * paginator.PageSize)
        .Take(paginator.PageSize)  // Sempre limitar!
        .Select(o => new OrderSummary(
            new OrderId(o.Id),
            o.Total,
            o.Status,
            o.CreatedAt))
        .ToListAsync(ct);

    return new PaginatedList<OrderSummary>(
        orders,
        totalCount,
        paginator.PageSize,
        paginator.PageNumber);
}

AsNoTracking para Consultas de Leitura

O rastreamento de alterações do EF Core é caro. Desabilite-o para consultas somente leitura.

// FAÇA: Desabilitar rastreamento para leituras
var users = await _context.Users
    .AsNoTracking()
    .Where(u => u.IsActive)
    .ToListAsync();

// NÃO FAÇA: Rastrear entidades que você não vai modificar
var users = await _context.Users
    .Where(u => u.IsActive)
    .ToListAsync();  // Rastreamento de alterações habilitado – desperdício

Configurar Comportamento Padrão

// Para aplicações com muitas leituras, considere isso no DbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

Então habilite o rastreamento explicitamente quando necessário:

var user = await _context.Users
    .AsTracking()  // Explícito – pretendemos modificar
    .FirstOrDefaultAsync(u => u.Id == userId);

Evite Consultas N+1

O problema N+1: buscar uma lista e depois consultar os dados relacionados de cada item.

O Problema

// RUIM: Consultas N+1
var orders = await _context.Orders.ToListAsync();

foreach (var order in orders)
{
    // Cada iteração atinge o banco de dados!
    var items = await _context.OrderItems
        .Where(i => i.OrderId == order.Id)
        .ToListAsync();
}

Solução 1: Include (EF Core)

// BOM: Única consulta com join
var orders = await _context.Orders
    .AsNoTracking()
    .Include(o => o.Items)
    .ToListAsync();

Solução 2: Consulta em Lote (Dapper)

// BOM: Duas consultas, sem N+1
const string sql = """
    SELECT id, customer_id, total FROM orders WHERE customer_id = @CustomerId;
    SELECT oi.* FROM order_items oi
    INNER JOIN orders o ON oi.order_id = o.id
    WHERE o.customer_id = @CustomerId;
    """;

using var multi = await connection.QueryMultipleAsync(sql, new { CustomerId = customerId });
var orders = (await multi.ReadAsync<OrderRow>()).ToList();
var items = (await multi.ReadAsync<OrderItemRow>()).ToList();

// Join em memória (aceitável – dados já foram buscados)
foreach (var order in orders)
{
    order.Items = items.Where(i => i.OrderId == order.Id).ToList();
}

Nunca Faça Joins no Lado da Aplicação

Joins devem acontecer no SQL, não no C#.

// RUIM: Join na aplicação – duas consultas, desperdício de memória
var customers = await _context.Customers.ToListAsync();
var orders = await _context.Orders.ToListAsync();

var result = customers.Select(c => new
{
    Customer = c,
    Orders = orders.Where(o => o.CustomerId == c.Id).ToList()  // O(n*m) em memória!
});

// BOM: Join no SQL – única consulta
var result = await _context.Customers
    .AsNoTracking()
    .Include(c => c.Orders)
    .ToListAsync();

// BOM: Join explícito (Dapper)
const string sql = """
    SELECT c.id, c.name, COUNT(o.id) as order_count
    FROM customers c
    LEFT JOIN orders o ON c.id = o.customer_id
    GROUP BY c.id, c.name
    """;

Evite Explosões Cartesianas

Múltiplas chamadas a Include podem causar produtos cartesianos.

// PERIGOSO: Pode explodir em milhões de linhas
var product = await _context.Products
    .Include(p => p.Reviews)      // 100 reviews
    .Include(p => p.Images)       // 20 imagens
    .Include(p => p.Categories)   // 5 categorias
    .FirstOrDefaultAsync(p => p.Id == id);
// Resultado: 100 * 20 * 5 = 10.000 linhas transferidas!

Solução: Consultas Divididas

// BOM: Múltiplas consultas, sem explosão cartesiana
var product = await _context.Products
    .AsSplitQuery()
    .Include(p => p.Reviews)
    .Include(p => p.Images)
    .Include(p => p.Categories)
    .FirstOrDefaultAsync(p => p.Id == id);
// Resultado: 4 consultas separadas, ~125 linhas no total

Solução: Projeção Explícita

// MELHOR: Busque apenas o que precisa
var product = await _context.Products
    .AsNoTracking()
    .Where(p => p.Id == id)
    .Select(p => new ProductDetail(
        p.Id,
        p.Name,
        p.Description,
        p.Reviews.OrderByDescending(r => r.CreatedAt).Take(10).ToList(),
        p.Images.Take(5).ToList(),
        p.Categories.Select(c => c.Name).ToList()))
    .FirstOrDefaultAsync();

Restrinja Tamanhos de Colunas

Defina comprimentos máximos no seu modelo EF Core para evitar dados superdimensionados.

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.Property(u => u.Email)
            .HasMaxLength(254)  // Limite RFC 5321
            .IsRequired();

        builder.Property(u => u.Name)
            .HasMaxLength(100)
            .IsRequired();

        builder.Property(u => u.Bio)
            .HasMaxLength(500);

        // Para conteúdo realmente grande, use explicitamente o tipo text
        builder.Property(u => u.Notes)
            .HasColumnType("text");
    }
}

Não Construa Repositórios Genéricos

Repositórios genéricos escondem a complexidade das consultas e dificultam a otimização.

// RUIM: Repositório genérico
public interface IRepository<T>
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();  // Sem limite!
    Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);  // Não pode otimizar
}

// BOM: Armazenamentos de leitura específicos
public interface IOrderReadStore
{
    Task<OrderDetail?> GetByIdAsync(OrderId id, CancellationToken ct = default);
    Task<IReadOnlyList<OrderSummary>> GetByCustomerAsync(CustomerId id, int limit, CancellationToken ct = default);
    Task<IReadOnlyList<OrderSummary>> GetPendingAsync(int limit, CancellationToken ct = default);
}

Problemas com repositórios genéricos:

  • Não é possível otimizar consultas específicas
  • Nenhuma maneira de impor limites
  • Escondem problemas N+1
  • Facilitam buscar dados demais
  • Incentivam pensamento preguiçoso sobre acesso a dados

Dapper para Cargas de Trabalho com Muitas Leituras

Para consultas de leitura complexas, Dapper com SQL explícito geralmente é mais limpo e rápido.

public sealed class PostgresUserReadStore : IUserReadStore
{
    private readonly NpgsqlDataSource _dataSource;

    public PostgresUserReadStore(NpgsqlDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task<UserProfile?> GetByIdAsync(UserId id, CancellationToken ct = default)
    {
        await using var connection = await _dataSource.OpenConnectionAsync(ct);

        const string sql = """
            SELECT id, email, name, bio, created_at
            FROM users
            WHERE id = @Id
            """;

        var row = await connection.QuerySingleOrDefaultAsync<UserRow>(
            sql, new { Id = id.Value });

        return row?.ToUserProfile();
    }

    // Tipo de linha interno para mapeamento Dapper
    private sealed class UserRow
    {
        public Guid id { get; set; }
        public string email { get; set; } = null!;
        public string name { get; set; } = null!;
        public string? bio { get; set; }
        public DateTime created_at { get; set; }

        public UserProfile ToUserProfile() => new(
            Id: new UserId(id),
            Email: new EmailAddress(email),
            Name: new PersonName(name),
            Bio: bio,
            CreatedAt: new DateTimeOffset(created_at, TimeSpan.Zero));
    }
}

Quando Usar EF Core vs Dapper

CenárioRecomendação
CRUD simplesEF Core
Consultas de leitura complexasDapper
Escritas com validaçãoEF Core
Operações em loteDapper ou SQL bruto
Relatórios/analíticaDapper
Escritas com domínio pesadoEF Core

Você pode usar ambos no mesmo projeto – EF Core para escritas, Dapper para leituras.


Referência Rápida

Anti-PadrãoSolução
Sem limite de linhasAdicione parâmetro limit em todo método de leitura
SELECT *Projete apenas as colunas necessárias
Consultas N+1Use Include ou consultas em lote
Joins na aplicaçãoFaça joins no SQL
Explosão cartesianaUse AsSplitQuery ou projeção
Rastreamento de dados somente leituraUse AsNoTracking
Repositório genéricoArmazenamentos de leitura/escrita específicos
Strings sem limitesConfigure MaxLength no modelo

Recursos

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

Habilidades Relacionadas