数据库性能优化模式Skill database-performance

数据库性能优化模式技能专注于提升.NET应用程序的数据访问性能。涵盖CQRS读写分离、EF Core与Dapper最佳实践、避免N+1查询、应用行限制、使用AsNoTracking、SQL层连接等关键技术。适用于后端开发人员优化数据访问层,解决慢查询问题,提高系统吞吐量。关键词:数据库优化、EF Core性能、Dapper、CQRS模式、N+1查询、AsNoTracking、行限制、SQL连接、数据访问层设计、.NET性能调优。

后端开发 0 次安装 0 次浏览 更新于 2/26/2026

name: database-performance description: 数据库性能访问模式。分离读写模型,避免N+1查询,使用AsNoTracking,应用行限制,绝不在应用层进行连接。适用于EF Core和Dapper。 invocable: false tags: [cqrs, performance, patterns]

数据库性能模式

何时使用此技能

在以下情况下使用此技能:

  • 设计数据访问层
  • 优化慢速数据库查询
  • 在EF Core和Dapper之间选择
  • 避免常见的性能陷阱

核心原则

  1. 分离读写模型 - 不要为两者使用相同的类型
  2. 批量思考 - 避免N+1查询
  3. 只检索所需内容 - 不要使用SELECT *
  4. 应用行限制 - 始终有可配置的Take/Limit
  5. 在SQL中进行连接 - 绝不在应用代码中
  6. 读取时使用AsNoTracking - EF Core变更跟踪开销大

读写模型分离(CQRS模式)

读写模型本质不同 - 它们具有不同的形状、列和用途。 不要创建单一的“用户”实体并在各处重复使用。

  • 读取模型是反规范化的,针对查询效率优化,返回多种投影类型(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、布尔标志)
  • 写入存储返回最小数据(创建时仅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)

// 正确:两个查询,无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#中。

// 错误:应用层连接 - 两个查询,内存浪费
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);

        // 对于真正大的内容,显式使用文本类型
        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 vs Dapper

场景 推荐
简单CRUD EF Core
复杂读取查询 Dapper
带验证的写入 EF Core
批量操作 Dapper或原始SQL
报告/分析 Dapper
领域密集的写入 EF Core

您可以在同一项目中使用两者 - EF Core用于写入,Dapper用于读取。


快速参考

反模式 解决方案
无行限制 为每个读取方法添加limit参数
SELECT * 仅投影所需列
N+1查询 使用Include或批量查询
应用层连接 在SQL中进行连接
笛卡尔积爆炸 使用AsSplitQuery或投影
跟踪只读数据 使用AsNoTracking
通用存储库 目的明确的读写存储
无界字符串 在模型中配置MaxLength

资源