编译查询与性能
使用 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 反馈 — 无需本地设置。