Communityコーディング&開発github.com

データベースパフォーマンス

データベースアクセスパターンを最適化するスキル。読み取り/書き込みモデルの分離、N+1クエリの回避、AsNoTrackingの使用、行制限の適用、アプリケーションサイドでの結合禁止。EF CoreとDapperに対応。

データベースパフォーマンス とは?

このスキルは、データベースアクセスレイヤーを設計・最適化するための包括的なベストプラクティスを提供します。読み取りモデルと書き込みモデルを明確に分離するCQRSパターンに基づき、EF CoreとDapperの両方で効率的なデータアクセスを実現する方法を解説しています。具体的には、N+1クエリやカーテシアン爆発といったパフォーマンスの落とし穴を避けるためのテクニック(AsNoTracking、Include/AsSplitQuery、バッチクエリなど)や、すべての読み取りメソッドに必須の行制限パラメータの追加、アプリケーションサイドでの結合禁止など、実践的なコーディングパターンが多数含まれています。また、汎用リポジトリパターンの問題点を指摘し、目的特化型の読み取り/書き込みストアを推奨するなど、スケーラブルで保守性の高いデータアクセス層を構築するための指針を提供します。EF CoreとDapperは排他的ではなく、用途に応じて併用するハイブリッドアプローチも紹介しています。

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

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

データベースパフォーマンスパターン

このスキルを使うタイミング

以下のような場合にこのスキルを活用してください:

  • データアクセスレイヤーを設計するとき
  • 遅いデータベースクエリを最適化するとき
  • EF CoreとDapperのどちらを選ぶか検討するとき
  • 一般的なパフォーマンスの落とし穴を避けたいとき

核となる原則

  1. 読み取りモデルと書き込みモデルを分離する - 両方に同じ型を使わない
  2. バッチで考える - N+1クエリを避ける
  3. 必要なものだけを取得する - SELECT * は禁止
  4. 行制限を適用する - 常に設定可能なTake/Limitを設ける
  5. 結合はSQLで行う - アプリケーションコードでは絶対に行わない
  6. 読み取りにはAsNoTrackingを使用する - EF Coreの変更追跡は高コスト

読み取り/書き込みモデルの分離(CQRSパターン)

読み取りモデルと書き込みモデルは根本的に異なります。形状、カラム、目的が異なるため、単一の「User」エンティティを作成してあらゆる場所で再利用してはいけません。

  • 読み取りモデルは非正規化され、クエリ効率に最適化され、複数の射影型(UserProfile、UserSummary、UserDetailForAdminなど)を返します。
  • 書き込みモデルは正規化され、バリデーションに重点を置き、強い型付けされたコマンド(CreateUserCommand、UpdateUserCommand)を受け取ります。

アーキテクチャ

src/
  MyApp.Data/
    Users/
      # 読み取り側 - 複数の最適化された射影
      IUserReadStore.cs
      PostgresUserReadStore.cs

      # 書き込み側 - コマンドハンドラ
      IUserWriteStore.cs
      PostgresUserWriteStore.cs

      # 読み取りDTO - 軽量、非正規化
      UserProfile.cs
      UserSummary.cs

      # 書き込みコマンド - バリデーション重視
      CreateUserCommand.cs
      UpdateUserCommand.cs
    Orders/
      IOrderReadStore.cs
      IOrderWriteStore.cs
      (同様の構造...)

読み取りストアインターフェース

// 読み取りモデル:異なるユースケースに最適化された複数の特殊な射影
public interface IUserReadStore
{
    // 単一ユーザー表示用の詳細プロフィールを返す
    Task<UserProfile?> GetByIdAsync(UserId id, CancellationToken ct = default);

    // ルックアップ用の軽量情報を返す
    Task<UserProfile?> GetByEmailAsync(EmailAddress email, CancellationToken ct = default);

    // ページネーションされたサマリーを返す - リストビューに必要なものだけ
    Task<IReadOnlyList<UserSummary>> GetAllAsync(int limit, UserId? cursor = null, CancellationToken ct = default);

    // ブールクエリ - エンティティは不要
    Task<bool> EmailExistsAsync(EmailAddress email, CancellationToken ct = default);
}

書き込みストアインターフェース

// 書き込みモデル:強い型付けされたコマンドを受け取り、最小限の戻り値
public interface IUserWriteStore
{
    // 作成されたIDのみを返す - 呼び出し側は完全なエンティティを必要としない
    Task<UserId> CreateAsync(CreateUserCommand command, CancellationToken ct = default);

    // 更新はコマンドを検証し、voidを返す(成功するか例外をスローする)
    Task UpdateAsync(UserId id, UpdateUserCommand command, CancellationToken ct = default);

    // 削除はシンプルで明示的
    Task DeleteAsync(UserId id, CancellationToken ct = default);
}

構造上の重要な違いの説明:

  • 読み取りストアは複数の異なるDTO(UserProfile、UserSummary、boolフラグなど)を返す
  • 書き込みストアは最小限のデータ(作成時はUserIdのみ)またはvoidを返す
  • 読み取りクエリはステートレスな射影であり、追跡は不要
  • 書き込み操作はコマンドのバリデーションに焦点を当て、その後のデータ取得は行わない
  • 異なるデータベース/テーブルが読み取りと書き込みをバックアップできる(結果整合性パターン)

常に行制限を適用する

無制限の結果セットを決して返してはいけません。 すべての読み取りメソッドには設定可能な制限が必要です。

