CommunityCodierung & Entwicklunggithub.com

database-performance

Datenbank-Zugriffsmuster für optimale Leistung. Trennung von Lese-/Schreibmodellen, Vermeidung von N+1-Abfragen, Einsatz von AsNoTracking, Begrenzung von Zeilen und keine applikationsseitigen Joins. Funktioniert mit EF Core und Dapper.

Was ist database-performance?

Dieses Skill bietet eine umfassende Sammlung von Datenbank-Performance-Patterns für .NET-Entwickler, die mit EF Core und Dapper arbeiten. Es legt den Fokus auf die klare Trennung von Lese- und Schreibmodellen (CQRS-Pattern), um unterschiedliche Optimierungsziele zu erreichen: Lesemodelle sind denormalisiert und abfrageoptimiert, Schreibmodelle validierungsorientiert und normalisiert. Zu den wichtigsten Prinzipien gehören die strikte Begrenzung von Ergebniszeilen durch konfigurierbare 'limit'-Parameter, das Deaktivieren des Change-Trackings in EF Core mit 'AsNoTracking' für rein lesende Abfragen und die Vermeidung von N+1-Problemen durch batching oder 'Include'. Es wird ausdrücklich empfohlen, Joins immer in SQL durchzuführen, da applikationsseitige Verknüpfungen zu hohem Speicherverbrauch führen. Das Skill zeigt praktische Code-Beispiele für CQRS-konforme Interfaces, Dapper-Implementierungen mit explizitem SQL und Lösungen für kartesische Explosionen (z. B. 'AsSplitQuery' oder explizite Projektionen). Es ist sowohl für EF Core als auch Dapper geeignet und gibt eine klare Entscheidungsmatrix, wann welches Tool verwendet werden sollte.

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

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Datenbank-Performance-Patterns

Wann Du Dieses Skill Verwenden Solltest

Verwende dieses Skill, wenn:

  • Du Datenzugriffsschichten entwirfst
  • Du langsame Datenbankabfragen optimierst
  • Du zwischen EF Core und Dapper wählst
  • Du häufige Performance-Fallen vermeiden möchtest

Kernprinzipien

  1. Lese- und Schreibmodelle trennen – Verwende nicht dieselben Typen für beides
  2. In Batches denken – Vermeide N+1-Abfragen
  3. Nur das abrufen, was benötigt wird – Kein SELECT *
  4. Zeilenlimiten anwenden – Immer ein konfigurierbares Take/Limit haben
  5. Joins in SQL durchführen – Nie im Anwendungscode
  6. AsNoTracking für Lesezugriffe – EF Core Change Tracking ist teuer

Trennung von Lese-/Schreibmodellen (CQRS-Pattern)

Lese- und Schreibmodelle unterscheiden sich grundlegend – sie haben unterschiedliche Formen, Spalten und Zwecke. Erstelle kein einzelnes „User"-Entity und verwende es überall wieder.

  • Lesemodelle sind denormalisiert, auf Abfrageeffizienz optimiert und geben mehrere Projektionstypen zurück (UserProfile, UserSummary, UserDetailForAdmin)
  • Schreibmodelle sind normalisiert, validierungsorientiert und akzeptieren stark typisierte Kommandos (CreateUserCommand, UpdateUserCommand)

Architektur

src/
  MyApp.Data/
    Users/
      # Leseseite – mehrere optimierte Projektionen
      IUserReadStore.cs
      PostgresUserReadStore.cs

      # Schreibseite – Command-Handler
      IUserWriteStore.cs
      PostgresUserWriteStore.cs

      # Lese-DTOs – leichtgewichtig, denormalisiert
      UserProfile.cs
      UserSummary.cs

      # Schreibkommandos – validierungsorientiert
      CreateUserCommand.cs
      UpdateUserCommand.cs
    Orders/
      IOrderReadStore.cs
      IOrderWriteStore.cs
      (ähnliche Struktur...)

Read-Store-Schnittstelle

// Lesemodelle: Mehrere spezialisierte Projektionen für verschiedene Anwendungsfälle
public interface IUserReadStore
{
    // Gibt detailliertes Profil für Einzelbenutzeransicht zurück
    Task<UserProfile?> GetByIdAsync(UserId id, CancellationToken ct = default);

