0Pricing
C# Academy · 课时

编译查询与性能

使用 EF.CompileQuery、AsNoTracking 和 Select 投影提升查询性能。

编译查询与性能 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

EF Core 查询编译

每次 EF Core 执行 LINQ 查询时,都必须解析表达式树,将其翻译为 SQL 并进行验证。对于热点路径,这种开销会不断累积。EF Core 提供了多种工具来将其降至最低。

对只读查询使用 AsNoTracking

AsNoTracking() 会跳过变更跟踪器注册。对于只读查询(API 响应、报告),这是最容易实现且收益最大的优化,通常速度可提升 2–5 倍,同时占用更少内存。

// With tracking (default) — for entities you'll modify
var tracked = await _db.Products.FirstOrDefaultAsync(p => p.Id == 1);

// Without tracking — for read-only
var dto = await _db.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .Select(p => new ProductDto { Id = p.Id, Name = p.Name })
    .ToListAsync();

编译查询

EF.CompileAsyncQuery 会将 LINQ 查询预编译为委托。翻译只在启动时进行一次;后续调用会完全跳过编译步骤。

// Define once, reuse many times
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
    EF.CompileAsyncQuery(
        (AppDbContext db, int id) =>
            db.Products.AsNoTracking().FirstOrDefault(p => p.Id == id));

// Usage
var product = await GetProductById(_db, 42);

使用 Select 投影提升性能

请始终投影到确切需要的字段。与加载完整实体相比,选择 DTO 可以减少获取的列数、网络负载和垃圾回收压力。

// Load entire entity (over-fetch)
var all = await _db.Products.ToListAsync();

// Projection: only fetch needed columns
var names = await _db.Products
    .AsNoTracking()
    .Select(p => new { p.Id, p.Name, p.Price })
    .ToListAsync();
// SQL: SELECT Id, Name, Price FROM Products

索引提示和原始查询优化

EF Core 会遵循您使用 HasIndex() 配置的数据库索引。复合索引、筛选索引和包含列都会影响查询计划的性能。

// Configure compound index in OnModelCreating
modelBuilder.Entity<Order>()
    .HasIndex(o => new { o.CustomerId, o.Status })
    .HasFilter("[Status] = 0")  // filtered index
    .IncludeProperties(o => o.PlacedAt);

// The query below will use this index efficiently
var pending = await _db.Orders
    .AsNoTracking()
    .Where(o => o.CustomerId == 5 && o.Status == OrderStatus.Pending)
    .ToListAsync();

使用 Skip 和 Take 分页

请始终对大型结果集进行分页。Skip 和 Take 会转换为 SQL OFFSET/FETCH,避免将数千行加载到内存中。

int page = 2, pageSize = 25;

var products = await _db.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .OrderBy(p => p.Name)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();
// SQL: ... ORDER BY Name OFFSET 25 ROWS FETCH NEXT 25 ROWS ONLY

高效计数

请使用 CountAsync() 或 AnyAsync(),而不是加载记录后再计数。只需检查是否存在记录时,AnyAsync() 甚至比 COUNT 更快。

// Total count for pagination
int totalCount = await _db.Products
    .Where(p => p.IsActive)
    .CountAsync();

// Existence check — faster than Count > 0
bool hasExpensive = await _db.Products
    .AnyAsync(p => p.Price > 1000m);
// SQL: IF EXISTS (SELECT 1 FROM ...) SELECT 1

使用 AddRange 批量插入

使用 AddRange 一次添加多个实体。EF Core 会将 INSERT 语句批量处理为更少的往返操作,批处理大小由提供程序中的 MaxBatchSize 控制。

var newProducts = Enumerable.Range(1, 1000)
    .Select(i => new Product { Name = $"Product {i}", Price = i * 1.5m })
    .ToList();

_db.Products.AddRange(newProducts);
await _db.SaveChangesAsync();
// Batched: typically 100-1000 rows per round-trip

用于数据库函数的 EF.Functions

EF.Functions 提供数据库特定的函数(LIKE、日期部分、全文搜索),EF Core 会将它们翻译为 SQL,而不是在内存中计算。

// LIKE pattern search (SQL-side)
var results = await _db.Products
    .Where(p => EF.Functions.Like(p.Name, "%keyboard%"))
    .AsNoTracking()
    .ToListAsync();

// Date difference (SQL-side)
var recentOrders = await _db.Orders
    .Where(o => EF.Functions.DateDiffDay(o.PlacedAt, DateTime.UtcNow) <= 7)
    .ToListAsync();

诊断慢查询

启用 EF Core 查询日志记录以查看生成的 SQL。在开发环境中使用 EnableSensitiveDataLogging() 可以查看参数值。

builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(connStr)
       .LogTo(Console.WriteLine, LogLevel.Information)
       .EnableSensitiveDataLogging() // dev only!
       .EnableDetailedErrors());     // dev only!

实战:优化列表端点

适用于生产环境的列表端点会结合使用 AsNoTracking、投影、索引和分页,以获得最高性能。

app.MapGet("/products", async (
    AppDbContext db,
    string? search,
    int page = 1, int size = 20) =>
{
    var query = db.Products
        .AsNoTracking()
        .Where(p => p.IsActive);

    if (!string.IsNullOrEmpty(search))
        query = query.Where(p => EF.Functions.Like(p.Name, $"%{search}%"));

    var total = await query.CountAsync();
    var items = await query
        .OrderBy(p => p.Name)
        .Skip((page - 1) * size).Take(size)
        .Select(p => new { p.Id, p.Name, p.Price })
        .ToListAsync();

    return Results.Ok(new { total, items });
});

快速检查

与普通 LINQ 查询相比,EF.CompileAsyncQuery 的主要优势是什么?

回顾:编译查询与性能

要点:

  • AsNoTracking():只读查询跳过变更跟踪器——最容易实现且收益最大的优化
  • EF.CompileAsyncQuery:将热点查询预编译为委托
  • 使用 Select 投影:只获取所需列
  • 正确的索引(HasIndex)可以显著改善查询计划
  • 使用 Skip/Take 分页;使用 AnyAsync 检查是否存在记录
  • 在开发环境中启用查询日志记录,以发现缓慢或异常的 SQL

常见问题解答

「编译查询与性能」课时是免费的吗?

是的 — 「编译查询与性能」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「编译查询与性能」这节课中我会学到什么?

使用 EF.CompileQuery、AsNoTracking 和 Select 投影提升查询性能。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「编译查询与性能」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 预先加载、延迟加载与显式加载
  2. 原始 SQL、存储过程与插值
  3. 编译查询与性能
  4. 全局查询筛选器与拥有实体
← 返回 C# Academy