パターン:制限パラメータ

public interface IOrderReadStore
{
    // 制限は必須であり、オプションではない
    Task<IReadOnlyList<OrderSummary>> GetByCustomerAsync(
        CustomerId customerId,
        int limit,
        OrderId? cursor = null,
        CancellationToken ct = default);
}

// 実装
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とページネーション

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)  // 常に制限をかける!
        .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を使用する

EF Coreの変更追跡は高コストです。読み取り専用クエリでは無効にしましょう。

// 推奨:読み取りでは追跡を無効にする
var users = await _context.Users
    .AsNoTracking()
    .Where(u => u.IsActive)
    .ToListAsync();

// 非推奨:変更しないエンティティを追跡する
var users = await _context.Users
    .Where(u => u.IsActive)
    .ToListAsync();  // 変更追跡が有効 - 無駄が多い

デフォルト動作の設定

// 読み取りが多いアプリケーションでは、DbContextで以下を検討する
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

必要なときに明示的に追跡を有効にします:

var user = await _context.Users
    .AsTracking()  // 明示的 - 変更する意図がある
    .FirstOrDefaultAsync(u => u.Id == userId);

N+1クエリを避ける

N+1問題:リストを取得した後、各アイテムの関連データを個別にクエリすること。

問題

// 悪い例:N+1クエリ
var orders = await _context.Orders.ToListAsync();

foreach (var order in orders)
{
    // 各イテレーションでデータベースにアクセス!
    var items = await _context.OrderItems
        .Where(i => i.OrderId == order.Id)
        .ToListAsync();
}

解決策1:Include(EF Core)

// 良い例:結合を使用した単一クエリ
var orders = await _context.Orders
    .AsNoTracking()
    .Include(o => o.Items)
    .ToListAsync();

解決策2:バッチクエリ(Dapper)

// 良い例:2つのクエリ、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();

// メモリ内で結合(許容範囲 - データは既に取得済み)
foreach (var order in orders)
{
    order.Items = items.Where(i => i.OrderId == order.Id).ToList();
}

アプリケーションサイドでの結合は絶対に行わない

結合はSQLで行い、C#では行わないでください。

// 悪い例:アプリケーション結合 - 2回のクエリ、メモリの無駄
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)!
});

// 良い例:SQL結合 - 単一クエリ
var result = await _context.Customers
    .AsNoTracking()
    .Include(c => c.Orders)
    .ToListAsync();

// 良い例:明示的な結合(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
    """;

カーテシアン爆発を避ける

複数のInclude呼び出しは、デカルト積を引き起こす可能性があります。

// 危険:数百万行に爆発する可能性
var product = await _context.Products
    .Include(p => p.Reviews)      // 100件のレビュー
    .Include(p => p.Images)       // 20枚の画像
    .Include(p => p.Categories)   // 5つのカテゴリ
    .FirstOrDefaultAsync(p => p.Id == id);
// 結果:100 * 20 * 5 = 10,000行が転送される!

解決策:分割クエリ

// 良い例:複数のクエリ、カーテシアン爆発なし
var product = await _context.Products
    .AsSplitQuery()
    .Include(p => p.Reviews)
    .Include(p => p.Images)
    .Include(p => p.Categories)
    .FirstOrDefaultAsync(p => p.Id == id);
// 結果:4つの個別クエリ、合計約125行

解決策:明示的な射影

// 最良の例:必要なものだけを取得する
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();

カラムサイズを制約する

過剰なデータを防ぐために、EF Coreモデルで最大長を定義します。

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

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

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

        // 本当に大きなコンテンツの場合は、明示的にtext型を使用
        builder.Property(u => u.Notes)
            .HasColumnType("text");
    }
}

汎用リポジトリは構築しない

汎用リポジトリはクエリの複雑さを隠蔽し、最適化を困難にします。

// 悪い例:汎用リポジトリ
public interface IRepository<T>
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();  // 制限なし!
    Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);  // 最適化不可
}

// 良い例:目的特化型の読み取りストア
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);
}

汎用リポジトリの問題点:

  • 特定のクエリを最適化できない
  • 制限を強制する方法がない
  • N+1問題を隠蔽する
  • 過剰なデータ取得を容易にする
  • データアクセスに関する怠慢な思考を促す

読み取り中心のワークロードにはDapper

複雑な読み取りクエリには、明示的なSQLを使用したDapperの方が、よりクリーンで高速な場合が多いです。

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

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

EF CoreとDapperの使い分け

シナリオ推奨
シンプルなCRUDEF Core
複雑な読み取りクエリDapper
バリデーションを含む書き込みEF Core
一括操作Dapperまたは生SQL
レポート/分析Dapper
ドメイン中心の書き込みEF Core

同じプロジェクトで両方を使用できます。EF Coreを書き込みに、Dapperを読み取りに使用します。


クイックリファレンス

アンチパターン解決策
行制限なしすべての読み取りメソッドにlimitパラメータを追加
SELECT *必要なカラムのみを射影
N+1クエリIncludeまたはバッチクエリを使用
アプリケーション結合SQLで結合を行う
カーテシアン爆発AsSplitQueryまたは射影を使用
読み取り専用データの追跡AsNoTrackingを使用
汎用リポジトリ目的特化型の読み取り/書き込みストア
無制限の文字列モデルにMaxLengthを設定

リソース

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.

関連スキル