    // Gibt leichte Informationen für Nachschlagevorgänge zurück
    Task<UserProfile?> GetByEmailAsync(EmailAddress email, CancellationToken ct = default);

    // Gibt paginierte Zusammenfassungen zurück – nur das, was die Listenansicht benötigt
    Task<IReadOnlyList<UserSummary>> GetAllAsync(int limit, UserId? cursor = null, CancellationToken ct = default);

    // Boolesche Abfrage – kein Entity nötig
    Task<bool> EmailExistsAsync(EmailAddress email, CancellationToken ct = default);
}

Write-Store-Schnittstelle

// Schreibmodell: Akzeptiert stark typisierte Kommandos, minimale Rückgabewerte
public interface IUserWriteStore
{
    // Gibt nur die erstellte ID zurück – Aufrufer benötigt nicht das vollständige Entity
    Task<UserId> CreateAsync(CreateUserCommand command, CancellationToken ct = default);

    // Update validiert das Kommando, gibt void zurück (Erfolg oder wirft Exception)
    Task UpdateAsync(UserId id, UpdateUserCommand command, CancellationToken ct = default);

    // Löschen ist einfach und explizit
    Task DeleteAsync(UserId id, CancellationToken ct = default);
}

Wesentliche strukturelle Unterschiede:

  • Read-Store gibt mehrere verschiedene DTOs zurück (UserProfile, UserSummary, bool-Flag)
  • Write-Store gibt minimale Daten zurück (nur UserId bei Erstellung) oder void
  • Leseabfragen sind zustandslose Projektionen – kein Tracking nötig
  • Schreiboperationen konzentrieren sich auf Kommandovalidierung, nicht auf das Abrufen von Daten im Anschluss
  • Unterschiedliche Datenbanken/Tabellen können Lese- und Schreibzugriffe unterstützen (Eventual-Consistency-Pattern)

Immer Zeilenlimiten Anwenden

Gib niemals unbegrenzte Ergebnismengen zurück. Jede Lesemethode sollte ein konfigurierbares Limit haben.

Pattern: Limit-Parameter

public interface IOrderReadStore
{
    // Limit ist erforderlich, nicht optional
    Task<IReadOnlyList<OrderSummary>> GetByCustomerAsync(
        CustomerId customerId,
        int limit,
        OrderId? cursor = null,
        CancellationToken ct = default);
}

// Implementierung
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 mit Paginierung

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)  // Immer limitieren!
        .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 für Leseabfragen

Das Change Tracking von EF Core ist teuer. Deaktiviere es für rein lesende Abfragen.

// DO: Tracking für Lesezugriffe deaktivieren
var users = await _context.Users
    .AsNoTracking()
    .Where(u => u.IsActive)
    .ToListAsync();

// DON'T: Entities tracken, die Du nicht ändern wirst
var users = await _context.Users
    .Where(u => u.IsActive)
    .ToListAsync();  // Change Tracking aktiviert – verschwenderisch

Standardverhalten konfigurieren

// Für leseintensive Anwendungen dies im DbContext in Betracht ziehen
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

Dann Tracking explizit aktivieren, wenn nötig:

var user = await _context.Users
    .AsTracking()  // Explizit – wir beabsichtigen zu ändern
    .FirstOrDefaultAsync(u => u.Id == userId);

N+1-Abfragen Vermeiden

Das N+1-Problem: Eine Liste abrufen und dann für jedes Element die zugehörigen Daten abfragen.

Das Problem

// SCHLECHT: N+1-Abfragen
var orders = await _context.Orders.ToListAsync();

foreach (var order in orders)
{
    // Jede Iteration trifft die Datenbank!
    var items = await _context.OrderItems
        .Where(i => i.OrderId == order.Id)
        .ToListAsync();
}

Lösung 1: Include (EF Core)

// GUT: Einzelne Abfrage mit Join
var orders = await _context.Orders
    .AsNoTracking()
    .Include(o => o.Items)
    .ToListAsync();

Lösung 2: Batch-Abfrage (Dapper)

// GUT: Zwei Abfragen, kein 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();

// Im Speicher joinen (akzeptabel – Daten bereits abgerufen)
foreach (var order in orders)
{
    order.Items = items.Where(i => i.OrderId == order.Id).ToList();
}

Nie Applikationsseitige Joins Durchführen

Joins müssen in SQL erfolgen, nicht in C#.

// SCHLECHT: Applikations-Join – zwei Abfragen, Speicherverschwendung
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) im Speicher!
});

// GUT: SQL-Join – einzelne Abfrage
var result = await _context.Customers
    .AsNoTracking()
    .Include(c => c.Orders)
    .ToListAsync();

// GUT: Expliziter Join (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
    """;

Kartesische Explosionen Vermeiden

Mehrere Include-Aufrufe können kartesische Produkte verursachen.

// GEFÄHRLICH: Kann in Millionen von Zeilen explodieren
var product = await _context.Products
    .Include(p => p.Reviews)      // 100 Rezensionen
    .Include(p => p.Images)       // 20 Bilder
    .Include(p => p.Categories)   // 5 Kategorien
    .FirstOrDefaultAsync(p => p.Id == id);
// Ergebnis: 100 * 20 * 5 = 10.000 Zeilen übertragen!

Lösung: Aufgeteilte Abfragen

// GUT: Mehrere Abfragen, keine kartesische Explosion
var product = await _context.Products
    .AsSplitQuery()
    .Include(p => p.Reviews)
    .Include(p => p.Images)
    .Include(p => p.Categories)
    .FirstOrDefaultAsync(p => p.Id == id);
// Ergebnis: 4 separate Abfragen, ~125 Zeilen gesamt

Lösung: Explizite Projektion

// AM BESTEN: Nur das abrufen, was benötigt wird
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();

Spaltengrößen Beschränken

Definiere maximale Längen in Deinem EF Core-Modell, um übermäßig große Daten zu verhindern.

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

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

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

        // Für wirklich große Inhalte explizit Text-Typ verwenden
        builder.Property(u => u.Notes)
            .HasColumnType("text");
    }
}

Keine Generischen Repositories Bauen

Generische Repositories verstecken die Abfragekomplexität und erschweren die Optimierung.

// SCHLECHT: Generisches Repository
public interface IRepository<T>
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();  // Kein Limit!
    Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);  // Kann nicht optimiert werden
}

// GUT: Zweckgebundene Read-Stores
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);
}

Probleme mit generischen Repositories:

  • Können spezifische Abfragen nicht optimieren
  • Keine Möglichkeit, Limiten durchzusetzen
  • Verstecken N+1-Probleme
  • Machen es leicht, zu viele Daten abzurufen
  • Fördern nachlässiges Denken über Datenzugriff

Dapper für Leseintensive Arbeitslasten

Für komplexe Leseabfragen ist Dapper mit explizitem SQL oft sauberer und schneller.

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();
    }

    // Interner Zeilentyp für Dapper-Mapping
    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));
    }
}

Wann EF Core vs. Dapper Verwenden

SzenarioEmpfehlung
Einfaches CRUDEF Core
Komplexe LeseabfragenDapper
Schreibzugriffe mit ValidierungEF Core
MassenoperationenDapper oder rohes SQL
Berichte/AnalysenDapper
Domänenintensive SchreibzugriffeEF Core

Du kannst beide im selben Projekt verwenden – EF Core für Schreibzugriffe, Dapper für Lesezugriffe.


Kurzreferenz

Anti-PatternLösung
Kein Zeilenlimitlimit-Parameter zu jeder Lesemethode hinzufügen
SELECT *Nur benötigte Spalten projizieren
N+1-AbfragenInclude oder Batch-Abfragen verwenden
Applikations-JoinsJoins in SQL durchführen
Kartesische ExplosionAsSplitQuery oder Projektion verwenden
Tracking von Nur-Lese-DatenAsNoTracking verwenden
Generisches RepositoryZweckgebundene Lese-/Schreib-Stores
Unbegrenzte ZeichenkettenMaxLength im Modell konfigurieren

Ressourcen

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.

Verwandte Skills